Developing Calorie Tracker App with Huawei Health Kit - Huawei Developers

Developing Calorie Tracker App with Huawei Health Kit
Hi everyone, In this article, we will develop a simple calorie tracker app with Huawei Health Kit using Kotlin language in Android Studio.
{
"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"
}
What is Huawei Health Kit?
It is a free kit that allows users to store fitness and health data collected by smartphones or other devices like smartwatches, smart bracelets, and pedometers. Besides, these data can be shared securely within the ecosystem.
Main Functions of Health Kit
Data Storage: Store your fitness and health data easily.
Click to expand...
Click to collapse
Data Openness: In addition to offering many fitness and healthcare APIs, it supports the sharing of various fitness and health data, including step count, weight, and heart rate.
Click to expand...
Click to collapse
Data Access Authorization Management: Users can manage the developer’s access to their health and fitness data, guaranteeing users’ data privacy and legal rights.
Click to expand...
Click to collapse
Device Access: It measures data from hardware devices via Bluetooth and allows us to upload these data.
Click to expand...
Click to collapse
Features of Health Tracker App
Our application consists of two screens. We can log in to our app by touching the “Login in with Huawei” button on the first screen through Huawei Account Kit. The next screen includes areas where we can add our “calorie” and “weight” information. We can graphically display the information we’ve recorded. And, we used the free library called MPAndroidChart to display it graphically. You can access the source codes of the application through this link via Github.
1- Huawei Core Integration
First, we need to create an account on the Console. And then, we should create a project and integrate it into our implementation. We can do this quickly by following the steps outlined in the link below, or we can do it with the official codelab.
Android | Integrating Your Apps With Huawei HMS Core
In this article, I will explain how you can integrate Huawei Mobile Services. Integrating Huawei Mobile Services (HMS)…medium.com
2- Huawei Health Kit Integration
We need to apply to the Health Kit service. After you log in to the Console through this link, click “Health Kit” as displayed in the image below.
Then we click on the “Apply for Health Kit” to complete our application.
We will request permission to use the data in our application. We will use “weight” and “calorie” data. So, we only ask for necessary permissions: “Access and add body height and weight” and“Access and add calories (include BMR)”
Then click the “Submit” button and finish all our processes.
Note: You’ll see that some of the options here are locked because they’re sensitive data. If you want to use sensitive data in your application, you can send an email titled “Applying for Health Kit open permissions” to “[email protected]” They will reply to your email as soon as possible. You can get more detailed information from this link.
After getting the necessary permissions on the console, let’s turn Android Studio to continue developing our app.
build.gradle(project) -> Add the required dependencies to the project-level build.gradle file.
Note: We added jitpack url for graphics library.
Code:
maven {url ‘https://developer.huawei.com/repo/'}
maven { url ‘https://jitpack.io' }
build.gradle(app) -> Open the build.gradle (app level) file. The following dependency will be sufficient for the Health Kit. But we will add the dependencies of the Account Kit and the graphics library.
Code:
implementation 'com.huawei.agconnect:agconnect-core:1.4.2.301'
implementation 'com.huawei.hms:hwid:5.1.0.301'
implementation 'com.huawei.hms:health:5.1.0.301'
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
Lastly, open the AndroidManifest.xml file and add the app id into Application tags as metadata info. There are two ways to learn our app id. The first one is: go to the Console, click the “Huawei Id” under the Development Section, then select your project and you will see the app id.
The second way is that you can find it inside “agconnect-services.json” file.
Code:
<meta-data
android:name="com.huawei.hms.client.appid"
android:value="Your App Id"/>
3- Developing the Application
Health Kit provides us 3 APIs. These are:
DataController: To perform operations such as data adding, updating, deleting, and reading on the fitness and health data.
ActivityRecordsController: To write activity records into the platform and update them.
AutoRecordController: To read real-time Fitness and Health data
We use DataController to process calories and weight data in our application. We already mentioned that the Health Kit is safe. We ask the user to permit us to use their health data.
activity_main.xml contains the logo, application name, an input button, as shown in the above screenshot.
Spoiler: activity_main.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tvAppName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/app_name"
android:textColor="@color/colorPrimary"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ivLogo" />
<com.huawei.hms.support.hwid.ui.HuaweiIdAuthButton
android:id="@+id/btnLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.7" />
<ImageView
android:id="@+id/ivLogo"
android:layout_width="172dp"
android:layout_height="113dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.3"
app:srcCompat="@drawable/ic_logo" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainAcitivity.kt contains the necessary codes for the login process.
Spoiler: MainActivity.kt
Code:
package com.huawei.healthtracker
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.huawei.hms.common.ApiException
import com.huawei.hms.hihealth.data.Scopes
import com.huawei.hms.support.api.entity.auth.Scope
import com.huawei.hms.support.hwid.HuaweiIdAuthManager
import com.huawei.hms.support.hwid.request.HuaweiIdAuthParams
import com.huawei.hms.support.hwid.request.HuaweiIdAuthParamsHelper
import com.huawei.hms.support.hwid.service.HuaweiIdAuthService
import com.huawei.hms.support.hwid.ui.HuaweiIdAuthButton
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var btnLogin: HuaweiIdAuthButton
private lateinit var mAuthParam: HuaweiIdAuthParams
private lateinit var mAuthService: HuaweiIdAuthService
private val REQUEST_SIGN_IN_LOGIN = 1001
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnLogin = findViewById(R.id.btnLogin)
btnLogin.setOnClickListener {
signIn()
}
}
private fun signIn() {
val scopeList = listOf(
Scope(Scopes.HEALTHKIT_CALORIES_BOTH),
Scope(Scopes.HEALTHKIT_HEIGHTWEIGHT_BOTH),
)
mAuthParam =
HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM).apply {
setIdToken()
.setAccessToken()
.setScopeList(scopeList)
}.createParams()
mAuthService = HuaweiIdAuthManager.getService(this, mAuthParam)
val authHuaweiIdTask = mAuthService.silentSignIn()
authHuaweiIdTask.addOnSuccessListener {
val intent = Intent(this, CalorieTrackerActivity::class.java)
startActivity(intent)
}
.addOnFailureListener {
startActivityForResult(mAuthService.signInIntent, REQUEST_SIGN_IN_LOGIN)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
REQUEST_SIGN_IN_LOGIN -> {
val authHuaweiIdTask = HuaweiIdAuthManager.parseAuthResultFromIntent(data)
if (authHuaweiIdTask.isSuccessful) {
val intent = Intent(this, CalorieTrackerActivity::class.java)
startActivity(intent)
} else {
Log.i(
TAG,
"signIn failed: ${(authHuaweiIdTask.exception as ApiException).statusCode}"
)
}
}
}
}
}
You should also make sure that you have added the permissions of the data as Scope. The user will see an authorization page when clicked the log in button. And, the authorization page displays permissions in the Scope field. These permissions are not marked by default so, the user should mark them.
On the CalorieTrackerActivity page, we can add and view our calorie and weight information.
activity_calorie_tracker.xml contains the design codes for Calorie Tracker Page.
Spoiler: activity_calorie_tracker.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CalorieTrackerActivity">
<Button
android:id="@+id/btnShowConsCal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Show Cons. Cal."
android:textAllCaps="false"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnAddConsumedCal" />
<Button
android:id="@+id/btnShowWeight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Show Weight"
android:textAllCaps="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/btnAddWeight" />
<Button
android:id="@+id/btnAddConsumedCal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Add"
android:textAllCaps="false"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/etConsumedCal" />
<Button
android:id="@+id/btnAddWeight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Add"
android:textAllCaps="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/etBurntCal" />
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="Consumed Calorie"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="Weight"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/etConsumedCal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:ems="10"
android:hint="Kcal"
android:inputType="number"
android:maxLength="4"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<EditText
android:id="@+id/etBurntCal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:ems="10"
android:hint="Kg"
android:inputType="number"
android:maxLength="3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/view_vertical"
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="@android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="@+id/btnAddConsumedCal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="32dp"
android:background="@color/colorCardBackground"
app:cardCornerRadius="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnShowWeight">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tvChartHead"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:text="Weekly Consumed Calories"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold" />
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/barchartWeeklyCal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:background="@android:color/white" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
We have already introduced Data Controllers. Now, let’s create a Data Controller and write permissions for the data we’re going to use.
Spoiler: DataController and Permissions
Code:
class CalorieTrackerActivity : AppCompatActivity() {
// ...
private lateinit var dataController: DataController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_calorie_tracker)
initDataController()
//...
}
private fun initDataController() {
val hiHealthOptions = HiHealthOptions.builder()
.addDataType(DataType.DT_CONTINUOUS_CALORIES_CONSUMED, HiHealthOptions.ACCESS_READ)
.addDataType(DataType.DT_CONTINUOUS_CALORIES_CONSUMED, HiHealthOptions.ACCESS_WRITE)
.addDataType(DataType.DT_INSTANTANEOUS_BODY_WEIGHT, HiHealthOptions.ACCESS_READ)
.addDataType(DataType.DT_INSTANTANEOUS_BODY_WEIGHT, HiHealthOptions.ACCESS_WRITE)
.build()
val signInHuaweiId = HuaweiIdAuthManager.getExtendedAuthResult(hiHealthOptions)
dataController = HuaweiHiHealth.getDataController(this, signInHuaweiId)
}
}
Using the addConsumedCalorie method, we can record our data through the Health Kit. But to do this, we need to set a time interval. Therefore, we entered the current time as the end time and a second before it as the start time.
Spoiler: addConsumedCalorie
Code:
private fun addConsumedCalorie(calorie: Float) {
val dataCollector: DataCollector = DataCollector.Builder().setPackageName(this)
.setDataType(DataType.DT_CONTINUOUS_CALORIES_CONSUMED)
.setDataGenerateType(DataCollector.DATA_TYPE_RAW)
.build()
val sampleSet = SampleSet.create(dataCollector)
val currentTime = System.currentTimeMillis()
val samplePoint = sampleSet.createSamplePoint()
.setTimeInterval(currentTime - 1, currentTime, TimeUnit.MILLISECONDS)
samplePoint.getFieldValue(Field.FIELD_CALORIES).setFloatValue(calorie)
sampleSet.addSample(samplePoint)
val insertTask: Task<Void> = dataController.insert(sampleSet)
insertTask.addOnSuccessListener {
We created a method which name is addWeightData, similar to the addConsumedCalorie method. But this time, the values we entered as the start time and end time must be the same. Otherwise, when we try to enter the weight information, the application will crash. We also changed the data types.
Spoiler: addWeightData
Code:
private fun addWeightData(weight: Float) {
val dataCollector: DataCollector = DataCollector.Builder().setPackageName(this)
.setDataType(DataType.DT_INSTANTANEOUS_BODY_WEIGHT)
.setDataGenerateType(DataCollector.DATA_TYPE_RAW)
.build()
val sampleSet = SampleSet.create(dataCollector)
val currentTime = System.currentTimeMillis()
val samplePoint = sampleSet.createSamplePoint()
.setTimeInterval(currentTime, currentTime, TimeUnit.MILLISECONDS)
samplePoint.getFieldValue(Field.FIELD_BODY_WEIGHT).setFloatValue(weight)
sampleSet.addSample(samplePoint)
val insertTask: Task<Void> = dataController.insert(sampleSet)
insertTask.addOnSuccessListener {
Toast.makeText(this, "Weight added successfully", Toast.LENGTH_SHORT).show()
}.addOnFailureListener { e ->
Toast.makeText(this, e.message.toString(), Toast.LENGTH_SHORT).show()
}
}
Let’s read the calories we consumed with the readConsumedData method. We’ve chosen a time range from a week ago to the current time. Then we’ve retrieved all the data in this time range and put it on the Map as a time-value. Lastly, we called the showCaloriesWeekly method to show these data in the Bar Chart.
Spoiler: readConsumedData
Code:
private fun readConsumedData() {
val caloriesMap = mutableMapOf<Long, Float>()
val endDate = System.currentTimeMillis()
val startDate = endDate - SIX_DAY_MILLIS
val readOptions = ReadOptions.Builder()
.read(DataType.DT_CONTINUOUS_CALORIES_CONSUMED)
.setTimeRange(startDate, endDate, TimeUnit.MILLISECONDS).build()
val readReplyTask = dataController.read(readOptions)
readReplyTask.addOnSuccessListener { readReply ->
for (sampleSet in readReply.sampleSets) {
if (sampleSet.isEmpty.not()) {
sampleSet.samplePoints.forEach {
caloriesMap.put(
it.getStartTime(TimeUnit.MILLISECONDS),
it.getFieldValue(Field.FIELD_CALORIES).asFloatValue()
)
}
} else {
Toast.makeText(this, "No data to show", Toast.LENGTH_SHORT).show()
}
}
showCaloriesWeekly(caloriesMap)
}.addOnFailureListener {
Log.i(TAG, it.message.toString())
}
}
We also use the readWeightData method to retrieve recorded weight information.
Spoiler: readWeightData
Code:
private fun readWeightData() {
val weightsMap = mutableMapOf<Long, Float>()
val endDate = System.currentTimeMillis()
val startDate = endDate - SIX_DAY_MILLIS
val readOptions = ReadOptions.Builder().read(DataType.DT_INSTANTANEOUS_BODY_WEIGHT)
.setTimeRange(startDate, endDate, TimeUnit.MILLISECONDS).build()
val readReplyTask = dataController.read(readOptions)
readReplyTask.addOnSuccessListener { readReply ->
for (sampleSet in readReply.sampleSets) {
if (sampleSet.isEmpty.not()) {
sampleSet.samplePoints.forEach {
weightsMap.put(
it.getStartTime(TimeUnit.MILLISECONDS),
it.getFieldValue(Field.FIELD_BODY_WEIGHT).asFloatValue()
)
}
} else {
Toast.makeText(this, "No data to show", Toast.LENGTH_SHORT).show()
}
}
showWeightsWeekly(weightsMap)
}.addOnFailureListener {
Log.i(TAG, it.message.toString())
}
}
We use the showCaloriesWeekly method to get last week’s data as a time-value. After getting values, we sum the data for every day in the last week. Finally, we call the initBarChart method to show our daily data on the Bar Chart.
Spoiler: showCaloriesWeekly
Code:
private fun showCaloriesWeekly(dataList: Map<Long, Float>) {
val arrangedValuesAsMap = mutableMapOf<Long, Float>()
val currentTimeMillis = System.currentTimeMillis()
var firstDayCal = 0f
var secondDayCal = 0f
var thirdDayCal = 0f
var fourthDayCal = 0f
var fifthDayCal = 0f
var sixthDayCal = 0f
var seventhDayCal = 0f
dataList.forEach { (time, value) ->
when (time) {
in getTodayStartInMillis()..currentTimeMillis -> {
seventhDayCal += value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS until getTodayStartInMillis() -> {
sixthDayCal += value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS * 2 until
getTodayStartInMillis() - ONE_DAY_MILLIS -> {
fifthDayCal += value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS * 3 until
getTodayStartInMillis() - ONE_DAY_MILLIS * 2 -> {
fourthDayCal += value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS * 4 until
getTodayStartInMillis() - ONE_DAY_MILLIS * 3 -> {
thirdDayCal += value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS * 5 until
getTodayStartInMillis() - ONE_DAY_MILLIS * 4 -> {
secondDayCal += value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS * 6 until
getTodayStartInMillis() - ONE_DAY_MILLIS * 5 -> {
firstDayCal += value
}
}
}
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS * 6, firstDayCal)
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS * 5, secondDayCal)
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS * 4, thirdDayCal)
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS * 3, fourthDayCal)
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS * 2, fifthDayCal)
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS, sixthDayCal)
arrangedValuesAsMap.put(getTodayStartInMillis(), seventhDayCal)
initBarChart(arrangedValuesAsMap)
}
showWeightWeekly works almost like the showCaloriesWeekly method. The only difference between them is that we don’t sum all the values for every day in the showWeightWeekly method. We only get the last value for every day.
Spoiler: showWeightsWeekly
Code:
private fun showWeightsWeekly(dataList: Map<Long, Float>) {
val arrangedValuesAsMap = mutableMapOf<Long, Float>()
val currentTimeMillis = System.currentTimeMillis()
var firstDayWeight = 0f
var secondDayWeight = 0f
var thirdDayWeight = 0f
var fourthDayWeight = 0f
var fifthDayWeight = 0f
var sixthDayWeight = 0f
var seventhDayWeight = 0f
dataList.forEach { (time, value) ->
when (time) {
in getTodayStartInMillis()..currentTimeMillis -> {
seventhDayWeight = value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS until getTodayStartInMillis() -> {
sixthDayWeight = value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS * 2 until
getTodayStartInMillis() - ONE_DAY_MILLIS -> {
fifthDayWeight = value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS * 3 until
getTodayStartInMillis() - ONE_DAY_MILLIS * 2 -> {
fourthDayWeight = value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS * 4 until
getTodayStartInMillis() - ONE_DAY_MILLIS * 3 -> {
thirdDayWeight = value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS * 5 until
getTodayStartInMillis() - ONE_DAY_MILLIS * 4 -> {
secondDayWeight = value
}
in getTodayStartInMillis() - ONE_DAY_MILLIS * 6 until
getTodayStartInMillis() - ONE_DAY_MILLIS * 5 -> {
firstDayWeight = value
}
}
}
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS * 6, firstDayWeight)
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS * 5, secondDayWeight)
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS * 4, thirdDayWeight)
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS * 3, fourthDayWeight)
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS * 2, fifthDayWeight)
arrangedValuesAsMap.put(getTodayStartInMillis() - ONE_DAY_MILLIS, sixthDayWeight)
arrangedValuesAsMap.put(getTodayStartInMillis(), seventhDayWeight)
initBarChart(arrangedValuesAsMap)
}
InitBarChart displays our data in graphical form.
Spoiler: initBarChart
Code:
private fun initBarChart(values: MutableMap<Long, Float>) {
var barIndex = 0f
val labelWeekdayNames = arrayListOf<String>()
val entries = ArrayList<BarEntry>()
val simpleDateFormat = SimpleDateFormat("E", Locale.US)
values.forEach { (time, value) ->
labelWeekdayNames.add(simpleDateFormat.format(time))
entries.add(BarEntry(barIndex, value))
barIndex++
}
barChart.apply {
setDrawBarShadow(false)
setDrawValueAboveBar(false)
description.isEnabled = false
setDrawGridBackground(false)
isDoubleTapToZoomEnabled = false
}
barChart.xAxis.apply {
setDrawGridLines(false)
position = XAxis.XAxisPosition.BOTTOM
granularity = 1f
setDrawLabels(true)
setDrawAxisLine(false)
valueFormatter = IndexAxisValueFormatter(labelWeekdayNames)
axisMaximum = labelWeekdayNames.size.toFloat()
}
barChart.axisRight.isEnabled = false
val legend = barChart.legend
legend.isEnabled = false
val dataSets = arrayListOf<IBarDataSet>()
val barDataSet = BarDataSet(entries, " ")
barDataSet.color = Color.parseColor("#76C33A")
barDataSet.setDrawValues(false)
dataSets.add(barDataSet)
val data = BarData(dataSets)
barChart.data = data
barChart.invalidate()
barChart.animateY(1500)
}
Extra
Besides adding and reading health & fitness data, Health Kit also includes to update data, delete data, and clear all data features. We didn’t use these features in our application, but let’s take a quick look.
updateWeight -> We can update data within a specified time range. If we want to use the weight information, we should give both sections the same time value. But If we would like to update a calorie value, we can give it a long time range. Additionally, It automatically adds a new weight or calorie value when there is no value to update at the specified time.
Spoiler: updateWeight
Code:
private fun updateWeight(weight: Float, startTimeInMillis: Long, endTimeInMillis: Long) {
val dataCollector: DataCollector = DataCollector.Builder().setPackageName(this)
.setDataType(DataType.DT_INSTANTANEOUS_BODY_WEIGHT)
.setDataGenerateType(DataCollector.DATA_TYPE_RAW)
.build()
val sampleSet = SampleSet.create(dataCollector)
val samplePoint = sampleSet.createSamplePoint()
.setTimeInterval(startTimeInMillis, endTimeInMillis, TimeUnit.MILLISECONDS)
samplePoint.getFieldValue(Field.FIELD_BODY_WEIGHT).setFloatValue(weight)
sampleSet.addSample(samplePoint)
val updateOptions = UpdateOptions.Builder()
.setTimeInterval(startTimeInMillis, endTimeInMillis, TimeUnit.MILLISECONDS)
.setSampleSet(sampleSet)
.build()
dataController.update(updateOptions)
.addOnSuccessListener {
Toast.makeText(this, "Weight has been updated successfully", Toast.LENGTH_SHORT)
.show()
}
.addOnFailureListener { e ->
Toast.makeText(this, e.message.toString(), Toast.LENGTH_SHORT).show()
}
}
deleteWeight -> It deletes the values in the specified range.
Spoiler: deleteWeight
Code:
private fun deleteWeight(startTimeInMillis: Long, endTimeInMillis: Long) {
val dataCollector: DataCollector = DataCollector.Builder().setPackageName(this)
.setDataType(DataType.DT_INSTANTANEOUS_BODY_WEIGHT)
.setDataGenerateType(DataCollector.DATA_TYPE_RAW)
.build()
val deleteOptions = DeleteOptions.Builder()
.addDataCollector(dataCollector)
.setTimeInterval(startTimeInMillis, endTimeInMillis, TimeUnit.MILLISECONDS)
.build()
dataController.delete(deleteOptions).addOnSuccessListener {
Toast.makeText(this, "Weight has been deleted successfully", Toast.LENGTH_SHORT)
.show()
}
.addOnFailureListener { e ->
Toast.makeText(this, e.message.toString(), Toast.LENGTH_SHORT).show()
}
}
clearHealthData -> It deletes all the data in the Health Kit.
Spoiler: clearHealthData
Code:
private fun clearHealthData() {
dataController.clearAll()
.addOnSuccessListener {
Toast.makeText(this, "All Health Kit data has been deleted.", Toast.LENGTH_SHORT)
.show()
}
.addOnFailureListener { e ->
Toast.makeText(this, e.message.toString(), Toast.LENGTH_SHORT).show()
}
}
Conclusion
In summary, we developed a calorie-weight tracking application called “Health Tracker” with the help of the Huawei Health Kit. We experienced how to add, read, update, and delete health & fitness data. Please do not hesitate to ask your questions as a comment. You can also ask your questions via the Huawei Developer Forum.
Thank you for your time and dedication. I hope it was helpful. See you in other articles!
HUAWEI Developer Forum - HUAWEI Developer
forums.developer.huawei.com
Github Repository
acillioglu/HealthTrackerApp
Health Tracker is an Android App for tracking calories and weight of people using Huawei Health Kit.github.com
References
Huawei Health Kit Documentation
Official Pagedeveloper.huawei.com
Health Kit Codelab
HUAWEI Health Kit provides an open platform for fitness and health ecosystem data.Being responsible for managing users'…developer.huawei.com

Very nice features in covid time.

while applying for huawei healthkit in app gallery connect , from the list of permission that we need to check, i couldn't find anything like "access calorie data", So i went with "Activity data", i also had to upload two excel documents with screenshots , permission list and its path where users can read these data. and i got test permission, but im still getting error 50005, i thought maybe im getting this error because its not available in india, i even used a vpn with uk server , still couldn't save /read calorie data

devsarathsvs said:
while applying for huawei healthkit in app gallery connect , from the list of permission that we need to check, i couldn't find anything like "access calorie data", So i went with "Activity data", i also had to upload two excel documents with screenshots , permission list and its path where users can read these data. and i got test permission, but im still getting error 50005, i thought maybe im getting this error because its not available in india, i even used a vpn with uk server , still couldn't save /read calorie data
Click to expand...
Click to collapse
Hello, devsarathsvs. Yes, the names of the access permissions have changed as you applied. "50005" error code is related to the authentication. Please make sure to access within the scope granted to the app.
Also, HMS Core caches the scope permission information for 24 hours. It might cause this issue. So, you can try to clear the caches. To clear the caches:
On your phone, open Settings, search for and access Apps, and clear the cache and data of HMS Core.
Go to Settings > HUAWEI ID > Privacy center > Control account access to cancel the app authorization. Then re-open the app, sign in to your HUAWEI ID, and apply for the scopes.

Thank you so much for sharing..

Does it support heart rate data?

Basavaraj.navi said:
Does it support heart rate data?
Click to expand...
Click to collapse
Yes, Huawei Health Kit supports the various heart rate data. You can get heart rate data with the "DataController" methods. It includes "Heart Rate", "Resting Heart Rate", and "Heart Rate During Workout". In addition, you can obtain real-time heart rate data using a Huawei wearable device or an applicable device listed in Devices > Add in the Huawei Health app.

Related

Pagination for Huawei Site Kit with Paging3

{
"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"
}
Introduction
Hi everyone, In this article, we’re going to explore how to paginate with the Paging 3 library for Huawei Site Kit and, we’ll use Kotlin in Android Studio. Paging library is a generic solution for pagination. It helps us load and display data from any local or network sources. We’ve used Site Kit as a sample.
Why we need Pagination?
While we get data from any source, pagination provides us getting small chunks of data at a time. That way, we don’t load and display to the user extra data. As a result, we reduce unnecessary network requests and system resource usage.
What is Paging 3 Library?
Paging 3 library is a part of Android Jetpack and enables to load large sets of data gradually. It suits with Android App Architecture and integrates easily with other Jetpack components.
We’ve listed some of the features of the library below.
It supports Kotlin coroutines, Flow, RxJava, and LiveData.
It works with RecyclerView in an integrated manner.
It caches the paged data in-memory to use system resources efficiently.
It makes simple error handling, page refreshing, and retry.
Huawei Site Kit
Site Kit provides place search services including keyword search, nearby place search, place detail search, and place search suggestion, helping your app provide convenient place-related services to attract more users and improve user loyalty.
We’re not going to go into the details of adding Sit Kit to a project. You can follow the instructions to add Site Kit to your project via official docs or codelab.
Our Sample Pagination Project
In this project, we’ll develop a sample app showing nearby places of the user.
You can see the package structure of our application in the image below.
We used MVVM architecture and recent libraries.
Setup the Project
Add the necessary dependencies to build.gradle (app level)
Code:
// HMS Core
implementation 'com.huawei.agconnect:agconnect-core:1.4.2.300'
// Huawei Site Kit
implementation 'com.huawei.hms:site:5.1.0.300'
// Hilt for Dependency Injection
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
// Coroutines for Asynchronous Programming
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
def lifecycle_version = "2.2.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Annotation processor
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// to use viewModels() property delegate
implementation "androidx.activity:activity-ktx:1.1.0"
// Paging 3 for pagination
implementation "androidx.paging:paging-runtime:3.0.0-alpha13"
Layout Files
activity_main.xml includes RecyclerView to display nearby places, a progress bar for the loading process, and a button for retry the data.
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerSite"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/retryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Retry"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
item_site.xml -> We need a item for RecyclerView.
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:background="#f9f9f9"
android:orientation="vertical">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_place"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/siteName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="Eiffel Tower" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.2"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/siteDistance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/siteName"
tools:text="800m" />
</androidx.constraintlayout.widget.ConstraintLayout>
Model Classes
We have 2 model classes in our app named Site and Result. Site class is a part of the Site Kit so, we don’t create it.
Result.kt -> Result is a wrapper class which contains Success and Error.
Code:
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
Source Of Data
Firstly we need a source to get data so, use the Site Kit as a source. We need a location to use the NearbyLocation feature of the Site Kit and, we selected a mock location near the Eiffel Tower.
HuaweiSiteSource.kt -> With the help of the getNearbyPlaces() function, we get the Site List. This suspend function takes the page number as a parameter. If an error occurs in this process, it returns the Error as a result.
Code:
@Singleton
class HuaweiSiteSource @Inject constructor(@ApplicationContext context: Context) {
private val PAGE_SIZE = 20
private val searchService by lazy {
SearchServiceFactory.create(
context,
URLEncoder.encode(Constant.API_KEY, "UTF-8")
)
}
suspend fun getNearbyPlaces(
page: Int,
): Result<List<Site>> {
val eiffelTowerCoordinate = Coordinate(48.858093, 2.294694)
return suspendCoroutine { cont ->
val radius = 10000
val request = NearbySearchRequest()
request.location = eiffelTowerCoordinate
request.radius = radius
request.pageSize = PAGE_SIZE
request.pageIndex = page
searchService?.nearbySearch(
request,
object : SearchResultListener<NearbySearchResponse> {
override fun onSearchResult(nearbySearchResponse: NearbySearchResponse?) {
val siteList = nearbySearchResponse?.sites
siteList?.let {
cont.resume(Result.Success(it))
}
}
override fun onSearchError(searchStatus: SearchStatus?) {
cont.resume(Result.Error(Exception(searchStatus?.errorMessage)))
}
})
}
}
}
Then, we create the SitePagingSource.kt and implement a PagingSource<Key, Value> to define a data source. It takes a Key and Value as parameters. The Key is the index numbers for pages and, Value is the type of data loaded. We specified Int as the page number, Site as the data type.
PagingSource requires us to implement load() and getRefreshKey() functions.
load() is a suspend function. So, we can make our network or local database requests without blocking the main thread.
getRefreshKey() provides a Key for the initial load for the next Paging Source due to invalidation of this PagingSource. The last accessed position can be retrieved via "state.anchorPosition" so, we used the "state.anchorPosition".
Code:
@Singleton
class SitePagingSource(
private val huaweiSiteSource: HuaweiSiteSource
) : PagingSource<Int, Site>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Site> {
return try {
val page = params.key ?: FIRST_PAGE_INDEX
val result = huaweiSiteSource.getNearbyPlaces(page)
return when (result) {
is Result.Success -> LoadResult.Page(
data = result.data,
prevKey = if (page == FIRST_PAGE_INDEX) null else page - 1,
nextKey = if (result.data.isEmpty() || page >= LAST_PAGE_INDEX) null else page + 1
)
else -> LoadResult.Error(Throwable("Error occurred"))
}
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Site>): Int? {
return state.anchorPosition
}
companion object {
const val FIRST_PAGE_INDEX = 1
const val LAST_PAGE_INDEX = 3
}
}
HuaweiRepository.kt -> getDefaultPageConfig() provides us to configure our settings such as page size, placeholders, initialloadsize etc.
getSiteListAsFlow() calls the load() method from the SitePagingSource to get data and, we’re transferring the data through the flow.
Code:
@Singleton
class HuaweiRepository @Inject constructor(
private val huaweiSiteSource: HuaweiSiteSource
) {
fun getSiteListAsFlow(
pagingConfig: PagingConfig = getDefaultPageConfig()
): Flow<PagingData<Site>> {
return Pager(
config = pagingConfig,
pagingSourceFactory = { SitePagingSource(huaweiSiteSource) }
).flow
}
private fun getDefaultPageConfig(): PagingConfig {
return PagingConfig(
pageSize = 20,
enablePlaceholders = false
)
}
}
ViewModel
MainViewModel.kt -> fetchSiteList() helps us getting the data and cache it to survive configuration changes like screen rotation.
Code:
@HiltViewModel
class MainViewModel @Inject constructor(
private val huaweiRepository: HuaweiRepository
) : ViewModel() {
fun fetchSiteList(): Flow<PagingData<Site>> {
return huaweiRepository.getSiteListAsFlow()
.cachedIn(viewModelScope)
}
}
Before get into the showing places on the view, I want to mention about RecyclerView Adapter. Paging3 has a special Adapter to list items in the recyclerview.
SiteAdapter.kt -> In this class, we extend our class from PagingDataAdapter. It takes two parameters; a Model class (Site) and a ViewHolder (SiteViewHolder). Also, we used ViewBinding to interact with the views that have an assigned id value in a null-safe and type-safe way. And, we applied the Higher-order function for item clicks.
Code:
class SiteAdapter : PagingDataAdapter<Site, SiteAdapter.SiteViewHolder>(REPO_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SiteViewHolder {
val binding = ItemSiteBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SiteViewHolder(binding)
}
companion object {
private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<Site>() {
override fun areItemsTheSame(oldItem: Site, newItem: Site): Boolean {
return oldItem.siteId == newItem.siteId
}
override fun areContentsTheSame(oldItem: Site, newItem: Site): Boolean {
return oldItem.equals(newItem)
}
}
}
private var onItemClickListener: ((Site) -> Unit)? = null
override fun onBindViewHolder(holder: SiteViewHolder, position: Int) {
val site = getItem(position) ?: return
holder.binding.apply {
this.siteName.text = site.name
this.siteDistance.text = "${site.distance}m"
}
holder.itemView.apply {
setOnClickListener {
onItemClickListener?.let { it(site) }
}
}
}
inner class SiteViewHolder(val binding: ItemSiteBinding) : RecyclerView.ViewHolder(binding.root)
fun setOnItemClickListener(listener: (Site) -> Unit) {
onItemClickListener = listener
}
}
View
MainActivity.kt -> Firstly, we set up our RecyclerView and SiteAdapter. After that, we collected the data from ViewModel and passed it to the adapter. Finally, we observed the load state with the help of the addLoadStateListener() method. When there is a change in the load state of the adapter, it notifies us. According to these states, we can change our UI status such as loading, displaying, or retrying.
Code:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
private lateinit var binding: ActivityMainBinding
private val siteAdapter by lazy {
SiteAdapter().apply {
setOnItemClickListener {
Toast.makeText(
[email protected],
"Clicked Site Name : ${it.name}",
Toast.LENGTH_SHORT
).show()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.recyclerSite.apply {
layoutManager = LinearLayoutManager(context)
adapter = siteAdapter
layoutManager = layoutManager
}
binding.retryButton.setOnClickListener { siteAdapter.retry() }
siteAdapter.addLoadStateListener { loadState ->
when (loadState.source.refresh) {
is LoadState.NotLoading -> {
binding.recyclerSite.isVisible = true
binding.progressBar.isVisible = false
binding.retryButton.isVisible = false
}
is LoadState.Loading -> {
binding.progressBar.isVisible = true
binding.retryButton.isVisible = false
}
is LoadState.Error -> {
binding.progressBar.isVisible = false
binding.retryButton.isVisible = true
}
}
val errorState = loadState.source.append as? LoadState.Error
?: loadState.append as? LoadState.Error
errorState?.let {
Toast.makeText(this, errorState.error.localizedMessage, Toast.LENGTH_SHORT).show()
}
}
lifecycleScope.launch {
viewModel.fetchSiteList().collectLatest { pagingData ->
viewModel.fetchSiteList().distinctUntilChanged().collectLatest {
siteAdapter.submitData(it)
}
}
}
}
Tips & Tricks
⚠While using the Flow, make sure you import correctly. Adding unambiguous imports on the fly in Android Studio can cause the adding of wrong imports.
⚠ Paging3 supports LiveData and RxJava beside Flow.
⚠Configuring your page size relies upon how your data is being loaded and used. Smaller page sizes improve memory usage, latency. Larger pages generally improve loading throughput.
Conclusion
In summary, we have developed a simple app that shows the nearby places of the user. We’ve used the Paging 3 library to make it easier to work with large sets of data and Huawei Site kit to get nearby places information of the users. Please do not hesitate to ask your questions as a comment.
Thank you for your time and dedication. I hope it was helpful. See you in other articles!
References
Paging 3 Official Documentation
Huawei Site Kit Official Documentation

Developing a Download Manager App with Huawei Network Kit

{
"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"
}
Introduction
Hi everyone, In this article, we’ll explore how to develop a download manager app using the Huawei Network Kit. And, we’ll use Kotlin as a programming language in Android Studio.
Huawei Network Kit
Network Kit provides us to upload or download files with additional features such as multithreaded, concurrent, resumable uploads and downloads. Also, it allows us to perform our network operations quickly and safely. It provides a powerful interacting with Rest APIs and sending synchronous and asynchronous network requests with annotated parameters. Finally, we can use it with other Huawei kits such as hQUIC Kit and Wireless Kit to get faster network traffic.
If you want to learn how to use Network Kit with Rest APIs, you can check my article about it.
Download Manager — Sample App
In this project, we’re going to develop a download manager app that helps users download files quickly and reliably to their devices.
Key features:
Start, Pause, Resume or Cancel downloads.
Enable or Disable Sliced Download.
Set’s the speed limit for downloading a file.
Calculate downloaded size/total file size.
Calculate and display download speed.
Check the progress in the download bar.
Support HTTP and HTTPS protocols.
Copy URL from clipboard easily
We started a download task. Then, we paused and resumed it. When the download is finished, it showed a snackbar to notify us.
Setup the Project
We’re not going to go into the details of integrating Huawei HMS Core into a project. You can follow the instructions to integrate HMS Core into your project via official docs or codelab. After integrating HMS Core, let’s add the necessary dependencies.
Add the necessary dependencies to build.gradle (app level).
Java:
dependencies {
...
// HMS Network Kit
implementation 'com.huawei.hms:filemanager:5.0.3.300'
// For runtime permission
implementation 'androidx.activity:activity-ktx:1.2.3'
implementation 'androidx.fragment:fragment-ktx:1.3.4'
...
}
Let’s add the necessary permissions to our manifest.
XML:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.huawei.networkkitsample">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
We added the Internet Permission to access the Internet and the storage permissions to read and write data to the device memory. Also, we will dynamically request the permissions at runtime for storage permissions on devices that runs Android 6.0 (API Level 23) or higher.
Configure the AndroidManifest file to use clear text traffic
If you try to download a file from an HTTP URL on Android 9.0 (API level 28) or higher, you’ll get an error like this:
Code:
ErrorCodeFromException errorcode from resclient: 10000802,message:CLEARTEXT communication to ipv4.download.thinkbroadband.com(your url) not permitted by network security policy
Because cleartext support is disabled by default on Android 9.0 or higher. You should add the android:usesClearTextTraffic="true" flag in the AndroidManifest.xml file. If you don’t want to enable it for all URLs, you can create a network security config file. If you are only working with HTTPS files, you don’t need to add this flag.
XML:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.huawei.networkkitsample">
...
<application
...
android:usesCleartextTraffic="true"
...
</application>
</manifest>
Layout File
activity_main.xml is the only layout file in our project. There are:
A TextInputEditText to enter URL,
Four buttons to control the download process,
A button to paste URL to the TextInputEditText,
A progress bar to show download status,
A seekbar to adjust download speed limit,
A checkbox to enable or disable the “Slide Download” feature,
TextViews to show various information.
activity_main.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<Button
android:id="@+id/startDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Start"
app:layout_constraintEnd_toStartOf="@+id/pauseDownload_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/enableSliced_checkBox" />
<ProgressBar
android:id="@+id/downloadProgress_progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:progressBackgroundTint="@color/design_default_color_primary_variant"
android:progressTint="@color/design_default_color_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/percentProgress_textView" />
<TextView
android:id="@+id/percentProgress_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="0%"
app:layout_constraintStart_toStartOf="@+id/downloadProgress_progressBar"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
<TextView
android:id="@+id/finishedSize_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="0"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/percentProgress_textView"
tools:text="2.5" />
<TextView
android:id="@+id/sizeSeparator_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="/"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/finishedSize_textView" />
<TextView
android:id="@+id/totalSize_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="0"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/sizeSeparator_textView"
tools:text="29.6 MB" />
<SeekBar
android:id="@+id/speedLimit_seekBar"
style="@style/Widget.AppCompat.SeekBar.Discrete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:max="7"
android:progress="7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fixSpeedLimit_textView" />
<TextView
android:id="@+id/fixSpeedLimit_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="Download Speed Limit:"
app:layout_constraintStart_toStartOf="@+id/speedLimit_seekBar"
app:layout_constraintTop_toBottomOf="@+id/remainingTime_textView" />
<TextView
android:id="@+id/speedLimit_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Limitless"
app:layout_constraintBottom_toBottomOf="@+id/fixSpeedLimit_textView"
app:layout_constraintStart_toEndOf="@+id/fixSpeedLimit_textView" />
<TextView
android:id="@+id/currentSpeed_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0 kB/s"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintEnd_toEndOf="@+id/downloadProgress_progressBar"
tools:text="912 kB/s" />
<Button
android:id="@+id/pauseDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pause"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/startDownload_button"
app:layout_constraintTop_toTopOf="@+id/startDownload_button" />
<Button
android:id="@+id/resumeDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Resume"
app:layout_constraintEnd_toEndOf="@+id/startDownload_button"
app:layout_constraintStart_toStartOf="@+id/startDownload_button"
app:layout_constraintTop_toBottomOf="@+id/startDownload_button" />
<Button
android:id="@+id/cancelDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Cancel"
app:layout_constraintEnd_toEndOf="@+id/pauseDownload_button"
app:layout_constraintStart_toStartOf="@+id/pauseDownload_button"
app:layout_constraintTop_toBottomOf="@+id/pauseDownload_button" />
<TextView
android:id="@+id/remainingTime_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0s left"
app:layout_constraintStart_toStartOf="@+id/downloadProgress_progressBar"
app:layout_constraintTop_toBottomOf="@+id/downloadProgress_progressBar" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/pasteClipboard_imageButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/url_textInputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="URL"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/pasteClipboard_imageButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:background="@android:color/transparent"
android:scaleType="fitXY"
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textInputLayout"
app:srcCompat="@drawable/ic_paste_content" />
<CheckBox
android:id="@+id/enableSliced_checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:checked="true"
android:text="Enable Slice Download"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/speedLimit_seekBar" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
Let’s interpret some of the functions on this page.
onCreate() - Firstly we used viewBinding instead of findViewById. It generates a binding class for each XML layout file present in that module. With the instance of a binding class, we can access the view hierarchy with type and null safety.
Then, we initialized the ButtonClickListeners and the ViewChangeListeners. And we create a FileRequestCallback object. We’ll go into the details of this object later.
startDownloadButton() - When the user presses the start download button, it requests permissions at runtime. If the user allows accessing device memory, it will start the download process.
startDownload() - First, we check the downloadManager is initialized or not. Then, we check if there is a download task or not. getRequestStatus function provides us the result status as INIT, PROCESS, PAUSE and, INVALID.
Code:
If auto-import is active in your Android Studio, It can import the wrong package for the Result Status. Please make sure to import the "com.huawei.hms.network.file.api.Result" package.
The Builder helps us to create a DownloadManager object. We give a name to our task. If you plan to use the multiple download feature, please be careful to give different names to your download managers.
The DownloadManagerBuilder helps us to create a DownloadManager object. We give a tag to our task. In our app, we only allow single downloading to make it simple. If you plan to use the multiple download feature, please be careful to give different tags to your download managers.
When creating a download request, we need a file path to save our file and a URL to download. Also, we can set a speed limit or enable the slice download.
Code:
Currently, you can only set the speed limit for downloading a file. The speed limit value ranges from 1 B/s to 1 GB/s. speedLimit() takes a variable of the type INT as a byte value.
You can enable or disable the sliced download.
Code:
Sliced Download: It slices the file into multiple small chunks and downloads them in parallel.
Finally, we start an asynchronous request with downloadManager.start() command. It takes the getRequest and the fileRequestCallback.
FileRequestCallback object contains four callback methods: onStart, onProgress, onSuccess and onException.
onStart -> It will be called when the file download starts. We take the startTime to calculate the remaining download time here.
onProgress -> It will be called when the file download progress changes. We can change the progress status here.
Code:
These methods run asynchronously. If we want to update the UI, we should change our thread to the UI thread using the runOnUiThread methods.
onSuccess -> It will be called when file download is completed. We show a snackbar to the user after the file download completes here.
onException -> It will be called when an exception occurs.
Code:
onException also is triggered when the download is paused or resumed. If the exception message contains the "10042002" number, it is paused, if it contains the "10042003", it is canceled.
MainActivity.kt
Java:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var downloadManager: DownloadManager
private lateinit var getRequest: GetRequest
private lateinit var fileRequestCallback: FileRequestCallback
private val TAG = "MainActivity"
private var downloadURL = "http://ipv4.download.thinkbroadband.com/20MB.zip"
private var downloadSpeedLimit: Int = 0
private var startTime: Long = 0L
private var isEnableSlicedDownload = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.urlTextInputEditText.setText(downloadURL)
initButtonClickListeners()
initViewChangeListeners()
fileRequestCallback = object : FileRequestCallback() {
override fun onStart(getRequest: GetRequest): GetRequest {
startTime = System.nanoTime()
return getRequest
}
override fun onProgress(getRequest: GetRequest, progress: Progress) {
runOnUiThread {
binding.downloadProgressProgressBar.progress = progress.progress
binding.percentProgressTextView.text = "${progress.progress}%"
convertByteToMb(progress.totalSize)?.let {
binding.totalSizeTextView.text = "$it MB"
}
convertByteToMb(progress.finishedSize)?.let {
binding.finishedSizeTextView.text = it
}
showCurrentDownloadSpeed(progress.speed)
showRemainingTime(progress)
}
}
override fun onSuccess(response: Response<GetRequest, File, Closeable>?) {
if (response?.content != null) {
runOnUiThread {
binding.downloadProgressProgressBar.progress = 100
binding.percentProgressTextView.text = "100%"
binding.remainingTimeTextView.text = "0s left"
convertByteToMb(response.content.length())?.let {
binding.finishedSizeTextView.text = it
binding.totalSizeTextView.text = "$it MB"
}
showSnackBar(binding.mainConstraintLayout, "Download Completed")
}
}
}
override fun onException(
getRequest: GetRequest?,
exception: NetworkException?,
response: Response<GetRequest, File, Closeable>?
) {
if (exception != null) {
val pauseTaskValue = "10042002"
val cancelTaskValue = "10042003"
val errorMessage = exception.message
errorMessage?.let {
if (!it.contains(pauseTaskValue) && !it.contains(cancelTaskValue)) {
Log.e(TAG, "Error Message:$it")
exception.cause?.let { throwable ->
runOnUiThread {
Toast.makeText(
[email protected],
throwable.message,
Toast.LENGTH_SHORT
)
.show()
}
}
}
}
}
}
}
}
private fun initViewChangeListeners() {
binding.speedLimitSeekBar.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
downloadSpeedLimit = calculateSpeedLimitAsByte(progress)
showDownloadSpeedLimit(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
}
})
binding.enableSlicedCheckBox.setOnCheckedChangeListener { _, isChecked ->
isEnableSlicedDownload = isChecked
}
}
private fun initButtonClickListeners() {
binding.startDownloadButton.setOnClickListener {
activityResultLauncher.launch(
arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
)
}
binding.pauseDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) [email protected]
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PROCESS -> {
downloadManager.pauseRequest(getRequest.id)
}
else -> {
Toast.makeText(this, "No valid download request", Toast.LENGTH_SHORT).show()
}
}
}
binding.resumeDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) [email protected]
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PAUSE -> {
downloadManager.resumeRequest(getRequest, fileRequestCallback)
}
else -> {
Toast.makeText(this, "No download process", Toast.LENGTH_SHORT).show()
}
}
}
binding.cancelDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) [email protected]
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PROCESS -> {
downloadManager.cancelRequest(getRequest.id)
clearAllViews()
}
Result.STATUS.PAUSE -> {
downloadManager.cancelRequest(getRequest.id)
clearAllViews()
}
else -> {
Toast.makeText(this, "No valid download request", Toast.LENGTH_SHORT).show()
}
}
}
binding.pasteClipboardImageButton.setOnClickListener {
pasteClipboardData()
}
}
private val activityResultLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
)
{ permissions ->
val allGranted = permissions.entries.map {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
checkSelfPermission(it.key)
} else {
true
}
}.map { it == PackageManager.PERMISSION_GRANTED }.find { !it } ?: true
if (!allGranted) {
Toast.makeText(this, "Permission are not granted", Toast.LENGTH_SHORT).show()
} else {
startDownload()
}
}
private fun startDownload() {
if (this::downloadManager.isInitialized) {
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PAUSE -> {
Toast.makeText(
this,
"Press Resume Button to continue download process",
Toast.LENGTH_SHORT
).show()
return
}
Result.STATUS.PROCESS -> {
Toast.makeText(
this,
"First cancel the current download process",
Toast.LENGTH_SHORT
).show()
return
}
}
}
downloadManager = DownloadManager.Builder("downloadManager")
.build(this)
val fileName = downloadURL.substringAfterLast("/")
val downloadFilePath = this.cacheDir.path + File.separator + fileName
val currentDownloadURL = binding.urlTextInputEditText.text.toString()
getRequest = DownloadManager.newGetRequestBuilder()
.filePath(downloadFilePath)
.url(currentDownloadURL)
.speedLimit(downloadSpeedLimit)
.enableSlice(isEnableSlicedDownload)
.build()
val result = downloadManager.start(getRequest, fileRequestCallback)
if (result.code != Result.SUCCESS) {
Log.d(TAG, "An Error occurred when downloading")
}
}
private fun convertByteToMb(sizeInByte: Long): String? {
return if (sizeInByte < 0 || sizeInByte == 0L) {
null
} else {
val sizeInMb: Float = sizeInByte / (1024 * 1024).toFloat()
String.format("%.2f", sizeInMb)
}
}
private fun showCurrentDownloadSpeed(speedInByte: Long) {
val downloadSpeedText = if (speedInByte <= 0) {
"-"
} else {
val sizeInKb: Float = speedInByte / 1024.toFloat()
String.format("%.2f", sizeInKb) + "kB/s"
}
binding.currentSpeedTextView.text = downloadSpeedText
}
private fun calculateSpeedLimitAsByte(progressBarValue: Int): Int {
return when (progressBarValue) {
0 -> 512 * 1024
1 -> 1024 * 1024
2 -> 2 * 1024 * 1024
3 -> 4 * 1024 * 1024
4 -> 6 * 1024 * 1024
5 -> 8 * 1024 * 1024
6 -> 16 * 1024 * 1024
7 -> 0
else -> 0
}
}
private fun showDownloadSpeedLimit(progressValue: Int) {
val message = when (progressValue) {
0 -> "512 kB/s"
1 -> "1 mB/s"
2 -> "2 mB/s"
3 -> "4 mB/s"
4 -> "6 mB/s"
5 -> "8 mB/s"
6 -> "16 mB/s"
7 -> "Limitless"
else -> "Error"
}
binding.speedLimitTextView.text = message
}
private fun isDownloadManagerInitialized(): Boolean {
return if (this::downloadManager.isInitialized) {
true
} else {
Toast.makeText(this, "First start the download", Toast.LENGTH_SHORT).show()
false
}
}
private fun pasteClipboardData() {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = clipboardManager.primaryClip
val clipItem = clipData?.getItemAt(0)
val text = clipItem?.text.toString()
if (text == "null") {
Toast.makeText(this, "There is no text on clipboard", Toast.LENGTH_SHORT).show()
} else {
binding.urlTextInputEditText.setText(text)
}
}
private fun showRemainingTime(progress: Progress) {
val elapsedTime = System.nanoTime() - startTime
val allTimeForDownloading =
(elapsedTime * progress.totalSize / progress.finishedSize)
val remainingTime = allTimeForDownloading - elapsedTime
val hours = TimeUnit.NANOSECONDS.toHours(remainingTime)
val minutes = TimeUnit.NANOSECONDS.toMinutes(remainingTime) % 60
val seconds = TimeUnit.NANOSECONDS.toSeconds(remainingTime) % 60
val remainingTimeAsText = if (hours > 0) {
"${hours}h ${minutes}m ${seconds}s left"
} else {
if (minutes > 0) {
"${minutes}m ${seconds}s left"
} else {
"${seconds}s left"
}
}
binding.remainingTimeTextView.text = remainingTimeAsText
}
private fun showSnackBar(rootView: View, message: String) {
val snackBar = Snackbar.make(rootView, message, Snackbar.LENGTH_SHORT)
snackBar.show()
}
private fun clearAllViews() {
binding.percentProgressTextView.text = "0%"
binding.finishedSizeTextView.text = "0"
binding.totalSizeTextView.text = "0"
binding.currentSpeedTextView.text = "0 kB/s"
binding.downloadProgressProgressBar.progress = 0
binding.remainingTimeTextView.text = "0s left"
}
}
Tips & Tricks
According to the Wi-Fi status awareness capability of the Huawei Awareness Kit, you can pause or resume your download task. It will reduce the cost to the user and help to manage your download process properly.
Before starting the download task, you can check that you’re connected to the internet using the ConnectivityManager.
If the download file has the same name as an existing file, it will overwrite the existing file. Therefore, you should give different names for your files.
Even if you minimize the application, the download will continue in the background.
Conclusion
In this article, we have learned how to use Network Kit in your download tasks. And, we’ve developed the Download Manager app that provides many features. In addition to these features, you can also use Network Kit in your upload tasks. Please do not hesitate to ask your questions as a comment.
Thank you for your time and dedication. I hope it was helpful. See you in other articles.
References
Huawei Network Kit Official Documentation
Huawei Network Kit Official Codelab
Huawei Network Kit Official Github
Thanks for sharing.

