[Huawei Developers] XDARoni:
Huawei's Nearby SDK is sort of like Android Beam on steroids. Apps implementing it can use it for local file transfer, but that's not all. Nearby also enables apps to do realtime local communication, which is useful for things like locak multiplayer gaming. Finally, it also supports messaging, in the form of "beacons" that an implementing app can use to retrieve relevant localized information.

If any of this seems like it could be useful for you, read on, cause we're going to implement it.

Preparation

First up, make sure you have a Huawei Developer Account. This process can take a couple days, and you'll need one to use this SDK, so be sure to start that as soon as possible. You can sign up at https://developer.huawei.com.

Next, you'll want to obtain the SHA-256 representation of your app's signing key. If you don't have a signing key yet, be sure to create one before continuing. To obtain your signing key's SHA-256, you'll need to use Keytool which is part of the JDK installation. Keytool is a command-line program. If you're on Windows, open CMD. If you're on Linux, open Terminal.

On Windows, you'll need to "cd" into the directory containing the Keytool executable. For example, if you have JDK 1.8 v231 installed, Keytool will be located at the following path:

C:\Program Files\Java\jdk1.8.0_231\bin\

Once you find the directory, "cd" into it:

C: #Make sure you're in the right drive
cd C:\Program Files\Java\jdk1.8.0_231\bin\

Next, you need to find the location of your keystore. Using Android's debug keystore as an example, where the Android SDK is hosted on the "E:" drive in Windows, the path will be as follows:

E:\AndroidSDK\.android\debug.keystore

(Keytool also supports JKS-format keystores.)

Now you're ready to run the command. On Windows, it'll look something like this:

keytool -list -v -keystore E:\AndroidSDK\.android\debug.keystore

On Linux, the command should be similar, just using UNIX-style paths instead.

Enter the keystore password, and the key name (if applicable), and you'll be presented with something similar to the following:

Make note of the SHA256 field.

SDK Setup

Now we're ready to add the Nearby SDK to your Android Studio project. Go to your Huawei Developer Console and click the HUAWEI AppGallery tile. Agree to the terms of use if prompted.

Click the "My projects" tile here. If you haven't already added your project to the AppGallery, add it now. You'll be asked for a project name. Make it something descriptive so you know what it's for.

Now, you should be on a screen that looks something like the following:

Click the "Add app" button. Here, you'll need to provide some details about your app, like its name and package name.

Once you click OK, some SDK setup instructions will be displayed. Follow them to get everything added to your project. You'll also need to add the following to the "dependencies" section of your app-level build.gradle file:

implementation 'com.huawei.hms:nearby:4.0.4.300'

If you ever need to come back to these instructions, you can always click the "Add SDK" button after "App information" on the "Project setting" page.

Now you should be back on the "Project setting" page. Find the "SHA-256 certificate fingerprint" field under "App information," click the "+" button, and paste your SHA-256.

Now, go to the Manage APIs tab on the "Project setting" page. Scroll down until you find "Nearby Service" and make sure it's enabled.

Now, if you're using obfuscation in your app, you'll need to whitelist a few things for HMS to work properly.

For ProGuard:

-ignorewarnings
-keepattributes *Annotation*
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable
-keep class com.hianalytics.android.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}

For AndResGuard:

"R.string.hms*",
"R.string.agc*",
"R.string.connect_server_fail_prompt_toast",
"R.string.getting_message_fail_prompt_toast",
"R.string.no_available_network_prompt_toast",
"R.string.third_app_*",
"R.string.upsdk_*",
"R.layout.hms*",
"R.layout.upsdk_*",
"R.drawable.upsdk*",
"R.color.upsdk*",
"R.dimen.upsdk*",
"R.style.upsdk*

That's it! The Nearby SDK should now be available in your project.

Basic Usage

There are currently three ways to use the Nearby SDK: Nearby Connection, Nearby Message, and Beacon Management.

Nearby Connection

Nearby Connection is the API that allows you to locally transmit and receive data to and from another device. The first thing you'll need to do to implement this is declare the use of quite a few permissions. These should go in your 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" />
<!-- If you don't care about devices running Android 10 or later, this can be replaced with 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" />

Some of these permissions, like "ACCESS_FINE_LOCATION" are "dangerous"-level permissions, so make sure you request access to them on Android Marshmallow and above.

Now, there are three usage "modes" for this API: Mesh, Star, and P2P.

Mesh allows all involved devices to send and receive data to and from all other involved devices. This comes at the cost of bandwidth speed, though, so this should only be used for things with low data throughput, like game-state sharing.

