Implementing Nearby Data Transmission for Your App Using HUAWEI Nearby Service - Huawei Developers

Imagine these scenarios. When you transfer a file to your friend using a mobile phone, the progress seems never to reach the end. Finally, the transfer ends, but a single file has exhausted your mobile data. Or you are on a flight with your friends and feel bored, but you cannot play a multi-player game together because there is no Internet connection.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
You are so near, yet you cannot transmit data to each other's device conveniently. Nearby Service is designed to help you resolve these pain points. You only need to integrate it, its Nearby Connection capability will allow your users to transfer data in forms like text, streams, and files. Typical use cases of this capability include multi-player gaming, real-time collaboration, multi-screen gaming, and offline file transfer.
The following figure shows how the function works.
If you are interested in the implementation details, download the source code from GitHub: https://github.com/HMS-Core/hms-nearby-demo/tree/master/NearbyConnection
The following will elaborate on the development process of Nearby Connection.
1. Service Process
The process can be divided into four phases.
Broadcast and scanning phase: The broadcaster starts broadcasting, and the discoverer starts scanning to discover the broadcaster.
1) The broadcaster calls startBroadcasting() to start broadcasting.
2) The discoverer calls startScan() to start scanning for nearby devices.
3) The onFound() method notifies the scanning result.
Connection setup phase: The discoverer initiates a connection and starts a symmetric authentication process, and the two endpoints independently accept or reject the connection request.
1) The discoverer calls requestConnect() to initiate a connection request to the broadcaster.
2) After onEstablish() notifies the two endpoints of the connection startup, the two endpoints can call acceptConnect() to accept the connection or call rejectConnect() to reject the connection.
3) The onResult() method notifies the two endpoints of the connection result. The connection can be set up only when both endpoints accept the connection.
Data transmission phase: After the connection is set up, the two endpoints start data exchange.
1) After the connection is set up, both endpoints can call sendData() to send data to the remote endpoint.
2) The data subscriber is notified by onReceived() that the data is received. Both endpoints are notified by onTransferUpdate() of the current transmission status.
Disconnection phase: Either endpoint initiates a disconnection request to notify the remote endpoint of the disconnection.
1) The endpoint that actively disconnects from the remote endpoint calls disconnect() to disconnect from the remote endpoint. The remote endpoint is notified by onDisconnected() of the disconnection.
2. Development Procedure
2.1 Getting Started
If you are new to Huawei Mobile Services (HMS), you need to configure app information in AppGallery Connect, enable Nearby Service on the HUAWEI Developers console, and integrate the HMS Core SDK. For details, please refer to the documentation.
2.2 Declaring System Permissions
To use Nearby Discovery and Nearby Transfer APIs in Nearby Connection development scenarios, your app must declare the appropriate permissions based on the policies used. For example, to use the POLICY_STAR policy to develop a file transfer app, you need to add specific permissions to the AndroidManifest.xml file.
Code:
<!-- 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" />
The ACCESS_FINE_LOCATION, WRITE_EXTERNAL_STORAGE, and READ_EXTERNAL_STORAGE permissions are dangerous system permissions, so you must dynamically apply for these permissions. If any required permission is not available, Nearby Service will reject the application to enable broadcast or scanning.
2.3 Selecting a Policy
Nearby Discovery supports three connection policies: POLICY_MESH, POLICY_STAR, and POLICY_P2P. You can select a policy based on the actual scenario.
The sample code for selecting a policy and creating the BroadcastOption object is as follows:
Code:
Policy policy = Policy.POLICY_STAR;
BroadcastOption broadcastOption = new BroadcastOption.Builder().setPolicy (policy).build();
2.4 Broadcast and Scanning
Once you grant the permissions required by the app and select a policy for your app, you can start broadcast and scanning to find nearby devices.
2.4.1 Starting Broadcast
The broadcaster uses the selected policy and serviceId parameters to call startBroadcasting() to start broadcasting. The serviceId parameter uniquely identifies your app. You are advised to use the package name of your app as the value of serviceId, for example, com.huawei.example.myapp.
Sample code:
Code:
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 is a connection listening callback class instance that notifies you of the connection status. For details about the connectCallback class and sample code, please refer to section 2.5.2.
2.4.2 Starting Scanning
The discoverer uses the selected policy and serviceId parameters to call startScan() to start scanning to discover nearby devices.
Sample code:
Code:
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. */
}
});
}
In the preceding information, scanEndpointCallback is a scanning listening callback class instance that notifies the discoverer of the scanning result, for example, a device is discovered or a device is lost.
Code:
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 Stopping Broadcast
To stop broadcast, you can call stopBroadcasting(). The broadcaster may still receive a connection request from the discoverer afterward.
2.4.4 Stopping Scanning
To stop scanning, you can call stopScan(). The discoverer can still request a connection to a discovered device afterward. Generally, once you find the device you want to connect to, call stopScan() to stop scanning.
2.5 Connection Setup
2.5.1 Requesting a Connection
When nearby devices are found, the discoverer can call requestConnect() to initiate connections.
Sample code:
Code:
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. */
}
});
}
You can show the list of discovered devices to users and allow them to choose which devices to connect to based on your requirements.
2.5.2 Confirming the Connection
After the discoverer requests to set up a connection with the broadcaster, the discoverer notifies the two parties of the connection setup by calling back the onEstablish() method of connectCallback. Both parties must accept the connection by calling acceptConnect() or reject the connection by calling rejectConnect(). The connection can be set up only when both endpoints accept the connection. If one or both of them choose to reject the connection, the connection fails. In either mode, the connection result is notified through the onResult() method.
Sample code:
Code:
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. */
}
};
This example shows a way for both parties to accept the connection automatically. You can use other methods to confirm the connection as required.
2.5.3 Verifying the Connection
Your app can provide a way for users to confirm the connection to a specified device. For example, you can use an authentication token, which may be a short random character string or number. Typically, this involves displaying the token on two devices and requiring the user to enter or confirm the token manually, similar to a Bluetooth pairing dialog box.
The following describes how to authenticate the connection by confirming the pairing code in a dialog box:
Sample code:
Code:
@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 Transmitting Data
After devices are connected, data objects can be transmitted through the connection. Data objects are classified into byte arrays, files, and streams. You can call sendData() to send data and call onReceived() of DataCallback to receive data.
2.6.1 Data Types
1) BYTES
You can call Data.fromBytes() to create a data object of the Data.Type.BYTES type.
The following is the sample code for sending byte arrays:
Code:
Data bytesData = Data.fromBytes(new byte[] {0xA, 0xA, 0xA, 0xA, 0xA});
Nearby.getTransferEngine(getApplicationContext()).sendData(toEndpointId, bytesData);
Note: The size of byte arrays cannot exceed 32 KB.
The following is the sample code for receiving byte arrays:
Code:
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. */
}
}
Note: Different from data of FILE and STREAM types, data of the BYTES type is sent as chunks, and the subscriber does not need to wait for the TRANSFER_STATE_SUCCESS status code. When onReceived() is called, you can call data.asBytes() to obtain the full data.
2) FILE
You can call Data.fromFile() to create a data object of the Data.Type.FILE type.
The following is the sample code for sending a file:
Code:
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. */
}
To be more efficient, you can create a FILE type using ParcelFileDescriptor, which minimizes file replication.
Sample code:
Code:
ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
Data fileData = Data.fromFile(pfd);
When a file is received, it is saved in the Download directory on the subscriber's device with the name defined as the character string converted by fileData.getId(). After the transmission is complete, you can obtain the FILE object.
Sample code:
Code:
/* We can get the received file in the Download folder. */
File payloadFile = fileData.asFile().asJavaFile();
3) STREAM
You can call Data.fromStream() to create a data object of the Data.Type.STREAM type.
Sample code:
Code:
URL url = new URL("https://developers.huawei.com");
Data streamData = Data.fromStream(url.openStream());
Nearby.getTransferEngine(getApplicationContext()).sendData(toEndpointId, streamData);
When the onTransferUpdate() callback is successful, the subscriber can call streamData.asStream().asInputStream() or streamData.asStream().asParcelFileDescriptor() to obtain the stream object.
Sample code:
Code:
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 Updating the Data Transmission Progress
The onTransferUpdate() method in the DataCallBack callback class provides the data sending or receiving progress update. Both parties can display the transmission progress to users in forms like a progress bar.
2.6.3 Canceling Data Transmission
To cancel the transmission during data receiving or sending, you can call the cancelDataTransfer() method of the TransferEngine class.
2.7 Disconnecting from the Remote Endpoint
To disconnect from the remote endpoint, you can call the disconnect() method of the DiscoveryEngine class. Once this API is called, this endpoint cannot send or receive data.
Conclusion
Using Nearby Connection, you can implement the following functions for your app:
1. Local multi-player gaming: Players can connect with each other's device flexibly and enjoy a smooth gaming experience enabled by low-latency, stable, and reliable data transmission.
2. Offline file transfer: Files can be transferred at a speed of up to 80 Mbit/s without consuming any mobile data.
If you are interested and want to learn more, check our development guide at https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/introduction-0000001050040566.

