Dữ liệu của sàn giao dịch

Sau khi thiết lập kết nối giữa các thiết bị, bạn có thể trao đổi dữ liệu bằng cách gửi và nhận đối tượng Payload. Đáp Payload có thể biểu thị một mảng byte đơn giản, chẳng hạn như một thông báo văn bản ngắn; một tệp, chẳng hạn như ảnh hoặc video; hoặc luồng, chẳng hạn như luồng âm thanh từ micrô.

Tải trọng được gửi bằng phương thức sendPayload() và nhận trong quá trình triển khai PayloadCallback. Tệp này được chuyển tới acceptConnection() như mô tả trong phần Quản lý kết nối.

Các loại tải trọng

Byte

Tải trọng byte là loại tải trọng đơn giản nhất. Chúng phù hợp để gửi dữ liệu đơn giản như tin nhắn hoặc siêu dữ liệu có kích thước tối đa là Connections.MAX_BYTES_DATA_SIZE. Dưới đây là ví dụ về cách gửi tải trọng BYTES:

Payload bytesPayload = Payload.fromBytes(new byte[] {0xa, 0xb, 0xc, 0xd});
Nearby.getConnectionsClient(context).sendPayload(toEndpointId, bytesPayload);

Nhận tải trọng BYTES bằng cách triển khai phương thức onPayloadReceived() của PayloadCallback mà bạn đã truyền đến acceptConnection().

static class ReceiveBytesPayloadListener extends PayloadCallback {

  @Override
  public void onPayloadReceived(String endpointId, Payload payload) {
    // This always gets the full data of the payload. Is null if it's not a BYTES payload.
    if (payload.getType() == Payload.Type.BYTES) {
      byte[] receivedBytes = payload.asBytes();
    }
  }

  @Override
  public void onPayloadTransferUpdate(String endpointId, PayloadTransferUpdate update) {
    // Bytes payloads are sent as a single chunk, so you'll receive a SUCCESS update immediately
    // after the call to onPayloadReceived().
  }
}

Không giống như các tải trọng FILESTREAM, tải trọng BYTES được gửi dưới dạng một tải trọng phân đoạn, nên không cần phải đợi bản cập nhật SUCCESS (mặc dù mã này sẽ vẫn được gửi ngay sau khi gọi đến onPayloadReceived()). Thay vào đó, bạn có thể gọi payload.asBytes() một cách an toàn để nhận toàn bộ dữ liệu của ngay khi onPayloadReceived() được gọi.

Tệp

Tải trọng tệp được tạo từ một tệp được lưu trữ trên thiết bị cục bộ, chẳng hạn như tệp ảnh hoặc video. Dưới đây là một ví dụ đơn giản về việc gửi tải trọng FILE:

File fileToSend = new File(context.getFilesDir(), "hello.txt");
try {
  Payload filePayload = Payload.fromFile(fileToSend);
  Nearby.getConnectionsClient(context).sendPayload(toEndpointId, filePayload);
} catch (FileNotFoundException e) {
  Log.e("MyApp", "File not found", e);
}

Có thể hiệu quả hơn nếu bạn sử dụng ParcelFileDescriptor để tạo tải trọng FILES nếu có sẵn, ví dụ: tải trọng từ ContentResolver. Việc này sẽ giảm thiểu việc sao chép các byte của tệp:

ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
filePayload = Payload.fromFile(pfd);

Khi nhận được tệp, tệp sẽ được lưu trong thư mục Tệp đã tải xuống (DIRECTORY_DOWNLOADS) trên thư mục Tệp của người nhận một thiết bị có tên chung và không có đuôi. Sau khi quá trình chuyển hoàn tất, được biểu thị bằng một lệnh gọi đến onPayloadTransferUpdate() với PayloadTransferUpdate.Status.SUCCESS, bạn có thể truy xuất đối tượng File như vậy nếu ứng dụng của bạn đang nhắm mục tiêu < Hỏi:

