بيانات Exchange

وبمجرد إنشاء الاتصالات بين الأجهزة، يمكنك تبادل البيانات عن طريق إرسال عناصر 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().
  }
}

على عكس حمولات البيانات 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);

بالنسبة إلى المستلِم، يُرجى الاتصال بالرقم 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());
  }
}