Star is similar to Mesh, but with a twist: the bandwidth is higher. What's the trade-off? Well, one device has to act as a "hub" of sorts. That hub can send to and receive from all other involved devices, but the rest can only communicate to the hub itself. This is useful for things like mass video and file sharing.

Finally, there's P2P. This is a simple 1-on-1 connection, where one device sends a file to another. This method has the highest bandwidth, but obviously the strictest constraints.

Below is an example of how to broadcast the ability to connect using the Star policy. This would be used on potential clients for the "hub" to connect to.

//Begin broadcasting the ability to receive connections.
fun startBroadcasting() {
val policy = Policy.POLICY_STAR
val option = BroadcastOption.Builder()
.setPolicy(policy)
.build()

val dataCallback = object : DataCallback() {
override fun onReceived(p0: String?, p1: Data?) {
//We'll get to the implementation of this later.
}

override fun onTransferUpdate(p0: String?, p1: TransferStateUpdate?) {

}
}

val connectionCallback = object : ConnectCallback() {
override fun onDisconnected(endpointId: String) {
//The connection has been terminated
}

override fun onEstablish(endpointId: String, connectInfo: ConnectInfo) {
//A connection has been established.
//This is where you either accept or reject the connection.
//Both parties need to accept the connection. You can either
//present a confirmation to the user or silently accept.

//Similarly to Bluetooth pairing, you can display an auth code
//on both devices. Obtain the auth code:
val authCode = connectInfo.authCode

//Silently accept:
Nearby.getDiscoveryEngine(context)
.acceptConnect(endpointId, dataCallback)
}

override fun onResult(endpointId: String, connectResult: ConnectResult) {
//Handle the result of a connection request.

when (connectResult.status.statusCode) {
StatusCode.STATUS_SUCCESS -> {
//The connection was accepted.
//We can start exchanging data.
}
StatusCode.STATUS_CONNECT_REJECTED -> {
//The connection was rejected.
//Notify the user.
}
else -> {
//This shouldn't happen...
}
}
}
}

Nearby.getDiscoveryEngine(context)
//"NAME" should be something descriptive (what is this device?)
//"SERVICE_ID" should be your app's package name
.startBroadcasting("NAME", "SERVICE_ID", connectionCallback, option)
.addOnSuccessListener {
//Broadcasting successfully started
}
.addOnFailureListener {
//Broadcasting failed to start
}
}

//This device is no longer accepting connections
fun stopBroadcasting() {
Nearby.getDiscoveryEngine(context)
.stopBroadcasting()
}

Next, a device needs to scan for potential connections. The following code shows how to do that.

//Start scanning for available connections.
fun startScanning() {
val policy = Policy.POLICY_STAR
val option = ScanOption.Builder()
.setPolicy(policy)
.build()

val scanEndpointCallback = object : ScanEndpointCallback() {
override fun onFound(endpointId: String?, endpointInfo: ScanEndpointInfo?) {
//A device has been found. Use this opportunity to either automatically
//connect to it, or add it to a list for the user to select from.
}

override fun onLost(endpointId: String?) {
//A device has gone out of range, been turned off, etc.
//It's no longer available to connect to, so if you're
//presenting a list, make sure to remove it.
}
}

Nearby.getDiscoveryEngine(context)
.startScan("SERVICE_ID", scanEndpointCallback, option)
.addOnSuccessListener {
//Scanning started successfully
}
.addOnFailureListener {
//Scanning couldn't start
}
}

//Stop scanning for new devices
fun stopScanning() {
Nearby.getDiscoveryEngine(context)
.stopScan()
}

Finally, once the scan is complete and the user (or your code) has selected a device, you'll need to initiate the connection:

//Connect to a device. This should be called
//from the scanner.
//endpointId comes from the onFound() method of
//the ScanEndpointCallback
fun startConnection(endpointId: String) {
val dataCallback = object : DataCallback() {
override fun onReceived(endpointId: String, data: Data) {
//We'll get to the implementation of this later.
}

override fun onTransferUpdate(endpointId: String, update: TransferStateUpdate) {

}
}

val connectionCallback = object : ConnectCallback() {
override fun onDisconnected(endpointId: String) {
//The connection has been terminated
}

override fun onEstablish(endpointId: String, connectInfo: ConnectInfo) {
//A connection has been established.
//This is where you either accept or reject the connection.
//Both parties need to accept the connection. You can either
//present a confirmation to the user or silently accept.

//Similarly to Bluetooth pairing, you can display an auth code
//on both devices. Obtain the auth code:
val authCode = connectInfo.authCode

//Silently accept:
Nearby.getDiscoveryEngine(context)
.acceptConnect(endpointId, dataCallback)
}

override fun onResult(endpointId: String, connectResult: ConnectResult) {
//Handle the result of a connection request.

when (connectResult.status.statusCode) {
StatusCode.STATUS_SUCCESS -> {
//The connection was accepted.
//We can start exchanging data.
}
StatusCode.STATUS_CONNECT_REJECTED -> {
//The connection was rejected.
//Notify the user.
}
else -> {
//This shouldn't happen...
}
}
}
}

Nearby.getDiscoveryEngine(context)
.requestConnect("NAME", endpointId, connectionCallback)
.addOnSuccessListener {
//Request was sent successfully
}
.addOnFailureListener {
//Request failed to send
}
}