Related

Implementing HMS Nearby SDK into your App

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:
Code:
C:\Program Files\Java\jdk1.8.0_231\bin\
Once you find the directory, "cd" into it:
Code:
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:
Code:
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:
Code:
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:
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
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:
Code:
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:
Code:
-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:
Code:
"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.
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.
Code:
//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.
Code:
//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:
Code:
//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:
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:
Code:
//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.
Code:
//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:
Code:
//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.
Code:
//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:
XML:
<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:
Code:
//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:
Code:
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:
Code:
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.

Implementing Nearby Data Transmission for Your App with HUAWEI Nearby Service

More articles like this, you can visit HUAWEI Developer Forum​
Imagine these scenarios. When you transfer a file to your friend using a mobile phone, the progress seems never to reach the end. Finally, the transfer ends, but a single file has exhausted your mobile data. Or you are on a flight with your friends and feel bored, but you cannot play a multi-player game together because there is no Internet connection.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
You are so near, yet you cannot transmit data to each other’s device conveniently. Nearby Service is designed to help you resolve these pain points. You only need to integrate it, its Nearby Connection capability will allow your users to transfer data in forms like text, streams, and files. Typical use cases of this capability include multi-player gaming, real-time collaboration, multi-screen gaming, and offline file transfer.
The following figure shows how the function works.
If you are interested in the implementation details, download the source code from GitHub:
https://github.com/HMS-Core/hms-nearby-demo/tree/master/NearbyConnection
The following will elaborate on the development process of Nearby Connection.
1. Service Process
The process can be divided into four phases.
Broadcast and scanning phase: The broadcaster starts broadcasting, and the discoverer starts scanning to discover the broadcaster.
1) The broadcaster calls startBroadcasting() to start broadcasting.
2) The discoverer calls startScan() to start scanning for nearby devices.
3) The onFound() method notifies the scanning result.
Connection setup phase: The discoverer initiates a connection and starts a symmetric authentication process, and the two endpoints independently accept or reject the connection request.
1) The discoverer calls requestConnect() to initiate a connection request to the broadcaster.
2) After onEstablish() notifies the two endpoints of the connection startup, the two endpoints can call acceptConnect() to accept the connection or call rejectConnect() to reject the connection.
3) The onResult() method notifies the two endpoints of the connection result. The connection can be set up only when both endpoints accept the connection.
Data transmission phase: After the connection is set up, the two endpoints start data exchange.
1) After the connection is set up, both endpoints can call sendData() to send data to the remote endpoint.
2) The data subscriber is notified by onReceived() that the data is received. Both endpoints are notified by onTransferUpdate() of the current transmission status.
Disconnection phase: Either endpoint initiates a disconnection request to notify the remote endpoint of the disconnection.
1) The endpoint that actively disconnects from the remote endpoint calls disconnect() to disconnect from the remote endpoint. The remote endpoint is notified by onDisconnected() of the disconnection.
2. Development Procedure
2.1 Getting Started
If you are new to Huawei Mobile Services (HMS), you need to configure app information in AppGallery Connect, enable Nearby Service on the HUAWEI Developers console, and integrate the HMS Core SDK. For details, please refer to the documentation.
2.2 Declaring System Permissions
To use Nearby Discovery and Nearby Transfer APIs in Nearby Connection development scenarios, your app must declare the appropriate permissions based on the policies used. For example, to use the POLICY_STAR policy to develop a file transfer app, you need to add specific permissions to the AndroidManifest.xml file.
Code:
<!-- 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" />
The ACCESS_FINE_LOCATION, WRITE_EXTERNAL_STORAGE, and READ_EXTERNAL_STORAGE permissions are dangerous system permissions, so you must dynamically apply for these permissions. If any required permission is not available, Nearby Service will reject the application to enable broadcast or scanning.
2.3 Selecting a Policy
Nearby Discovery supports three connection policies: POLICY_MESH, POLICY_STAR, and POLICY_P2P. You can select a policy based on the actual scenario.
The sample code for selecting a policy and creating the BroadcastOption object is as follows:
2.4 Broadcast and Scanning
Once you grant the permissions required by the app and select a policy for your app, you can start broadcast and scanning to find nearby devices.
2.4.1 Starting Broadcast
The broadcaster uses the selected policy and serviceId parameters to call startBroadcasting() to start broadcasting. The serviceId parameter uniquely identifies your app. You are advised to use the package name of your app as the value of serviceId, for example, com.huawei.example.myapp.
Sample code:
Code:
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 is a connection listening callback class instance that notifies you of the connection status. For details about the connectCallback class and sample code, please refer to section 2.5.2.
2.4.2 Starting Scanning
The discoverer uses the selected policy and serviceId parameters to call startScan() to start scanning to discover nearby devices.
Sample code:
Code:
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. */
}
});
}
In the preceding information, scanEndpointCallback is a scanning listening callback class instance that notifies the discoverer of the scanning result, for example, a device is discovered or a device is lost.
Code:
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 Stopping Broadcast
To stop broadcast, you can call stopBroadcasting(). The broadcaster may still receive a connection request from the discoverer afterward.
2.4.4 Stopping Scanning
To stop scanning, you can call stopScan(). The discoverer can still request a connection to a discovered device afterward. Generally, once you find the device you want to connect to, call stopScan() to stop scanning.
2.5 Connection Setup
2.5.1 Requesting a Connection
When nearby devices are found, the discoverer can call requestConnect() to initiate connections.
Sample code:
Code:
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. */
}
});
}
You can show the list of discovered devices to users and allow them to choose which devices to connect to based on your requirements.
2.5.2 Confirming the Connection
After the discoverer requests to set up a connection with the broadcaster, the discoverer notifies the two parties of the connection setup by calling back the onEstablish() method of connectCallback. Both parties must accept the connection by calling acceptConnect() or reject the connection by calling rejectConnect(). The connection can be set up only when both endpoints accept the connection. If one or both of them choose to reject the connection, the connection fails. In either mode, the connection result is notified through the onResult() method.
Sample code:
Code:
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. */
}
};
This example shows a way for both parties to accept the connection automatically. You can use other methods to confirm the connection as required.
2.5.3 Verifying the Connection
Your app can provide a way for users to confirm the connection to a specified device. For example, you can use an authentication token, which may be a short random character string or number. Typically, this involves displaying the token on two devices and requiring the user to enter or confirm the token manually, similar to a Bluetooth pairing dialog box.
The following describes how to authenticate the connection by confirming the pairing code in a dialog box:
Sample code:
Code:
@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 Transmitting Data
After devices are connected, data objects can be transmitted through the connection. Data objects are classified into byte arrays, files, and streams. You can call sendData() to send data and call onReceived() of DataCallback to receive data.
2.6.1 Data Types
1) BYTES
You can call Data.fromBytes() to create a data object of the Data.Type.BYTES type.
The following is the sample code for sending byte arrays:
Code:
Data bytesData = Data.fromBytes(new byte[] {0xA, 0xA, 0xA, 0xA, 0xA});
Nearby.getTransferEngine(getApplicationContext()).sendData(toEndpointId, bytesData); Note: The size of byte arrays cannot exceed 32 KB.
Note: The size of byte arrays cannot exceed 32 KB.
The following is the sample code for receiving byte arrays:
Code:
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. */
}
}
Note: Different from data of FILE and STREAM types, data of the BYTES type is sent as chunks, and the subscriber does not need to wait for the TRANSFER_STATE_SUCCESS status code. When onReceived() is called, you can call data.asBytes() to obtain the full data.
2) FILE
You can call Data.fromFile() to create a data object of the Data.Type.FILE type.
The following is the sample code for sending a file:
Code:
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. */
}
To be more efficient, you can create a FILE type using ParcelFileDescriptor, which minimizes file replication.
Sample code:
Code:
ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
Data fileData = Data.fromFile(pfd);
When a file is received, it is saved in the Download directory on the subscriber’s device with the name defined as the character string converted by fileData.getId(). After the transmission is complete, you can obtain the FILE object.
Sample code:
Code:
/* We can get the received file in the Download folder. */
File payloadFile = fileData.asFile().asJavaFile();
3) STREAM
You can call Data.fromStream() to create a data object of the Data.Type.STREAM type.
Sample code:
Code:
URL url = new URL("https://developers.huawei.com");
Data streamData = Data.fromStream(url.openStream());
Nearby.getTransferEngine(getApplicationContext()).sendData(toEndpointId, streamData);
When the onTransferUpdate() callback is successful, the subscriber can call streamData.asStream().asInputStream() or streamData.asStream().asParcelFileDescriptor() to obtain the stream object.
Sample code:
Code:
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 Updating the Data Transmission Progress
The onTransferUpdate() method in the DataCallBack callback class provides the data sending or receiving progress update. Both parties can display the transmission progress to users in forms like a progress bar.
2.6.3 Canceling Data Transmission
To cancel the transmission during data receiving or sending, you can call the cancelDataTransfer() method of the TransferEngine class.
2.7 Disconnecting from the Remote Endpoint
To disconnect from the remote endpoint, you can call the disconnect() method of the DiscoveryEngine class. Once this API is called, this endpoint cannot send or receive data.
Conclusion
Using Nearby Connection, you can implement the following functions for your app:
1. Local multi-player gaming: Players can connect with each other’s device flexibly and enjoy a smooth gaming experience enabled by low-latency, stable, and reliable data transmission.
2. Offline file transfer: Files can be transferred at a speed of up to 80 Mbit/s without consuming any mobile data.
If you are interested and want to learn more, check our development guide at https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/introduction-0000001050040566.

