简介

运动健康服务(Health Kit)是为华为生态应用打造的基于华为帐号和用户授权的运动健康数据开放平台。面向消费者,支持运动健康数据存储、灵活授权分享机制;面向开发者和合作伙伴提供数据平台和运动健康开放能力。开发者和合作伙伴可以基于多种数据类型,构建运动健康领域应用与服务。Health Kit连接硬件设备与生态应用,为消费者提供健康关怀和运动指导,打造优质服务体验。

本篇codelab介绍了睡眠记录应用SleepTracker,它集成了多个HMS Core服务,可以在装有HMS Core(APK)的安卓手机上运行。该应用基于MVVM架构开发,可生动展示从华为穿戴设备(手环、智能手表等)获取的数据,提供睡眠状态记录、闹钟等功能。

要使用HMS Core服务,您需要:

您需要什么

硬件要求

软件要求

需要的知识点

您将会学到什么

在这个codelab中,您将学到如何:

集成HMS Core能力,需要完成以下准备工作:

具体操作,请按照《HUAWEI HMS Core集成准备》中的详细说明来完成。

如需下载SleepTracker示例代码,请至:GitHub

提示:需要通过注册成为开发者才能完成集成准备中的操作。

服务开通

  1. 登录AppGallery Connect,单击"我的项目",找到您的项目及需要开通服务的应用。单击"项目设置">"API管理"。
  2. 找到"认证服务"、"华为帐号"及"推送服务",打开服务开关,即可开通服务。

集成SDK

华为提供Maven仓来集成SDK。开发应用前,需要先把相关服务的SDK集成到Android Studio项目中。

  1. 登录AppGallery Connect,单击"我的项目",找到您的项目及需要开通服务的应用。单击"项目设置">"常规"。
  2. 在"应用"区域,单击agconnect-services.json,下载配置文件。
  3. 复制该JSON文件到app目录下。
  4. 打开项目级build.gradle文件。
  5. 在其中添加以下配置:
    repositories { … maven { url 'http://developer.huawei.com/repo/' } ... } dependencies { ... classpath 'com.huawei.agconnect:agcp:1.6.0.300' ... } allprojects { repositories { ... maven { url 'http://developer.huawei.com/repo/' } ... } }
  6. 打开应用级build.gradle文件。
  7. 在应用级build.gradle文件中,添加以下配置,单击"Sync Now"同步配置。
    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}' }

申请使用Health Kit

  1. 进入华为开发者联盟管理中心。
  2. 点击"Health Kit"。
  3. 点击"申请 Health kit 服务",同意协议后,进入数据权限申请页面。
  4. 勾选所需的数据权限。

具体申请过程请参见申请Health Kit服务

接下来,我们将创建不同睡眠类型的图表、用于选择日期的日历,在睡眠详情页面,用不同颜色展示睡眠状况细节。

调用华为帐号服务,进行用户授权。在华为帐号授权登录界面上,用户可以授权应用获取相关数据,并且可以选择数据类型,仅授权应用获取部分数据。应用可获取的数据仅限于华为运动健康app(以下简称"运动健康app")允许获取且用户授权了的数据。
如需获取运动健康app数据,授权应用获取其数据,并引导用户完成华为帐号及运用健康app的授权工作。

首先,添加可获取的数据范围。

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; }

初始化SettingController和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); }

查看授权状态。如尚未授权,引导用户完成授权。

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, Void> 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,用户可在应用首页查询当天睡眠数据,或查询任意一天的睡眠数据。首先,我们来开发查询当日睡眠数据的功能。

查询当日睡眠数据

创建任务,设置日期类型为DT_CONTINUOUS_SLEEP,查询当日睡眠数据。

Kotlin

val todaySummaryTask = dataController.readTodaySummation(DataType.DT_CONTINUOUS_SLEEP)

Java

Task todaySummaryTask = dataController.readTodaySummation(DataType.DT_CONTINUOUS_SLEEP);

调用相关接口,查询当日某类型数据的统计数据。查询时间从当日00:00:00开始,到接口调用时系统的时间戳结束。

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); } });

最后,监听数据变化,并依此更新界面。

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"); } }); }

查询某日睡眠数据

接下来,查询某一天的睡眠数据,需要用到startTime和endTime参数。需要注意的是,在接下来的示例中,这两个参数的值是相同的。

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); } }); }); }

集成推送服务前,可以先分析本codelab中的代码。欲了解推送服务,请参见开发指南
通过PushManager类获取access token和设备ID token,然后发送消息。

通过SharedPreferences保存access token。

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()); } } ); }

获取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()); }

发送通知。

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()); } } ); }

通过NotificationCompat,将不同睡眠类型对应的通知显示在通知栏里。

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(); }

创建并在AndroidManifest.xml中定义NOTIFICATION_SERVICE,通过NOTIFICATION_SERVICE在onStart中获取access token。

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);

创建PushService类,通过NOTIFICATION_SERVICE接收信息、获取push token。(注意:需先在AndroidManifest.xml中定义NOTIFICATION_SERVICE。)

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()); }

用户可以通过扫二维码关闭闹钟,其中扫码在AlarmScanActivity中实现。使用统一扫码服务中FrameLayout类里的RemoteView获取二维码。

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);

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

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

运动健康服务 (Health Kit)

统一扫码服务(Scan Kit)

华为帐号服务(Account Kit)

认证服务(Auth Service)

推送服务(Push Kit)

本Codelab中的demo源码下载地址如下:
源码下载。

Code copied