如何用HMS Nearby Service給自己的App添加近距離數據傳輸功能

  當你給朋友發送手機資料時,過了很久進度條卻動也不動;當你想發送大文件給同事時,僅一個文件就用光了你所有流量;當你跟朋友乘坐飛機時想一起玩遊戲時,卻因沒有網路無奈放棄。

在這裡插入圖片描述

  們生活中似乎經常能遇到這種尷尬的場景,近距離數據傳輸功能是用戶的一個痛點。現在,只需要接入華為近距離通訊服務,通過Nearby Connection便可以輕鬆實現設備間的數據傳輸,傳輸類型支援短文本、流數據和文件數據等類型,可幫助app實現本地多人遊戲、實時協作、多屏遊戲和離線文件傳輸等功能。下圖是功能演示:

在這裡插入圖片描述

  如果你對實現方式感興趣,可以在Github上下載源碼:
  //github.com/HMS-Core/hms-nearby-demo/tree/master/NearbyConnection

  首先需要了解Nearby Connection 開發流程

在這裡插入圖片描述

1. 業務流程

  整體流程可以劃分為4個階段。

  廣播掃描階段:廣播端啟動廣播,發現端啟動掃描以發現廣播端。

  1. 廣播端調用startBroadcasting()啟動廣播。
  2. 發現端調用startScan()啟動掃描以發現附近的設備。
  3. 由onFound()方法通知掃描結果。

  建立連接階段:發現端發起連接並啟動對稱的身份驗證流程,雙端獨立接受或拒絕連接請求。

  1. 發現端調用requestConnect()向廣播端發起連接請求。
  2. 兩端由onEstablish()通知連接啟動後,均可以調用acceptConnect()接受連接或調用rejectConnect()拒絕連接。
  3. 兩端由onResult()通知連接結果。僅當兩端都接受連接時,連接才能建立。

  傳輸數據階段:建立連接後,雙端進行數據交換。

  1. 連接建立後,雙端均可以調用sendData()發送數據給對端。
  2. 接收數據的一端由onReceived()通知接收到數據;兩端由onTransferUpdate()通知當前的傳輸狀態。

  斷開連接階段:雙端任意一端發起斷開連接,通知對端連接斷開。

  1. 主動斷開連接的一端調用disconnect()斷開連接,對端由onDisconnected()通知連接斷開。

2. 開發步驟

2.1 開發準備

  如果你以前沒有集成華為移動服務的經驗,那麼需要先配置AppGallery Connect,開通近距離通訊服務並集成HMS SDK。相關步驟請參考官方文檔。

2.2 聲明系統許可權

  Nearby Connection開發場景需要使用Nearby Discovery API和Nearby Transfer API,你的應用必須根據所使用的策略聲明適當的許可權。例如:使用POLICY_STAR策略開發文件傳輸的應用,需要添加特定的許可權到AndroidManifest.xml:

<!-- Required for Nearby Discovery and Nearby Transfer --> 
<uses-permission android:name="android.permission.BLUETOOTH" /> 
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />   
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />    
<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" /> 
<!-- Required for FILE payloads --> 
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>  
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

  由於ACCESS_FINE_LOCATION,WRITE_EXTERNAL_STORAGE和READ_EXTERNAL_STORAGE 是危險的系統許可權,因此,必須動態的申請這些許可權。如果許可權不足,近距離通訊服務(Nearby Service)將會拒絕應用開啟廣播或者開啟發現。

2.3 選擇策略

  Nearby Discovery支援3種不同的連接策略:POLICY_MESH,POLICY_STAR和POLICY_P2P。可以根據應用場景優選策略。

  策略選擇並創建BroadcastOption對象的示例程式碼如下:

Policy policy = Policy.POLICY_STAR;  
BroadcastOption broadcastOption = new BroadcastOption.Builder().setPolicy (policy).build();

2.4 廣播和掃描

  一旦授予應用所需的許可權,並為應用選擇一個策略,就可以開始廣播和掃描以發現附近的設備。

2.4.1 啟動廣播

  廣播端以選定的policy和serviceId為參數,調用startBroadcasting()啟動廣播。其中serviceId應該唯一標識的應用。建議使用應用的包名作為serviceId(例如:com.huawei.example.myapp)。示例程式碼如下:

private void doStartBroadcasting() {
    Policy policy = Policy.POLICY_STAR;
    BroadcastOption broadcastOption = new BroadcastOption.Builder().setPolicy(policy).build();
    Nearby.getDiscoveryEngine(getApplicationContext())
            .startBroadcasting(name, serviceId, connectCallback, broadcastOption)
            .addOnSuccessListener(
                    new OnSuccessListener<Void>() {
                        @Override
                        public void onSuccess(Void aVoid) {
                            /* We are broadcasting. */
                        }
                    })
            .addOnFailureListener(
                    new OnFailureListener() {
                        @Override
                        public void onFailure(Exception e) {
                            /* Fail to start broadcasting. */
                        }
                    });
}

  參數connectCallback是一個連接監聽回調類實例,用於通知連接狀態資訊。有關ConnectCallback類的詳細資訊及示例程式碼,參見確認連接章節。