Messaging Text Through HMS Nearby Connection

More information like this, you can visit HUAWEI Developer Forum​
Introduction to Nearby Connection:
Nearby Connection is one of the API of HUAWEI Nearby Service that allows applications to easily discover and connect to nearby devices, and exchange data with nearby devices without having Internet.
Example: We can chat to a friend and can share files such as photos and videos without connecting to the Internet.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Prerequisites:
Java JDK (1.8 or later)
Android (API level 21 or higher)
HMS Core (APK) 4.0.0.300 or later
Android Studio 3.X
Two Huawei phones (with USB cables) or Non Huawei Phone with updated version of HMS Core App for app running and debugging.
Integration:
1. Create a project in android studio and Huawei AGC.
2. Provide the SHA-256 Key in App Information Section.
3. Enable Huawei Nearby Service.
4. Download the agconnect-services.json from AGCand save into app directory.
5. In root build.gradle
Navigate to allprojects->repositories and buildscript->repositories and add the given line.
Code:
maven { url 'http://developer.huawei.com/repo/' }
In dependency add class path.
Code:
classpath 'com.huawei.agconnect:agcp:1.2.1.301'
6. In app build.gradle
Add compatibility mode to JDK 1.8
Code:
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
Add Implementation
Code:
implementation 'com.huawei.hms:nearby:5.0.1.300'
Apply plugin
Code:
apply plugin: 'com.huawei.agconnect'
7. Permissions in Manifest
Code:
<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" />
Nearby Connection Process:
Nearby connection process can be divided into four phases.
1. Broadcast and scanning
2. Connection setup
3. Data transmission
4. Disconnection
Let us go to the details.
1) Broadcast and scanning
This is the initial phase, here the broadcaster starts broadcasting, and the discoverer starts scanning to discover the broadcaster.
We can classify the functionalities of the initial phase as given:
a) Starting Broadcasting:
The broadcaster calls startBroadcasting() to start broadcasting
Code:
public void doStartBroadcasting() {
BroadcastOption.Builder advBuilder = new BroadcastOption.Builder();
advBuilder.setPolicy(Policy.POLICY_STAR);
discoveryEngine.startBroadcasting(myNameStr, myServiceId, connectCallback, advBuilder.build());
}
b) Starting Scanning:
The discoverer calls startScan() to start scanning to discover nearby devices.
Code:
public void doStartScan() {
ScanOption.Builder discBuilder = new ScanOption.Builder();
discBuilder.setPolicy(Policy.POLICY_STAR);
discoveryEngine.startScan(myServiceId, scanEndpointCallback, discBuilder.build());
}
c) Notifies the scanning result:
The scanning listening callback class instance notifies the discoverer of the scanning result. For example, a device is discovered or a device is lost, onFound() and onLost() methods are used here for the purpose.
Code:
private ScanEndpointCallback scanEndpointCallback =
new ScanEndpointCallback() {
@Override
public void onFound(String endpointId, ScanEndpointInfo discoveryEndpointInfo) {
mEndpointId = endpointId;
discoveryEngine.requestConnect(myNameStr, mEndpointId, connectCallback);
}
@Override
public void onLost(String endpointId) {
Log.d(TAG, "Nearby Connection Demo app: Lost endpoint: " + endpointId);
}
};
d) Stopping Broadcasting:
To stop broadcasting, we can call stopBroadcasting() method. The broadcaster cannot receive a connection request from the discoverer afterward.
Code:
discoveryEngine.stopBroadcasting();
e) Stopping Scanning:
To stop scanning, we can call stopScan() method. Once you find the device you want to connect to, call stopScan() to stop scanning.
Code:
discoveryEngine.stopScan();
2) Connection setup
In this phase the discoverer initiates a connection and starts a symmetric authentication process, and the two endpoints independently accept or reject the connection request.
We can classify the functionalities of the second phase as given:
a) Initiating a Connection:
When nearby devices are found, the discoverer can call requestConnect() to initiate connections request to the broadcaster.
Code:
discoveryEngine.requestConnect(myNameStr, mEndpointId, connectCallback);
b) Confirming the Connection:
After the discoverer requests to set up a connection with the broadcaster, the discoverer notifies the two parties of the connection setup by calling back the onEstablish() method of connectCallback. Both parties must accept the connection by calling acceptConnect() or reject the connection by calling rejectConnect(). The connection can be set up only when both parties accept the connection. If one or both of them choose to reject the connection, the connection fails. In either mode, the connection result is notified through the onResult() method.
Code:
private ConnectCallback connectCallback =
new ConnectCallback() {
@Override
public void onEstablish(String endpointId, ConnectInfo connectionInfo) {
transferEngine = Nearby.getTransferEngine(getApplicationContext());
discoveryEngine.acceptConnect(endpointId, dataCallback);
ToastUtil.showShortToastTop(getApplicationContext(), "Let's chat!");
sendBtn.setEnabled(true);
msgEt.setEnabled(true);
connectBtn.setEnabled(false);
connectTaskResult = StatusCode.STATUS_SUCCESS;
if (myNameStr.compareTo(friendNameStr) > 0) {
discoveryEngine.stopScan();
} else {
discoveryEngine.stopBroadcasting();
}
}
@Override
public void onResult(String endpointId, ConnectResult resolution) {
mEndpointId = endpointId;
}
@Override
public void onDisconnected(String endpointId) {
ToastUtil.showShortToastTop(getApplicationContext(), "Disconnect.");
connectTaskResult = StatusCode.STATUS_NOT_CONNECTED;
sendBtn.setEnabled(false);
connectBtn.setEnabled(true);
msgEt.setEnabled(false);
myNameEt.setEnabled(true);
friendNameEt.setEnabled(true);
}
};
3) Data transmission:
After the connection is set up, the two endpoints start data exchange in this phase.
We can classify the functionalities of the third phase as given:
a) Sending data:
After the connection is set up, both endpoints can call sendData() to send data to the remote endpoint.
Code:
private void sendMessage() {
msgStr = msgEt.getText().toString() ;
Data data = Data.fromBytes(msgStr.getBytes(Charset.defaultCharset()));
Log.d(TAG, "myEndpointId " + mEndpointId);
transferEngine.sendData(mEndpointId, data).addOnCompleteListener(task -> {
task.addOnSuccessListener(su -> {
Log.i(TAG, "sendData success. Message:" + msgStr);
}).addOnFailureListener(e -> {
Log.e(TAG, "sendData failed, Message:" + msgStr + "cause: " + e.getMessage());
});
});
MessageBean item = new MessageBean();
item.setMyName(myNameStr);
item.setFriendName(friendNameStr);
item.setMsg(msgStr.split(":")[0]);
item.setType(MessageBean.TYPE_SEND_TEXT);
msgList.add(item);
adapter.notifyDataSetChanged();
msgEt.setText("");
messageListView.setSelection(messageListView.getBottom());
}
b) Receiving data:
The data subscriber is notified by onReceived() that the data is received.
Code:
@Override
public void onReceived(String string, Data data) {
Log.d(TAG, "onPayloadReceived, payload.getType() = " + data.getType());
Log.d(TAG, "onPayloadReceived, string=" + string);
switch (data.getType()) {
case Data.Type.BYTES:
msgStr = new String(data.asBytes(), UTF_8);
Log.d(TAG, "onReceived success. msgSt: " + msgStr);
MessageBean item = new MessageBean();
item.setMyName(myNameStr);
item.setFriendName(friendNameStr);
item.setMsg(msgStr);
item.setType(MessageBean.TYPE_RECEIVE_TEXT);
msgList.add(item);
adapter.notifyDataSetChanged();
messageListView.setSelection(messageListView.getBottom());
break;
default:
Log.i(TAG, "the other Unknown data type.");
return;
}
c) Updating the Progress:
The onTransferUpdate() method provides the data sending or receiving progress update.
Code:
@Override
public void onTransferUpdate(String string, TransferStateUpdate update) {
long payloadId = update.getDataId();
Log.d(TAG, "onTransferUpdate.payloadId:" + payloadId);
if (update.getStatus() == TransferStateUpdate.Status.TRANSFER_STATE_SUCCESS) {
Log.d(TAG, "PayloadTransferUpdate.Status.SUCCESS");
Data payload = incomingFilePayloads.remove(payloadId);
if (payload != null) {
Log.d(TAG, "payload.getType() " + payload.getType());
completedFilePayloads.put(payloadId, payload);
}
} else {
Log.d(TAG, "PayloadTransferUpdate.Status=" + update.getStatus());
}
}
d) Cancelling Transmission:
To cancel the transmission during data receiving or sending, we can call the cancelDataTransfer() method of the TransferEngine class.
4) Disconnection
This is final phase, here we can call the disconnect() method of the DiscoveryEngine class to disconnect from the remote endpoint.
Code:
discoveryEngine.disconnectAll();
The remote endpoint is notified by onDisconnected() of the disconnection. Once this API is called, this endpoint cannot send or receive data.
Code:
@Override
public void onDisconnected(String endpointId) {
ToastUtil.showShortToastTop(getApplicationContext(), "Disconnect.");
connectTaskResult = StatusCode.STATUS_NOT_CONNECTED;
sendBtn.setEnabled(false);
connectBtn.setEnabled(true);
msgEt.setEnabled(false);
myNameEt.setEnabled(true);
friendNameEt.setEnabled(true);
}
Screenshots:
Reference:
https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/Nearby-Service-Nearby-Connection-Service-Nearby-Connection