//End a connection
fun stopConnection(endpointId: String) {
Nearby.getDiscoveryEngine(context)
.disconnect(endpointId)
}

Finally, once everything is connected, it's time to start transfering data. There are currently three forms of data transfer: bytes, files, and streams.

Bytes

Nearby Connection allows you to send small packets of data in the form of byte arrays. This could be useful for if you only need to send some simple data, like a chat message, or a game-state update. The size limit is 32KB.

To send a byte array, use the following code:

//Send some data to a client in the form
//of a byte array.
fun sendByteData(endpointId: String, data: ByteArray) {
Nearby.getTransferEngine(context)
.sendData(endpointId, Data.fromBytes(data))
}

Files

If you have a file you want to send (e.g. a video or music file), use this method.

Sending a file is similar to sending a byte array:

//Send some data to a client in the form
//of a File.
//Files received with this method are stored
//in the receiving device's Download folder.
fun sendFileData(endpointId: String, file: File) {
try {
Nearby.getTransferEngine(context)
.sendData(endpointId, Data.fromFile(file))
} catch (e: FileNotFoundException) {
//Handle accordingly
}
}

//Send some data to a client in the form
//of a File using ParcelFileDescriptor
//Files received with this method are stored
//in the receiving device's Download folder.
fun sendFileData(endpointId: String, file: ParcelFileDescriptor) {
try {
Nearby.getTransferEngine(context)
.sendData(endpointId, Data.fromFile(file))
} catch (e: FileNotFoundException) {
//Handle accordingly
}
}

Streams

If it's easier for you to send your data in the form of a stream,

you can also do that.

Yet again, the process is very similar to the previous two methods.

//Send some data to a client in the form
//of a stream.
fun sendStreamData(endpointId: String, stream: InputStream) {
Nearby.getTransferEngine(context)
.sendData(endpointId, Data.fromStream(stream))
}

//Send soe data to a client in the form
//of a stream using ParcelFileDescriptor.
fun sendStreamData(endpointId: String, stream: ParcelFileDescriptor) {
Nearby.getTransferEngine(context)
.sendData(endpointId, Data.fromStream(stream))
}

_______

If you want to cancel a transfer, it's also fairly simple:

//Cancel the transmission of data.
//The dataId can be obtained from the
//Data instance being sent.
fun cancelTransmission(dataId: Long) {
Nearby.getTransferEngine(context)
.cancelDataTransfer(dataId)
}

Now that you know how to send data, it's time to go over receiving it. In the code examples above for broadcasting and connecting, there's an unimplemented DataCallback. Well, it's time to implement it. Below is an example of how you might do that.

//An example implementation of a DataCallback
class DataReceiver : DataCallback() {
//A method to hold received data until we can
//properly retrieve files and streams.
private val receivedData = HashMap<Long, Data>()

override fun onReceived(endpointId: String, data: Data) {
//There's some new data.
when (data.type) {
Data.Type.BYTES -> {
//The data received is in the byte array format.
//Retrieve it as such, and handle accordingly.
//This is the only format where it's safe to retrieve the data here.
val bytes = data.asBytes()

//However, in this implementation, we're going to temporarily store
//the data reference and retrieve it the same way as we do files
//and streams.
receivedData[data.id] = data
}
Data.Type.FILE, Data.Type.STREAM -> {
//Temporarily store the data reference until the transfer is complete.
receivedData[data.id] = data
}
}
}

override fun onTransferUpdate(endpointId: String, update: TransferStateUpdate) {
when (update.status) {
TransferStateUpdate.Status.TRANSFER_STATE_SUCCESS -> {
//The transfer is complete. Retrieve data and handle it.
val data = receivedData[update.dataId]
when (data?.type) {
Data.Type.BYTES -> {
val bytes = data.asBytes()
}
Data.Type.FILE -> {
val file = data.asFile()
}
Data.Type.STREAM -> {
val stream = data.asStream()
}
}
}
TransferStateUpdate.Status.TRANSFER_STATE_CANCELED -> {
//The transfer was canceled
}
TransferStateUpdate.Status.TRANSFER_STATE_FAILURE -> {
//The transfer failed
}
TransferStateUpdate.Status.TRANSFER_STATE_IN_PROGRESS -> {
//The transfer is still in progress. You can use this event to display and
//update a progress indicator.

val progressPercent = (update.bytesTransferred.toFloat() / update.totalBytes.toFloat() * 100).toInt()
}
}
}
}