2.4.2 啟動掃描

  發現端以選定的policy和serviceId為參數,調用startScan()啟動掃描以發現附近的設備。示例程式碼如下:

private void doStartScan() {
    Policy policy = Policy.POLICY_STAR;
    ScanOption scanOption = new ScanOption.Builder().setPolicy(policy).build();
    Nearby.getDiscoveryEngine(getApplicationContext())
            .startScan(serviceId, scanEndpointCallback, scanOption)
            .addOnSuccessListener(
                    new OnSuccessListener<Void>() {
                        @Override
                        public void onSuccess(Void aVoid) {
                            /* Start scan success. */
                        }
                    })
            .addOnFailureListener(
                    new OnFailureListener() {
                        @Override
                        public void onFailure(Exception e) {
                            /* Fail to start scan. */
                        }
                    }); 
}

  參數scanEndpointCallback是一個掃描監聽回調類實例,通知發現端掃描結果,發現設備或者已發現設備丟失。

private ScanEndpointCallback scanEndpointCallback =
            new ScanEndpointCallback() {
                @Override
                public void onFound(String endpointId, ScanEndpointInfo discoveryEndpointInfo) {
                    mEndpointId = endpointId;
                    mDiscoveryEngine.requestConnect(myNameStr, mEndpointId, mConnCb);
                }
                @Override
                public void onLost(String endpointId) {
                    Log.d(TAG, "Nearby Connection Demo app: Lost endpoint: " + endpointId);
                }
            };

2.4.3 停止廣播

  當需要停止廣播時,調用stopBroadcasting()。停止廣播後,廣播端不可以接收來自發現端的連接請求。

2.4.4 停止掃描

  當需要停止掃描時,調用stopScan()。停止掃描後,發現端仍可以向已發現的設備請求連接。一種常見的做法是:一旦發現需要連接的設備,就調用stopScan()停止掃描。

2.5 建立連接

2.5.1 請求連接

  當附近的設備被發現,發現端可以調用requestConnect()發起連接。示例程式碼如下:

private void doStartConnect(String name, String endpointId) throws RemoteException {
    Nearby.getDiscoveryEngine(getApplicationContext())
            .requestConnect(name, endpointId, connectCallback)
            .addOnSuccessListener(
                    new OnSuccessListener<Void>() {
                        @Override
                        public void onSuccess(Void aVoid) {
                            /* Request success. */
                        }
                    })
            .addOnFailureListener(
                    new OnFailureListener() {
                        @Override
                        public void onFailure(Exception e) {
                            /* Fail to request connect. */
                        }
                    });
}

  當然,根據需要,可以向用戶展示發現的設備列表,並允許他們選擇連接哪些設備。

2.5.2 確認連接

  發現端發起連接後,通過回調connectCallback的onEstablish()方法將連接建立事件通知給雙方。雙方必須通過調用acceptConnect()接受連接或者通過調用rejectConnect()拒絕連接。僅當雙方都接受連接時,連接才會建立成功。如果一方或雙方都選擇拒絕,則連接失敗。無論哪種方式,連接結果都會通過onResult()方法通知。示例程式碼如下:

private final ConnectCallback connectCallback =
        new ConnectCallback() {
            @Override
            public void onEstablish(String endpointId, ConnectInfo connectInfo) {
                /* Accept the connection request without notifying user. */
                Nearby.getDiscoveryEngine(getApplicationContext())
.acceptConnect(endpointId, dataCallback);
            }
            @Override
            public void onResult(String endpointId, ConnectResult result) { 
                switch (result.getStatus().getStatusCode()) {
                    case StatusCode.STATUS_SUCCESS:
                        /* The connection was established successfully, we can exchange data. */  
                        break;  
                    case StatusCode.STATUS_CONNECT_REJECTED:  
                        /* The Connection was rejected. */
                        break;   
                    default:
                        /* other unknown status code. */  
                }   
            }
            @Override   
            public void onDisconnected(String endpointId) {
                /* The connection was disconneted. */
            }  
        };

  此示例顯示了一種雙方自動接受連接的確認連接方式。根據需要,可以使用其他的確認連接方式。

2.5.3 驗證連接

  應用程式可以提供一種讓用戶確認連接到指定設備的方法,例如:通過驗證token(token可以是一個短隨機字元串或者數字)。通常這涉及在兩個設備上顯示token並要求用戶手動輸入或者確認,類似於藍牙配對對話框。
  下面演示一種通過彈窗確認配對碼的方式驗證連接。示例程式碼如下:

@Override 
public void onEstablish(String endpointId, ConnectInfo connectInfo) { 
    AlertDialog.Builder builder = new AlertDialog.Builder(getApplicationContext());   
    builder.setTitle(connectInfo.getEndpointName() + " request connection") 
            .setMessage("Please confirm the match code is: " + connectInfo.getAuthCode()) 
            .setPositiveButton(
                    "Accept",   
                    (DialogInterface dialog, int which) ->  
                            /* Accept the connection. */
                            Nearby.getDiscoveryEngine(getApplicationContext())  
                                    .acceptConnect(endpointId, dataCallback))
            .setNegativeButton(
                    "Reject",
                    (DialogInterface dialog, int which) ->
                            /* Reject the connection. */
                            Nearby.getDiscoveryEngine(getApplicationContext())
                                    .rejectConnect(endpointId))
            .setIcon(android.R.drawable.ic_dialog_alert);
    AlertDialog alert = builder.create();
    alert.show(); 
}

