Once connections are established between devices, you can exchange data by
sending and receiving [`Payload`](https://developers.google.com/android/reference/com/google/android/gms/nearby/connection/Payload) objects. A
`Payload` can represent a simple byte array, such as a short text message; a file, such as
a photo or video; or a stream, such as the audio stream from the device's
microphone.

Payloads are sent using the [`sendPayload()`](https://developers.google.com/android/reference/com/google/android/gms/nearby/connection/ConnectionsClient#sendPayload%28java.lang.String,%20com.google.android.gms.nearby.connection.Payload%29) method, and received in an implementation of [`PayloadCallback`](https://developers.google.com/android/reference/com/google/android/gms/nearby/connection/PayloadCallback) that's passed to [`acceptConnection()`](https://developers.google.com/android/reference/com/google/android/gms/nearby/connection/ConnectionsClient#acceptConnection%28java.lang.String,%20com.google.android.gms.nearby.connection.PayloadCallback%29) as described in [Manage Connections](https://developers.google.com/nearby/connections/android/manage-connections).

> [!NOTE]
> **Note:** `PayloadCallback.onPayloadReceived()` is called when the first byte of a Payload is received; it does *not* indicate that the entire Payload has been received. The completion of the transfer is indicated when `PayloadCallback.onPayloadTransferUpdate()` is called with a status of `PayloadTransferUpdate.Status.SUCCESS` (or `PayloadTransferUpdate.Status.ERROR` in the case of an error). An exception is for payloads of type `BYTES`; read more in the [Bytes](https://developers.google.com/nearby/connections/android/exchange-data#bytes) section.

## Types of payloads

### Bytes

Byte payloads are the simplest type of payloads. They are suitable for sending
simple data like messages or metadata up to a maximum size of [`Connections.MAX_BYTES_DATA_SIZE`](https://developers.google.com/android/reference/com/google/android/gms/nearby/connection/ConnectionsClient#MAX_BYTES_DATA_SIZE). Here's an example of sending a `BYTES` payload:

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

Receive a `BYTES` payload by implementing the `onPayloadReceived()` method of the `PayloadCallback` you passed to `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().
  }
}
```

Unlike `FILE` and `STREAM` payloads, `BYTES` payloads are sent as a single
chunk, so there is no need to wait for the `SUCCESS` update (although it will
still be delivered, immediately after the call to `onPayloadReceived()`).
Instead, you can safely call `payload.asBytes()` to get the full data of the
payload as soon as `onPayloadReceived()` is called.

### File

File payloads are created from a file stored on the local device, such as a
photo or video file. Here's a simple example of sending a `FILE` payload:

```
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);
}
```

It can be more efficient to use a [`ParcelFileDescriptor`](https://developer.android.com/reference/android/os/ParcelFileDescriptor) to create the `FILES` payload if one is available, for example from a `ContentResolver`. This minimizes copying of the file's bytes:

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

When a file is received, it is saved in the Downloads folder ([`DIRECTORY_DOWNLOADS`](https://developer.android.com/reference/android/os/Environment#DIRECTORY_DOWNLOADS)) on the recipient's
device with a generic name and no extension. Once the transfer has completed,
indicated by a call to `onPayloadTransferUpdate()` with
`PayloadTransferUpdate.Status.SUCCESS`, you can retrieve the `File` object like so if your app is targeting \< Q devices:

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

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

If your app is targeting Q devices, you can add android:requestLegacyExternalStorage="true" in the application element of your manifest to continue using the previous code.
Otherwise, for Q+ you will need to obey the [`Scoped Storage`](https://developer.android.com/preview/privacy/scoped-storage) rules and access the received file using the uri passed from the service.

```
// 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);
}
```

In the following more complex example, the [`ACTION_OPEN_DOCUMENT`](https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT) intent prompts the user to choose a file and the file is efficiently sent as a payload using `ParcelFileDescriptor`. The file name is also sent as a `BYTES` payload.

```
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);
  }
}
```

Since the filename was sent as a payload, our receiver can move or rename the file so that it has an appropriate extension:

```
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();
    }
  }
}
```

### Stream

Stream payloads are suitable when you want to send large amounts of data that is
generated on the fly, such as an audio stream. Create a `STREAM` Payload by
calling `Payload.fromStream()`, passing in either an `InputStream` or a
`ParcelFileDescriptor`. For example:

```
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);
```

On the recipient, call `payload.asStream().asInputStream()` or `payload.asStream().asParcelFileDescriptor()` in a successful `onPayloadTransferUpdate` callback:

```
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);
    }
  }
}
```

## Ordering with multiple payloads

Payloads of the same type are guaranteed to arrive in the order they were sent,
but there is no guarantee of preserving the ordering amongst payloads of
different types. For example, if a sender sends a `FILE` payload followed by a
`BYTES` payload, the receiver could get the `BYTES` payload first, followed by
the `FILE` payload.

## Progress updates

The `onPayloadTransferUpdate()` method provides updates about the progress of both incoming and outgoing payloads. In both cases, this in an opportunity to display the progress of the transfer to the user, such as with a progress bar. For incoming payloads, updates also indicate when new data has been received.

The following sample code demonstrates one way to display the progress of
incoming and outgoing payloads via notifications:

```
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);
      }
      case PayloadTransferUpdate.Status.SUCCESS ->
          // SUCCESS always means that we transferred 100%.
          notification
              .setProgress(100, 100, /* indeterminate= */ false)
              .setContentText("Transfer complete!");
      case PayloadTransferUpdate.Status.FAILURE, PayloadTransferUpdate.Status.CANCELED ->
          notification.setProgress(0, 0, false).setContentText("Transfer failed");
      default -> {
        // Unknown status.
      }
    }

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