Find the CRUD operations in Room Database in Attendance Tracker Android app (Kotlin) – Part 3

Introduction
In this article, we can learn about the CRUD (Create, Read, Update and Delete) operations in Room Database in Attendance Tracker app.
Room is an ORM (Object Relational Mapping) library. In other words, Room will map our database objects to Java objects. Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. Room can provide to create sqlite databases and perform the operations like create, read, update and delete.
So, I will provide the series of articles on this Attendance Tracker App, in upcoming articles I will integrate other Huawei Kits.
If you are new to this application, follow my previous articles
Beginner: Integration of Huawei Account Kit of Obtaining Icon Resources feature in Attendance Tracker Android app (Kotlin) - Part 1
Beginner: Find Log in via SMS and Forgot password? Features using Huawei Account Kit, and Ads Kit in Attendance Tracker Android app (Kotlin) – Part 2
Components of Room DB
1. Entity
2. Dao
3. Database
1. Entity
Represents a table within the database. Room creates a table for each class that has @entity annotation, the fields in the class correspond to columns in the table. Therefore, the entity classes tend to be small model classes that does not contain any logic.
Entities annotations
Before we get started with modelling our entities, we need to know some useful annotations and their attributes.
@entity - every model class with this annotation will have a mapping table in DB.
foreignKeys — names of foreign keys
indices — list of indicates on the table
primaryKeys — names of entity primary keys
tableName
@primarykey - as name indicates, this annotation points is the primary key of the entity. autoGenerate - if set to true, then SQLite will be generating a unique id for the column.
Example: @PrimaryKey(autoGenerate = true)
@ColumnInfo - allows specifying custom information about column.
Example: @ColumnInfo(name = “column_name”)
@ignore - field will not be persisted by Room.
@embeded - nested fields can be referenced directly in the SQL queries.
2. Dao
DAOs(Data Access Objects) are responsible for defining the methods that access the database. In the initial SQLite, we use the Cursor objects. With Room, we do not need all the Cursor related code and can simply define our queries using annotations in the Dao class.
3. Database
Contains the database holder and serves as the main access point for the underlying connection to your app’s persisted, relational data.
To create a database, we need to define an abstract class that extends RoomDatabase. This class is annotated with @database, lists the entities contained in the database, and the DAOs which access them.
The class which is annotated with @database should satisfy the following conditions:
Be an abstract class that extends RoomDatabase.
Include the list of entities associated with the database within the annotation.
Contain an abstract method that has 0 arguments and returns the class that is annotated with @dao.
Requirements
1. Any operating system (MacOS, Linux and Windows).
2. Must have a Huawei phone with HMS 4.0.0.300 or later.
3. Must have a laptop or desktop with Android Studio, Jdk 1.8, SDK platform 26 and Gradle 4.6 and above installed.
4. Minimum API Level 24 is required.
5. Required EMUI 9.0.0 and later version devices.
How to integrate HMS Dependencies
1. First register as Huawei developer and complete identity verification in Huawei developers website, refer to register a Huawei ID.
2. Create a project in android studio, refer Creating an Android Studio Project.
3. Generate a SHA-256 certificate fingerprint.
4. To generate SHA-256 certificate fingerprint. On right-upper corner of android project click Gradle, choose Project Name > Tasks > android, and then click signingReport, as follows.
{
"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"
}
Note: Project Name depends on the user created name.
5. Create an App in AppGallery Connect.
6. Download the agconnect-services.json file from App information, copy and paste in android Project under app directory, as follows.
7. Enter SHA-256 certificate fingerprint and click Save button, as follows.
Note: Above steps from Step 1 to 7 is common for all Huawei Kits.
8. Add the below maven URL in build.gradle(Project) file under the repositories of buildscript, dependencies and allprojects, refer Add Configuration.
Java:
maven { url 'http://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.6.0.300'
9. Add the below plugin and dependencies in build.gradle(Module) file.
Java:
apply plugin: id 'com.huawei.agconnect'
apply plugin: id 'kotlin-kapt'
// Huawei AGC
implementation 'com.huawei.agconnect:agconnect-core:1.6.0.300'
// Room Database
implementation "androidx.room:room-runtime:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
androidTestImplementation "androidx.room:room-testing:2.4.2"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
// Recyclerview
implementation 'androidx.recyclerview:recyclerview:1.2.1'
10. Now Sync the gradle.
Let us move to development
I have created a project on Android studio with empty activity let us start coding.
Create a DataRecord.kt class annotated with @entity to create a table for each class.
Java:
@Entity(tableName = "datarecords")
data class DataRecord(
@PrimaryKey(autoGenerate = true)
val id: Long,
val name: String,
val age: String,
val phone: String
)
Create a DataRecordDao.kt interface class annotated with @dao and responsible for defining the methods that access the database.
Java:
@Dao
interface DataRecordDao {
@Query("SELECT * from datarecords")
fun getall(): LiveData<List<DataRecord>>
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(item: DataRecord)
@Query("SELECT * FROM datarecords WHERE datarecords.id == :id")
fun get(id: Long): LiveData<DataRecord>
@Update
suspend fun update(vararg items: DataRecord)
@Delete
suspend fun delete(vararg items: DataRecord)
}
Create a AppRoomDatabase.kt abstract class that extends RoomDatabase annotated with @database[/USER] to lists the entities contained in the database, and the DAOs which access them.
Java:
@Database(entities = [DataRecord::class], version = 1)
abstract class AppRoomDatabase : RoomDatabase() {
abstract fun datarecordDao(): DataRecordDao
companion object {
@Volatile
private var INSTANCE: RoomDatabase? = null
fun getDatabase(context: Context): AppRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance as AppRoomDatabase
}
synchronized(this) {
val instance = Room.databaseBuilder(context.applicationContext,AppRoomDatabase::class.java,
"datarecords_database").fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
Create a Repository.kt class to find the functions.
Java:
class Repository(private val dataDao: DataRecordDao) {
val allItems: LiveData<List<DataRecord>> = dataDao.getall()
fun get(id: Long):LiveData<DataRecord> {
return dataDao.get(id)
}
suspend fun update(item: DataRecord) {
dataDao.update(item)
}
suspend fun insert(item: DataRecord) {
dataDao.insert(item)
}
suspend fun delete(item: DataRecord) {
dataDao.delete(item)
}
}
Create a ViewModel.kt class that extends AndroidViewModel and provides the Repository functions.
Java:
class ViewModel(application: Application): AndroidViewModel(application) {
private val repository: Repository
val allItems: LiveData<List<DataRecord>>
init {
Log.d(TAG, "Inside ViewModel init")
val dao = AppRoomDatabase.getDatabase(application).datarecordDao()
repository = Repository(dao)
allItems = repository.allItems
}
fun insert(item: DataRecord) = viewModelScope.launch {
repository.insert(item)
}
fun update(item: DataRecord) = viewModelScope.launch {
repository.update(item)
}
fun delete(item: DataRecord) = viewModelScope.launch {
repository.delete(item)
}
fun get(id: Long) = repository.get(id)
}
In the EmployeesList.kt activity to find the business logic for button click.
Java:
class EmployeesList : AppCompatActivity() {
private var buttonAddEmp: Button? = null
private lateinit var myViewModel: ViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_employees_list)
val recyclerView = recyclerview_tasks
val adapter = EmpAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
buttonAddEmp = findViewById(R.id.btn_add)
buttonAddEmp!!.setOnClickListener(View.OnClickListener {
val intent = Intent(this, AddEmployees::class.java)
startActivity(intent)
})
myViewModel = ViewModelProvider(this)[ViewModel::class.java]
myViewModel.allItems.observe(this, Observer { items ->
items?.let { adapter.setItems(it) }
})
}
}
Create a EmpAdapter.kt adapter class to hold the list.
Java:
const val TAG = "EmpAdapter"
class EmpAdapter internal constructor (context: Context) : RecyclerView.Adapter<EmpAdapter.EmpRecordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var itemsList = emptyList<DataRecord>().toMutableList()
private val onClickListener: View.OnClickListener
init {
onClickListener = View.OnClickListener { v ->
val item = v.tag as DataRecord
Log.d(TAG, "Setting onClickListener for item ${item.id}")
val intent = Intent(v.context, AddEmployees::class.java).apply {
putExtra(DATA_RECORD_ID, item.id)
}
v.context.startActivity(intent)
}
}
inner class EmpRecordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val itemId: TextView = itemView.findViewById(R.id.datarecord_viewholder_id)
val itemName: TextView = itemView.findViewById(R.id.txt_name)
val itemAge: TextView = itemView.findViewById(R.id.txt_age)
val itemPhone: TextView = itemView.findViewById(R.id.txt_phone)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmpRecordViewHolder {
val itemView = inflater.inflate(R.layout.detail_list, parent, false)
return EmpRecordViewHolder(itemView)
}
override fun onBindViewHolder(holder: EmpRecordViewHolder, position: Int) {
val current = itemsList[position]
// It will be referenced in the View.OnClickListener above
holder.itemView.tag = current
with(holder) {
// Set UI values
itemId.text = current.id.toString()
// itemRecord.text = current.record
itemName.text = current.name
itemAge.text = current.age
itemPhone.text = current.phone
// Set handlers
itemView.setOnClickListener(onClickListener)
}
}
internal fun setItems(items: List<DataRecord>) {
this.itemsList = items.toMutableList()
notifyDataSetChanged()
}
override fun getItemCount() = itemsList.size
companion object {
const val DATA_RECORD_ID : String = "datarecord_id"
}
}
In the AddEmployees.kt activity to find the business logic create, update, read and delete functions.
Java:
class AddEmployees : AppCompatActivity() {
private lateinit var dataViewModel: ViewModel
private var recordId: Long = 0L
private var isEdit: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_employees)
dataViewModel = ViewModelProvider(this)[ViewModel::class.java]
if (intent.hasExtra(DATA_RECORD_ID)) {
recordId = intent.getLongExtra(DATA_RECORD_ID, 0L)
dataViewModel.get(recordId).observe(this, Observer {
val viewId = findViewById<TextView>(R.id.datarecord_id)
val viewName = findViewById<EditText>(R.id.edt_name)
val viewAge = findViewById<EditText>(R.id.edt_age)
val viewPhone = findViewById<EditText>(R.id.edt_phone)
if (it != null) {
// populate with data
viewId.text = it.id.toString()
viewName.setText(it.name)
viewAge.setText(it.age)
viewPhone.setText(it.phone)
}
})
isEdit = true
}
val save = btn_save
save.setOnClickListener { view ->
val id = 0L
val mName = edt_name.text.toString()
val mAge = edt_age.text.toString()
val mPhone = edt_phone.text.toString()
if (mName.isBlank() ) {
Toast.makeText(this, "Name is blank", Toast.LENGTH_SHORT).show()
}
else if (mAge.isBlank()) {
Toast.makeText(this, "Age is blank", Toast.LENGTH_SHORT).show()
}
else if(mPhone.isBlank()) {
Toast.makeText(this, "Phone is blank", Toast.LENGTH_SHORT).show()
}
else {
val item = DataRecord( id = id, name = mName, age = mAge, phone = mPhone)
dataViewModel.insert(item)
finish()
}
}
val update = btn_update
update.setOnClickListener { view ->
val id = datarecord_id.text.toString().toLong()
val nName = edt_name.text.toString()
val nAge = edt_age.text.toString()
val nPhone = edt_phone.text.toString()
if (nName.isBlank() && nAge.isBlank() && nPhone.isBlank()) {
Toast.makeText(this, "Empty data is not allowed", Toast.LENGTH_SHORT).show()
} else {
val item = DataRecord(id = id, name = nName, age = nAge, phone = nPhone)
dataViewModel.update(item)
finish()
}
}
val delete = btn_del
delete.setOnClickListener {
val id = datarecord_id.text.toString().toLong()
val nName = edt_name.text.toString()
val nAge = edt_age.text.toString()
val nPhone = edt_phone.text.toString()
val item = DataRecord( id =id, name = nName, age = nAge, phone = nPhone)
dataViewModel.delete(item)
finish()
}
}
}
In the activity_employees_list.xml we can create the UI screen.
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".RoomDatabase.EmployeesList">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:layout_gravity="center_horizontal"
android:text="Employees List"
android:textColor="#B602D5"
android:textSize="24sp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="10dp"
android:layout_marginTop="40dp"
android:clickable="true" >
<ImageView
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_marginRight="20dp"
android:drawableTint="@color/design_default_color_primary"
android:src="@drawable/add_emp"/>
<Button
android:id="@+id/btn_add"
android:layout_width="match_parent"
android:layout_height="50dp"
android:textSize="18dp"
android:textAllCaps="false"
android:text="Add Employees"/>
</LinearLayout>
</LinearLayout>
In the activity_add_employees.xml we can create the UI screen.
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".RoomDatabase.AddEmployees">
<TextView
android:id="@+id/datarecord_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="id "
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textSize="18sp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edt_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="Name "
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textSize="18sp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edt_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="Age "
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textSize="18sp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edt_phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="Phone "
android:textSize="18sp"
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textColor="@color/black" />
<Button
android:id="@+id/btn_save"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Save"/>
<Button
android:id="@+id/btn_update"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Update"/>
<Button
android:id="@+id/btn_del"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Delete"/>
</LinearLayout>
In the detail_list.xml we can create the UI screen for customized items.
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/datarecord_viewholder_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="10dp"
android:textSize="18sp"
android:text="id: "
android:textColor="@color/purple_500"
android:textStyle="bold" />
<TextView
android:id="@+id/txt_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="name: " />
<TextView
android:id="@+id/txt_age"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="age: " />
<TextView
android:id="@+id/txt_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="phone: " />
</LinearLayout>
</androidx.cardview.widget.CardView>
Demo
Tips and Tricks
1. Make sure you are already registered as Huawei developer.
2. Set minSDK version to 24 or later, otherwise you will get AndriodManifest merge issue.
3. Make sure you have added the agconnect-services.json file to app folder.
4. Make sure you have added SHA-256 fingerprint without fail.
5. Make sure all the dependencies are added properly.
Conclusion
In this article, we have learned the integration of Room database. We, have learnt about the room database and its components such as DAO, Entity and Database. How to create, read, update and delete the content in room database and which helps the user to access the data when they are in offline.
I hope you have read this article. If you found it is helpful, please provide likes and comments.
Reference
Room Database