File payloadFile = filePayload.asFile().asJavaFile();

// Rename the file.
payloadFile.renameTo(new File(payloadFile.getParentFile(), filename));

Nếu ứng dụng của bạn đang nhắm đến các thiết bị Q, thì bạn có thể thêm android:requestlegacyExternalStorage="true" trong phần tử ứng dụng của tệp kê khai để tiếp tục sử dụng mã trước đó. Nếu không, đối với Q+, bạn sẽ phải tuân theo các quy tắc Scoped Storage và truy cập vào tệp đã nhận bằng cách sử dụng URI được truyền từ dịch vụ.

// Because of https://developer.android.com/preview/privacy/scoped-storage, we are not
// allowed to access filepaths from another process directly. Instead, we must open the
// uri using our ContentResolver.
Uri uri = filePayload.asFile().asUri();
try {
  // Copy the file to a new location.
  InputStream in = context.getContentResolver().openInputStream(uri);
  copyStream(in, new FileOutputStream(new File(context.getCacheDir(), filename)));
} catch (IOException e) {
  // Log the error.
} finally {
  // Delete the original file.
  context.getContentResolver().delete(uri, null, null);
}

Trong ví dụ phức tạp hơn sau đây, ý định ACTION_OPEN_DOCUMENT sẽ nhắc người dùng chọn một tệp và tệp đó được gửi dưới dạng một tải trọng một cách hiệu quả bằng cách sử dụng ParcelFileDescriptor. Tên tệp cũng được gửi dưới dạng một tải trọng BYTES.

private static final int READ_REQUEST_CODE = 42;
private static final String ENDPOINT_ID_EXTRA = "com.foo.myapp.EndpointId";

/**
 * Fires an intent to spin up the file chooser UI and select an image for sending to endpointId.
 */
private void showImageChooser(String endpointId) {
  Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
  intent.addCategory(Intent.CATEGORY_OPENABLE);
  intent.setType("image/*");
  intent.putExtra(ENDPOINT_ID_EXTRA, endpointId);
  startActivityForResult(intent, READ_REQUEST_CODE);
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
  super.onActivityResult(requestCode, resultCode, resultData);
  if (requestCode == READ_REQUEST_CODE
      && resultCode == Activity.RESULT_OK
      && resultData != null) {
    String endpointId = resultData.getStringExtra(ENDPOINT_ID_EXTRA);

    // The URI of the file selected by the user.
    Uri uri = resultData.getData();

    Payload filePayload;
    try {
      // Open the ParcelFileDescriptor for this URI with read access.
      ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
      filePayload = Payload.fromFile(pfd);
    } catch (FileNotFoundException e) {
      Log.e("MyApp", "File not found", e);
      return;
    }

    // Construct a simple message mapping the ID of the file payload to the desired filename.
    String filenameMessage = filePayload.getId() + ":" + uri.getLastPathSegment();

    // Send the filename message as a bytes payload.
    Payload filenameBytesPayload =
        Payload.fromBytes(filenameMessage.getBytes(StandardCharsets.UTF_8));
    Nearby.getConnectionsClient(context).sendPayload(endpointId, filenameBytesPayload);

    // Finally, send the file payload.
    Nearby.getConnectionsClient(context).sendPayload(endpointId, filePayload);
  }
}

Vì tên tệp được gửi dưới dạng tải trọng, nên receiver của chúng ta có thể di chuyển hoặc đổi tên tệp để có phần mở rộng thích hợp:

static class ReceiveFilePayloadCallback extends PayloadCallback {
  private final Context context;
  private final SimpleArrayMap<Long, Payload> incomingFilePayloads = new SimpleArrayMap<>();
  private final SimpleArrayMap<Long, Payload> completedFilePayloads = new SimpleArrayMap<>();
  private final SimpleArrayMap<Long, String> filePayloadFilenames = new SimpleArrayMap<>();

  public ReceiveFilePayloadCallback(Context context) {
    this.context = context;
  }

