Dados da troca

Depois que as conexões forem estabelecidas entre dispositivos, você poderá trocar dados enviando e recebendo objetos Payload. Um Payload pode representar uma matriz de bytes simples, como uma mensagem de texto curta, um arquivo, como uma foto ou vídeo, ou um stream, como o stream de áudio do microfone do dispositivo.

Os payloads são enviados usando o método sendPayload() e são recebidos em uma implementação de PayloadCallback que é transmitida para acceptConnection() conforme descrito em Gerenciar conexões.

Tipos de payload

Bytes

Os payloads de byte são o tipo mais simples. Elas são adequadas para enviar dados simples, como mensagens ou metadados, até um tamanho máximo de Connections.MAX_BYTES_DATA_SIZE. Veja um exemplo de envio de um payload BYTES:

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

Receba um payload BYTES implementando o método onPayloadReceived() do PayloadCallback transmitido para 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().
  }
}

Ao contrário dos payloads FILE e STREAM, os payloads de BYTES são enviados como um único bloco, então não é necessário aguardar a atualização de SUCCESS, embora ainda seja entregue, logo após a chamada para onPayloadReceived(). Em vez disso, você pode chamar payload.asBytes() com segurança para receber os dados completos do payload assim que onPayloadReceived() for chamado.

Arquivo

Os payloads são criados de um arquivo armazenado no dispositivo local, como um arquivo de fotos ou vídeos. Veja um exemplo simples de envio de um payload 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);
}

Pode ser mais eficiente usar um ParcelFileDescriptor para criar o payload FILES, se houver um disponível, por exemplo, de uma ContentResolver. Isso minimiza a cópia dos bytes do arquivo:

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

Quando um arquivo é recebido, ele é salvo na pasta "Downloads" (DIRECTORY_DOWNLOADS) no dispositivo do destinatário com um nome genérico e sem extensão. Quando a transferência for concluída, indicada por uma chamada para onPayloadTransferUpdate() com PayloadTransferUpdate.Status.SUCCESS, você poderá recuperar o objeto File desta forma se o app for destinado a dispositivos Q:

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

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

Caso seu app seja destinado a dispositivos Q, adicione android:requestLegacyExternalStorage="true" no elemento do app do manifesto para continuar usando o código anterior. Caso contrário, você vai seguir as regras do Scoped Storage e acessar o arquivo recebido usando o URI transmitido do serviço.

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

No exemplo mais complexo a seguir, a intent ACTION_OPEN_DOCUMENT solicita que o usuário escolha um arquivo, que é enviado com eficiência como um payload usando ParcelFileDescriptor. O nome do arquivo também é enviado como um payload 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);
  }
}

Como o nome do arquivo foi enviado como um payload, nosso receptor pode mover ou renomear o arquivo para que ele tenha uma extensão apropriada:

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

Os payloads de stream são adequados quando você quer enviar grandes quantidades de dados que são gerados instantaneamente, como um stream de áudio. Crie um payload STREAM chamando Payload.fromStream(), transmitindo um InputStream ou um ParcelFileDescriptor. Exemplo:

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

No destinatário, chame payload.asStream().asInputStream() ou payload.asStream().asParcelFileDescriptor() em um callback onPayloadTransferUpdate bem-sucedido:

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

Ordenação com vários payloads

Os payloads do mesmo tipo têm garantia de chegar na ordem em que foram enviados, mas não há garantia de preservar a ordem entre os payloads de tipos diferentes. Por exemplo, se um remetente enviar um payload FILE seguido por um payload BYTE, o destinatário vai poder receber o payload BYTE primeiro, seguido pelo payload FILE.

Atualizações de progresso

O método onPayloadTransferUpdate() fornece atualizações sobre o progresso dos payloads de entrada e saída. Em ambos os casos, isso é uma oportunidade de exibir o progresso da transferência para o usuário, por exemplo, com uma barra de progresso. Para os payloads de entrada, as atualizações também indicam quando novos dados são recebidos.

Na amostra de código a seguir, demonstramos uma maneira de exibir o progresso dos payloads de entrada e saída por meio de notificações:

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