2.6 傳輸數據

  設備間建立連接後,可以使用該連接傳輸Data對象。Data對象的類型包括位元組序列、文件和流。通過調用sendData()方法發送數據,通過DataCallback類實例的onReceived()方法接收數據。

2.6.1 數據類型

  1. BYTES
    通過調用Data.fromBytes()創建Data.Type.BYTES類型的Data對象。
    發送BYTES類型的數據,示例程式碼如下:
Data bytesData = Data.fromBytes(new byte[] {0xA, 0xA, 0xA, 0xA, 0xA});   
Nearby.getTransferEngine(getApplicationContext()).sendData(toEndpointId, bytesData);

  注意:BYTES類型數據的長度大小不能超過32KB。
  接收BYTES類型的數據,示例程式碼如下:

static class BytesDataReceiver extends DataCallback {  
    @Override
    public void onReceived(String endpointId, Data data) {
        /* BYTES data is sent as a single block, so we can get complete data. */
        if (data.getType() == Data.Type.BYTES) {
            byte[] receivedBytes = data.asBytes();
        }
    }
    @Override
    public void onTransferUpdate(String endpointId, TransferStateUpdate update) {
        /* We will receive TRANSFER_STATE_SUCCESS update after onReceived() called. */
    }
}

  注意:BYTES與FILE和STREAM類型不同,BYTES是以單個數據塊發送的,因此接收端不用等待BYTES類型的狀態更新為TRANSFER_STATE_SUCCESS,當onReceived()被調用時候,你就可以調用data.asBytes()以獲取全部數據。

  1. FILE
    通過調用Data.fromFile()創建Data.Type.FILE類型的Data對象。
    發送FILE類型數據的示例程式碼如下:
File fileToSend = new File(getApplicationContext().getFilesDir(), "fileSample.txt");
try {  
    Data fileData = Data.fromFile(fileToSend);
    Nearby.getTransferEngine(getApplicationContext())
.sendData(endpointList, fileData);
} catch (FileNotFoundException e) { 
    /* Exception handle. */
}

  一種更高效方法是使用ParcelFileDescriptor創建FILE類型,可以最大程度地減少文件的複製。示例程式碼如下:

ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
Data fileData = Data.fromFile(pfd);

  接收設備收到文件後,文件保存在Download目錄,並且將以fileData.getId()轉化後的字元串命名。傳輸完成後,可以獲取FILE對象。示例程式碼如下:

/* We can get the received file in the Download folder. */    
File payloadFile = fileData.asFile().asJavaFile();
)
  1. STREAM
      通過調用Data.fromStream()創建Data.Type.STREAM類型的Data對象。發送流的示例程式碼如下:
URL url = new URL("//developers.huawei.com");  
Data streamData = Data.fromStream(url.openStream());  
Nearby.getTransferEngine(getApplicationContext()).sendData(toEndpointId, streamData);

  接收端,當onTransferUpdate()回調成功時,可以調用streamData.asStream().asInputStream()或者 streamData.asStream().asParcelFileDescriptor()獲取流對象。示例程式碼如下:

static class StreamDataReceiver extends DataCallback {  
    private final HashMap<Long, Data> incomingData = new HashMap<>();
    @Override
    public void onTransferUpdate(String endpointId, TransferStateUpdate update) {
        if (update.getStatus() == TransferStateUpdate.Status.TRANSFER_STATE_SUCCESS) {
            Data data = incomingData.get(update.getDataId());
            InputStream inputStream = data.asStream().asInputStream();
            /* Further processing... */
        }
    }
    @Override
    public void onReceived(String endpointId, Data data) {
        incomingData.put(data.getId(), data);
    } 
}

2.6.2 進度更新

  DataCallBack回調類onTransferUpdate()方法提供數據發送或接收的進度更新,基於此可以向用戶顯示傳輸進度,例如:進度條。

2.6.3 取消傳輸

  如果需要在接收或發送過程中取消傳輸,調用TransferEngine類實例方法cancelDataTransfer()。

2.7 斷開連接

  如果需要斷開與對端的連接,調用DiscoveryEngine類實例方法disconnect()。一旦調用此介面,將不能從此endpoint收發數據。

結後語

  基於Nearby Connection, 可以幫助你的APP實現如下相關功能:

  1. 本地多人遊戲:自組網,提供低延時、穩定可靠的傳輸體驗。
  2. 離線文件傳輸:無需流量,可達80MB/S的傳輸速度。

  更詳細的開發指南參考華為開發者聯盟官網://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/introduction-0000001050040566


原文鏈接://developer.huawei.com/consumer/cn/forum/topicview?tid=0201296701616220024&fid=18
原作者:趙照

Tags: