简介

近距离设备间数据传输(Nearby Connection)提供附近多台设备间组建一个本地网络进行数据传输的能力,直接收发数据,无需互联网中转。用于文件分享、多人面对面游戏、手机数据迁移等场景。通过示例工程,您可以体验到:
通过Nearby Connection实现手机之间的文件传输功能。

您将建立什么

在本篇codelab中,您将通过Nearby Connection方便、快捷的让两台智能手机建立连接,进而完成两台手机的文件传输。

您将会学到什么

硬件要求

软件要求

若您需要正式发布集成HUAWEI Nearby Service的应用,请参见《HUAWEI HMS Core集成准备》中详细说明来完成接入准备。若您使用本CodeLab Demo验证时,直接使用Sample Code中的设置,可以忽略此步骤。

  1. 登录"华为开发者联盟"后,点击并进入右上角的"管理中心"。先点击"我的API",然后选择您要开通服务的项目,最后点击"申请新的HMS API服务",进入HMS API库的详情页面。
  2. 进入HMS API库的详情页面之后,点击"近距离通信服务",进入近距离通信服务的页面。
  3. 进入近距离通信服务的页面之后,点击"启用"开关,启用近距离通信服务。
  4. 点击"启用"后,需要签署同意《华为Nearby服务使用协议》和《华为APIs使用协议》才可以成功启用近距离通信服务。如图所示(该示例仅做参考,请以实际为准)
  5. 启用成功之后,在"HMS API服务 > 我的API"中可以看到当前项目下的API服务列表。

在上一小节中,您已经做好了Demo开发前的准备工作。HUAWEI Nearby Service SDK主要分为广播扫描模块、建立连接模块、数据传输模块。
本小节您将尝试自己编写一个Demo,学会如何使用HUAWEI Nearby Service SDK的主要接口进行文件传输。

构建工程

  1. 复制NearbyAgent.java到您新建的Android工程"src\main\java\com\example\myapplication"目录下。
  2. 复制目录res下的所有资源文件到您新建Android工程的"src\main\res"对应目录下。

配置HMS Core SDK的Maven仓地址

Android Studio的代码库配置在Gradle 插件7.0以下版本、7.0版本和7.1及以上版本有所不同。请根据您当前的Gradle 插件版本,选择对应的配置过程。

7.0以下版本

7.0版本

7.1及以上版本

添加编译依赖

  1. 打开应用级别的build.gradle文件。
  2. 添加Java版本配置。
    compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 }
  3. 将编译依赖项添加到"dependencies"中。
    dependencies { implementation 'com.huawei.hms:nearby:{version}' implementation 'com.huawei.hms:scan:1.2.3.300' implementation 'com.github.hedzr:android-file-chooser:v1.2.0-final' }

    如图所示:

修改AndroidManifest.xml

Nearby Connection开发前,需要添加特定的权限到"AndroidManifest.xml"。

权限添加位置与标签"application"同级。代码如下:

<!--查看Wi-Fi网络状态权限--> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"" /> <!--修改Wi-Fi网络状态权限--> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"" /> <!--获取设备的位置权限--> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"" /> <!--获取设备的精确位置权限--> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"" /> <!--读存储权限--> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"" /> <!--写存储权限--> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"" /> <!--蓝牙权限--> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN"" /> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"" />

ACCESS_FINE_LOCATION、WRITE_EXTERNAL_STORAGE和READ_EXTERNAL_STORAGE是危险的系统权限,在NearbyAgent.java中动态申请了这些权限。具体参见ActivityCompat.requestPermissions((Activity) context, REQUIRED_PERMISSIONS, REQUEST_CODE_REQUIRED_PERMISSIONS)。如果权限不足,HUAWEI Nearby Service将会因为缺少必需权限无法开启广播或者扫描。
从Android 10版本开始,应用默认只能访问外部存储的私有目录。若是需要访问私有目录以外的其他储存则会抛出FileNotFoundException异常。因此您需要在AndroidManifest.xml的application节点设置requestLegacyExternalStorage="true"。代码如下:

同步工程

点击"File > Sync Project with Gradle Files"同步工程。

如下图出现successful,即代表同步成功。

删除额外功能

本Codelab聚焦HUAWEI Nearby Service的文件传输功能,需要删除NearbyAgent.java中生成二维码和扫描二维码的相关代码。

  1. 删除sendFilesInner()中扫码功能代码。
  2. 使用如下接收文件代码替换receiveFile()中扫码功能代码。
    ScanOption.Builder scanBuilder = new ScanOption.Builder(); scanBuilder.setPoli***.POLICY_P2P***OLICY_P2P); mDiscoveryEngine.startScan(mFileServiceId, mDiscCb, scanBuilder.build()); Log.d(TAG, "Start Scan."); mDescText.setText("Connecting to " + mScanInfo + "...");

  3. 删除扫码结果监听类中onFound()关于扫码功能认证的代码:

