Po nawiązaniu połączenia między urządzeniami można wymieniać dane przez
wysyłania i odbierania obiektów Payload
. O
Payload
może reprezentować prostą tablicę bajtów, na przykład krótki komunikat tekstowy. plik, taki jak
zdjęcie lub film; lub strumienia, np. ze strumienia audio z
mikrofon.
Ładunki są wysyłane przy użyciu metody sendPayload()
i odbierane w implementacji PayloadCallback
, która jest przekazywana do acceptConnection()
zgodnie z opisem w artykule Zarządzanie połączeniami.
Typy ładunków
Bajty
Ładunki bajtów to najprostszy typ ładunków. Nadają się do wysyłania
takich jak wiadomości czy metadane, których rozmiar nie przekracza Connections.MAX_BYTES_DATA_SIZE
. Oto przykład wysyłania ładunku BYTES
:
Payload bytesPayload = Payload.fromBytes(new byte[] {0xa, 0xb, 0xc, 0xd}); Nearby.getConnectionsClient(context).sendPayload(toEndpointId, bytesPayload);
Odbierz ładunek BYTES
, implementując metodę onPayloadReceived()
obiektu PayloadCallback
przekazanego do 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(). } }
W przeciwieństwie do ładunków FILE
i STREAM
ładunki typu BYTES
są wysyłane jako pojedynczy element
nie trzeba czekać na aktualizację SUCCESS
(chociaż
wciąż dostarczane są bezpośrednio po połączeniu z numerem onPayloadReceived()
).
Zamiast tego można bezpiecznie zadzwonić pod numer payload.asBytes()
, aby uzyskać pełne dane
ładunek natychmiast po wywołaniu onPayloadReceived()
.
Plik
Ładunki plików są tworzone z pliku zapisanego na urządzeniu lokalnym, np.
plik zdjęcia lub filmu. Oto prosty przykład wysłania ładunku 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); }
Efektywniejsze może być użycie elementu ParcelFileDescriptor
do utworzenia ładunku FILES
, jeśli jest on dostępny, na przykład z ContentResolver
. Pozwala to zminimalizować kopiowanie bajtów pliku:
ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r"); filePayload = Payload.fromFile(pfd);
Po odebraniu pliku jest on zapisywany w folderze Pobrane (DIRECTORY_DOWNLOADS
) na koncie adresata
o ogólnej nazwie i bez rozszerzenia. Po zakończeniu przenoszenia
wskazywane przez połączenie z numerem onPayloadTransferUpdate()
z
PayloadTransferUpdate.Status.SUCCESS
, możesz pobrać obiekt File
w ten sposób, jeśli Twoja aplikacja jest kierowana na < Urządzenia Q:
File payloadFile = filePayload.asFile().asJavaFile(); // Rename the file. payloadFile.renameTo(new File(payloadFile.getParentFile(), filename));
Jeśli Twoja aplikacja jest kierowana na urządzenia Q, możesz dodać atrybut android:requestLegacyExternalStorage="true" w elemencie aplikacji pliku manifestu, aby nadal używać poprzedniego kodu.
W przypadku Q+ musisz przestrzegać reguł Scoped Storage
i uzyskiwać dostęp do otrzymanego pliku za pomocą identyfikatora URI przekazanego przez usługę.
// 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); }
W bardziej złożonym przykładzie intencja ACTION_OPEN_DOCUMENT
prosi użytkownika o wybranie pliku, który jest skutecznie wysyłany jako ładunek za pomocą ParcelFileDescriptor
. Nazwa pliku jest też wysyłana jako ładunek 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); } }
Nazwa pliku została wysłana jako ładunek, więc nasz odbiorca może przenieść plik lub zmienić jego nazwę, tak aby miał odpowiednie rozszerzenie:
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(); } } }
Strumień
Ładunki strumieniowe są odpowiednie, gdy chcesz wysyłać duże ilości danych
generowane na bieżąco, np. strumień audio. Utwórz ładunek typu STREAM
przez
Wywołuję Payload.fromStream()
, podając wartość InputStream
lub
ParcelFileDescriptor
Na przykład:
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);
Odbiorca zadzwoń pod numer payload.asStream().asInputStream()
lub payload.asStream().asParcelFileDescriptor()
w pomyślnym wywołaniu zwrotnym 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); } } }
Zamawianie z wieloma ładunkami
Gwarantujemy, że ładunki tego samego typu będą dostarczane w kolejności, w jakiej zostały wysłane,
ale nie ma gwarancji zachowania kolejności między ładunkami
różnych typów. Jeśli na przykład nadawca wysyła ładunek FILE
, po którym następuje
BYTE
, odbiorca może jako pierwszy pobrać ładunek BYTE
, a po nim
FILE
ładunek.
Informacje o postępach
Metoda onPayloadTransferUpdate()
dostarcza informacje o postępie zarówno ładunków przychodzących, jak i wychodzących. W obu przypadkach umożliwia to wyświetlenie użytkownikowi postępu przenoszenia, na przykład na pasku postępu. W przypadku ładunków przychodzących aktualizacje wskazują też, kiedy otrzymano nowe dane.
Poniższy przykładowy kod przedstawia jeden sposób wyświetlania postępu ładunki przychodzące i wychodzące za pomocą powiadomień:
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()); } }