  @Override
  public void onPayloadReceived(String endpointId, Payload payload) {
    if (payload.getType() == Payload.Type.BYTES) {
      String payloadFilenameMessage = new String(payload.asBytes(), StandardCharsets.UTF_8);
      long payloadId = addPayloadFilename(payloadFilenameMessage);
      processFilePayload(payloadId);
    } else if (payload.getType() == Payload.Type.FILE) {
      // Add this to our tracking map, so that we can retrieve the payload later.
      incomingFilePayloads.put(payload.getId(), payload);
    }
  }

  /**
   * Extracts the payloadId and filename from the message and stores it in the
   * filePayloadFilenames map. The format is payloadId:filename.
   */
  private long addPayloadFilename(String payloadFilenameMessage) {
    String[] parts = payloadFilenameMessage.split(":");
    long payloadId = Long.parseLong(parts[0]);
    String filename = parts[1];
    filePayloadFilenames.put(payloadId, filename);
    return payloadId;
  }

  private void processFilePayload(long payloadId) {
    // BYTES and FILE could be received in any order, so we call when either the BYTES or the FILE
    // payload is completely received. The file payload is considered complete only when both have
    // been received.
    Payload filePayload = completedFilePayloads.get(payloadId);
    String filename = filePayloadFilenames.get(payloadId);
    if (filePayload != null && filename != null) {
      completedFilePayloads.remove(payloadId);
      filePayloadFilenames.remove(payloadId);

      // Get the received file (which will be in the Downloads folder)
      // Because of https://developer.android.com/preview/privacy/scoped-storage, we are not
      // allowed to access filepaths from another process directly. Instead, we must open the
      // uri using our ContentResolver.
      Uri uri = filePayload.asFile().asUri();
      try {
        // Copy the file to a new location.
        InputStream in = context.getContentResolver().openInputStream(uri);
        copyStream(in, new FileOutputStream(new File(context.getCacheDir(), filename)));
      } catch (IOException e) {
        // Log the error.
      } finally {
        // Delete the original file.
        context.getContentResolver().delete(uri, null, null);
      }
    }
  }

  // add removed tag back to fix b/183037922
  private void processFilePayload2(long payloadId) {
    // BYTES and FILE could be received in any order, so we call when either the BYTES or the FILE
    // payload is completely received. The file payload is considered complete only when both have
    // been received.
    Payload filePayload = completedFilePayloads.get(payloadId);
    String filename = filePayloadFilenames.get(payloadId);
    if (filePayload != null && filename != null) {
      completedFilePayloads.remove(payloadId);
      filePayloadFilenames.remove(payloadId);

      // Get the received file (which will be in the Downloads folder)
      if (VERSION.SDK_INT >= VERSION_CODES.Q) {
        // Because of https://developer.android.com/preview/privacy/scoped-storage, we are not
        // allowed to access filepaths from another process directly. Instead, we must open the
        // uri using our ContentResolver.
        Uri uri = filePayload.asFile().asUri();
        try {
          // Copy the file to a new location.
          InputStream in = context.getContentResolver().openInputStream(uri);
          copyStream(in, new FileOutputStream(new File(context.getCacheDir(), filename)));
        } catch (IOException e) {
          // Log the error.
        } finally {
          // Delete the original file.
          context.getContentResolver().delete(uri, null, null);
        }
      } else {
        File payloadFile = filePayload.asFile().asJavaFile();

        // Rename the file.
        payloadFile.renameTo(new File(payloadFile.getParentFile(), filename));
      }
    }
  }

  @Override
  public void onPayloadTransferUpdate(String endpointId, PayloadTransferUpdate update) {
    if (update.getStatus() == PayloadTransferUpdate.Status.SUCCESS) {
      long payloadId = update.getPayloadId();
      Payload payload = incomingFilePayloads.remove(payloadId);
      completedFilePayloads.put(payloadId, payload);
      if (payload.getType() == Payload.Type.FILE) {
        processFilePayload(payloadId);
      }
    }
  }