Enter the attendance timings using Room Database and Huawei Analytics Kit in Attendance Tracker Android app (Kotlin) – Part 5

{
"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"
}
Introduction
In this article, we can learn about that how to enter In Time and Out time of an users by the Room Database and also we can integrate the Analytics Kit for event tracking.
Room is an ORM (Object Relational Mapping) library. In other words, Room will map our database objects to Java objects. Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. Room can provide to create sqlite databases and perform the operations like create, read, update and delete.
So, I will provide the series of articles on this Attendance Tracker App, in upcoming articles I will integrate other Huawei Kits.
If you are new to this application, follow my previous articles
Beginner: Integration of Huawei Account Kit of Obtaining Icon Resources feature in Attendance Tracker Android app (Kotlin) - Part 1
Beginner: Find Log in via SMS and Forgot password? Features using Huawei Account Kit, and Ads Kit in Attendance Tracker Android app (Kotlin) – Part 2
Beginner: Find the CRUD operations in Room Database in Attendance Tracker Android app (Kotlin) – Part 3
Beginner: Integration of Huawei Crash Service in Attendance Tracker Android app (Kotlin) – Part 4
Analytics Kit
HUAWEI Analytics Kit provides analysis models to understand user behaviour and gain in-depth insights into users, products and content. It helps you to gain insight about user behaviour on different platforms based on the user behaviour events and user attributes reported by through apps.
AppGallery Connect
Find the Analytics using AppGallery connect dashboard.
Choose My Projects > Huawei Analytics > Overview > Project overview.
Project overview displays the core indicators of current project, such as the number of new users, User activity, User acquisition, User revisit, New user retention, Active user retention, User characteristics and Popular pages etc. providing a quick overview of the users and how they are using your app.
Requirements
1. Any operating system (MacOS, Linux and Windows).
2. Must have a Huawei phone with HMS 4.0.0.300 or later.
3. Must have a laptop or desktop with Android Studio, Jdk 1.8, SDK platform 26 and Gradle 4.6 and above installed.
4. Minimum API Level 24 is required.
5. Required EMUI 9.0.0 and later version devices.
How to integrate HMS Dependencies
1. First register as Huawei developer and complete identity verification in Huawei developers website, refer to register a Huawei ID.
2. Create a project in android studio, refer Creating an Android Studio Project.
3. Generate a SHA-256 certificate fingerprint.
4. To generate SHA-256 certificate fingerprint. On right-upper corner of android project click Gradle, choose Project Name > Tasks > android, and then click signingReport, as follows.
Note: Project Name depends on the user created name.
5. Create an App in AppGallery Connect.
6. Download the agconnect-services.json file from App information, copy and paste in android Project under app directory, as follows.
7. Enter SHA-256 certificate fingerprint and click Save button, as follows.
Note: Above steps from Step 1 to 7 is common for all Huawei Kits.
8. Click Manage APIs tab and enable HUAWEI Analytics.
9. Add the below maven URL in build.gradle(Project) file under the repositories of buildscript, dependencies and allprojects, refer Add Configuration.
Java:
maven { url 'http://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.6.0.300'
10. Add the below plugin and dependencies in build.gradle(Module) file.
Java:
apply plugin: id 'com.huawei.agconnect'
apply plugin: id 'kotlin-kapt'
// Huawei AGC
implementation 'com.huawei.agconnect:agconnect-core:1.6.0.300'
// Room Database
implementation "androidx.room:room-runtime:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
androidTestImplementation "androidx.room:room-testing:2.4.2"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
// Recyclerview
implementation 'androidx.recyclerview:recyclerview:1.2.1'
// Huawei Analytics
implementation 'com.huawei.hms:hianalytics:6.4.0.300'
11. Now Sync the gradle.
12. Add the required permission to the AndroidManifest.xml file.
XML:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
Let us move to development
I have created a project on Android studio with empty activity let us start coding.
Initialize Huawei Analytics and enable it.
Java:
// Initialize the Analytics
var mInstance: HiAnalyticsInstance? = null
// Initialize the Analytics function
initAna()
private fun initAna() {
// Enable Analytics Kit Log
HiAnalyticsTools.enableLog()
// Generate the Analytics Instance
mInstance = HiAnalytics.getInstance(this)
// Enable collection capability
mInstance?.setAnalyticsEnabled(true)
}
Create a PunchRecord.kt class annotated with @entity to create a table for each class.
Java:
@Entity(tableName = "punchrecords")
data class PunchRecord(
@PrimaryKey(autoGenerate = true)
val id: Long,
val empid: String,
val intime: String,
val outtime: String
)
Create a PunchRecordDao.kt interface class annotated with @dao and responsible for defining the methods that access the database.
Code:
@Dao
interface PunchRecordDao {
@Query("SELECT * from punchrecords")
fun punchgetall(): LiveData<List<PunchRecord>>
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(item: PunchRecord)
@Query("SELECT * FROM punchrecords WHERE punchrecords.id == :id")
fun get(id: Long): LiveData<PunchRecord>
@Update
suspend fun update(vararg items: PunchRecord)
@Delete
suspend fun delete(vararg items: PunchRecord)
}
Create a AppRoomDatabase.kt abstract class that extends RoomDatabase annotated with @database to lists the entities contained in the database, and the DAOs which access them.
Code:
@Database(entities = [PunchRecord::class], version = 1)
abstract class PunchAppRoomDatabase : RoomDatabase() {
abstract fun punchrecordDao(): PunchRecordDao
companion object {
@Volatile
private var INSTANCE: RoomDatabase? = null
fun getDatabase(context: Context): PunchAppRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance as PunchAppRoomDatabase
}
synchronized(this) {
val instance = Room.databaseBuilder(context.applicationContext,PunchAppRoomDatabase::class.java,
"datarecords_database").fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
Create a PunchRepository.kt class to find the functions.
Code:
class PunchRepository(private val punchDao: PunchRecordDao) {
val myallItems: LiveData<List<PunchRecord>> = punchDao.punchgetall()
fun getrepo(id: Long):LiveData<PunchRecord> {
return punchDao.get(id)
}
suspend fun updatepunch(item: PunchRecord) {
punchDao.update(item)
}
suspend fun insertpunch(item: PunchRecord) {
punchDao.insert(item)
}
suspend fun deletepunch(item: PunchRecord) {
punchDao.delete(item)
}
}
Create a PunchViewModel.kt class that extends AndroidViewModel and provides the PunchRepository functions.
Java:
class PunchViewModel(application: Application): AndroidViewModel(application) {
private val repository: PunchRepository
val allPunchItems: LiveData<List<PunchRecord>>
init {
Log.d(ContentValues.TAG, "Inside ViewModel init")
val dao = PunchAppRoomDatabase.getDatabase(application).punchrecordDao()
repository = PunchRepository(dao)
allPunchItems = repository.myallItems
}
fun insert(item: PunchRecord) = viewModelScope.launch {
repository.insertpunch(item)
}
fun update(item: PunchRecord) = viewModelScope.launch {
repository.updatepunch(item)
}
fun delete(item: PunchRecord) = viewModelScope.launch {
repository.deletepunch(item)
}
fun get(id: Long) = repository.getrepo(id)
}
In the EmpLoginActivity.kt activity to find the business logic for button click.
Java:
class EmpLoginActivity : AppCompatActivity() {
private var buttonAddPunch: Button? = null
private lateinit var myViewModel: PunchViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_emp_login)
val recyclerView = recyclerview_list
val adapter = PunchAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
buttonAddPunch = findViewById(R.id.btn_punch)
buttonAddPunch!!.setOnClickListener(View.OnClickListener {
val intent = Intent(this, TimeActivity::class.java)
startActivity(intent)
})
myViewModel = ViewModelProvider(this)[PunchViewModel::class.java]
myViewModel.allPunchItems.observe(this, Observer { items ->
items?.let { adapter.setItems(it) }
})
}
}
Create a PunchAdapter.kt adapter class to hold the list.
Java:
class PunchAdapter internal constructor (context: Context) : RecyclerView.Adapter<PunchAdapter.PunchRecordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var itemsList = emptyList<PunchRecord>().toMutableList()
private val onClickListener: View.OnClickListener
init {
onClickListener = View.OnClickListener { v ->
val item = v.tag as PunchRecord
Log.d(TAG, "Setting onClickListener for item ${item.id}")
val intent = Intent(v.context, TimeActivity::class.java).apply {
putExtra(DATA_RECORD_ID, item.id)
}
v.context.startActivity(intent)
}
}
inner class PunchRecordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val itemId: TextView = itemView.findViewById(R.id.datarecord_viewholder_id)
val itemID: TextView = itemView.findViewById(R.id.punch_id)
val itemInTime: TextView = itemView.findViewById(R.id.punch_in)
val itemOutTime: TextView = itemView.findViewById(R.id.punch_out)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PunchRecordViewHolder {
val itemView = inflater.inflate(R.layout.punch_list, parent, false)
return PunchRecordViewHolder(itemView)
}
override fun onBindViewHolder(holder: PunchRecordViewHolder, position: Int) {
val current = itemsList[position]
// Needed: will be referenced in the View.OnClickListener above
holder.itemView.tag = current
with(holder) {
// Set UI values
itemId.text = current.id.toString()
// itemRecord.text = current.record
itemID.text = current.empid
itemInTime.text = current.intime
itemOutTime.text = current.outtime
// Set handlers
itemView.setOnClickListener(onClickListener)
}
}
internal fun setItems(items: List<PunchRecord>) {
this.itemsList = items.toMutableList()
notifyDataSetChanged()
}
override fun getItemCount() = itemsList.size
companion object {
const val DATA_RECORD_ID : String = "datarecord_id"
}
}
In the TimeActivity.kt activity to find the business logic to add In Time and Out Time.
Java:
class TimeActivity : AppCompatActivity(), DatePickerDialog.OnDateSetListener, TimePickerDialog.OnTimeSetListener {
private lateinit var dataViewModel: PunchViewModel
private var recordId: Long = 0L
private var isEdit: Boolean = false
var day = 0
var month = 0
var year = 0
var hour = 0
var minute = 0
var savedDay = 0
var savedMonth = 0
var savedYear = 0
var savedHour = 0
var savedMinute = 0
var isInTime : Boolean = false
var isOutTime : Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_time)
dataViewModel = ViewModelProvider(this)[PunchViewModel::class.java]
if (intent.hasExtra(PunchAdapter.DATA_RECORD_ID)) {
recordId = intent.getLongExtra(PunchAdapter.DATA_RECORD_ID, 0L)
dataViewModel.get(recordId).observe(this, Observer {
val viewId = findViewById<TextView>(R.id.datarecord_id)
val viewEmpid = findViewById<EditText>(R.id.edtpunch_id)
val viewInTime = findViewById<EditText>(R.id.edtpunch_intime)
val viewOutTime = findViewById<EditText>(R.id.edtpunch_outtime)
if (it != null) {
// populate with data
viewId.text = it.id.toString()
viewEmpid.setText(it.empid)
viewInTime.setText(it.intime)
viewOutTime.setText(it.outtime)
}
})
isEdit = true
}
edtpunch_intime.setOnClickListener{
isInTime = true
isOutTime = false
getDateTimeCalendar()
DatePickerDialog(this,this,year,month,day ).show()
}
edtpunch_outtime.setOnClickListener{
getDateTimeCalendar()
isInTime = false
isOutTime = true
DatePickerDialog(this,this,year, month, day ).show()
}
btnpunch_save.setOnClickListener { view ->
val id = 0L
val mEmpid = edtpunch_id.text.toString()
val mInTime = edtpunch_intime.text.toString()
val mOutTime = edtpunch_outtime.text.toString()
if (mEmpid.isBlank() ) {
Toast.makeText(this, "Employee ID is blank", Toast.LENGTH_SHORT).show()
}
else if (mInTime.isBlank()) {
Toast.makeText(this, "In Time is blank", Toast.LENGTH_SHORT).show()
}
else if(mOutTime.isBlank()) {
Toast.makeText(this, "Out Time is blank", Toast.LENGTH_SHORT).show()
}
else {
val item = PunchRecord(id = id, empid = mEmpid, intime = mInTime, outtime = mOutTime)
dataViewModel.insert(item)
finish()
}
}
btnpunch_update.setOnClickListener { view ->
val id = datarecord_id.text.toString().toLong()
val nEmpid = edtpunch_id.text.toString()
val nInTime = edtpunch_intime.text.toString()
val nOutTime = edtpunch_outtime.text.toString()
if (nEmpid.isBlank() && nInTime.isBlank() && nOutTime.isBlank()) {
Toast.makeText(this, "Empty data is not allowed", Toast.LENGTH_SHORT).show()
} else {
val item = PunchRecord(id = id, empid = nEmpid, intime = nInTime, outtime = nOutTime)
dataViewModel.update(item)
finish()
}
}
btnpunch_delete.setOnClickListener {
val id = datarecord_id.text.toString().toLong()
val nEmpid = edtpunch_id.text.toString()
val nInTime = edtpunch_intime.text.toString()
val nOutTime = edtpunch_outtime.text.toString()
val item = PunchRecord( id =id, empid = nEmpid, intime = nInTime, outtime = nOutTime)
dataViewModel.delete(item)
finish()
}
}
private fun getDateTimeCalendar(){
val cal: Calendar = Calendar.getInstance()
day = cal.get(Calendar.DAY_OF_MONTH)
month = cal.get(Calendar.MONTH)
year = cal.get(Calendar.YEAR)
hour = cal.get(Calendar.HOUR)
minute = cal.get(Calendar.MINUTE)
}
override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) {
savedDay = dayOfMonth
savedMonth = month
savedYear = year
getDateTimeCalendar()
TimePickerDialog(this, this, hour, minute, true).show()
}
override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) {
savedHour = hourOfDay
savedMinute = minute
if (isInTime) {
edtpunch_intime.setText("$savedDay-$savedMonth-$savedYear, $savedHour:$savedMinute")
}
if (isOutTime) {
edtpunch_outtime.setText("$savedDay-$savedMonth-$savedYear, $savedHour:$savedMinute")
}
}
}
In the activity_emp_login.xml we can create the UI screen.
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".RoomDatabase.EmpLoginActivity">
<Button
android:id="@+id/btn_punch"
android:layout_width="200dp"
android:layout_height="50dp"
android:textSize="18dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:layout_gravity="center_horizontal"
android:textAllCaps="false"
android:text="Click to Punch"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
In the activity_time.xml we can create the UI screen.
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".RoomDatabase.TimeActivity">
<TextView
android:id="@+id/datarecord_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="id "
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textSize="18sp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edtpunch_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="EmpID: "
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textSize="18sp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edtpunch_intime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="In Time: "
android:textSize="18sp"
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edtpunch_outtime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="Out Time: "
android:textSize="18sp"
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textColor="@color/black" />
<Button
android:id="@+id/btnpunch_save"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Save"/>
<Button
android:id="@+id/btnpunch_update"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Update"/>
<Button
android:id="@+id/btnpunch_delete"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Delete"/>
</LinearLayout>
In the punch_list.xml we can create the UI screen for customized items.
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/datarecord_viewholder_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="10dp"
android:textSize="18sp"
android:text="id: "
android:textColor="@color/black"
android:textStyle="bold" />
<TextView
android:id="@+id/punch_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/black"
android:text="ID: " />
<TextView
android:id="@+id/punch_in"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/black"
android:text="IN Time: " />
<TextView
android:id="@+id/punch_out"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/black"
android:text="OUT Time: " />
</LinearLayout>
</androidx.cardview.widget.CardView>
Demo
Tips and Tricks
1. Make sure you are already registered as Huawei developer.
2. Set minSDK version to 24 or later, otherwise you will get AndriodManifest merge issue.
3. Make sure you have added the agconnect-services.json file to app folder.
4. Make sure you have added SHA-256 fingerprint without fail.
5. Make sure all the dependencies are added properly.
Conclusion
In this article, we can learn about that how to enter In Time and Out time of an users by the Room Database and also, we can integrate the Analytics Kit for event tracking.
I hope you have read this article. If you found it is helpful, please provide likes and comments.
Reference
Analytics Kit - Documentation
Analytics Kit – Training Video
Room Database