Health Kit | Data Controller Sample

{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Hello everyone, in this article, we’ll develop an android application using the Huawei Health kit’s data controller feature. Lets get start it.
About the Service
HUAWEI Health Kit (Health Kit for short) allows ecosystem apps to access fitness and health data of users based on their HUAWEI ID and authorization. For consumers, Health Kit provides a mechanism for fitness and health data storage and sharing based on flexible authorization. For developers and partners, Health Kit provides a data platform and fitness and health open capabilities, so that they can build related apps and services based on a multitude of data types. Health Kit connects the hardware devices and ecosystem apps to provide consumers with health care, workout guidance, and ultimate service experience.
Configure your project on AppGallery Connect
Registering a Huawei ID
You need to register a Huawei ID to use the plugin. If you don’t have one, follow the instructions here.
Preparations for Integrating HUAWEI HMS Core
First of all, you need to integrate Huawei Mobile Services with your application. I will not get into details about how to integrate your application but you can use this tutorial as step by step guide.
Add required dependency to the app-level build.gradle file.
Code:
defaultConfig {
minSdkVersion 24
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "en", "zh-rCN", "tr"
}
}
dependencies {
implementation 'com.huawei.hms:health:5.0.3.300'
}
Lets add the required permissions to the AndroidManifest.xml file.
Code:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
Applying for Health Kit
You should select the data access permissions that must be applied for the product.
For more detail you should visit: https://developer.huawei.com/consumer/en/doc/apply-kitservice-0000001050071707-V5
Developing Your App
Signing In and Applying for Scopes
The developer’s app calls the related APIs to display HUAWEI ID sign-in screen and authorization screen. The app can only access data upon user authorization. The user can select the data types to be authorized and grant only some data permissions.
Code:
public class HealthkitActivity extends AppCompatActivity {
private static final String TAG = "KitConnectActivity";
// Request code for displaying the authorization screen using the startActivityForResult method.
// The value can be defined by developers.
private static final int REQUEST_SIGN_IN_LOGIN = 1002;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_healthkit);
signIn();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// Handle the sign-in response.
handleSignInResult(requestCode, data);
}
private void signIn() {
Log.i(TAG, "begin sign in");
List<Scope> scopeList = new ArrayList<>();
// Add scopes to apply for. The following only shows an example.
// Developers need to add scopes according to their specific needs.
// View and save steps in HUAWEI Health Kit.
scopeList.add(new Scope(Scopes.HEALTHKIT_STEP_BOTH));
// View and save height and weight in HUAWEI Health Kit.
scopeList.add(new Scope(Scopes.HEALTHKIT_HEIGHTWEIGHT_BOTH));
// View and save the heart rate data in HUAWEI Health Kit.
scopeList.add(new Scope(Scopes.HEALTHKIT_HEARTRATE_BOTH));
// Used for recording real-time steps in HUAWEI Health Kit.
// scopeList.add(new Scope(Scopes.HEALTHKIT_STEP_REALTIME));
// Used for recording real-time heartRate in HUAWEI Health Kit.
//scopeList.add(new Scope(Scopes.HEALTHKIT_HEARTRATE_REALTIME));
// View and save activityRecord in HUAWEI Health Kit.
// scopeList.add(new Scope(Scopes.HEALTHKIT_ACTIVITY_RECORD_BOTH));
// Configure authorization parameters.
HuaweiIdAuthParamsHelper authParamsHelper =
new HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM);
HuaweiIdAuthParams authParams =
authParamsHelper.setIdToken().setAccessToken().setScopeList(scopeList).createParams();
// Initialize the HuaweiIdAuthService object.
final HuaweiIdAuthService authService = HuaweiIdAuthManager.getService(getApplicationContext(), authParams);
Task<AuthHuaweiId> authHuaweiIdTask = authService.silentSignIn();
final Context context = this;
// Add the callback for the call result.
authHuaweiIdTask.addOnSuccessListener(new OnSuccessListener<AuthHuaweiId>() {
@Override
public void onSuccess(AuthHuaweiId huaweiId) {
// The silent sign-in is successful.
Log.i(TAG, "silentSignIn success");
Toast.makeText(context, "silentSignIn success", Toast.LENGTH_LONG).show();
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception exception) {
// The silent sign-in fails.
// This indicates that the authorization has not been granted by the current account.
if (exception instanceof ApiException) {
ApiException apiException = (ApiException) exception;
Log.i(TAG, "sign failed status:" + apiException.getStatusCode());
Toast.makeText(context, "sign failed status:" + apiException.getStatusCode(), Toast.LENGTH_LONG).show();
Log.i(TAG, "begin sign in by intent");
Toast.makeText(context, "begin sign in by intent", Toast.LENGTH_LONG).show();
// Call the sign-in API using the getSignInIntent() method.
Intent signInIntent = authService.getSignInIntent();
startActivityForResult(signInIntent, REQUEST_SIGN_IN_LOGIN);
}
}
});
}
private void handleSignInResult(int requestCode, Intent data) {
// Handle only the authorized responses
if (requestCode != REQUEST_SIGN_IN_LOGIN) {
return;
}
// Obtain the authorization response from the intent.
HuaweiIdAuthResult result = HuaweiIdAuthAPIManager.HuaweiIdAuthAPIService.parseHuaweiIdFromIntent(data);
if (result != null) {
Log.d(TAG, "handleSignInResult status = " + result.getStatus() + ", result = " + result.isSuccess());
Toast.makeText(this, "handleSignInResult status = "+ result.getStatus() + ", result = " + result.isSuccess(), Toast.LENGTH_LONG).show();
if (result.isSuccess()) {
Log.d(TAG, "sign in is success");
Toast.makeText(this, "sign in is success", Toast.LENGTH_LONG).show();
// Obtain the authorization result.
HuaweiIdAuthResult authResult =
HuaweiIdAuthAPIManager.HuaweiIdAuthAPIService.parseHuaweiIdFromIntent(data);
}
}
}
}
For details about the sign-in process, please refer to HUAWEI Account Kit Development Guide.
DataController
After integrating Health Kit, the app is able to call ten methods in DataController to perform operations on the fitness and health data. The methods include:
insert: inserts data.
delete: deletes data.
update: updates data.
read: reads data.
readTodaySummation: queries the statistical data of the current day.
readDailySummation: queries the statistical data of multiple days.
clearAll: clears data of the app from the device and cloud.
Inserting the User’s Fitness and Health Data
Insert the user’s fitness and health data into the Health platform.
Code:
public class HealthDataControllerActivity extends AppCompatActivity {
private static final String TAG = "DataController";
// Object of controller for fitness and health data, providing APIs for read/write, batch read/write, and listening
private DataController dataController;
// Internal context object of the activity
private Context context;
// PendingIntent, required when registering or unregistering a listener within the data controller
private PendingIntent pendingIntent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_health_data_controller);
context = this;
logInfoView = (TextView) findViewById(R.id.data_controller_log_info);
logInfoView.setMovementMethod(ScrollingMovementMethod.getInstance());
initDataController();
syncAllData = findViewById(R.id.syncAllData);
syncAllData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
syncAllData(dataController);
}
});
}
/**
* Initialize a data controller object.
*/
private void initDataController() {
// Obtain and set the read & write permissions for DT_CONTINUOUS_STEPS_DELTA and DT_INSTANTANEOUS_HEIGHT.
// Use the obtained permissions to obtain the data controller object.
HiHealthOptions hiHealthOptions = HiHealthOptions.builder()
.addDataType(DataType.DT_CONTINUOUS_STEPS_DELTA, HiHealthOptions.ACCESS_READ)
.addDataType(DataType.DT_CONTINUOUS_STEPS_DELTA, HiHealthOptions.ACCESS_WRITE)
.addDataType(DataType.DT_INSTANTANEOUS_HEIGHT, HiHealthOptions.ACCESS_READ)
.addDataType(DataType.DT_INSTANTANEOUS_HEIGHT, HiHealthOptions.ACCESS_WRITE)
.build();
AuthHuaweiId signInHuaweiId = HuaweiIdAuthManager.getExtendedAuthResult(hiHealthOptions);
dataController = HuaweiHiHealth.getDataController(context, signInHuaweiId);
}
/**
* Use the data controller to add a sampling dataset.
*
* @param view (indicating a UI object)
* @throws ParseException (indicating a failure to parse the time string)
*/
public void insertData(View view) throws ParseException {
// 1. Build a DataCollector object.
DataCollector dataCollector = new DataCollector.Builder().setPackageName(context)
.setDataType(DataType.DT_CONTINUOUS_STEPS_DELTA)
.setDataStreamName("STEPS_DELTA")
.setDataGenerateType(DataCollector.DATA_TYPE_RAW)
.build();
// 2. Create a sampling dataset set based on the data collector.
final SampleSet sampleSet = SampleSet.create(dataCollector);
// 3. Build the start time, end time, and incremental step count for a DT_CONTINUOUS_STEPS_DELTA sampling point.
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date startDate = dateFormat.parse("2020-09-23 09:00:00");
//Date startDate = dateFormat.parse(start_Date.getText().toString());
Date endDate = dateFormat.parse("2020-09-23 09:05:00");
//Date startDate = dateFormat.parse(end_Date.getText().toString());
/*try {
// Enter the start time and end time. The standard UNIX timestamp is used for storage, without considering the time zone differences.
startDate = dateFormat.parse("2020-03-17 09:00:00");
endDate = dateFormat.parse("2020-03-17 09:05:00");
} catch (ParseException e) {
logger("Time parsing error");
}*/
int stepsDelta = 1000;
// 4. Build a DT_CONTINUOUS_STEPS_DELTA sampling point.
SamplePoint samplePoint = sampleSet.createSamplePoint()
.setTimeInterval(startDate.getTime(), endDate.getTime(), TimeUnit.MILLISECONDS);
samplePoint.getFieldValue(Field.FIELD_STEPS_DELTA).setIntValue(stepsDelta);
// 5. Save a DT_CONTINUOUS_STEPS_DELTA sampling point to the sampling dataset.
// You can repeat steps 3 through 5 to add more sampling points to the sampling dataset.
sampleSet.addSample(samplePoint);
// 6. Call the data controller to insert the sampling dataset into the Health platform.
Task<Void> insertTask = dataController.insert(sampleSet);
// 7. Calling the data controller to insert the sampling dataset is an asynchronous operation.
// Therefore, a listener needs to be registered to monitor whether the data insertion is successful or not.
insertTask.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void result) {
logger("Success insert an SampleSet into HMS core");
showSampleSet(sampleSet);
logger(SPLIT);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
printFailureMessage(e, "insert");
}
});
}
Deleting the User’s Fitness and Health Data
Only historical data that has been inserted by the current app can be deleted from the Health platform.
Code:
/**
* Use the data controller to delete the sampling data by specific criteria.
*
* @param view (indicating a UI object)
* @throws ParseException (indicating a failure to parse the time string)
*/
public void deleteData(View view) throws ParseException {
// 1. Build the condition for data deletion: a DataCollector object.
DataCollector dataCollector = new DataCollector.Builder().setPackageName(context)
.setDataType(DataType.DT_CONTINUOUS_STEPS_DELTA)
.setDataStreamName("STEPS_DELTA")
.setDataGenerateType(DataCollector.DATA_TYPE_RAW)
.build();
// 2. Build the time range for the deletion: start time and end time.
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date startDate = dateFormat.parse("2020-08-27 09:00:00");
Date endDate = dateFormat.parse("2020-08-27 09:05:00");
// 3. Build a parameter object as the conditions for the deletion.
DeleteOptions deleteOptions = new DeleteOptions.Builder().addDataCollector(dataCollector)
.setTimeInterval(startDate.getTime(), endDate.getTime(), TimeUnit.MILLISECONDS)
.build();
// 4. Use the specified condition deletion object to call the data controller to delete the sampling dataset.
Task<Void> deleteTask = dataController.delete(deleteOptions);
// 5. Calling the data controller to delete the sampling dataset is an asynchronous operation.
// Therefore, a listener needs to be registered to monitor whether the data deletion is successful or not.
deleteTask.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void result) {
logger("Success delete sample data from HMS core");
logger(SPLIT);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
String errorCode = e.getMessage();
String errorMsg = HiHealthStatusCodes.getStatusCodeMessage(Integer.parseInt(errorCode));
logger(errorCode + ": " + errorMsg);
printFailureMessage(e, "delete");
}
});
}
Updating the User’s Fitness and Health Data
Code:
/**
* Use the data controller to modify the sampling data by specific criteria.
*
* @param view (indicating a UI object)
* @throws ParseException (indicating a failure to parse the time string)
*/
public void updateData(View view) throws ParseException {
// 1. Build the condition for data update: a DataCollector object.
DataCollector dataCollector = new DataCollector.Builder().setPackageName(context)
.setDataType(DataType.DT_CONTINUOUS_STEPS_DELTA)
.setDataStreamName("STEPS_DELTA")
.setDataGenerateType(DataCollector.DATA_TYPE_RAW)
.build();
// 2. Build the sampling dataset for the update: create a sampling dataset
// for the update based on the data collector.
SampleSet sampleSet = SampleSet.create(dataCollector);
// 3. Build the start time, end time, and incremental step count for
// a DT_CONTINUOUS_STEPS_DELTA sampling point for the update.
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date startDate = dateFormat.parse("2020-08-27 09:00:00");
Date endDate = dateFormat.parse("2020-08-27 09:05:00");
int stepsDelta = 2000;
// 4. Build a DT_CONTINUOUS_STEPS_DELTA sampling point for the update.
SamplePoint samplePoint = sampleSet.createSamplePoint()
.setTimeInterval(startDate.getTime(), endDate.getTime(), TimeUnit.MILLISECONDS);
samplePoint.getFieldValue(Field.FIELD_STEPS_DELTA).setIntValue(stepsDelta);
// 5. Add an updated DT_CONTINUOUS_STEPS_DELTA sampling point to the sampling dataset for the update.
// You can repeat steps 3 through 5 to add more updated sampling points to the sampling dataset for the update.
sampleSet.addSample(samplePoint);
// 6. Build a parameter object for the update.
// Note: (1) The start time of the modified object updateOptions cannot be greater than the minimum
// value of the start time of all sample data points in the modified data sample set
// (2) The end time of the modified object updateOptions cannot be less than the maximum value of the
// end time of all sample data points in the modified data sample set
UpdateOptions updateOptions =
new UpdateOptions.Builder().setTimeInterval(startDate.getTime(), endDate.getTime(), TimeUnit.MILLISECONDS)
.setSampleSet(sampleSet)
.build();
// 7. Use the specified parameter object for the update to call the
// data controller to modify the sampling dataset.
Task<Void> updateTask = dataController.update(updateOptions);
// 8. Calling the data controller to modify the sampling dataset is an asynchronous operation.
// Therefore, a listener needs to be registered to monitor whether the data update is successful or not.
updateTask.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void result) {
logger("Success update sample data from HMS core");
logger(SPLIT);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
printFailureMessage(e, "update");
String errorCode = e.getMessage();
String errorMsg = HiHealthStatusCodes.getStatusCodeMessage(Integer.parseInt(errorCode));
logger(errorCode + ": " + errorMsg);
}
});
}
Querying the User’s Fitness and Health Data
To read historical data from the Health platform, for example, to read the number of steps taken within a period of time, you can specify the read conditions in ReadOptions. For example, you can specify the data collector, data type, and detailed data. The dataset that matches the query criteria will be returned.
Code:
/**
* Use the data controller to query the sampling dataset by specific criteria.
*
* @param view (indicating a UI object)
* @throws ParseException (indicating a failure to parse the time string)
*/
public void readData(View view) throws ParseException {
// 1. Build the condition for data query: a DataCollector object.
DataCollector dataCollector = new DataCollector.Builder().setPackageName(context)
.setDataType(DataType.DT_CONTINUOUS_STEPS_DELTA)
.setDataStreamName("STEPS_DELTA")
.setDataGenerateType(DataCollector.DATA_TYPE_RAW)
.build();
// 2. Build the time range for the query: start time and end time.
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date startDate = dateFormat.parse("2020-08-27 09:00:00");
Date endDate = dateFormat.parse("2020-08-27 09:05:00");
try {
// Enter the start time and end time. The standard UNIX timestamp is used for storage, without considering the time zone differences. Data points within the specified timestamp range will be queried.
startDate = dateFormat.parse("2020-03-17 09:00:00");
endDate = dateFormat.parse("2020-03-17 09:05:00");
} catch (ParseException exception) {
logger("Time parsing error");
}
// 3. Build the condition-based query objec
ReadOptions readOptions = new ReadOptions.Builder().read(dataCollector)
.setTimeRange(startDate.getTime(), endDate.getTime(), TimeUnit.MILLISECONDS)
.build();
// 4. Use the specified condition query object to call the data controller to query the sampling dataset.
Task<ReadReply> readReplyTask = dataController.read(readOptions);
// 5. Calling the data controller to query the sampling dataset is an asynchronous operation.
// Therefore, a listener needs to be registered to monitor whether the data query is successful or not.
readReplyTask.addOnSuccessListener(new OnSuccessListener<ReadReply>() {
@Override
public void onSuccess(ReadReply readReply) {
logger("Success read an SampleSets from HMS core");
for (SampleSet sampleSet : readReply.getSampleSets()) {
showSampleSet(sampleSet);
}
logger(SPLIT);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
printFailureMessage(e, "read");
String errorCode = e.getMessage();
String errorMsg = HiHealthStatusCodes.getStatusCodeMessage(Integer.parseInt(errorCode));
logger(errorCode + ": " + errorMsg);
}
});
}
Querying the Statistical Fitness and Health Data of the User of the Day
Code:
/**
* Use the data controller to query the summary data of the current day by data type.
*
* @param view (indicating a UI object)
*/
public void readToday(View view) {
// 1. Use the specified data type (DT_CONTINUOUS_STEPS_DELTA) to call the data controller to query
// the summary data of this data type of the current day.
Task<SampleSet> todaySummationTask = dataController.readTodaySummation(DataType.DT_CONTINUOUS_STEPS_DELTA);
// 2. Calling the data controller to query the summary data of the current day is an
// asynchronous operation. Therefore, a listener needs to be registered to monitor whether
// the data query is successful or not.
// Note: In this example, the inserted data time is fixed at 2020-08-27 09:05:00.
// When commissioning the API, you need to change the inserted data time to the current date
// for data to be queried.
todaySummationTask.addOnSuccessListener(new OnSuccessListener<SampleSet>() {
@Override
public void onSuccess(SampleSet sampleSet) {
logger("Success read today summation from HMS core");
if (sampleSet != null) {
showSampleSet(sampleSet);
}
logger(SPLIT);
}
});
todaySummationTask.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
printFailureMessage(e, "readTodaySummation");
String errorCode = e.getMessage();
String errorMsg = HiHealthStatusCodes.getStatusCodeMessage(Integer.parseInt(errorCode));
logger(errorCode + ": " + errorMsg);
}
});
}
Querying the Statistical Fitness and Health Data of the User of Multiple Days
Code:
/**
* Querying the Summary Fitness and Health Data of the User on the Local Device of the Current Day
*
* @param view (indicating a UI object)
*/
public void currentDay(View view) {
//Call the DataController to query the statistical value of the DT_CONTINUOUS_STEPS_DELTA data type of the current day.
// The query time range starts from 00:00:00 of the day and ends at the system timestamp when the API is called.
// Calling this API will query all data points with the start time or end time being in the specified time range.
// The sum value of the queried data points will be returned.
int endTime = 20200827;
int startTime = 20200818;
Task<SampleSet> daliySummationTask =dataController.readDailySummation(DataType.DT_CONTINUOUS_STEPS_DELTA, startTime, endTime);
//Calling the data controller to query the summary data of the current day is an asynchronous operation.
// Therefore, a listener needs to be registered to monitor whether the data query is successful or not.
daliySummationTask.addOnSuccessListener(new OnSuccessListener<SampleSet>() {
@Override
public void onSuccess(SampleSet sampleSet) {
logger("Success read daily summation from HMS core");
if (sampleSet != null) {
showSampleSet(sampleSet);
}
logger(SPLIT);
}
});
daliySummationTask.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
logger("readTodaySummation" + e.toString());
}
});
}
Clearing the User’s Fitness and Health Data from the Device and Cloud
Call the clearAll method of the DataController to delete data inserted by the current app from the device and cloud
Code:
/**
* Clear all user data from the device and cloud.
*
* @param view (indicating a UI object)
*/
public void clearCloudData(View view) {
// 1. Call the clearAll method of the data controller to delete data
// inserted by the current app from the device and cloud.
Task<Void> clearTask = dataController.clearAll();
// 2. Calling the data controller to clear user data from the device and cloud is an asynchronous operation.
// Therefore, a listener needs to be registered to monitor whether the clearance is successful or not.
clearTask.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void result) {
logger("clearAll success");
logger(SPLIT);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
printFailureMessage(e, "clearAll");
}
});
}
We successfully integrated Huawei Health Kit’s Data Controller feature into our project. Here’s the result.
Resources:
https://developer.huawei.com/consumer/en/doc/datacontroller-develop-0000001050071677-V5
Related Links
Original post: https://medium.com/huawei-developers/health-kit-data-controller-sample-a9d29b3ba651
Hi Nice information, does Huawei Health Kit provide auto sync data from health devices ( fit bit ) or we need to gather data and provide them to HMS health kit.