  /** Copies a stream from one location to another. */
  private static void copyStream(InputStream in, OutputStream out) throws IOException {
    try {
      byte[] buffer = new byte[1024];
      int read;
      while ((read = in.read(buffer)) != -1) {
        out.write(buffer, 0, read);
      }
      out.flush();
    } finally {
      in.close();
      out.close();
    }
  }
}

Luồng

Tải trọng luồng phù hợp khi bạn muốn gửi một lượng lớn dữ liệu được tạo ra một cách nhanh chóng, chẳng hạn như luồng âm thanh. Tạo tải trọng STREAM theo gọi Payload.fromStream(), truyền vào InputStream hoặc một ParcelFileDescriptor. Ví dụ:

URL url = new URL("https://developers.google.com/nearby/connections/android/exchange-data");
Payload streamPayload = Payload.fromStream(url.openStream());
Nearby.getConnectionsClient(context).sendPayload(toEndpointId, streamPayload);

Trên người nhận, hãy gọi payload.asStream().asInputStream() hoặc payload.asStream().asParcelFileDescriptor() khi gọi lại onPayloadTransferUpdate thành công:

static class ReceiveStreamPayloadCallback extends PayloadCallback {
  private final SimpleArrayMap<Long, Thread> backgroundThreads = new SimpleArrayMap<>();

  private static final long READ_STREAM_IN_BG_TIMEOUT = 5000;

  @Override
  public void onPayloadTransferUpdate(String endpointId, PayloadTransferUpdate update) {
    if (backgroundThreads.containsKey(update.getPayloadId())
        && update.getStatus() != PayloadTransferUpdate.Status.IN_PROGRESS) {
      backgroundThreads.get(update.getPayloadId()).interrupt();
    }
  }

  @Override
  public void onPayloadReceived(String endpointId, Payload payload) {
    if (payload.getType() == Payload.Type.STREAM) {
      // Read the available bytes in a while loop to free the stream pipe in time. Otherwise, the
      // bytes will block the pipe and slow down the throughput.
      Thread backgroundThread =
          new Thread() {
            @Override
            public void run() {
              InputStream inputStream = payload.asStream().asInputStream();
              long lastRead = SystemClock.elapsedRealtime();
              while (!Thread.interrupted()) {
                if ((SystemClock.elapsedRealtime() - lastRead) >= READ_STREAM_IN_BG_TIMEOUT) {
                  Log.e("MyApp", "Read data from stream but timed out.");
                  break;
                }

                try {
                  int availableBytes = inputStream.available();
                  if (availableBytes > 0) {
                    byte[] bytes = new byte[availableBytes];
                    if (inputStream.read(bytes) == availableBytes) {
                      lastRead = SystemClock.elapsedRealtime();
                      // Do something with is here...
                    }
                  } else {
                    // Sleep or just continue.
                  }
                } catch (IOException e) {
                  Log.e("MyApp", "Failed to read bytes from InputStream.", e);
                  break;
                } // try-catch
              } // while
            }
          };
      backgroundThread.start();
      backgroundThreads.put(payload.getId(), backgroundThread);
    }
  }
}

Sắp xếp theo thứ tự có nhiều tải trọng

Các tải trọng cùng loại được đảm bảo đến theo thứ tự chúng được gửi, nhưng không đảm bảo việc duy trì thứ tự trong các tải trọng của các loại khác nhau. Ví dụ: nếu một người gửi gửi một tải trọng FILE, theo sau là một tải trọng BYTE thì receiver có thể lấy tải trọng BYTE trước, sau đó là tải trọng FILE.

Thông tin cập nhật về tiến trình

Phương thức onPayloadTransferUpdate() cung cấp thông tin cập nhật về tiến trình của cả tải trọng đến và tải đi. Trong cả hai trường hợp, đây là cơ hội để hiển thị tiến trình chuyển cho người dùng, chẳng hạn như bằng thanh tiến trình. Đối với các tải trọng đến, bản cập nhật cũng cho biết thời điểm nhận được dữ liệu mới.