Save the patient details on notification by Huawei Push Kit in the Patient Tracking Android app (Kotlin) – Part 2

{
"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"
}
Introduction
In this article, we can learn how to send push notifications using this Huawei Push Kit to the device in the Patient Tracking app. Users can add patient details in this app, so the data will be saved in the room database, it can be accessed offline also. User can easily track their patient list who are visited the hospital. In this app, users can add, update, delete and fetch operations.
So, I will provide a series of articles on this Patient Tracking App, in upcoming articles I will integrate other Huawei Kits.
If you are new to this application, follow my previous articles.
https://forums.developer.huawei.com/forumPortal/en/topic/0201902220661040078
Push Kit
Huawei Push Kit is a messaging service developed by Huawei for developers to send messages to apps on users’ devices in real-time. Push Kit supports two types of messages: notification messages and data messages. You can send notifications and data messages to your users from your server using the Push Kit APIs or directly from the AppGallery Push Kit Console.
AppGallery Connect
Find the Push Kit message service in AppGallery connect dashboard.
Choose My Projects > Grow > Push Kit, and click Enable now.
Follow the steps to send the notification message to device from AppGallery Connect, Sending a Notification Message.
Requirements
1. Any operating system (MacOS, Linux and Windows).
2. Must have a Huawei phone with HMS 4.0.0.300 or later.
3. Must have a laptop or desktop with Android Studio, Jdk 1.8, SDK platform 26 and Gradle 4.6 and above installed.
4. Minimum API Level 24 is required.
5. Required EMUI 9.0.0 and later version devices.
How to integrate HMS Dependencies
1. First register as Huawei developer and complete identity verification in Huawei developers website, refer to register a Huawei ID.
2. Create a project in android studio, refer Creating an Android Studio Project.
3. Generate a SHA-256 certificate fingerprint.
4. To generate SHA-256 certificate fingerprint. On right-upper corner of android project click Gradle, choose Project Name > Tasks > android, and then click signingReport, as follows.
Note: Project Name depends on the user created name.
5. Create an App in AppGallery Connect.
6. Download the agconnect-services.json file from App information, copy and paste in android Project under app directory, as follows.
7. Enter SHA-256 certificate fingerprint and click Save button, as follows.
Note: Above steps from Step 1 to 7 is common for all Huawei Kits.
8. Click Manage APIs tab and enable Push Kit.
9. Add the below maven URL in build.gradle(Project) file under the repositories of buildscript, dependencies and allprojects, refer Add Configuration.
Java:
maven { url 'http://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.6.0.300'
10. Add the below plugin and dependencies in build.gradle(Module) file.
Java:
apply plugin: id 'com.huawei.agconnect'
apply plugin: id 'kotlin-kapt'
// Huawei AGC
implementation 'com.huawei.agconnect:agconnect-core:1.6.0.300'
// Room Database
implementation "androidx.room:room-runtime:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
androidTestImplementation "androidx.room:room-testing:2.4.2"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
// Recyclerview
implementation 'androidx.recyclerview:recyclerview:1.2.1'
11. Now Sync the gradle.
12. Add the required permission to the AndroidManifest.xml file.
Java:
// Push Kit
<uses-permission android:name="android.permission.INTERNET" />
<service
android:name=".PushService"
android:exported="false">
<intent-filter>
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
</intent-filter>
</service>
Let us move to development
I have created a project on Android studio with empty activity let us start coding.
In the MainActivity.kt to find the get token method for Push service.
Java:
class MainActivity : AppCompatActivity() {
getToken()
}
private fun getToken() {
showLog("getToken:begin")
object : Thread() {
override fun run() {
try {
// read from agconnect-services.json
val appId = "106429807"
val token = HmsInstanceId.getInstance([email protected]).getToken(appId, "HCM")
Log.i(TAG, "get token:$token")
if (!TextUtils.isEmpty(token)) {
sendRegTokenToServer(token)
}
showLog("get token:$token")
} catch (e: ApiException) {
Log.e(TAG, "get token failed, $e")
showLog("get token failed, $e")
}
}
}.start()
}
fun showLog(log: String?) {
runOnUiThread {
val tvView = findViewById<View?>(R.id.tv_log)
val svView = findViewById<View?>(R.id.sv_log)
if (tvView is TextView) {
tvView.text = log
}
if (svView is ScrollView) {
svView.fullScroll(View.FOCUS_DOWN)
}
}
}
private fun sendRegTokenToServer(token: String?) {
Log.i(TAG, "sending token to server. token:$token")
}
companion object {
private const val TAG: String = "PushDemoLog"
private const val CODELABS_ACTION: String = "com.huawei.codelabpush.action"
}
}
Create PushService.kt class to send the push notification to device.
Java:
class PushService : HmsMessageService() {
// When an app calls the getToken method to apply for a token from the server,
// if the server does not return the token during current method calling, the server can return the token through this method later.
// This method callback must be completed in 10 seconds. Otherwise, you need to start a new Job for callback processing.
// @param token token
override fun onNewToken(token: String?) {
Log.i(TAG, "received refresh token:$token")
// send the token to your app server.
if (!token.isNullOrEmpty()) {
// This method callback must be completed in 10 seconds. Otherwise, you need to start a new Job for callback processing.
refreshedTokenToServer(token)
}
val intent = Intent()
intent.action = CODELABS_ACTION
intent.putExtra("method", "onNewToken")
intent.putExtra("msg", "onNewToken called, token: $token")
sendBroadcast(intent)
}
private fun refreshedTokenToServer(token: String) {
Log.i(TAG, "sending token to server. token:$token")
}
// This method is used to receive downstream data messages.
// This method callback must be completed in 10 seconds. Otherwise, you need to start a new Job for callback processing.
// @param message RemoteMessage
override fun onMessageReceived(message: RemoteMessage?) {
Log.i(TAG, "onMessageReceived is called")
if (message == null) {
Log.e(TAG, "Received message entity is null!")
return
}
// getCollapseKey() Obtains the classification identifier (collapse key) of a message.
// getData() Obtains valid content data of a message.
// getMessageId() Obtains the ID of a message.
// getMessageType() Obtains the type of a message.
// getNotification() Obtains the notification data instance from a message.
// getOriginalUrgency() Obtains the original priority of a message.
// getSentTime() Obtains the time when a message is sent from the server.
// getTo() Obtains the recipient of a message.
Log.i(TAG, """getCollapseKey: ${message.collapseKey}
getData: ${message.data}
getFrom: ${message.from}
getTo: ${message.to}
getMessageId: ${message.messageId}
getMessageType: ${message.messageType}
getSendTime: ${message.sentTime}
getTtl: ${message.ttl}
getSendMode: ${message.sendMode}
getReceiptMode: ${message.receiptMode}
getOriginalUrgency: ${message.originalUrgency}
getUrgency: ${message.urgency}
getToken: ${message.token}""".trimIndent())
// getBody() Obtains the displayed content of a message
// getTitle() Obtains the title of a message
// getTitleLocalizationKey() Obtains the key of the displayed title of a notification message
// getTitleLocalizationArgs() Obtains variable parameters of the displayed title of a message
// getBodyLocalizationKey() Obtains the key of the displayed content of a message
// getBodyLocalizationArgs() Obtains variable parameters of the displayed content of a message
// getIcon() Obtains icons from a message
// getSound() Obtains the sound from a message
// getTag() Obtains the tag from a message for message overwriting
// getColor() Obtains the colors of icons in a message
// getClickAction() Obtains actions triggered by message tapping
// getChannelId() Obtains IDs of channels that support the display of messages
// getImageUrl() Obtains the image URL from a message
// getLink() Obtains the URL to be accessed from a message
// getNotifyId() Obtains the unique ID of a message
val notification = message.notification
if (notification != null) {
Log.i(TAG, """
getTitle: ${notification.title}
getTitleLocalizationKey: ${notification.titleLocalizationKey}
getTitleLocalizationArgs: ${Arrays.toString(notification.titleLocalizationArgs)}
getBody: ${notification.body}
getBodyLocalizationKey: ${notification.bodyLocalizationKey}
getBodyLocalizationArgs: ${Arrays.toString(notification.bodyLocalizationArgs)}
getIcon: ${notification.icon}
getImageUrl: ${notification.imageUrl}
getSound: ${notification.sound}
getTag: ${notification.tag}
getColor: ${notification.color}
getClickAction: ${notification.clickAction}
getIntentUri: ${notification.intentUri}
getChannelId: ${notification.channelId}
getLink: ${notification.link}
getNotifyId: ${notification.notifyId}
isDefaultLight: ${notification.isDefaultLight}
isDefaultSound: ${notification.isDefaultSound}
isDefaultVibrate: ${notification.isDefaultVibrate}
getWhen: ${notification.`when`}
getLightSettings: ${Arrays.toString(notification.lightSettings)}
isLocalOnly: ${notification.isLocalOnly}
getBadgeNumber: ${notification.badgeNumber}
isAutoCancel: ${notification.isAutoCancel}
getImportance: ${notification.importance}
getTicker: ${notification.ticker}
getVibrateConfig: ${notification.vibrateConfig}
getVisibility: ${notification.visibility}""".trimIndent())
showNotification(notification.title,notification.body)
}
val intent = Intent()
intent.action = CODELABS_ACTION
intent.putExtra("method", "onMessageReceived")
intent.putExtra("msg", "onMessageReceived called, message id:" + message.messageId + ", payload data:" + message.data)
sendBroadcast(intent)
val judgeWhetherIn10s = false
// If the messages are not processed in 10 seconds, the app needs to use WorkManager for processing.
if (judgeWhetherIn10s) {
startWorkManagerJob(message)
} else {
// Process message within 10s
processWithin10s(message)
}
}
private fun showNotification(title: String?, body: String?) {
val intent = Intent(this, MainActivity::class.java)
// intent.putExtra("URL", "https://document.desiringgod.org/the-scars-that-have-shaped-me-en.pdf")
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)
val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.sym_def_app_icon)
.setContentTitle(title)
.setContentText(body)
.setAutoCancel(true)
.setSound(soundUri)
.setContentIntent(pendingIntent)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(0, notificationBuilder.build())
}
private fun startWorkManagerJob(message: RemoteMessage?) {
Log.d(TAG, "Start new Job processing.")
}
private fun processWithin10s(message: RemoteMessage?) {
Log.d(TAG, "Processing now.")
}
override fun onMessageSent(msgId: String?) {
Log.i(TAG, "onMessageSent called, Message id:$msgId")
val intent = Intent()
intent.action = CODELABS_ACTION
intent.putExtra("method", "onMessageSent")
intent.putExtra("msg", "onMessageSent called, Message id:$msgId")
sendBroadcast(intent)
}
override fun onSendError(msgId: String?, exception: Exception?) {
Log.i(TAG, "onSendError called, message id:$msgId, ErrCode:${(exception as SendException).errorCode}, " +
"description:${exception.message}")
val intent = Intent()
intent.action = CODELABS_ACTION
intent.putExtra("method", "onSendError")
intent.putExtra("msg", "onSendError called, message id:$msgId, ErrCode:${exception.errorCode}, " +
"description:${exception.message}")
sendBroadcast(intent)
}
override fun onTokenError(e: Exception) {
super.onTokenError(e)
}
companion object {
private const val TAG: String = "PushDemoLog"
private const val CODELABS_ACTION: String = "com.huawei.codelabpush.action"
}
}
Create a PatientRecord.kt class annotated with @entity to create a table for each class.
Java:
@Entity(tableName = "patient_records")
data class PatientRecord(
@PrimaryKey(autoGenerate = true)
val id: Long,
val name: String,
val age: String,
val gender: String,
val phoneNumber: String,
val address: String,
val disease: String
)
Create a PatientDao.kt interface class annotated with @dao and responsible for defining the methods that access the database.
Java:
@Dao
interface PatientDao {
@Query("SELECT * from patient_records")
fun getall(): LiveData<List<PatientRecord>>
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(item: PatientRecord)
@Query("SELECT * FROM patient_records WHERE patient_records.id == :id")
fun get(id: Long): LiveData<PatientRecord>
@Update
suspend fun update(vararg items: PatientRecord)
@Delete
suspend fun delete(vararg items: PatientRecord)
}
Create a AppRoomDatabase.kt abstract class that extends RoomDatabase annotated with @database to lists the entities contained in the database, and the DAOs which access them.
Java:
@Database(entities = [PatientRecord::class], version = 1)
abstract class AppRoomDatabase : RoomDatabase() {
abstract fun patientrecordDao(): PatientDao
companion object {
@Volatile
private var INSTANCE: RoomDatabase? = null
fun getDatabase(context: Context): AppRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance as AppRoomDatabase
}
synchronized(this) {
val instance = Room.databaseBuilder(context.applicationContext,AppRoomDatabase::class.java,
"patient_record_database").fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
Create a Repository.kt class to find the functions.
Java:
class Repository(private val mDao: PatientDao) {
val allItems: LiveData<List<PatientRecord>> = mDao.getall()
fun get(id: Long): LiveData<PatientRecord> {
return mDao.get(id)
}
suspend fun update(item: PatientRecord) {
mDao.update(item)
}
suspend fun insert(item: PatientRecord) {
mDao.insert(item)
}
suspend fun delete(item: PatientRecord) {
mDao.delete(item)
}
}
Create a ViewModel.kt class that extends AndroidViewModel and provides the Repository functions.
Java:
class ViewModel(application: Application): AndroidViewModel(application) {
private val repository: Repository
val allItems: LiveData<List<PatientRecord>>
init {
Log.d(ContentValues.TAG, "Inside ViewModel init")
val dao = AppRoomDatabase.getDatabase(application).patientrecordDao()
repository = Repository(dao)
allItems = repository.allItems
}
fun insert(item: PatientRecord) = viewModelScope.launch {
repository.insert(item)
}
fun update(item: PatientRecord) = viewModelScope.launch {
repository.update(item)
}
fun delete(item: PatientRecord) = viewModelScope.launch {
repository.delete(item)
}
fun get(id: Long) = repository.get(id)
}
In the PatientActivity.kt activity to find the business logic for button click.
Java:
class PatientActivity : AppCompatActivity() {
private lateinit var myViewModel: ViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_patient)
val recyclerView = recyclerview_patients
val adapter = PatientAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
// buttonAddPat = findViewById(R.id.btn_float)
btn_float!!.setOnClickListener(View.OnClickListener {
val intent = Intent(this, AddPatientDetails::class.java)
startActivity(intent)
})
myViewModel = ViewModelProvider(this)[ViewModel::class.java]
myViewModel.allItems.observe(this, Observer { items ->
items?.let { adapter.setItems(it) }
})
}
}
Create a PatientAdapter.kt adapter class to hold the list.
Java:
class PatientAdapter internal constructor (context: Context) : RecyclerView.Adapter<PatientAdapter.PatientRecordViewHolder>(){
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var itemsList = emptyList<PatientRecord>().toMutableList()
private val onClickListener: View.OnClickListener
init {
onClickListener = View.OnClickListener { v ->
val item = v.tag as PatientRecord
Log.d(ContentValues.TAG, "Setting onClickListener for item ${item.id}")
val intent = Intent(v.context, AddPatientDetails::class.java).apply {
putExtra(PATIENT_RECORD_ID, item.id)
}
v.context.startActivity(intent)
}
}
inner class PatientRecordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val itemId: TextView = itemView.findViewById(R.id.patientrecord_viewholder_id)
val itemName: TextView = itemView.findViewById(R.id.txt_name)
val itemAge: TextView = itemView.findViewById(R.id.txt_age)
val itemGender: TextView = itemView.findViewById(R.id.txt_gender)
val itemPhone: TextView = itemView.findViewById(R.id.txt_phone)
val itemAddress: TextView = itemView.findViewById(R.id.txt_address)
val itemDisease: TextView = itemView.findViewById(R.id.txt_disease)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PatientRecordViewHolder {
val itemView = inflater.inflate(R.layout.patient_list, parent, false)
return PatientRecordViewHolder(itemView)
}
override fun onBindViewHolder(holder: PatientRecordViewHolder, position: Int) {
val current = itemsList[position]
// Needed: will be referenced in the View.OnClickListener above
holder.itemView.tag = current
with(holder) {
// Set UI values
itemId.text = current.id.toString()
// itemRecord.text = current.record
itemName.text = current.name
itemAge.text = current.age
itemGender.text = current.gender
itemPhone.text = current.phoneNumber
itemAddress.text = current.address
itemDisease.text = current.disease
// Set handlers
itemView.setOnClickListener(onClickListener)
}
}
override fun getItemCount() = itemsList.size
internal fun setItems(items: List<PatientRecord>) {
this.itemsList = items.toMutableList()
notifyDataSetChanged()
}
companion object {
const val PATIENT_RECORD_ID : String = "patientrecord_id"
// val Tag = "Punch"
}
}
In the AddPatientDetails.kt activity to find the business logic to add items.
Java:
class AddPatientDetails : AppCompatActivity() {
private lateinit var dataViewModel: ViewModel
private var recordId: Long = 0L
private var isEdit: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_patient_details)
dataViewModel = ViewModelProvider(this)[ViewModel::class.java]
if (intent.hasExtra(PATIENT_RECORD_ID)) {
recordId = intent.getLongExtra(PATIENT_RECORD_ID, 0L)
dataViewModel.get(recordId).observe(this, Observer {
val viewId = findViewById<TextView>(R.id.patient_record_id)
val viewName = findViewById<EditText>(R.id.edt_name)
val viewAge = findViewById<EditText>(R.id.edt_age)
val viewGender = findViewById<EditText>(R.id.edt_gender)
val viewPhone = findViewById<EditText>(R.id.edt_phone)
val viewAddress = findViewById<EditText>(R.id.edt_address)
val viewDisease = findViewById<EditText>(R.id.edt_disease)
if (it != null) {
// populate with data
viewId.text = it.id.toString()
viewName.setText(it.name)
viewAge.setText(it.age)
viewGender.setText(it.gender)
viewPhone.setText(it.phoneNumber)
viewAddress.setText(it.address)
viewDisease.setText(it.disease)
}
})
isEdit = true
}
val save = btn_save
save.setOnClickListener { view ->
val id = 0L
val mName = edt_name.text.toString()
val mAge = edt_age.text.toString()
val mGender = edt_gender.text.toString()
val mPhone = edt_phone.text.toString()
val mAddress = edt_address.text.toString()
val mDisease = edt_disease.text.toString()
if (mName.isBlank() ) {
Toast.makeText(this, "Name is blank", Toast.LENGTH_SHORT).show()
}
else if (mAge.isBlank()) {
Toast.makeText(this, "Age is blank", Toast.LENGTH_SHORT).show()
}
else if(mGender.isBlank()) {
Toast.makeText(this, "Gender is blank", Toast.LENGTH_SHORT).show()
}
else if(mPhone.isBlank()) {
Toast.makeText(this, "Phone Number is blank", Toast.LENGTH_SHORT).show()
}
else if(mAddress.isBlank()) {
Toast.makeText(this, "Address is blank", Toast.LENGTH_SHORT).show()
}
else if(mDisease.isBlank()) {
Toast.makeText(this, "Disease is blank", Toast.LENGTH_SHORT).show()
}
else {
val item = PatientRecord( id = id, name = mName, age = mAge, gender = mGender, phoneNumber = mPhone,
address = mAddress, disease = mDisease)
dataViewModel.insert(item)
finish()
}
}
val update = btn_update
update.setOnClickListener { view ->
val id = patient_record_id.text.toString().toLong()
val nName = edt_name.text.toString()
val nAge = edt_age.text.toString()
val nGender = edt_gender.text.toString()
val nPhone = edt_phone.text.toString()
val nAddress = edt_address.text.toString()
val nDisease = edt_disease.text.toString()
if (nName.isBlank() && nAge.isBlank() && nPhone.isBlank()) {
Toast.makeText(this, "Empty data is not allowed", Toast.LENGTH_SHORT).show()
} else {
val item = PatientRecord(id = id, name = nName, age = nAge, gender = nGender, phoneNumber = nPhone,
address = nAddress, disease = nDisease)
dataViewModel.update(item)
finish()
}
}
val delete = btn_delete
delete.setOnClickListener {
val id = patient_record_id.text.toString().toLong()
val nName = edt_name.text.toString()
val nAge = edt_age.text.toString()
val nGender = edt_gender.text.toString()
val nPhone = edt_phone.text.toString()
val nAddress = edt_address.text.toString()
val nDisease = edt_disease.text.toString()
val item = PatientRecord(id = id, name = nName, age = nAge, gender = nGender, phoneNumber = nPhone,
address = nAddress, disease = nDisease)
dataViewModel.delete(item)
finish()
}
}
}
In the activity_patient.xml we can create the UI screen for button.
Java:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".room.PatientActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview_patients"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_float"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:backgroundTint="@color/teal_200"
android:clickable="true"
app:borderWidth="0dp"
app:srcCompat="@android:drawable/ic_input_add"
android:focusable="true" />
</RelativeLayout>
In the activity_add_patient_details.xml we can create the UI screen for adding items.
Java:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp"
android:layout_marginTop="10dp"
android:orientation="vertical"
tools:context=".room.AddPatientDetails">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginTop="32dp"
android:layout_marginRight="62dp"
android:background="@drawable/blue_border_rounded_cornwe"
tools:ignore="MissingConstraints">
<TextView
android:id="@+id/patient_record_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/username_icons"
android:background="@android:color/transparent"
android:hint="ID "
android:inputType="textEmailAddress"
android:maxLines="1"
android:paddingLeft="17dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:textSize="13sp">
</TextView>
<ImageView
android:id="@+id/username_icons"
android:layout_width="20dp"
android:layout_height="17dp"
android:layout_centerVertical="true"
android:layout_marginLeft="17dp"
android:src="@drawable/id_icon" />
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginTop="12dp"
android:layout_marginRight="62dp"
android:background="@drawable/blue_border_rounded_cornwe"
tools:ignore="MissingConstraints">
<EditText
android:id="@+id/edt_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/username_icon"
android:background="@android:color/transparent"
android:hint="Name "
android:inputType="textEmailAddress"
android:maxLines="1"
android:paddingLeft="17dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:textSize="13sp">
</EditText>
<ImageView
android:id="@+id/username_icon"
android:layout_width="20dp"
android:layout_height="17dp"
android:layout_centerVertical="true"
android:layout_marginLeft="17dp"
android:src="@drawable/username" />
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginTop="12dp"
android:layout_marginRight="62dp"
android:background="@drawable/blue_border_rounded_cornwe"
tools:ignore="MissingConstraints">
<EditText
android:id="@+id/edt_age"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/username_icon2"
android:background="@android:color/transparent"
android:hint="Age "
android:inputType="textEmailAddress"
android:maxLines="1"
android:paddingLeft="17dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:textSize="13sp">
</EditText>
<ImageView
android:id="@+id/username_icon2"
android:layout_width="20dp"
android:layout_height="17dp"
android:layout_centerVertical="true"
android:layout_marginLeft="17dp"
android:src="@drawable/age_icon" />
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginTop="12dp"
android:layout_marginRight="62dp"
android:background="@drawable/blue_border_rounded_cornwe"
tools:ignore="MissingConstraints">
<EditText
android:id="@+id/edt_gender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:layout_toRightOf="@id/username_icon3"
android:hint="Gender "
android:inputType="textEmailAddress"
android:maxLines="1"
android:paddingLeft="17dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:textSize="13sp">
</EditText>
<ImageView
android:id="@+id/username_icon3"
android:layout_width="20dp"
android:layout_height="17dp"
android:layout_centerVertical="true"
android:layout_marginLeft="17dp"
android:src="@drawable/gender_icon" />
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginTop="12dp"
android:layout_marginRight="62dp"
android:background="@drawable/blue_border_rounded_cornwe"
tools:ignore="MissingConstraints">
<EditText
android:id="@+id/edt_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:layout_toRightOf="@id/username_icon4"
android:hint="Phone Number "
android:inputType="textEmailAddress"
android:maxLines="1"
android:paddingLeft="17dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:textSize="13sp">
</EditText>
<ImageView
android:id="@+id/username_icon4"
android:layout_width="20dp"
android:layout_height="17dp"
android:layout_centerVertical="true"
android:layout_marginLeft="17dp"
android:src="@drawable/phonenumber_icon" />
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginTop="12dp"
android:layout_marginRight="62dp"
android:background="@drawable/blue_border_rounded_cornwe"
tools:ignore="MissingConstraints">
<EditText
android:id="@+id/edt_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:layout_toRightOf="@id/username_icon5"
android:hint="Address "
android:inputType="textEmailAddress"
android:maxLines="1"
android:paddingLeft="17dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:textSize="13sp">
</EditText>
<ImageView
android:id="@+id/username_icon5"
android:layout_width="20dp"
android:layout_height="17dp"
android:layout_centerVertical="true"
android:layout_marginLeft="17dp"
android:src="@drawable/address_icon" />
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginTop="12dp"
android:layout_marginRight="62dp"
android:background="@drawable/blue_border_rounded_cornwe"
tools:ignore="MissingConstraints">
<EditText
android:id="@+id/edt_disease"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:layout_toRightOf="@id/username_icon6"
android:hint="Disease "
android:inputType="textEmailAddress"
android:maxLines="1"
android:paddingLeft="17dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:textSize="13sp">
</EditText>
<ImageView
android:id="@+id/username_icon6"
android:layout_width="20dp"
android:layout_height="17dp"
android:layout_centerVertical="true"
android:layout_marginLeft="17dp"
android:src="@drawable/disease_icon" />
</RelativeLayout>
<Button
android:id="@+id/btn_save"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="15dp"
android:text="Save"/>
<Button
android:id="@+id/btn_update"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="15dp"
android:text="Update"/>
<Button
android:id="@+id/btn_delete"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="15dp"
android:text="Delete"/>
</LinearLayout>
In the patient_list.xml we can create the UI screen for customized items.
Java:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="-3dp"
android:layout_marginRight="0dp"
android:layout_marginBottom="5dp"
app:cardCornerRadius="8dp"
app:cardElevation="3dp"
app:contentPadding="5dp"
tools:ignore="RtlHardcoded">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/patientrecord_viewholder_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="10dp"
android:textSize="18sp"
android:text="id: "
android:textColor="@color/purple_500"
android:textStyle="bold" />
<TextView
android:id="@+id/txt_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="name: " />
<TextView
android:id="@+id/txt_age"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="age: " />
<TextView
android:id="@+id/txt_gender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="gender: " />
<TextView
android:id="@+id/txt_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="phone number: " />
<TextView
android:id="@+id/txt_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="address: " />
<TextView
android:id="@+id/txt_disease"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="disease: " />
</LinearLayout>
</androidx.cardview.widget.CardView>
Demo
For result, please click here and check Demo in original content.
Tips and Tricks
1. Make sure you are already registered as Huawei developer.
2. Set minSDK version to 24 or later, otherwise you will get AndriodManifest merge issue.
3. Make sure you have added the agconnect-services.json file to app folder.
4. Make sure you have added SHA-256 fingerprint without fail.
5. Make sure all the dependencies are added properly.
Conclusion
In this article, we have learned that how to send push notifications using this Huawei Push Kit to the device in the Patient Tracking app. Users can add patient details in this app, so the data will be saved in the room database, it can be accessed offline also. User can easily track their patient list who are visited the hospital. In this app, users can add, update, delete and fetch operations.
I hope you have read this article. If you found it is helpful, please provide likes and comments.
Reference
Push Kit – Document
Push Kit – Training Video
Room Database

Categories

Resources