پس از برقراری ارتباط بین دستگاهها، میتوانید با ارسال و دریافت اشیاء Payload
تبادل داده کنید. یک Payload
می تواند یک آرایه بایت ساده، مانند یک پیام متنی کوتاه را نشان دهد. یک فایل، مانند عکس یا ویدیو؛ یا یک جریان، مانند پخش جریانی صدا از میکروفون دستگاه.
Payloadها با استفاده از روش sendPayload()
ارسال میشوند و در پیادهسازی PayloadCallback
دریافت میشوند که به acceptConnection()
ارسال میشود، همانطور که در Manage Connections توضیح داده شده است.
انواع محموله
بایت ها
بارهای بایتی ساده ترین نوع محموله ها هستند. آنها برای ارسال داده های ساده مانند پیام ها یا ابرداده ها تا حداکثر اندازه Connections.MAX_BYTES_DATA_SIZE
مناسب هستند.MAX_BYTES_DATA_SIZE . در اینجا نمونه ای از ارسال بار BYTES
آورده شده است:
Payload bytesPayload = Payload.fromBytes(new byte[] {0xa, 0xb, 0xc, 0xd}); Nearby.getConnectionsClient(context).sendPayload(toEndpointId, bytesPayload);
با پیاده سازی متد onPayloadReceived()
از PayloadCallback
که به acceptConnection()
ارسال کردید، یک 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()
اطلاعات کامل payload را دریافت کنید.
فایل
بارهای فایل از یک فایل ذخیره شده در دستگاه محلی مانند یک فایل عکس یا ویدیو ایجاد می شود. در اینجا یک مثال ساده از ارسال یک 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
برای ایجاد محموله FILES
در صورت در دسترس بودن، برای مثال از ContentResolver
می تواند کارآمدتر باشد. این کار کپی بایت های فایل را به حداقل می رساند:
ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r"); filePayload = Payload.fromFile(pfd);
وقتی فایلی دریافت میشود، در پوشه دانلودها ( DIRECTORY_DOWNLOADS
) در دستگاه گیرنده با نام عمومی و بدون پسوند ذخیره میشود. هنگامی که انتقال کامل شد، با فراخوانی به onPayloadTransferUpdate()
با PayloadTransferUpdate.Status.SUCCESS
نشان داده شده است، اگر برنامه شما < دستگاه های Q را هدف قرار می دهد، می توانید شی File
را بازیابی کنید:
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(); } } }
جریان
بارهای جریان زمانی مناسب هستند که می خواهید مقادیر زیادی از داده هایی را که در جریان تولید می شوند، مانند جریان صوتی ارسال کنید. با فراخوانی Payload.fromStream()
و ارسال یک InputStream
یا ParcelFileDescriptor
یک STREAM
Payload ایجاد کنید. به عنوان مثال:
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()
بهروزرسانیهایی را درباره پیشرفت بارهای ورودی و خروجی ارائه میکند. در هر دو مورد، این فرصتی برای نمایش پیشرفت انتقال به کاربر است، مانند نوار پیشرفت. برای بارهای دریافتی، بهروزرسانیها همچنین زمان دریافت دادههای جدید را نشان میدهند.
کد نمونه زیر یک راه برای نمایش پیشرفت بارهای ورودی و خروجی از طریق اعلان ها را نشان می دهد:
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()); } }