Mã mẫu sau đây minh hoạ một cách hiển thị tiến trình của tải trọng đến và đi thông qua thông báo:

class ReceiveWithProgressCallback extends PayloadCallback {
  private final SimpleArrayMap<Long, NotificationCompat.Builder> incomingPayloads =
      new SimpleArrayMap<>();
  private final SimpleArrayMap<Long, NotificationCompat.Builder> outgoingPayloads =
      new SimpleArrayMap<>();

  NotificationManager notificationManager =
      (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

  private void sendPayload(String endpointId, Payload payload) {
    if (payload.getType() == Payload.Type.BYTES) {
      // No need to track progress for bytes.
      return;
    }

    // Build and start showing the notification.
    NotificationCompat.Builder notification = buildNotification(payload, /*isIncoming=*/ false);
    notificationManager.notify((int) payload.getId(), notification.build());

    // Add it to the tracking list so we can update it.
    outgoingPayloads.put(payload.getId(), notification);
  }

  private NotificationCompat.Builder buildNotification(Payload payload, boolean isIncoming) {
    NotificationCompat.Builder notification =
        new NotificationCompat.Builder(context)
            .setContentTitle(isIncoming ? "Receiving..." : "Sending...");
    boolean indeterminate = false;
    if (payload.getType() == Payload.Type.STREAM) {
      // We can only show indeterminate progress for stream payloads.
      indeterminate = true;
    }
    notification.setProgress(100, 0, indeterminate);
    return notification;
  }

  @Override
  public void onPayloadReceived(String endpointId, Payload payload) {
    if (payload.getType() == Payload.Type.BYTES) {
      // No need to track progress for bytes.
      return;
    }

    // Build and start showing the notification.
    NotificationCompat.Builder notification = buildNotification(payload, true /*isIncoming*/);
    notificationManager.notify((int) payload.getId(), notification.build());

    // Add it to the tracking list so we can update it.
    incomingPayloads.put(payload.getId(), notification);
  }

  @Override
  public void onPayloadTransferUpdate(String endpointId, PayloadTransferUpdate update) {
    long payloadId = update.getPayloadId();
    NotificationCompat.Builder notification = null;
    if (incomingPayloads.containsKey(payloadId)) {
      notification = incomingPayloads.get(payloadId);
      if (update.getStatus() != PayloadTransferUpdate.Status.IN_PROGRESS) {
        // This is the last update, so we no longer need to keep track of this notification.
        incomingPayloads.remove(payloadId);
      }
    } else if (outgoingPayloads.containsKey(payloadId)) {
      notification = outgoingPayloads.get(payloadId);
      if (update.getStatus() != PayloadTransferUpdate.Status.IN_PROGRESS) {
        // This is the last update, so we no longer need to keep track of this notification.
        outgoingPayloads.remove(payloadId);
      }
    }

    if (notification == null) {
      return;
    }

    switch (update.getStatus()) {
      case PayloadTransferUpdate.Status.IN_PROGRESS:
        long size = update.getTotalBytes();
        if (size == -1) {
          // This is a stream payload, so we don't need to update anything at this point.
          return;
        }
        int percentTransferred =
            (int) (100.0 * (update.getBytesTransferred() / (double) update.getTotalBytes()));
        notification.setProgress(100, percentTransferred, /* indeterminate= */ false);
        break;
      case PayloadTransferUpdate.Status.SUCCESS:
        // SUCCESS always means that we transferred 100%.
        notification
            .setProgress(100, 100, /* indeterminate= */ false)
            .setContentText("Transfer complete!");
        break;
      case PayloadTransferUpdate.Status.FAILURE:
      case PayloadTransferUpdate.Status.CANCELED:
        notification.setProgress(0, 0, false).setContentText("Transfer failed");
        break;
      default:
        // Unknown status.
    }

    notificationManager.notify((int) payloadId, notification.build());
  }
}