裝置之間建立連線後,即可交換資料
傳送及接收 Payload
物件。A 罩杯
Payload
可以代表簡單的位元組陣列,例如簡短簡訊;檔案 (例如
相片或影片或串流,例如裝置的音訊串流
麥克風。
酬載是透過 sendPayload()
方法傳送,並會接收傳遞至 acceptConnection()
的 PayloadCallback
實作,如管理連線中所述。
酬載類型
位元組
位元組酬載是最簡單的酬載類型。適合傳送
簡單的資料 (例如訊息或中繼資料),大小上限為 Connections.MAX_BYTES_DATA_SIZE
。以下是傳送 BYTES
酬載的範例:
Payload bytesPayload = Payload.fromBytes(new byte[] {0xa, 0xb, 0xc, 0xd}); Nearby.getConnectionsClient(context).sendPayload(toEndpointId, bytesPayload);
實作您傳遞至 acceptConnection()
的 PayloadCallback
的 onPayloadReceived()
方法,藉此接收 BYTES
酬載。
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(). } }
與 FILE
和 STREAM
酬載不同,BYTES
酬載將做為單一形式傳送
因此無須等待 SUCCESS
更新 (不過
仍會在呼叫 onPayloadReceived()
後立即傳送)。
您可以改為安全地呼叫 payload.asBytes()
來取得
有效酬載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 裝置:
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); }
在下列較複雜的範例中,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);
在接收端上,於成功的 onPayloadTransferUpdate
回呼中呼叫 payload.asStream().asInputStream()
或 payload.asStream().asParcelFileDescriptor()
:
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()
方法會提供有關傳入和傳出酬載的進度更新。無論是何種情況,系統都有機會向使用者顯示轉移的進度,例如透過進度列顯示。對於傳入的酬載,更新也會指出已收到新資料的時間。
以下程式碼範例示範了顯示 透過通知接收的傳入和傳出酬載:
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()); } }