נתוני הבורסה

לאחר יצירת חיבורים בין מכשירים, ניתן להעביר נתונים באמצעות שולחים ומקבלים אובייקטים מסוג Payload. א' Payload יכול לייצג מערך בייטים פשוט, כמו הודעת טקסט קצרה; קובץ, כמו תמונה או סרטון. או שידור, כמו שידור האודיו המיקרופון.

מטענים ייעודיים נשלחים באמצעות השיטה sendPayload() ומתקבלים ביישום של PayloadCallback שמועבר אל acceptConnection() כפי שמתואר בניהול חיבורים.

סוגים של מטענים ייעודיים

בייטים

מטענים ייעודיים (payloads) של בייטים הם הסוג הפשוט ביותר של מטענים ייעודיים. הם מתאימים לשליחה נתונים פשוטים, כמו הודעות או מטא-נתונים, עד גודל מקסימלי של Connections.MAX_BYTES_DATA_SIZE. דוגמה לשליחת מטען ייעודי (payload) של BYTES:

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

מקבלים מטען ייעודי (payload) של BYTES על ידי הטמעה של השיטה onPayloadReceived() של PayloadCallback שהעברת אל 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().
  }
}

בניגוד למטענים ייעודיים (payloads) של FILE ו-STREAM, מטענים ייעודיים (payloads) של BYTES נשלחים כמטענים נפרדים של מקטעים, כך שאין צורך להמתין לעדכון SUCCESS (למרות שזה עדיין נמסר, מיד לאחר השיחה אל onPayloadReceived()). במקום זאת, אפשר להתקשר בבטחה אל payload.asBytes() כדי לקבל את כל הנתונים של המטען הייעודי (payload) מיד לאחר הקריאה של onPayloadReceived().

קובץ

מטענים ייעודיים של קבצים נוצרים מקובץ שמאוחסן במכשיר המקומי, כמו של תמונה או סרטון. דוגמה פשוטה לשליחת מטען ייעודי (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);
}

כדאי יותר להשתמש ב-ParcelFileDescriptor כדי ליצור את המטען הייעודי (payload) של FILES אם הוא זמין, למשל מ-ContentResolver. הפעולה הזו תצמצם את העתקת הבייטים של הקובץ:

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

כשקובץ מתקבל, הוא נשמר בתיקיית ההורדות (DIRECTORY_DOWNLOADS) בחשבון של הנמען למכשיר עם שם גנרי וללא סיומת. לאחר שההעברה הושלמה, צוין על ידי קריאה אל onPayloadTransferUpdate() עם PayloadTransferUpdate.Status.SUCCESS, אפשר לאחזר את האובייקט File למשל אם האפליקציה מטרגטת ל-< מכשירי Q:

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

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

אם האפליקציה שלך מטרגטת מכשירי Q, אפשר להוסיף android:requestLegacyExternalStorage="true" ברכיב האפליקציה של המניפסט כדי להמשיך להשתמש בקוד הקודם. אחרת, עבור Q+ יהיה עליך לפעול בהתאם לכללים של Scoped Storage ולגשת לקובץ שהתקבל באמצעות ה-URI שהועבר מהשירות.

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

בדוגמה המורכבת יותר הבאה, ה-Intent ACTION_OPEN_DOCUMENT מבקש מהמשתמש לבחור קובץ והקובץ נשלח ביעילות כמטען ייעודי (payload) באמצעות ParcelFileDescriptor. שם הקובץ נשלח גם כמטען ייעודי (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);
  }
}

מכיוון ששם הקובץ נשלח כמטען ייעודי (payload), המקבל שלנו יכול להעביר את הקובץ או לשנות את שמו כדי שיהיה לו סיומת מתאימה:

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

מקור נתונים

מטענים ייעודיים (payloads) של סטרימינג מתאימים כשרוצים לשלוח כמויות גדולות של נתונים שנוצר תוך כדי תנועה, למשל שידור אודיו. יצירת מטען ייעודי (payload) של STREAM באמצעות קוראת ל-Payload.fromStream(), מועברת באמצעות InputStream או ParcelFileDescriptor לדוגמה:

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

אצל הנמען, צריך להתקשר אל payload.asStream().asInputStream() או אל payload.asStream().asParcelFileDescriptor() בהתקשרות חוזרת (callback) של onPayloadTransferUpdate שהתבצעה בהצלחה:

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

הזמנה עם מטענים ייעודיים (payloads) מרובים

מובטח שמטענים ייעודיים (payloads) מאותו סוג יגיעו בסדר שבו הם נשלחו, אבל אין ערובה לשמירת הסדר בין מטענים ייעודיים (payloads) של מסוגים שונים. לדוגמה, אם שולח שולח מטען ייעודי (payload) של FILE ואחריו המטען הייעודי (payload) של BYTE, המקבל יכול לקבל קודם את המטען הייעודי (payload) של BYTE, ואחריו מטען ייעודי (payload) של FILE.

עדכונים לגבי ההתקדמות

ה-method onPayloadTransferUpdate() מספקת עדכונים לגבי ההתקדמות של מטענים ייעודיים (payloads) נכנסים ויוצאים. בשני המקרים, זוהי הזדמנות להציג למשתמש את התקדמות ההעברה, למשל באמצעות סרגל התקדמות. עבור מטענים ייעודיים (payloads) נכנסים, העדכונים מציינים גם מתי התקבלו נתונים חדשים.

הקוד לדוגמה הבא מדגים דרך אחת להציג את ההתקדמות מטענים ייעודיים (payloads) נכנסים ויוצאים באמצעות התראות:

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