修改MainActivity类

  1. 导入代码所需的依赖。
    import androidx.appcompat.app.AppCompatActivity; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.Toast; import com.obsez.android.lib.filechooser.ChooserDialog; import java.io.File; import java.util.ArrayList; import java.util.List;
  2. 修改onCreate(),为操作界面添加"Send File"和"Receive File"两个按钮。
    public class MainActivity extends AppCompatActivity { private static final int FILE_SELECT_CODE = 0; private Button sendBtn; private Button recvBtn; private NearbyAgent nearbyAgent; private List<File> files = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_file_main); nearbyAgent = new NearbyAgent(this); sendBtn = (Button)findViewById(R.id.sendBtn); recvBtn = (Button)findViewById(R.id.recvBtn); }
  3. 在"onCreate"方法中为"Send File"添加点击按钮。按钮的功能为打开文件选择器。
    sendBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showFileChooser(); } });

    在"MainActivity"中为文件选择器添加具体代码。

    private void showFileChooser() { files.clear(); new ChooserDialog(MainActivity.this) .enableMultiple(false) .withChosenListener(new ChooserDialog.Result() { @Override public void onChoosePath(String path, File pathFile) { // call nearby agent nearbyAgent.sendFile(pathFile); return; } }) // to handle the back key pressed or clicked outside the dialog: .withOnCancelListener(new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { dialog.cancel(); // MUST have } }) .build() .show(); }
  4. 在"MainActivity"中新增onActivityResult()方法,为结果添加回调接口。调用NearbyAgent.sendFile()接口,发送文件。
    @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case FILE_SELECT_CODE: if (resultCode == RESULT_OK) { // Get the Uri of the selected file Uri uri = data.getData(); nearbyAgent.sendFile(new File(uri.getPath())); } break; case NearbyAgent.REQUEST_CODE_SCAN_ONE: nearbyAgent.onScanResult(data); break; default: break; } super.onActivityResult(requestCode, resultCode, data); }
  5. 在"onCreate"方法中为"Receive File"添加点击按钮。点击调用NearbyAgent.receiveFile()接口。
    recvBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { nearbyAgent.receiveFile(); } });

    包含两个按钮的文件传输App就已经开发完成。完成编译后,生成APK,安装到两台智能手机上运行。

NearbyAgent.java内部实现详解

首先当您点击发送文件按钮时,会调用sendFile()方法来实现广播功能。

public void sendFile(File file) { init(); mFiles.add(file); BroadcastOption.Builder advBuilder = new BroadcastOption.Builder(); advBuilder.setPolicy(Policy.POLICY_P2P); mDiscoveryEngine.startBroadcasting(mEndpointName, mFileServiceId, mConnCbSender, advBuilder.build()); Log.d(TAG, "Start Broadcasting."); }

当您点击接收文件按钮时,会调用receiveFile()来开启扫描功能以发现附近的设备。

public void receiveFile() { init(); ScanOption.Builder scanBuilder = new ScanOption.Builder(); scanBuilder.setPolicy(Policy.POLICY_P2P); mDiscoveryEngine.startScan(mFileServiceId, mDiscCb, scanBuilder.build()); Log.d(TAG, "Start Scan."); mDescText.setText("Connecting to " + mScanInfo + "..."); }

接收端和发送端分别通过调用上面两个接口就可以完成文件传输功能。而文件传输的具体流程可以划分为4个阶段:

  1. 相互发现。
    a) 发送端调用startBroadcasting()启动广播。
    private void sendFilesInner() { /* start broadcast */ BroadcastOption.Builder advBuilder = new BroadcastOption.Builder(); advBuilder.setPolicy(Policy.POLICY_P2P); mDiscoveryEngine.startBroadcasting(mEndpointName, mFileServiceId, mConnCbSender, advBuilder.build()); Log.d(TAG, "Start Broadcasting."); }
    b) 接收端调用startScan()启动扫描以发现附近的设备。
    public void receiveFile() { init(); ScanOption.Builder scanBuilder = new ScanOption.Builder(); scanBuilder.setPolicy(Policy.POLICY_P2P); mDiscoveryEngine.startScan(mFileServiceId, mDiscCb, scanBuilder.build()); Log.d(TAG, "Start Scan."); mDescText.setText("Connecting to " + mScanInfo + "..."); }
    c) 接收端由onFound()方法通知扫描结果,调用requestConnect()向发送端发起连接请求。
    private ScanEndpointCallback mDiscCb = new ScanEndpointCallback() { @Override public void onFound(String endpointId, ScanEndpointInfo discoveryEndpointInfo) { if (discoveryEndpointInfo.getName().equals(mScanInfo)) { Log.d(TAG, "Found endpoint:" + discoveryEndpointInfo.getName() + ". Connecting."); mDiscoveryEngine.requestConnect(mEndpointName, endpointId, mConnCbRcver); } } @Override public void onLost(String endpointId) { Log.d(TAG, "Lost endpoint."); } };
  2. 建立连接。
    a) 发送端由onEstablish()通知连接请求后,调用acceptConnect()接受连接。接收端此时也会收到onEstablish(),同样调用acceptConnect()。
    发送端:
    private ConnectCallback mConnCbSender = new ConnectCallback() { @Override public void onEstablish(String endpointId, ConnectInfo connectionInfo) { Log.d(TAG, "Accept connection."); mDiscoveryEngine.acceptConnect(endpointId, mDataCbSender); mRemoteEndpointName = connectionInfo.getEndpointName(); mRemoteEndpointId = endpointId; } }

    接收端:

    pprivate ConnectCallback mConnCbRcver = new ConnectCallback() { @Override public void onEstablish(String endpointId, ConnectInfo connectionInfo) { Log.d(TAG, "Accept connection."); mRemoteEndpointName = connectionInfo.getEndpointName(); mRemoteEndpointId = endpointId; mDiscoveryEngine.acceptConnect(endpointId, mDataCbRcver); } }
    b) 两端由[onResult](https://developer.huawei.com/consumer/cn/doc/development/system-References/connectcallback-0000001050132589)()通知连接结果。

    发送端:

    @Override public void onResult(String endpointId, ConnectResult result) { if (result.getStatus().getStatusCode() == StatusCode.STATUS_SUCCESS) { Log.d(TAG, "Connection Established. Stop discovery. Start to send file."); mDiscoveryEngine.stopScan(); mDiscoveryEngine.stopBroadcasting(); sendOneFile(); mBarcodeImage.setVisibility(View.INVISIBLE); mDescText.setText("Sending file " + mFileName + " to " + mRemoteEndpointName + "."); mProgress.setVisibility(View.VISIBLE); } }

    接收端:

    @Override public void onResult(String endpointId, ConnectResult result) { if (result.getStatus().getStatusCode() == StatusCode.STATUS_SUCCESS) { Log.d(TAG, "Connection Established. Stop Discovery."); mDiscoveryEngine.stopBroadcasting(); mDiscoveryEngine.stopScan(); mDescText.setText("Connected."); } }
  3. 传输数据文件。
    a) 连接建立后,发送端调用sendData()发送文件给对端。文件名使用Bytes类型发送,Payload采用File类型发送。
    filenameMsg = Data.fromBytes(mFileName.getBytes(StandardCharsets.UTF_8)); Log.d(TAG, "Send filename: " + mFileName); mTransferEngine.sendData(mRemoteEndpointId, filenameMsg); Log.d(TAG, "Send Payload."); mTransferEngine.sendData(mRemoteEndpointId, filePayload);
    b) 接收端由onReceived()接收数据,根据数据类型分别处理文件名和Payload。
    @Override public void onReceived(String endpointId, Data data) { if (data.getType() == Data.Type.BYTES) { String msg = new String(data.asBytes(), UTF_8); if (msg.equals("Receive Success")) { Log.d(TAG, "Received ACK. Send next."); sendOneFile(); } } }
    c) 接收端由onTransferUpdate()通知当前的传输状态,当文件传输完成时,向发送端发送Bytes类型的传输完成消息。并且对接收到的文件进行重命名。
    @Override public void onTransferUpdate(String string, TransferStateUpdate update) { if (update.getStatus() == TransferStateUpdate.Status.TRANSFER_STATE_SUCCESS) { } else if (update.getStatus() == TransferStateUpdate.Status.TRANSFER_STATE_IN_PROGRESS) { showProgressSpeed(update); if (update.getBytesTransferred() == update.getTotalBytes()) { Log.d(TAG, "File transfer done. Rename File."); renameFile(); Log.d(TAG, "Send Ack."); mDescText.setText("Transfer success. Speed: " + mSpeedStr + "MB/s. \nView the File at /Sdcard/Download/Nearby"); mTransferEngine.sendData(mRemoteEndpointId, Data.fromBytes("ReceiveSuccess".getBytes(StandardCharsets.UTF_8))); isTransfer = false; } } else if (update.getStatus() == TransferStateUpdate.Status.TRANSFER_STATE_FAILURE) { Log.d(TAG, "Transfer failed."); } else { Log.d(TAG, "Transfer cancelled."); } }
  4. 文件传输完成后断开连接。
    a) 发送端收到传输完成消息后,调用disconnectAll()断开连接。
    if (mFiles.isEmpty()) { Log.d(TAG, "All Files Done. Disconnect"); mDescText.setText("All Files Sent Successfully."); mProgress.setVisibility(View.INVISIBLE); mDiscoveryEngine.disconnectAll(); isTransfer = false; return; }
    b) 接收端由onDisconnected()通知连接断开。
    @Override public void onDisconnected(String endpointId) { Log.d(TAG, "Disconnected."); if (isTransfer == true) { mProgress.setVisibility(View.INVISIBLE); mDescText.setText("Connection lost."); } }

当您完成了上述步骤时后,就可以进行编译。
完成编译后,生成APK,安装到华为手机上,并调试、运行。

运行界面介绍

此处展示了智能手机上运行示例应用的截图。
应用共包含两个按钮,他们的功能分别是:

运行步骤

干得好,你已经成功完成了Codelab并学到了:

现在,马上把您自己的创意用HUAWEI Nearby Service SDK来实现吧!

您可以阅读下面链接,了解更多相关的信息。

相关文档
本codelab中所用Demo源码下载地址如下:

下载 source code

Code copied