Health Kit allows ecosystem apps to access the fitness and health data of users based on their HUAWEI IDs 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 open fitness and health capabilities, so that you can build related apps and services based on a multitude of data types. Health Kit connects hardware devices and ecosystem apps to provide consumers with health care, workout guidance, and ultimate service experience.
This codelab introduces SleepTracker. This app integrates multiple HMS Core services and runs on Android phones with HMS Core (APK) installed. SleepTracker, using the MVVM architecture, provides the sleep tracking ability and an alarm clock by visualizing data coming from wearable Huawei devices like bands and smart watches.
To use those kits, you will need to:
In this codelab, you will learn how to:
To integrate HMS Core services, you must complete the following preparations:
For details, please refer to Preparations for Integrating HUAWEI HMS Core
You can download the sample code for SleepTracker at GitHub.
Note: You need to register as a developer to complete the operations above.
Huawei provides the HMS Core SDK that can be integrated by using the Maven repository. Before developing your app, you will need to integrate the HMS Core SDK into your Android Studio project.
dependencies {
implementation 'com.huawei.hms:hwid:{version}'
implementation 'com.huawei.hms:push:{version}'
implementation 'com.huawei.agconnect:agconnect-core:{version}'
implementation 'com.huawei.agconnect:agconnect-auth:{version}'
implementation 'com.huawei.hms:health:{version}'
implementation 'com.huawei.hms:scan:{version}'
}
For details, please refer to Applying for Health Kit.
In this codelab, we will create a chart for different sleep types and a calendar for date selection. Also, we will display sleep details on the details page with different colors.
The app uses Account Kit to start the user authorization process. When the HUAWEI ID sign-in and authorization pages are displayed, the user can grant the corresponding data access permissions. The user can select the data types to be authorized and grant only some data permissions. The data that your app can access is within the range of data approved by the Health app and also within the scope granted by the user.
If you want to access the Health app data, enable the Health app authorization process and guide the user to complete HUAWEI ID and Health app authorization.
Add the data scopes.
Kotlin:
private fun getScopes(): MutableList {
val scopeList: MutableList = LinkedList()
scopeList.add(HuaweiIdAuthAPIManager.HUAWEIID_BASE_SCOPE)
scopeList.add(Scope(Scopes.HEALTHKIT_SLEEP_READ))
return scopeList
}
Java:
private List getScopes() {
List scopeList = new LinkedList<>();
scopeList.add(HuaweiIdAuthAPIManager.HUAWEIID_BASE_SCOPE);
scopeList.add(new Scope(Scopes.HEALTHKIT_SLEEP_READ));
return scopeList;
}
Initialize SettingController and HuaweiIdAuthManager.
Kotlin:
private var mHuaweiIdAuthService: HuaweiIdAuthService
private val mapper = HuaweiAccountMapper()
init {
val params = HuaweiIdAuthParamsHelper(
HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM
)
.setAccessToken()
.setScopeList(getScopes())
.setIdToken()
.setEmail()
.createParams()
dataController = DataControllerImpl(androidContext()).dataController,
mSettingController = settingController
mHuaweiIdAuthService = HuaweiIdAuthManager.getService(context, params)
}
Java:
public HuaweiAccountServiceImpl(@NotNull Context context, @NotNull DataController dataController, SettingController mSettingController) {
this.context = context;
this.dataController = dataController;
this.mSettingController = mSettingController;
this.mapper = new HuaweiAccountMapper();
HuaweiIdAuthParams params = (new HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM))
.setAccessToken().setScopeList(this.getScopes()).setIdToken().setEmail().createParams();
this.mHuaweiIdAuthService = HuaweiIdAuthManager.getService(this.context, params);
}
Check whether the app has been authorized. If not, prompt the user to perform the authorization.
Kotlin:
override fun checkOrAuthorizeHealth(intent: (Intent) -> Unit) {
val healthAppSettingDataShareHealthKitActivityScheme =
"huaweischeme://healthapp/achievement?module=kit"
val authTask = mSettingController.healthAppAuthorization
authTask.addOnSuccessListener { result ->
if (Boolean.TRUE == result) {
Log.i(Constants.loginActivityTAG, context.getString(R.string.check_authorize_success))
} else {
val healthKitSchemaUri: Uri =
Uri.parse(healthAppSettingDataShareHealthKitActivityScheme)
val healtIntent = Intent(Intent.ACTION_VIEW, healthKitSchemaUri)
if (healtIntent.resolveActivity(context.packageManager) != null) {
intent.invoke(healtIntent)
} else {
Log.w(
Constants.loginActivityTAG,
context.getString(R.string.auth_not_resolve)
)
}
}
}.addOnFailureListener { exception ->
if (exception != null) {
Log.i(Constants.loginActivityTAG, exception.message.toString())
}
}
}
Java:
@Override
public void checkOrAuthorizeHealth(Function1 intent) {
String healthAppSettingDataShareHealthKitActivityScheme =
"huaweischeme://healthapp/achievement?module=kit";
Task<Boolean> authTask = mSettingController.getHealthAppAuthorization();
authTask.addOnSuccessListener(aBoolean -> {
if (Boolean.TRUE.equals(aBoolean)) {
Log.i(Constants.loginActivityTAG, context.getString(R.string.check_authorize_success));
} else {
Uri healthKitSchemaUri = Uri.parse(healthAppSettingDataShareHealthKitActivityScheme);
Intent healthIntent = new Intent(Intent.ACTION_VIEW, healthKitSchemaUri);
if (healthIntent.resolveActivity(context.getPackageManager()) != null) {
intent.invoke(healthIntent);
} else {
Log.w(
Constants.loginActivityTAG,
context.getString(R.string.auth_not_resolve)
);
}
}
});
authTask.addOnFailureListener(e -> {
if (e != null) {
Log.i(Constants.loginActivityTAG, e.getMessage());
}
});
}
Health Kit allows users to query the sleep data of the current day on the home screen, or the sleep data of any selected day. First we will develop our query function for the sleep data of the current day.
Create a task to query the statistical data of the date type DT_CONTINUOUS_SLEEP of the current day.
Kotlin:
val todaySummaryTask = dataController.readTodaySummation(DataType.DT_CONTINUOUS_SLEEP)
Java:
Task<SampleSet> todaySummaryTask = dataController.readTodaySummation(DataType.DT_CONTINUOUS_SLEEP);
Call the relevant API for querying the statistical data of the current day. This API is used to query the statistics of a certain data type on 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.
Kotlin:
todaySummaryTask.addOnSuccessListener { sampleSet ->
emitter.onSuccess(sampleSet)
}
todaySummaryTask.addOnFailureListener { exception ->
if (exception.message!!.contains("50005")) {
emitter.onError(exception)
}
}
Java:
todaySummaryTask.addOnSuccessListener(emitter::onSuccess);
todaySummaryTask.addOnFailureListener(e -> {
if (Objects.requireNonNull(e.getMessage()).contains("50005")) {
emitter.onError(e);
}
});
Listen to data changes and update the UI accordingly.
Kotlin:
viewModel.readToday()
private fun listenPreferencesData() {
viewModel.dailyDataResultLiveData.observe(viewLifecycleOwner, Observer {
when (it?.responseType) {
Status.SUCCESSFUL -> {
logger("Success read daily summation from HMS core")
if (it.data!!.isEmpty) {
setNoDataDisplay()
} else {
it.data?.let { sampleSet -> cleanSampleSet(sampleSet) }
setDataDisplay()
}
logger(splitSeparator)
}
Status.ERROR -> {
viewModel.silentSignIn()
}
}
})
}
Java:
viewModel.getValue().readToday();
private void listenPreferencesData() {
viewModel.getValue().dailyDataResultLiveData.observe(getViewLifecycleOwner(), sampleSetData -> {
switch (sampleSetData.getResponseType()) {
case ERROR:
viewModel.getValue().silentSignIn();
case SUCCESSFUL:
logger("Success read daily summation from HMS core");
assert sampleSetData.getData() != null;
if (sampleSetData.getData().isEmpty()) {
setNoDataDisplay();
sleepDataObj.sleepDate = Constants.defaultDate;
} else {
if (sampleSetData.getData() != null) {
cleanSampleSet(sampleSetData.getData());
}
setDataDisplay();
}
logger(splitSeparator);
case LOADING:
Log.i(Constants.mySleepFragmentTAG, "Loading");
}
});
}
Query the sleep data of a selected day. startTime and endTime parameters are used. In the following code, these two parameters have the same values.
Kotlin:
override fun readDailyData(startTime: Int, endTime: Int): Single<SampleSet> {
return Single.create { emitter ->
val dailySummationTask =
dataController.readDailySummation(
DataType.DT_CONTINUOUS_SLEEP,
startTime,
endTime)
dailySummationTask.addOnSuccessListener { sampleSet ->
emitter.onSuccess(sampleSet)
}
dailySummationTask.addOnFailureListener { exception ->
if (exception.message!!.contains("50005")) {
emitter.onError(exception)
}
}
}
}
Java:
@Override
public Single<SampleSet> readDailyData(Integer startTime, Integer endTime) {
return Single.create(emitter -> {
Task<SampleSet> dailySummaryTask = dataController
.readDailySummation(DataType.DT_CONTINUOUS_SLEEP, startTime, endTime);
dailySummaryTask.addOnSuccessListener(emitter::onSuccess);
dailySummaryTask.addOnFailureListener(e -> {
if (Objects.requireNonNull(e.getMessage()).contains("50005")) {
emitter.onError(e);
}
});
});
}
You can analyze the code in this codelab before integrating Push Kit. For more details about this kit, please refer to Push Kit Development Guide.
Use a PushManager class to obtain the access token and push token and send notifications.
Store the access token through SharedPreferences.
Kotlin:
fun getAccessToken(sleepType: String, context: Context) {
AccessTokenClient.getClient().create(AccessTokenInterface::class.java)
.createAccessToken(
Constants.grantType,
Constants.appSecret,
Constants.appId
).enqueue(object : Callback<AccessToken> {
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
Log.e(Constants.pushServiceTAG, "Error: ${t.message}")
}
override fun onResponse(
call: Call<AccessToken>,
response: Response<AccessToken>
) {
if (response.isSuccessful) {
Log.d(
Constants.pushServiceTAG,
"Access Token: ${response.body()?.accessToken}"
)
val sharedPreferences = context.getSharedPreferences(
Constants.packageName,
Context.MODE_PRIVATE
)
val sharedPushToken = sharedPreferences.getString(Constants.pushTokenStr, pushToken)
accessToken = response.body()?.accessToken
sharedPushToken?.let { sendNotification(it, sleepType) }
}
}
})
}
Java:
public static void getAccessToken(String sleepType, Context context) {
AccessTokenClient.getClient().create(AccessTokenInterface.class)
.createAccessToken(Constants.grantType, Constants.appSecret, Constants.appId).enqueue(
new Callback<AccessToken>() {
@Override
public void onResponse(Call<AccessToken> call, Response<AccessToken> response) {
if (response.isSuccessful()) {
if (response.body() != null) {
SharedPreferences sharedPreferences = context.getSharedPreferences(
Constants.packageName,
Context.MODE_PRIVATE
);
String sharedPushToken = sharedPreferences.getString(Constants.pushTokenStr, pushToken);
accessToken = response.body().getAccessToken();
if (sharedPushToken != null) {
sendNotification(sharedPushToken, sleepType);
}
}
}
}
@Override
public void onFailure(Call<AccessToken> call, Throwable t) {
Log.e(Constants.pushServiceTAG, "Error: " + t.getMessage());
}
}
);
}
Obtain the push token.
Kotlin:
val appId: String =
AGConnectServicesConfig.fromContext(context).getString("client/app_id")
pushToken = HmsInstanceId.getInstance(context).getToken(appId, "HCM")
if (!TextUtils.isEmpty(pushToken)) {
val sharedPreferences = context.getSharedPreferences(
Constants.packageName,
Context.MODE_PRIVATE
)
sharedPreferences.edit().putString(Constants.pushTokenStr, pushToken).apply()
Log.i(Constants.pushServiceTAG, "get token: $pushToken")
}
Java:
try {
String appId = AGConnectServicesConfig.fromContext(context).getString("client/app_id");
pushToken = HmsInstanceId.getInstance(context).getToken(appId, "HCM");
if (!TextUtils.isEmpty(pushToken)) {
SharedPreferences sharedPreferences = context.getSharedPreferences(
Constants.packageName,
Context.MODE_PRIVATE
);
sharedPreferences.edit().putString(Constants.pushTokenStr, pushToken).apply();
Log.i(Constants.pushServiceTAG, "Get Token: " + pushToken);
}
} catch (Exception e) {
Log.i(Constants.pushServiceTAG, "DeviceIdToken failed, " + e.getMessage());
}
Send the notification.
Kotlin:
private fun sendNotification(pushToken: String, sleepType: String) {
NotificationClient.getClient().create(NotificationInterface::class.java)
.createNotification(
"Bearer $accessToken",
selectNotificationMessageBody(sleepType, pushToken)
).enqueue(object : Callback<NotificationMessage> {
override fun onResponse(
call: Call<NotificationMessage>,
response: Response<NotificationMessage>
) {
if (response.isSuccessful) {
Log.d(Constants.pushServiceTAG, "Response ${response.body()}")
}
}
override fun onFailure(call: Call<NotificationMessage>, t: Throwable) {
Log.d(Constants.pushServiceTAG, "Error: ${t.message}")
}
})
}
Java:
private static void sendNotification(String pushToken, String sleepType) {
NotificationClient.getClient().create(NotificationInterface.class)
.createNotification("Bearer " + accessToken,
selectNotificationMessageBody(sleepType, pushToken)).enqueue(
new Callback<NotificationMessage>() {
@Override
public void onResponse(Call<NotificationMessage> call, Response<NotificationMessage> response) {
if (response.isSuccessful()) {
Log.d(Constants.pushServiceTAG, "Response " + response.body());
}
}
@Override
public void onFailure(Call<NotificationMessage> call, Throwable t) {
Log.e(Constants.pushServiceTAG, "Error: " + t.getMessage());
}
}
);
}
Show notifications on the notification bar by NotificationCompat according to the sleep scenario.
Kotlin:
when (sleepType) {
Constants.sleepStr -> {
return NotificationMessageBody.Builder(
Constants.goodNightTitle, Constants.goodNightBody[((0..4).random())],
arrayOf(pushToken)
).build()
}
Constants.wakeStr -> {
return NotificationMessageBody.Builder(
Constants.goodMorningTitle,
Constants.goodMorningBody[(0..5).random()],
arrayOf(pushToken)
).build()
}
Constants.sleepReportStr -> {
return NotificationMessageBody.Builder(
Constants.sleepReportTitle, Constants.sleepReportBody,
arrayOf(pushToken)
).build()
}
else -> {
return NotificationMessageBody.Builder(
Constants.notificationErrorTitle, Constants.notificationErrorBody,
arrayOf(pushToken)
).build()
}
}
Java:
Random rand = new Random();
switch (sleepType) {
case Constants.sleepStr:
return new NotificationMessageBody.Builder(
Constants.goodNightTitle,
Constants.goodNightBody.get(rand.nextInt(Constants.goodNightBody.size())),
Collections.singletonList(pushToken)
).build();
case Constants.wakeStr:
return new NotificationMessageBody.Builder(
Constants.goodMorningTitle,
Constants.goodMorningBody.get(rand.nextInt(Constants.goodNightBody.size())),
Collections.singletonList(pushToken)
).build();
case Constants.sleepReportStr:
return new NotificationMessageBody.Builder(
Constants.sleepReportTitle,
Constants.sleepReportBody,
Collections.singletonList(pushToken)
).build();
default:
return new NotificationMessageBody.Builder(
Constants.notificationErrorTitle,
Constants.notificationErrorBody,
Collections.singletonList(pushToken)
).build();
}
Create a NOTIFICATION_SERVICE which should be defined in AndroidManifest.xml. Obtain the access token in onStart through NOTIFICATION_SERVICE.
Kotlin:
val notification = NotificationCompat.Builder(this, Constants.CHANNEL_ID)
.setContentTitle(Constants.pushTitle)
.setContentText(Constants.pushBody)
.build()
startForeground(1001, notification)
PushManager.getAccessToken(intent!!.getStringExtra(Constants.sleepTypeStr)!!, this)
stopForeground(true)
return super.onStartCommand(intent, flags, startId)
<service android:name=".services.PushNotificationService" />
Java:
Notification notification = new NotificationCompat.Builder(this, Constants.CHANNEL_ID)
.setContentTitle(Constants.pushTitle)
.setContentText(Constants.pushBody)
.build();
startForeground(1001, notification);
if (intent != null) {
PushManager.getAccessToken(intent.getStringExtra(Constants.sleepTypeStr), this);
}
stopForeground(true);
return super.onStartCommand(intent, flags, startId);
Create a PushService class which helps with receiving messages from NOTIFICATION_SERVICE and obtaining the push token. (Note that NOTIFICATION_SERVICE should be defined in AndroidManifest.xml first.)
Kotlin:
if (!TextUtils.isEmpty(token)) {
val sharedPreferences = this.getSharedPreferences(
Constants.packageName,
android.content.Context.MODE_PRIVATE
)
sharedPreferences.edit().putString("pushToken", token).apply()
}
Log.i(Constants.pushServiceTAG, "Receive Token: $token")
Log.i(
Constants.pushServiceTAG, "getCollapseKey: " + message?.collapseKey
+ "\n getData: " + message?.data
+ "\n getFrom: " + message?.from
+ "\n getTo: " + message?.to
+ "\n getMessageId: " + message?.messageId
+ "\n getSendTime: " + message?.sentTime
+ "\n getMessageType: " + message?.messageType
+ "\n getTtl: " + message?.ttl
)
val notification: RemoteMessage.Notification = message!!.notification
Log.i(
Constants.pushServiceTAG, "\n getImageUrl: " + notification.imageUrl
+ "\n getTitle: " + notification.title
+ "\n getTitleLocalizationKey: " + notification.titleLocalizationKey
+ "\n getTitleLocalizationArgs: " + Arrays.toString(notification.titleLocalizationArgs)
+ "\n getBody: " + notification.body
+ "\n getBodyLocalizationKey: " + notification.bodyLocalizationKey
+ "\n getBodyLocalizationArgs: " + Arrays.toString(notification.bodyLocalizationArgs)
+ "\n getIcon: " + notification.icon
+ "\n getSound: " + notification.sound
+ "\n getTag: " + notification.tag
+ "\n getColor: " + notification.color
+ "\n getClickAction: " + notification.clickAction
+ "\n getChannelId: " + notification.channelId
+ "\n getLink: " + notification.link
+ "\n getNotifyId: " + notification.notifyId
)
Java:
super.onNewToken(token);
if (!TextUtils.isEmpty(token)) {
SharedPreferences sharedPreferences = this.getSharedPreferences(
Constants.packageName,
Context.MODE_PRIVATE
);
sharedPreferences.edit().putString("pushToken", token).apply();
}
Log.i(Constants.pushServiceTAG, "Receive Token: " + token);
super.onMessageReceived(message);
if (message != null) {
Log.i(
Constants.pushServiceTAG, "getCollapseKey: " + message.getCollapseKey()
+ "\n getData: " + message.getData()
+ "\n getFrom: " + message.getFrom()
+ "\n getTo: " + message.getTo()
+ "\n getMessageId: " + message.getMessageId()
+ "\n getSendTime: " + message.getSentTime()
+ "\n getMessageType: " + message.getMessageType()
+ "\n getTtl: " + message.getTtl());
}
In this codelab, QR code is scanned to stop alarm clocks. Enable QR code scanning in AlarmScanActivity. To obtain the QR Code, use RemoteView in the FrameLayout class of Scan Kit.
Kotlin:
val dm = resources.displayMetrics
val density = dm.density
mScreenWidth = resources.displayMetrics.widthPixels
mScreenHeight = resources.displayMetrics.heightPixels
val scanFrameSize = (scanFrameSize * density)
val rect = Rect()
rect.left = (mScreenWidth / 2 - scanFrameSize / 2).toInt()
rect.right = (mScreenWidth / 2 + scanFrameSize / 2).toInt()
rect.top = (mScreenHeight / 2 - scanFrameSize / 2).toInt()
rect.bottom = (mScreenHeight / 2 + scanFrameSize / 2).toInt()
remoteView =
RemoteView.Builder().setContext(this).setBoundingBox(rect)
.setFormat(HmsScan.QRCODE_SCAN_TYPE).build()
remoteView?.onCreate(savedInstanceState)
remoteView?.setOnResultCallback { result -> //judge the result is effective
println("QR CODE : " + result[0].getOriginalValue())
if (result != null && result.isNotEmpty() && result[0] != null && !TextUtils.isEmpty(
result[0].getOriginalValue()
) && result[0].originalValue == Constants.alarmOffQRResult
) {
val intentService = Intent(this, AlarmClock::class.java)
stopService(intentService)
finish()
}
}
val params =
FrameLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
rim.addView(remoteView, params)
Java:
DisplayMetrics dm = getResources().getDisplayMetrics();
float density = dm.density;
mScreenWidth = dm.widthPixels;
mScreenHeight = dm.heightPixels;
float scanFrameSize = (this.scanFrameSize * density);
Rect rect = new Rect();
rect.left = (int) (((double)mScreenWidth / 2) - (scanFrameSize / 2));
rect.left = (int) (((double)mScreenWidth / 2) - (scanFrameSize / 2));
rect.right = (int) ((double)mScreenWidth / 2 + scanFrameSize / 2);
rect.top = (int) ((double)mScreenHeight / 2 - scanFrameSize / 2);
rect.bottom = (int) ((double)mScreenHeight / 2 + scanFrameSize / 2);
remoteView = new RemoteView.Builder().setContext(this).setBoundingBox(rect)
.setFormat(HmsScan.QRCODE_SCAN_TYPE).build();
remoteView.onCreate(savedInstanceState);
remoteView.setOnResultCallback(result -> {
System.out.println("QR Code: " + result[0].getOriginalValue());
if (result[0] != null && !TextUtils.isEmpty(
result[0].getOriginalValue()
) && result[0].originalValue.equals(Constants.alarmOffQRResult)
) {
Intent intentService = new Intent(this, AlarmClock.class);
stopService(intentService);
finish();
}
});
FrameLayout.LayoutParams params =
new FrameLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
FrameLayout rim = findViewById(R.id.rim);
rim.addView(remoteView, params);
Well done. You have successfully completed this codelab and learned how to:
For more information, please click the following links:
You can download the incompleted sample code for SleepTracker at Github
Download