What is Huawei Cloud DB? How To Use?

{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Hello everyone,
In this article, I would like to tell you about Cloud DB, which online storage that Huawei offers to developers.
What is Cloud DB ?
Although Cloud DB is still in beta version, it is a successful and seamless database structure. In addition to the ease of use, attracts developers with its management and a user-friendly interface. In addition to providing data availability, consistency and security, CloudDB provides seamless data synchronization between the device and the cloud.
If you do not have a server when developing applications, Cloud DB server easily solves our data storage, maintenance and distribution. Also CloudDB is free.
Cloud DB provides 500 GB of data volume for each application, and supports 2000 connections. Looking at their counterparts, it is understood how large and how important these numbers are.
Cloud DB Structure
Object Type: It represents each table in the standard database. In other words, each table containing data and columns is called Object Type.
Cloud DB Zone : Represents the data zone on the cloud side. According to the classic database, Cloud DB Zone is the name of the database or schema name.
Data Entires : It is the area that shows the added data. Here, you can add, update and delete data. When you add data, you will realize that the tables you are used to are in the same way. And while using this technology, it prevents you from suffering from strangeness.
How To Using Cloud DB ?
Now let’s see how to use Cloud DB. Since Cloud DB is still in beta, you have to send a mail to activate this service in your app. To use Cloud DB, you need to create an app after creating your Huawei Developer account. After completing all the necessary steps, you have to request the activation of the service by sending an e-mail to [email protected] with the sample header below. The service will be activated within 1–3 business days after your mail arrives. And you can start using it freely.
Cloud DB –Company Name — Developer ID — App ID
After your service activated, log in to AppGallery Connect and select your app under the heading “My Apps”. You can then access the “Cloud DB” panel under the “Build” tab from the menu on the left side of the page, by moving to the “Develop” tab in the upper left. After the page is loaded, activate the service by clicking the “Enable Now” button in the upper right corner.
A Cloud DB Zone must first be created. After then object types should be created. When creating the object type, column names must be entered and the primary key must be identified in the window that opens. Also, at this step, you can edit access control options for the object type. In the image of the below you can find, which users have which permissions should be given. After creating DB Zone and Object Type, Cloud DB provides to export data models and Helper class as JSON or Java for use them in the app without wasting time. After exporting the model classes as Java, you have to add these classes to the relevant directory of the app and start communicate with Cloud DB.
Cloud DB library should be added to the build.gradle file under the project’s app directory and the compatibility mode of the Java source code should be set as 1.8. For this, the following codes should be added to the gradle file and wait downloading the necessary dependencies by click Sync Now.
Code:
dependencies {
implementation 'com.huawei.agconnect:agconnect-database:1.2.1.301'
}
compileOptions {
targetCompatibility = 1.8
}
Let’s create a class named CloudDBZoneWrapper for all database operations. By defining all the upsert, query operations in this class, call these methods in the activity/fragment to be used. Thanks to this metods you can coding your app without clutter.
Firstly, Cloud DB objects should created to be used in this class.
Code:
private AGConnectCloudDB mCloudDB;
private CloudDBZone mCloudDBZone;
private ListenerHandler mRegister;
private CloudDBZoneConfig mConfig;
After then create an instance from the AGConnectCloudDB object in a constructor method.
Code:
public CloudDBZoneWrapper() {
mCloudDB = AGConnectCloudDB.getInstance();
}
Then, initAGConnectCloudDB method must be created for calling on the app landing page. This method must be run before the app is opened, before starting all DB operations. I follow the code by adding a log in every process step to follow the errors more easily. In this way, you can easily find out which line is wrong.
Code:
public static void initAGConnectCloudDB(Context context) {
AGConnectCloudDB.initialize(context);
Log.w(Constants.DB_ZONE_WRAPPER, "initAGConnectCloudDB" );
}
Next, creating Object Type, and open/close DBZone operations should be coding. These methods will be used before upsert and query operations for open DBZone and create Object Types.
Code:
public void createObjectType() {
try {
mCloudDB.createObjectType(ObjectTypeInfoHelper.getObjectTypeInfo());
Log.w(Constants.DB_ZONE_WRAPPER, "createObjectTypeSuccess " );
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "createObjectTypeError: " + e.getMessage());
}
}
public void openCloudDBZone() {
mConfig = new CloudDBZoneConfig("DB ZONE NAME HERE",
CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE,
CloudDBZoneConfig.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC);
mConfig.setPersistenceEnabled(true);
Log.w(Constants.DB_ZONE_WRAPPER, "openCloudDBZoneSuccess " );
try {
mCloudDBZone = mCloudDB.openCloudDBZone(mConfig, true);
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "openCloudDBZoneError: " + e.getMessage());
}
}
public void closeCloudDBZone() {
try {
mCloudDB.closeCloudDBZone(mCloudDBZone);
Log.w(Constants.DB_ZONE_WRAPPER, "closeCloudDBZoneSuccess " );
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "closeCloudDBZoneError: " + e.getMessage());
}
}
Now, the necessary methods for upsert and query operations should be written. But first, a few callbacks have to be added to get the results of these actions to the activities and fragments where the actions are operated. In this way, all DB operations will be gathered in a single class, without the activity being too tired and without crowd of code.
Code:
public interface UiCallBack {
void onAddOrQuery(List<TableUser> userList);
void isLastID(int lastID);.
void isDataUpsert(Boolean state);
}
public void addCallBacks(UiCallBack uiCallBack) {
mUiCallBack = uiCallBack;
}
Now the necessary method for upsert operation should be written. Upsert contains both insert and update operations. If upsert with a certain ID, the data in the related line will update. If it is upsert by new ID, a new line will added. So, both insert and update are carried out with the same method.
First, it should be checked whether DBZone is created or not. If DBZone has an error, will not upsert data. Then, upsert with CloudDBZoneTask object. Since I will add data to the User table, I gave the user object as a parameter to this method. If you need to add data to other tables, you should create a new method and give the object of the related table as a parameter. When upsert operation complated , if upsert is successful, it must return true, if occured an error, return false. For this, at the beginning of the method, a variable named Boolean state was defined and its first value was set as false. Then, if upsert is successful, state is set to true, and if error occurs, method will return false.
Code:
public void insertUser(TableUser user) {
boolean state = false;
if (mCloudDBZone == null) {
Log.w(Constants.DB_ZONE_WRAPPER, "INSERT USER : CloudDBZone is null, try re-open it");
return;
}
CloudDBZoneTask<Integer> upsertTask = mCloudDBZone.executeUpsert(user);
if (mUiCallBack == null) {
return;
}
upsertTask.addOnSuccessListener(new OnSuccessListener<Integer>() {
@Override
public void onSuccess(Integer cloudDBZoneResult) {
state = true;
Log.w(Constants.DB_ZONE_WRAPPER, "INSERT USER : upsert " + cloudDBZoneResult + " records");
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
state = false;
mUiCallBack.updateUiOnError("INSERT USER : Insert user info failed");
}
});
if (mUiCallBack != null) {
mUiCallBack.isDataUpsert(state);
}
}
Now, let’s make a query in the this class. For this, I will get the list of users which I added to the database with the same model class. Two methods are required when making query request. The first is the getAllUsers method for DB operations, and the other is called userListResult method, for add data to array. Firstly, CloudDBZone control should be done in getAllUsers method. Then, query request will make by creating a task. If the request is successful, the userListResult method is calling with the user object. If the request is successful, the userListResult method will called with the user object. An arrayList is created in the userListResult method, and all results are thrown into this list. Then, by adding a callback, the results can be called up in the activity or fragment.
Code:
public void getAllUsers() {
if (mCloudDBZone == null) {
Log.w(Constants.DB_ZONE_WRAPPER, "GET USER DETAIL : CloudDBZone is null, try re-open it");
return;
}
CloudDBZoneTask<CloudDBZoneSnapshot<TableUser>> queryTask = mCloudDBZone.executeQuery(
CloudDBZoneQuery.where(TableUser.class),
CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY);
queryTask.addOnSuccessListener(new OnSuccessListener<CloudDBZoneSnapshot<TableUser>>() {
@Override
public void onSuccess(CloudDBZoneSnapshot<TableUser> snapshot) {
userListResult (snapshot);
Log.w(Constants.DB_ZONE_WRAPPER, "GET USER DETAIL : GoResults: ");
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (mUiCallBack != null) {
mUiCallBack.updateUiOnError("GET USER DETAIL : Query user list from cloud failed");
}
}
});
}
private void userListResult (CloudDBZoneSnapshot<TableUser> snapshot) {
CloudDBZoneObjectList<TableUser> userInfoCursor = snapshot.getSnapshotObjects();
List<TableUser> userInfoList = new ArrayList<>();
try {
while (userInfoCursor.hasNext()) {
TableUser userInfo = userInfoCursor.next();
userInfoList.add(userInfo);
Log.w(Constants.DB_ZONE_WRAPPER, "USER DETAIL RESULT : processQueryResult: " + userInfo.getUser_city());
}
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "USER DETAIL RESULT : processQueryResult: " + e.getMessage());
}
snapshot.release();
if (mUiCallBack != null) {
mUiCallBack.onAddOrQuery(userInfoList);
}
}
Thus, all database operations within the CloudDBZoneWrapper class have been completed. Now let’s examine how to data upsert or query in activity or fragment.
The UiCallBack method in the CloudDBZoneWrapper class should be implement as the CloudDBZoneWrapper.UiCallBack in the class you which want to do database operations. In this way, all added call back methods will override in this class. Then the CloduDBZoneWrapper object and a new Handler need to be created in the activity. The CloudDBZoneWrapper object must be called within the constructor method. Sample codes are as follows.
Code:
private MyHandler mHandler = new MyHandler();
private CloudDBZoneWrapper mCloudDBZoneWrapper;
private static final class MyHandler extends Handler {
@Override
public void handleMessage(@NonNull Message msg) {
// dummy
}
}
public ProfileFragment() {
mCloudDBZoneWrapper = new CloudDBZoneWrapper();
}
Then the object type must be created in onCreate and Cloud DB Zone must be opened. If query is not related to an event, if the data should be loaded while the page is opening, call the getAllUsers method after creating the object type and opening the DB Zone in onCreate.
Code:
mHandler.post(() -> {
mCloudDBZoneWrapper.addCallBacks(ProfileFragment.this);
mCloudDBZoneWrapper.createObjectType();
mCloudDBZoneWrapper.openCloudDBZone();
mCloudDBZoneWrapper.getAllUsers();
});
The callback method, which was added to the getAllUsers method in the CloudDBZoneWrapper class, was override in the fragment. In this override method, all data can be used. For example, if wants to access the information of a user with ID = 3 in the user list, the turned list by insert a for loop, user information with ID = 3 is obtained.
Code:
@Override
public void onAddOrQuery(List<TableUser> userList) {
for(int i = 0; i <= userList.size()-1; i++){
if(userList.get(i).getId().equals(“3”)){
userName = userList.get(i).getUser_name());
userPhone = userList.get(i).getUser_phone();
userMail = userList.get(i).getUser_mail();
userAge = userList.get(i).getUser_age();
userGender = userList.get(i).getUser_gender();
}
}
}
Now let’s make an upsert. As I writed before, Upsert includes update and insert operations. Both operations are uses the same method. If you want to update a row of data, you must post with the ID information of the data in that row. If a new data is to be added, it should be posted with a new ID.
At this point, Cloud DB has a lack. Unfortunately, the auto increment don’t have when creating the object type. In other words, ID value does not increase automatically when data is added. It has to be given manually. I solved this problem by getting the last ID in the table and increasing it.
Now, create a method called updateUser to update and send back the user information I have previously got in this fragment. Next, a new user object should created here and the values ​​should set. If there is an not to be changed data (as in the example, age and gender ), old values ​​must be set in them. Finally, make post request by calling the insertUser method created in the CloudDBZoneWrapper class.
Code:
public void updateProfile(){
TableUser user = new TableUser();
user.setUser_id(“3”);
user.setUser_name(“Yeni İsim”));
user.setUser_phone(“Yeni Telefon”);
user.setUser_mail(“Yeni Mail”);
user.setUser_age(userAge);
user.setUser_gender(userGender);
mHandler.post(() -> {
mCloudDBZoneWrapper.insertUser(user);
});
}
In the fragment, help from call back methods should be obtained to find out whether the transaction is successful. Status check can be done in call back method added to insertUser method.
Code:
@Override
public void isDataUpsert(Boolean state) {
if(state){
//successful
}else{
//unsuccessful
}
}
Finally, it is worth mentioning that there is an authentication requirement to upsert. Since Cloud DB is still in beta, absolutely has some errors. But as you can see, all of them are solved easily. For authentication, Auth Service offered by Huawei to developers should be used. A service that is very easy to use. You can find the Auth Service link on the below. After the authentication, your upsert will work. If authentication is not done, the result of upsert will return false.
"https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-auth-service-introduction"
Well explained, can we store large amount of data into cloud Db, is there any limitations.
sujith.e said:
Well explained, can we store large amount of data into cloud Db, is there any limitations.
Click to expand...
Click to collapse
Yes, Cloud DB has a limit but I think you can store large data in Cloud DB. Because Cloud DB provides 500 GB of data volume for each application, and supports 2000 connections.

Categories

Resources