Nearby Message

Next up is the Nearby Message API. This is a way to publish and subscribe to messages generated by other devices or by dedicated "beacons."

The first thing to do is declare permissions. In order to properly use this API, the following permissions must be requested and granted:

<uses-permission android:name="android.permission.INTERNET " />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Now, it's time to actually use it. First let's talk about publishing messages. Publishing and unpublishing is fairly simple:

//Publish a message for other devices to see.
//You can publish up to 50 messages within a
//5 second timespan.
//You can also specify a type string and/or
//a namespace string.
fun publishMessage(msg: String) {
Nearby.getMessageEngine(context)
.put(Message(msg.toByteArray()))
}

//Unpublish a message. The Message class
//takes care of checking message equality
//for you, so you only need to save the
//string itself.
//Make sure to call this in your component's
//onDestroy() method.
fun unpublishMessage(msg: String) {
Nearby.getMessageEngine(context)
.unput(Message(msg.toByteArray()))
}

Subscribing to messages is a bit more complex (obviously). There are two ways to subscribe: in the foreground, and in the background.

The following code is an example of how to subscribe in the foreground:

val handler = object : MessageHandler() {
override fun onFound(message: Message) {
//A new Message has been found.
}

override fun onLost(message: Message) {
//An existing message was lost.
}

//The following methods can be used to estimate the distance from a beacon
//or publisher, based on signal strength.

override fun onBleSignalChanged(message: Message, bleSignal: BleSignal) {
val strength = bleSignal.rssi
}

override fun onDistanceChanged(message: Message, distance: Distance) {
val dist = distance.meters
val precision = distance.precision
}
}

//Subscribe to new messages in the foreground.
//This should be run in a foreground Service or an Activity.
fun subscribeToMessagesForeground() {
val options = GetOption.Builder()
.setPicker(MessagePicker.Builder()
//MessagePicker.Builder has various options
//for filtering messages.
.build())
.setPolicy(com.huawei.hms.nearby.message.Policy.Builder()
//Policy.Builder has various options
//for filtering messages.
.build())
.build()
Nearby.getMessageEngine(context)
.get(handler, options)
}

Here's an example for subscribing in the background:

val intent = PendingIntent.getService(context, 0, Intent(context, MessageReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT)

//Subscribe to new messages in the background.
//To receive these messages, you'll need to set up
//an IntentService to handle a PendingIntent.
fun subscribeToMessagesBackground() {
val options = GetOption.Builder()
.setPicker(MessagePicker.Builder()
//MessagePicker.Builder has various options
//for filtering messages.
.build())
.setPolicy(com.huawei.hms.nearby.message.Policy.Builder()
//Policy.Builder has various options
//for filtering messages.
.build())
.build()
Nearby.getMessageEngine(context)
.get(intent, options)
}

//Usubscribe from new background messages.
//Make sure to call this on app exit.
fun unsubscribeFromMessagesBackground() {
Nearby.getMessageEngine(context)
.unget(intent)
}

class MessageReceiver : JobIntentService() {
override fun onHandleWork(intent: Intent) {
//Pass the data to the messaging API and let it
//call the appropriate callback methods.
Nearby.getMessageEngine(this)
.handleIntent(intent, object : MessageHandler() {
override fun onFound(message: Message) {
//A new Message has been found.
}

override fun onLost(message: Message) {
//An existing message was lost.
}

//The following methods can be used to estimate the distance from a beacon
//or publisher, based on signal strength.

override fun onBleSignalChanged(message: Message, bleSignal: BleSignal) {
val strength = bleSignal.rssi
}

override fun onDistanceChanged(message: Message, distance: Distance) {
val dist = distance.meters
val precision = distance.precision
}
})
}
}

Beacon Management

To set up beacons, please refer to Huawei's documentation.

Conclusion

And that's it! Be sure to check out Huawei's full documentation for more details.