ข้อมูลตลาดหลักทรัพย์

เมื่อสร้างการเชื่อมต่อระหว่างอุปกรณ์แล้ว คุณจะแลกเปลี่ยนข้อมูลโดยการส่งและรับออบเจ็กต์ Payload ได้ Payload อาจแสดงอาร์เรย์ไบต์ง่ายๆ เช่น ข้อความสั้น ไฟล์ต่างๆ เช่น รูปภาพหรือวิดีโอ หรือสตรีม เช่น สตรีมเสียงจากไมโครโฟนของอุปกรณ์

เพย์โหลดจะส่งโดยใช้เมธอด sendPayload() และได้รับการติดตั้งใช้งาน PayloadCallback ที่ส่งไปยัง acceptConnection() ตามที่อธิบายไว้ในจัดการการเชื่อมต่อ

ประเภทเพย์โหลด

ไบต์

เพย์โหลดแบบไบต์เป็นประเภทที่เรียบง่ายที่สุด ซึ่งเหมาะสําหรับการส่งข้อมูลทั่วไป เช่น ข้อความหรือข้อมูลเมตาที่ขนาดสูงสุด Connections.MAX_BYTES_DATA_SIZE นี่คือตัวอย่างการส่งเพย์โหลด BYTES

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

รับเปย์โหลด 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().
  }
}

เพย์โหลด BYTES จะต่างจากเพย์โหลด BYTES และ STREAM เพย์โหลด ดังนั้นจึงไม่จําเป็นต้องรอการอัปเดต SUCCESS (แต่จะยังคงนําส่งอยู่ทันทีหลังจากที่เรียกใช้ onPayloadReceived())

ไฟล์

เพย์โหลดของไฟล์สร้างขึ้นจากไฟล์ที่เก็บไว้ในอุปกรณ์ เช่น ไฟล์ภาพหรือวิดีโอ ต่อไปนี้เป็นตัวอย่างการส่งเพย์โหลด 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 เพื่อสร้างเพย์โหลด FILES จะมีประสิทธิภาพมากขึ้นหากมี เช่น จาก ContentResolver ซึ่งจะลดการคัดลอกไบต์ของไฟล์ ดังนี้

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

เมื่อได้รับไฟล์ ระบบจะบันทึกไว้ในโฟลเดอร์ดาวน์โหลด (DIRECTORY_DOWNLOADS) ในอุปกรณ์ของผู้รับที่มีชื่อทั่วไปโดยไม่มีนามสกุล เมื่อการโอนเสร็จสมบูรณ์ ให้ระบุด้วยการเรียก onPayloadTransferUpdate() ด้วย PayloadTransferUpdate.Status.SUCCESS เพื่อให้เรียกออบเจ็กต์ File ได้ เช่น หากแอปกําหนดเป้าหมาย < Q device:

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

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

หากแอปกําหนดเป้าหมายอุปกรณ์ Q คุณเพิ่ม android:requestLegacyExternalStorage="true" ในองค์ประกอบแอปพลิเคชันของไฟล์ Manifest เพื่อใช้โค้ดก่อนหน้าต่อได้ มิฉะนั้นสําหรับกฎ Q+ คุณจะต้องปฏิบัติตามกฎ Scoped Storage และเข้าถึงไฟล์ที่ได้รับโดยใช้ URL ที่ส่งจากบริการ

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

ในตัวอย่างที่ซับซ้อนยิ่งขึ้นต่อไปนี้ ความตั้งใจของ ACTION_OPEN_DOCUMENT จะแจ้งให้ผู้ใช้เลือกไฟล์และส่งไฟล์เป็นเพย์โหลดโดยใช้ ParcelFileDescriptor นอกจากนี้ ระบบจะส่งชื่อไฟล์เป็นเพย์โหลด 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);
  }
}

เนื่องจากชื่อไฟล์จะส่งไปยังเพย์โหลด ผู้รับของเราสามารถย้ายหรือเปลี่ยนชื่อไฟล์เพื่อให้มีนามสกุลที่เหมาะสมได้ ดังนี้

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 โดยการเรียกใช้ 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() ในการเรียกกลับ 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);
    }
  }
}

การสั่งซื้อที่มีเพย์โหลดหลายรายการ

รับประกันได้ว่าเพย์โหลดประเภทเดียวกันจะพร้อมใช้งานตามลําดับที่ส่ง เช่น หากผู้ส่งส่งเพย์โหลด FILE ตามด้วยเพย์โหลด BYTE ผู้รับอาจได้รับเพย์โหลด BYTE ก่อน ตามด้วยเพย์โหลด FILE

การอัปเดตความคืบหน้า

เมธอด onPayloadTransferUpdate() จะแสดงข้อมูลอัปเดตเกี่ยวกับเพย์โหลดทั้งขาเข้าและขาออก ในทั้ง 2 กรณี มีโอกาสในการแสดงความคืบหน้าในการโอนให้กับผู้ใช้ เช่น แถบความคืบหน้า สําหรับเพย์โหลดที่เข้ามาใหม่ การอัปเดตจะระบุด้วยว่าเราได้รับข้อมูลใหม่เมื่อใด

ตัวอย่างโค้ดต่อไปนี้แสดงวิธีแสดงความคืบหน้าของเพย์โหลดขาเข้าและขาออกผ่านการแจ้งเตือน

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