Bank Card Recognition
The bank card recognition service can quickly recognize information such as the bank card number, covering mainstream bank cards such as China UnionPay, American Express, Mastercard, Visa, and JCB around the world. It is widely used in finance and payment scenarios requiring bank card binding to quickly extract bank card information, realizing quick input of bank card information.
{
"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"
}
Configuring App Information in AppGallery Connect
Before you start developing an app, configure app information in AppGallery Connect.
Registering as a Developer
Before you get started, you must register as a Huawei developer and complete identity verification on HUAWEI Developers. For details refer to Registration and Verification.
Creating an App
Follow the instructions to create an app in Creating an AppGallery Connect Project and Adding an App to the Project.
Enabling the Service
1. Sign in to AppGallery Connect and select My projects.
2. Find your project from the project list and click the app for which you need to enable a service on the project card.
3. Click the Manage APIs tab and toggle the switch for the service to be enabled.
Adding the AppGallery Connect Configuration File
For adding configuration file refer this below link
https://developer.huawei.com/consum...MSCore-Guides/add-appgallery-0000001050038080
Configuring the Maven Repository Address
For adding Maven files refer this below link
https://developer.huawei.com/consum.../HMSCore-Guides/config-maven-0000001050040031
Integrating the Bank Card Recognition Plug-in
The bank card recognition service supports two SDK integration modes: full SDK and base SDK. You can select either one based on your needs.
Mode 1: Full SDK Integration (Recommended)
Combine the bank card recognition model and bank card recognition plug-in into a package.
The following is the sample code for integration in full SDK mode:
Code:
dependencies{
// Import the combined package of the bank card recognition plug-in and recognition capability.
implementation 'com.huawei.hms:ml-computer-card-bcr:2.0.3.301'
}
Mode 2: Base SDK Integration
The sample code is as follows:
Code:
dependencies{
// Import the bank card recognition plug-in package.
implementation 'com.huawei.hms:ml-computer-card-bcr-plugin:2.0.3.301'
}
Adding the Configuration to the File Header
After integrating the SDK in either mode, add the following information under apply plugin: 'com.android.application' in the file header:
Code:
apply plugin: 'com.huawei.agconnect'
Development Process
1. Create the recognition result callback function and reload the onSuccess, onCanceled, onFailure, and onDenied methods.
onSuccess indicates that the recognition is successful.
MLBcrCaptureResult indicates the recognition result.
onCanceled indicates that the user cancels the recognition.
onFailure indicates that the recognition fails.
onDenied indicates that the recognition request is denied, For example the camera is unavailable.
Code:
private void initCallBack() {
callback = new MLBcrCapture.Callback() {
@Override
public void onSuccess(MLBcrCaptureResult bankCardResult) {
if (bankCardResult != null) {
String cardNumber = bankCardResult.getNumber();
String cardExpire = bankCardResult.getExpire();
String cardIssuer = bankCardResult.getIssuer();
String cardType = bankCardResult.getType();
String cardOrganization = bankCardResult.getOrganization();
CardModel cardModel = new CardModel(cardNumber, cardExpire, cardIssuer, cardType, cardOrganization);
Intent intent = new Intent(ScanCardActivity.this, PaymentActivity.class);
intent.putExtra("CardData", cardModel);
startActivity(intent);
}
// Processing for successful recognition.
}
@Override
public void onCanceled() {
// Processing for recognition request cancelation.
}
// Callback method used when no text is recognized or a system exception occurs during recognition.
// retCode: result code.
// bitmap: bank card image that fails to be recognized.
@Override
public void onFailure(int retCode, Bitmap bitmap) {
// Processing logic for recognition failure.
}
@Override
public void onDenied() {
// Processing for recognition request deny scenarios, for example, the camera is unavailable.
}
};
}
2. Set the recognition parameters for calling the captureFrame API of the recognizer. The recognition result is returned through the callback function created in initCallBack method.
Code:
private void startCaptureActivity(MLBcrCapture.Callback callback) {
MLBcrCaptureConfig config = new MLBcrCaptureConfig.Factory()
// Set the expected result type of bank card recognition.
// MLBcrCaptureConfig.RESULT_NUM_ONLY: Recognize only the bank card number.
// MLBcrCaptureConfig.RESULT_SIMPLE: Recognize only the bank card number and validity period.
// MLBcrCaptureConfig.ALL_RESULT: Recognize information such as the bank card number, validity period, issuing bank, card organization, and card type.
.setResultType(MLBcrCaptureConfig.RESULT_ALL)
// Set the recognition screen display orientation.
// MLBcrCaptureConfig.ORIENTATION_AUTO: adaptive mode. The display orientation is determined by the physical sensor.
// MLBcrCaptureConfig.ORIENTATION_LANDSCAPE: landscape mode.
// MLBcrCaptureConfig.ORIENTATION_PORTRAIT: portrait mode.
.setOrientation(MLBcrCaptureConfig.ORIENTATION_AUTO)
.create();
MLBcrCapture bankCapture = MLBcrCaptureFactory.getInstance().getBcrCapture(config);
bankCapture.captureFrame(this, callback);
}
3. In the callback of the recognition button, call the method defined in startCaptureActivity method to implement bank card recognition
Code:
@Override
public void onClick(View view) {
switch (view.getId()) {
// Detection button.
case R.id.btn_scan_bank_card:
if (checkPermissions())
startCaptureActivity(callback);
break;
default:
break;
}
}
Adding Permissions
CAMERA: To use the camera on the device for recognition or detection, your app needs to apply for the camera permission.
READ_EXTERNAL_STORAGE: To use general card recognition plug-in, your app needs to apply for the file read permission.
The procedure is as follows:
1. Specify permissions in the AndroidManifest.xml file.
Code:
<!--Camera permission-->
<uses-permission android:name="android.permission.CAMERA" />
<!--Read permission-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
2. After specifying the permissions in the AndroidManifest.xml file, dynamically apply for the permissions in the code for Android 6.0 and later versions.
Code:
private int PERMISSION_REQUEST_CODE = 10;
private boolean checkPermissions() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
// The app has the camera permission.
return true;
} else {
// Apply for the camera permission.
requestCameraPermission();
}
return false;
}
private void requestCameraPermission() {
final String[] permissions = new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE};
ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0) {
boolean cameraAccepted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
boolean storageAccepted = grantResults[1] == PackageManager.PERMISSION_GRANTED;
if (cameraAccepted && storageAccepted ) {
Toast.makeText(ScanCardActivity.this, "Permission Granted, Now you can access camera", Toast.LENGTH_SHORT).show();
startCaptureActivity(callback);
} else {
requestCameraPermission();
}
}
}
}
Find the activity_scan_card.xml as follows
Code:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_scan_bank_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_margin="50dp"
android:background="@drawable/btn_background"
android:text="Scan Bank Card"
android:textAllCaps="false"
android:textColor="#fff" />
</RelativeLayout>
In the onSuccess method we used one CardModel class implemented with Parcelable interface. Here CardModel is used for passing data through intent to PaymentActivity. Check the CardModel class below
Code:
public class CardModel implements Parcelable {
private String cardNumber;
private String cardExpire;
private String cardIssuer;
private String cardType;
private String cardOrganization;
public CardModel(String cardNumber, String cardExpire, String cardIssuer, String cardType,String cardOrganization) {
this.cardNumber = cardNumber;
this.cardExpire = cardExpire;
this.cardIssuer = cardIssuer;
this.cardType = cardType;
this.cardOrganization = cardOrganization;
}
protected CardModel(Parcel in) {
cardNumber = in.readString();
cardExpire = in.readString();
cardIssuer = in.readString();
cardType = in.readString();
cardOrganization = in.readString();
}
public static final Creator<CardModel> CREATOR = new Creator<CardModel>() {
@Override
public CardModel createFromParcel(Parcel in) {
return new CardModel(in);
}
@Override
public CardModel[] newArray(int size) {
return new CardModel[size];
}
};
public String getCardNumber() {
return cardNumber;
}
public String getCardExpire() {
return cardExpire;
}
public String getCardIssuer() {
return cardIssuer;
}
public String getCardType() {
return cardType;
}
public String getCardOrganization() {
return cardOrganization;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(cardNumber);
parcel.writeString(cardExpire);
parcel.writeString(cardIssuer);
parcel.writeString(cardType);
parcel.writeString(cardOrganization);
}
}
After reconizing card details we are passing all the details to PaymentActivity for further steps. Check the activity_payment.xml below
Code:
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000">
<RelativeLayout
android:id="@+id/rl_tool"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Payment"
android:textColor="#fff"
android:textSize="16sp"
android:textStyle="bold" />
</RelativeLayout>
<androidx.cardview.widget.CardView
android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_below="@+id/rl_tool"
android:layout_margin="10dp"
app:cardCornerRadius="10dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/chip" />
<TextView
android:id="@+id/txt_user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="YOUR NAME"
android:textColor="#fff"
android:textStyle="bold" />
<TextView
android:id="@+id/txt_card_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/txt_valid"
android:layout_centerHorizontal="true"
android:shadowColor="#7F000000"
android:shadowDx="1"
android:shadowDy="2"
android:shadowRadius="5"
android:textColor="#FBFBFB"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txt_valid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/img_organization"
android:layout_centerHorizontal="true"
android:text="VALID\nTHRU"
android:textColor="#fff"
android:textSize="10sp" />
<TextView
android:id="@+id/txt_expiry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/txt_card_number"
android:layout_centerHorizontal="true"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_toEndOf="@id/txt_valid"
android:textColor="#fff"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txt_card_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:textAllCaps="true"
android:textColor="#fff"
android:textSize="18sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/img_organization"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/card"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="Verify your Card information"
android:textColor="#fff"
android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textColorHint="#fff"
app:hintTextColor="#fff">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edt_card_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Card Number"
android:textColor="#fff" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textColorHint="#fff"
app:hintTextColor="#fff">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edt_valid_thru"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Valid Thru"
android:textColor="#fff" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textColorHint="#fff"
app:hintTextColor="#fff">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edt_cvv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="CVV"
android:inputType="textPassword"
android:text="***"
android:textColor="#fff" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:background="@drawable/btn_background"
android:paddingStart="20dp"
android:paddingTop="5dp"
android:paddingEnd="20dp"
android:paddingBottom="5dp"
android:text="Pay Now"
android:textColor="#fff"
android:textSize="12sp" />
</RelativeLayout>
In the PaymentActivity, We differentiated cards based on getCardOrganization method. Added different background colors and related data. Check the PaymentActivity code below
Code:
public class PaymentActivity extends AppCompatActivity {
private TextView txtCardNumber;
private TextView txtCardType;
private TextView txtExpire;
private TextView txtUserName;
private ImageView imgOrganization;
private CardView cardBackground;
private TextInputEditText edtCardNumber;
private TextInputEditText edtValid;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_payment);
init();
Intent intent = getIntent();
if (intent != null) {
CardModel cardModel = intent.getParcelableExtra("CardData");
if (cardModel != null) {
setCardData(cardModel);
}
}
}
private void setCardData(CardModel cardModel) {
String cardNumber = cardModel.getCardNumber().replaceAll("....", "$0 ");
txtCardNumber.setText(cardNumber);
edtCardNumber.setText(cardNumber);
txtExpire.setText(cardModel.getCardExpire());
edtValid.setText(cardModel.getCardExpire());
if (cardModel.getCardType() == null) {
txtCardType.setText("CARD TYPE");
} else {
txtCardType.setText(cardModel.getCardType());
}
String cardOrganization = cardModel.getCardOrganization();
if (cardOrganization != null) {
if (cardOrganization.equalsIgnoreCase("MASTERCARD")) {
setCardBackgroundAndOrganization(R.drawable.master_card, "#c22e67");
} else if (cardOrganization.equalsIgnoreCase("VISA")) {
setCardBackgroundAndOrganization(R.drawable.visa, "#4812e8");
} else if (cardOrganization.equalsIgnoreCase("UnionPay")) {
imgOrganization.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.union));
cardBackground.setCardBackgroundColor(Color.parseColor("#918B8B"));
Shader shader = new LinearGradient(70, 50, 100, 100, Color.RED, Color.BLACK, Shader.TileMode.CLAMP);
txtCardType.getPaint().setShader(shader);
}
} else {
txtCardNumber.setTextColor(Color.BLACK);
txtExpire.setTextColor(Color.BLACK);
txtCardType.setTextColor(Color.BLACK);
txtUserName.setTextColor(Color.BLACK);
}
Toast.makeText(this, cardModel.getCardOrganization() + " " + cardModel.getCardIssuer() + " " + cardModel.getCardType(), Toast.LENGTH_LONG).show();
}
private void init() {
txtCardNumber = findViewById(R.id.txt_card_number);
txtCardType = findViewById(R.id.txt_card_type);
txtExpire = findViewById(R.id.txt_expiry);
imgOrganization = findViewById(R.id.img_organization);
cardBackground = findViewById(R.id.card);
edtCardNumber = findViewById(R.id.edt_card_number);
edtValid = findViewById(R.id.edt_valid_thru);
txtUserName = findViewById(R.id.txt_user_name);
}
private void setCardBackgroundAndOrganization(int cardOrganization, String backgroundColor) {
imgOrganization.setImageDrawable(ContextCompat.getDrawable(this, cardOrganization));
cardBackground.setCardBackgroundColor(Color.parseColor(backgroundColor));
Shader shader = new LinearGradient(0, 0, 0, 100, Color.WHITE, Color.DKGRAY, Shader.TileMode.CLAMP);
txtCardType.getPaint().setShader(shader);
}
}
Find the output in below images
Tips and Tricks
We need to use CAMERA and READ_EXTERNAL_STORAGE permissions. It is recognizing cardNumber, cardType, cardIssuer and cardOrganization but it is not recognizing cardHolderName. For some of the cards it is returning same value and some cards null values for cardType, cardIssuer and cardOrganization
Conclusion
In this article we can learn about how to scan and get bank card details by using ML Kit Bank card recognition and also populating the recognized data to payment screen with good User Interface.
Reference links
https://developer.huawei.com/consum...Guides/bank-card-recognition-0000001050038118
Related
In this article I will explain how to develop a basic messaging app by Huawei Push Kit. If you want to develop a messaging app or add messaging section into your application, I hope this article will be useful for you.
Before beginning, this application and article will be addition of my first ones. If you have not seen that article, you can see the link below. It also shows how to integrate Push Kit into your Xamarin.Android Application.
https://forums.developer.huawei.com/forumPortal/en/topicview?tid=0201314061860830181&fid=0101187876626530001
Layout of activity_main
We will only add our messaging button to pass to messaging section. Final version of “activity_main.xml” will be like this.
Code:
<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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:weightSum="100"
android:id="@+id/linearLayoutMain">
<Button
android:text="Get Token"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="25px"
android:layout_weight="15"
android:id="@+id/btnGetToken" />
<Button
android:text="Send Test Notification"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="25px"
android:layout_weight="15"
android:visibility="invisible"
android:id="@+id/btnNotification" />
<Button
android:text="Send Test Data Message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="25px"
android:layout_weight="15"
android:visibility="invisible"
android:id="@+id/btnDataMessage" />
<Button
android:text="Messaging"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="25px"
android:layout_weight="15"
android:visibility="invisible"
android:id="@+id/btnMessagingMain" />
</LinearLayout>
MainActivity Class
Firstly, we will inherit ViewAnimator.IOnClickListener interface to this class. Then, we will organize inside of OnCreate Method and implement OnClick method.
Code:
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
SetContentView(Resource.Layout.activity_main);
//Identifying Buttons
btnGetToken = FindViewById<Button>(Resource.Id.btnGetToken);
btnNotification = FindViewById<Button>(Resource.Id.btnNotification);
btnDataMessage = FindViewById<Button>(Resource.Id.btnDataMessage);
btnMessagingMain = FindViewById<Button>(Resource.Id.btnMessagingMain);
//Set onClick listener to buttons
btnGetToken.SetOnClickListener(this);
btnNotification.SetOnClickListener(this);
btnDataMessage.SetOnClickListener(this);
btnMessagingMain.SetOnClickListener(this);
//Create instance of Broadcast Receiver for data message service
myReceiver = new MyBroadcastReceiver();
CheckPermission(new string[] { Android.Manifest.Permission.Internet,
Android.Manifest.Permission.AccessNetworkState,
Android.Manifest.Permission.WakeLock}, 100);
}
Secondly, we will arrange OnClick method so that we can call these events from one method.
Code:
public void OnClick(View v)
{
try
{
switch (v.Id)
{
case Resource.Id.btnGetToken:
GetToken();
break;
case Resource.Id.btnNotification:
HmsPushKit.SendNotification(tokenFromGetToken, "Title", "Body");
break;
case Resource.Id.btnDataMessage:
HmsPushKit.SendDataMessage(tokenFromGetToken, "{\"Type\":\"Test\",\"Message\":\"Welcome\"}");
break;
case Resource.Id.btnMessagingMain:
StartMessagingMain();
break;
default:
break;
}
}
catch (Exception e)
{
Log.Error("OnClickListener", e.ToString());
}
}
Thirdly, as you guess we will add StartMessagingMain method to call MessagingMainActivity class. Calling StartActivity method is adequate here. We don’t have to send any Intent.
Finally, we will move AccessToken, SendDataMessage and SendNotification method to new HmsPushKit class.
HmsPushKit.cs
We should be able to call these methods from any other class. Because of that, methods in this class should be static.
Code:
public static class HmsPushKit
{
static readonly string appID = "AppID"; //AppGallery Connect > Project Setting > App information > App ID
static readonly string appKey = "AppKey"; //AppGallery Connect > Project Setting > App information > App key
static readonly HttpClient client = new HttpClient();
public static async Task<string> GetAccessToken()
{
string uri = "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
var values = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", appID },
{ "client_secret", appKey }
};
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync(uri, content);
var jsonResponse = JObject.Parse(await response.Content.ReadAsStringAsync()); //Install-Package Newtonsoft.Json
string accessToken = jsonResponse["access_token"].ToString(); //It is valid for 3600 seconds
return accessToken;
}
/// <summary>
/// Send notification by Huawei Push Kit.
/// </summary>
/// <param name="tokenUSendTo">This token get by GetToken function</param>
/// <param name="notTitle">Notification Title</param>
/// <param name="notBody">Notification Body</param>
public static async void SendNotification(string tokenUSendTo,string notTitle,string notBody)
{
string uriNot = "https://push-api.cloud.huawei.com/v1/" + appID + "/messages:send";
var jObject = new
{
message = new
{
notification = new
{
title = notTitle,
body = notBody
},
android = new
{
notification = new
{
click_action = new
{
type = 3
}
}
},
token = new[] { tokenUSendTo }
}
};
string myJson = JsonConvert.SerializeObject(jObject, Formatting.Indented);
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", await GetAccessToken());
var responseData = await client.PostAsync(uriNot, new StringContent(myJson, Encoding.UTF8, "application/json"));
}
/// <summary>
/// Send data message by Huawei Push Kit.
/// </summary>
/// <param name="tokenUSendTo">This token get by GetToken function</param>
/// <param name="serializedObject">Use JsonConvert.SerializeObject function</param>
public static async void SendDataMessage(string tokenUSendTo, string serializedObject)
{
string uriNot = "https://push-api.cloud.huawei.com/v1/" + appID + "/messages:send";
var jObject = new
{
message = new
{
data = serializedObject,
token = new[] { tokenUSendTo }
}
};
string myJson = JsonConvert.SerializeObject(jObject, Formatting.Indented);
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", await GetAccessToken());
var responseData = await client.PostAsync(uriNot, new StringContent(myJson, Encoding.UTF8, "application/json"));
}
}
DataBase
Actually, database is not essential for a sample application but I want to make it pretty. I will use sqlite-net-pcl by SQLite-net.
{
"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"
}
This application will need two essential database tables(Model) Person and Message. We will also use UniqueData table for minor data.
Code:
public class Person
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public string Icon { get; set; }
public string Token { get; set; }
public Person(string Code,string Name)
{
this.Code = Code;
this.Name = Name;
Random rnd = new Random();
Icon = "icon" + rnd.Next(1, 4);
}
public Person()
{
}
}
public class Message
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string CodeFrom { get; set; }
public string CodeTo { get; set; }
public string Text { get; set; }
}
public class UniqueData
{
public string Key { get; set; }
public string Value { get; set; }
}
And the Database class that I use is below. The most part is easy to understand. If you want to use another Model you should add this into CreateDataBase method. Moreover, you don’t have to check if there is a database. If database does not exist, it will create otherwise it will not.
Code:
public class DataBase
{
readonly string dbPath = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), "DB.db");
readonly string _SQLite = "SQLite";
public DataBase()
{
CreateDataBase();
}
public bool CreateDataBase()
{
try
{
using (var connection = new SQLiteConnection(dbPath))
{
connection.CreateTable<Person>();
connection.CreateTable<Message>();
connection.CreateTable<UniqueDatas>();
return true;
}
}
catch (SQLiteException e)
{
Log.Error(_SQLite, e.Message);
return false;
}
}
public bool InsertIntoTable<T>(T row)
{
try
{
using (var connection = new SQLiteConnection(dbPath))
{
connection.Insert(row);
return true;
}
}
catch (SQLiteException e)
{
Log.Error(_SQLite, e.Message);
return false;
}
}
public List<T> SelectTable<T>() where T:new()
{
try
{
using (var connection = new SQLiteConnection(dbPath))
{
return connection.Table<T>().ToList();
}
}
catch (SQLiteException e)
{
Log.Error(_SQLite, e.Message);
return null;
}
}
public bool DeleteFromTable<T>(T row)
{
try
{
using (var connection = new SQLiteConnection(dbPath))
{
connection.Delete(row);
return true;
}
}
catch (SQLiteException e)
{
Log.Error(_SQLite, e.Message);
return false;
}
}
}
Layout of activity_main_messaging
I will share with you a simple screen for the list of people.
My xml file will not contain the rows. We will add them from MessagingMainActivity class. Xml code for this layout below.
Code:
<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:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/material_grey_300"
android:weightSum="4"
android:paddingHorizontal="2dp">
<Button
android:text="Del"
android:gravity="center_horizontal"
android:textSize="24dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/btnDel"/>
<TextView
android:text="Chats"
android:gravity="center"
android:textStyle="bold"
android:textColor="@android:color/black"
android:textSize="24dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="4"
android:id="@+id/textView1"
/>
<Button
android:gravity="center_horizontal"
android:text="New"
android:textSize="24dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/btnNew"/>
</LinearLayout>
<ScrollView
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:scrollbars="vertical"
android:paddingHorizontal="20dp"
android:id="@+id/scrollView">
<LinearLayout
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/linearLayout"
android:divider="#B6B6B6"
android:dividerHeight="5px">
</LinearLayout>
</ScrollView>
</LinearLayout>
MessagingMainActivity Class
Things we will do in this class are bringing people from database, adding new person, deleting person and starting messaging. So let’s start with identifing Delete Button, New button and LinearLayout which is in the ScrollView.
After, we will set ClickListener on two buttons therefore, we will inherit ViewAnimator.IOnClickListener to this activity as well.
Later, we need to take instance of our DataBase class then add people into our LinearLayout. Furthermore, I will also add sample data.
At last, we will fill OnClick method.
Code:
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.activity_main_messaging);
linearLayout = FindViewById<LinearLayout>(Resource.Id.linearLayout);
btnNew = FindViewById<Button>(Resource.Id.btnNew);
btnDel = FindViewById<Button>(Resource.Id.btnDel);
btnNew.SetOnClickListener(this);
btnDel.SetOnClickListener(this);
db = new DataBase();
if (db.SelectTable<Person>().Count == 0)
{
db.InsertIntoTable(new Person { Code = "0", Name = "Enes Durmus", Icon = "icon1" });
db.InsertIntoTable(new Person { Code = "1", Name = "Ahmet Ercek", Icon = "icon2" });
db.InsertIntoTable(new Person { Code = "2", Name = "Gokberk Bardakci", Icon = "icon3" });
}
foreach (Person person in db.SelectTable<Person>())
{
linearLayout.AddView(OneRowLL(this, person));
Log.Info("SQLite", person.ToString());
linearLayout.AddView(GetLine(this, Resources.DisplayMetrics.WidthPixels / 3));// This is just decorative
}
}
public void OnClick(View v)
{
try
{
switch (v.Id)
{
case Resource.Id.btnNew:
AddPerson();
break;
case Resource.Id.btnDel:
IsDelete = true;
break;
default:
break;
}
}
catch (Exception e)
{
Log.Error("OnClickListener", e.ToString());
}
}
As you see there is OneRowLL method. It returns a linearlayout which contains CircleImageView from Refractored.Controls.CircleImage nuget and linearlayout contains two textviews.
Code:
LinearLayout OneRowLL(Context context, Person person)
{
LinearLayout result = new LinearLayout(context);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MatchParent, LinearLayout.LayoutParams.WrapContent);
result.LayoutParameters = layoutParams;
result.Orientation = Orientation.Horizontal;
result.TransitionName = person.Code;
result.WeightSum = 1;
ViewGroup.LayoutParams vgLayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.MatchParent);
CircleImageView imageView = new CircleImageView(context); // Nuget: Refractored.Controls.CircleImageView
LinearLayout.LayoutParams iLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WrapContent, LinearLayout.LayoutParams.MatchParent) { Weight = 0.6f };
imageView.LayoutParameters = iLayoutParams;
int id = Resources.GetIdentifier("com.companyname.pushdemoforarticle:drawable/" + person.Icon, null, null);
imageView.SetImageResource(id);
imageView.BorderWidth = 6;
imageView.BorderColor = -16777216;
result.AddView(imageView);
LinearLayout subLinearLayout = new LinearLayout(context);
layoutParams.Weight = 1;
subLinearLayout.LayoutParameters = layoutParams;
subLinearLayout.Orientation = Orientation.Vertical;
subLinearLayout.SetPadding(20, 0, 0, 0);
ViewGroup.LayoutParams txtViewLayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent);
subLinearLayout.AddView(new TextView(context)
{
Text = person.Name,
TextSize = 24,
LayoutParameters = txtViewLayoutParams,
Gravity = GravityFlags.Start
});
string _lastMessage = string.Empty;
List<Message> messages = db.SelectTable<Message>().Where(x => x.CodeFrom == person.Code || x.CodeTo == person.Code).ToList();
if (messages.Count != 0)
{
Message lastMessage = messages.TakeLast(1).First();
_lastMessage = lastMessage.CodeFrom == self ? "You: "+lastMessage.Text : lastMessage.Text;
}
subLinearLayout.AddView(new TextView(context)
{
Text = _lastMessage,
LayoutParameters = txtViewLayoutParams,
});
result.AddView(subLinearLayout);
result.Click += LinearLayoutClick;
return result;
}
Afterwards, we will create AddPerson method for btnNew. We will use LayoutInflater so I also create a xml file as well. We need user’s code to get his token and a name to display it.
Code:
private void AddPerson()
{
AlertDialog dialog = InitDialog();
dialog.Show();
}
public AlertDialog InitDialog()
{
//Setting xml from layouts
LayoutInflater layoutInflater = LayoutInflater.From(this);
View view = layoutInflater.Inflate(Resource.Layout.input_box, null);
AlertDialog.Builder alertbuilder = new AlertDialog.Builder(this);
alertbuilder.SetView(view);
//Identifing EditTexts
var userCode = view.FindViewById<EditText>(Resource.Id.txtUserCode);
var userName = view.FindViewById<EditText>(Resource.Id.txtUserName);
//Setting Positive and Negative Buttons
alertbuilder.SetCancelable(false)
.SetPositiveButton("OK", delegate
{
Person person = new Person(userCode.Text.Trim(), userName.Text);
if (db.InsertIntoTable(person))
linearLayout.AddView(OneRowLL(this, person));
}).SetNegativeButton("Cancel", delegate { });
AlertDialog dialog = alertbuilder.Create();
return dialog;
}
Code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/dialogTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add Your Friend"
android:textAppearance="?android:attr/textAppearanceLarge" />
<EditText
android:id="@+id/txtUserCode"
android:inputType="text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter Your Friend's Code" />
<EditText
android:id="@+id/txtUserName"
android:inputType="text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter Your Friend's Name" />
</LinearLayout>
Next step is btnDel. Delete button will be simple. Click delete then click person afterwards person is gone. That’s adequate for now but you can improve it by adding invisible red delete button to each row and ask for approve.
Finally, LinearLayoutClick for pass to messaging. We will send their code by Intent.
Code:
public void LinearLayoutClick(object sender, EventArgs e)
{
LinearLayout row = sender as LinearLayout;
if (!IsDelete)
{
Intent intent = new Intent(this, typeof(MessagingActivity));
intent.PutExtra("code", row.TransitionName);
StartActivity(intent);
}
else
{
linearLayout.RemoveView(row);
IsDelete = false;
}
}
Layout of activity_messaging.xml
There will be 3 sections. First one is the LinearLayout which shows who we message. Second one is ScrollView for messages. And the last one is EditText for typing and sending. Xml file below.
Code:
<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:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:weightSum="100"
android:id="@+id/linearLayoutMain">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="100"
android:id="@+id/ll0"
android:transitionName="0"
android:paddingLeft="@dimen/abc_select_dialog_padding_start_material">
<refractored.controls.CircleImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="60"
app:civ_border_width="2dp"
app:civ_border_color="#000000"
android:id="@+id/imageIcon"/>
<LinearLayout
android:paddingLeft="10dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="100">
<TextView
android:textSize="24dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:id="@+id/txtName"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/txtStatus"/>
</LinearLayout>
</LinearLayout>
<ScrollView
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="100"
android:gravity="bottom"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:scrollbars="vertical"
android:paddingHorizontal="20dp"
android:id="@+id/scrollView">
<LinearLayout
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/linearLayout" />
</ScrollView>
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="20"
android:id="@+id/textInput"
android:hint="Type here!"
android:layout_marginBottom="4dp"/>
</LinearLayout>
For more information, you can visit https://forums.developer.huawei.com/forumPortal/en/topicview?tid=0202327073597950015&fid=0101187876626530001
Overview
This application helps us for getting the direction from current location to the selected place. This app uses Huawei Site Kit, Location Kit, Map kit and Huawei Direction API for showing the direction. Let us see the uses of all kits in this application.
Site Kit: This kit is used for getting the places and near-by places on keyword search.
Location Kit: This kit is used for getting the current location of the user.
Map Kit: This kit is used for showing the map, adding a marker and drawing polyline on the Huawei Map.
Direction API: This API is used for getting the path, steps and polyline between two places.
Let us start with the project configuration part:
Step 1: Create an app on App Gallery Connect.
Step 2: Enable the Site Kit, Location Lit and Map Kit in Manage APIs menu.
{
"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"
}
Step 3: Create an Android Project with the same package name as App Gallery project package name.
Step 4: Enter the below maven url inside the repositories of buildscript and allprojects (project build.gradle file).
Java:
maven { url ‘http://developer.huawei.com/repo/’ }
Step 5: Add classpath to project’s build.gradle file.
Java:
dependencies {
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath 'com.huawei.agconnect:agcp:1.3.1.300'
}
Step 6: Apply plugin in App’s build.gradle file at top after application plugin.
Java:
apply plugin: 'com.huawei.agconnect'
Step 7: Add below dependencies to app’s build.gradle file.
Java:
implementation 'com.huawei.hms:site:5.0.2.300'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation "androidx.cardview:cardview:1.0.0"
implementation 'com.huawei.hms:maps:4.0.0.302'
implementation 'com.huawei.hms:location:4.0.1.300'
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.google.code.gson:gson:2.6.1'
Step 8: Add the app ID generated when the creating the app on HUAWEI Developers to manifest file.
Java:
<meta-data
android:name="com.huawei.hms.client.appid"
android:value="appid=your app id" />
Step 9: Add the below permissions to manifest file.
Java:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.huawei.appmarket.service.commondata.permission.GET_COMMON_DATA"/>
<!-- Allow the app to obtain the coarse longitude and latitude of a user through the Wi-Fi network or base station. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- Allow the app to receive location information from satellites through the GPS chip. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
Step 10: Generate SHA 256 key and add to App Gallery Connect Project.
Step 11: download the agconnect-services.json from App Information Section. Copy and paste the Json file in the app folder of the android project.
Step 12: Sync the project.
Let us start with the implementation part:
Part 1: Site Kit Integration
Using the Site Kit, we will search for place and get the latitude and longitude.
Step 1: Get the API_KEY from App Gallery and define the same in your MainActivity.Java.
Java:
public static final String MY_API_KEY = "Your API_KEY will come here";
Step 2: Declare a SearchService object and use SearchServiceFactory to initialize the object.
Java:
// Declare SearchService object
private SearchService searchService;
// Initialize the SearchService object
searchService = SearchServiceFactory.create(this, URLEncoder.encode(MY_API_KEY, "utf-8"));
Step 3: create the layout for search a place.
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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_gravity="bottom"
android:gravity="center"
android:paddingLeft="5dp"
android:text="Find your place"
android:textSize="18sp"
android:textStyle="bold"
android:visibility="visible" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Query: "
android:visibility="gone" />
<EditText
android:id="@+id/edit_text_text_search_query"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_weight="1"
android:autofillHints=""
android:background="@drawable/search_bg"
android:hint="Search here "
android:imeOptions="actionGo"
android:inputType="text"
android:paddingLeft="10dp"
android:paddingTop="5dp"
android:paddingRight="10dp"
android:paddingBottom="5dp"
android:visibility="visible"/>
</LinearLayout>
<Button
android:id="@+id/button_text_search"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginTop="5dp"
android:background="@drawable/search_btn_bg"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:text="Search"
android:textAllCaps="false"
android:textColor="@color/upsdk_white" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="center"
android:paddingLeft="5dp"
android:text="Note: Get site Id suing Keyword/Nearby/Place suggestion search"
android:textSize="18sp"
android:textStyle="bold"
android:visibility="gone"
android:padding="10dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_gravity="bottom"
android:background="#D3D3D3"
android:gravity="center_vertical"
android:paddingLeft="5dp"
android:text="Result"
android:textSize="16sp"
android:visibility="gone" />
<TextView
android:id="@+id/response_text_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textIsSelectable="true"
android:padding="10dp"
android:textColor="@color/colorPrimary"
android:textSize="18sp"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchResultList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:visibility="visible"/>
</LinearLayout>
</LinearLayout>
Step 4: Create the ListAdapter for showing the data.
Java:
public class SearchListAdapter extends RecyclerView.Adapter<SearchListAdapter.SearchViewHolder> {
List<SearchModel> searchModelList;
Context context;
public SearchListAdapter(List<SearchModel> searchModelList, Context context) {
this.searchModelList = searchModelList;
this.context = context;
}
@NonNull
@Override
public SearchViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new SearchViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.search_result_item, parent, false));
}
@Override
public void onBindViewHolder(@NonNull SearchViewHolder holder, final int position) {
final SearchModel searchModel = searchModelList.get(position);
holder.nameTv.setText(searchModel.getName());
holder.formattedAddress.setText(searchModel.getFormattedAddress());
holder.countryCodeTv.setText(searchModel.getCountryCode());
holder.countryTv.setText(searchModel.getCountry());
// Click listener for Row view
holder.btnGetDirection.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context,"Position is "+position,Toast.LENGTH_SHORT ).show();
Intent intent = new Intent(context, DirectionActivity.class);
intent.putExtra("latitude",searchModel.getLatitude());
intent.putExtra("longitude",searchModel.getLongitude());
context.startActivity(intent);
}
});
}
@Override
public int getItemCount() {
return searchModelList.size();
}
class SearchViewHolder extends RecyclerView.ViewHolder {
TextView nameTv;
TextView formattedAddress;
TextView countryTv;
TextView countryCodeTv;
LinearLayout row_layout;
Button btnGetDirection;
public SearchViewHolder(@NonNull View itemView) {
super(itemView);
nameTv = itemView.findViewById(R.id.name);
formattedAddress = itemView.findViewById(R.id.formattedAddress);
countryTv = itemView.findViewById(R.id.country);
countryCodeTv = itemView.findViewById(R.id.countryCode);
row_layout = itemView.findViewById(R.id.row_layout);
btnGetDirection = itemView.findViewById(R.id.get_direction);
}
}
}
Step 5: Create row_layout.xml inside layout folder.
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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="5dp"
app:cardElevation="5dp"
android:layout_marginBottom="3dp">
<LinearLayout
android:id="@+id/row_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Name: "
android:textStyle="bold"
android:layout_weight="0.3"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:paddingLeft="5dp"
android:id="@+id/name"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Address: "
android:textStyle="bold"
android:layout_weight="0.3"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingLeft="5dp"
android:id="@+id/formattedAddress"
android:layout_weight="0.7"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Country: "
android:textStyle="bold"
android:layout_weight="0.3"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingLeft="5dp"
android:id="@+id/country"
android:layout_weight="0.7"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Country code: "
android:textStyle="bold"
android:layout_weight="0.3"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/countryCode"
android:paddingLeft="5dp"
android:layout_weight="0.3"/>
<Button
android:id="@+id/get_direction"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_gravity="center"
android:background="@drawable/search_btn_bg"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:text="Get Direction"
android:textAllCaps="false"
android:textColor="@color/upsdk_white" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
Step 6: Initialize the Recycler view to MainActivity.Java.
Java:
private RecyclerView searchResultList;
searchResultList.setLayoutManager(new LinearLayoutManager(this));
Step 7: On Search button click, search places and set it to ListAdapter.
Java:
mSearchBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
searchModelList = new ArrayList<>();
search();
}
});
Java:
public void search() {
TextSearchRequest textSearchRequest = new TextSearchRequest();
textSearchRequest.setQuery(queryInput.getText().toString());
textSearchRequest.setHwPoiType(HwLocationType.ADDRESS);
textSearchRequest.setHwPoiType(HwLocationType.ENTERTAINMENT_PLACE);
textSearchRequest.setHwPoiType(HwLocationType.INDIAN_RESTAURANT);
textSearchRequest.setHwPoiType(HwLocationType.CITIES);
textSearchRequest.setHwPoiType(HwLocationType.REGIONS);
searchService.textSearch(textSearchRequest, new SearchResultListener<TextSearchResponse>() {
@Override
public void onSearchResult(TextSearchResponse textSearchResponse) {
List<Site> sites = textSearchResponse.getSites();
if (sites == null || sites == null || sites.size() <= 0) {
return;
}
AddressDetail addressDetail;
if (sites != null && sites.size() > 0) {
for (Site site : sites) {
searchModel = new SearchModel();
addressDetail = site.getAddress();
searchModel.setName(site.getName());
searchModel.setFormattedAddress(site.getFormatAddress());
searchModel.setCountry(addressDetail.getCountry());
searchModel.setCountryCode(addressDetail.getCountryCode());
searchModel.setLatitude(site.getLocation().getLat());
searchModel.setLongitude(site.getLocation().getLng());
searchModelList.add(searchModel);
}
SearchListAdapter searchListAdapter = new SearchListAdapter(searchModelList, MainActivity.this);
searchResultList.setAdapter(searchListAdapter);
}
}
Now getting the list of places completed.
Result:
Part 2: Map Kit Implementation
This Kit is being used for showing the Huawei map. After clicking on Get Direction button in searched places, its navigates to DirectionActivity.Java which loads the Huawei map using Map Kit and getting the current location using Huawei Location Kit.
Step 1: Create the xml layout which contains the MapView.
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.huawei.hms.maps.MapView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:map="http://schemas.android.com/apk/res-auto"
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
map:mapType="normal"
map:uiCompass="true"/>
</LinearLayout>
Step 2: To use the MapView, implement OnMapReadyCallbackAPI and override the onMapReady(HuaweiMap huaweiMap).
Java:
public class DirectionActivity extends AppCompatActivity implements OnMapReadyCallback{
}
Step 3: Add runtime permissions.
Java:
private static final String[] RUNTIME_PERMISSIONS = {
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.INTERNET
};
if (!hasPermissions(this, RUNTIME_PERMISSIONS)) {
ActivityCompat.requestPermissions(this, RUNTIME_PERMISSIONS, REQUEST_CODE);
}
Java:
private static boolean hasPermissions(Context context, String... permissions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && permissions != null) {
for (String permission : permissions) {
if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
}
return true;
}
Step 4: Load MapView inside onCreate() method of DirectionActivity.Java and call getMapAsync() to register the callback.
Java:
private HuaweiMap hMap;
private MapView mMapView;mMapView = findViewById(R.id.mapView);
Bundle mapViewBundle = null;
if (savedInstanceState != null) {
mapViewBundle = savedInstanceState.getBundle(MAP_BUNDLE_KEY);
}
mMapView.onCreate(mapViewBundle);
mMapView.getMapAsync(this);
Step 5: Inside onMapReady() callback, get the Huawei Map object and set the current location enabled.
Java:
public void onMapReady(HuaweiMap huaweiMap) {
Log.d(TAG, "onMapReady: ");
hMap = huaweiMap;
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return;
}
hMap.setMyLocationEnabled(true);
CameraPosition build = new CameraPosition.Builder().target(new LatLng(20.5937, 78.9629)).zoom(4).build();
CameraUpdate cameraUpdate = CameraUpdateFactory.newCameraPosition(build);
hMap.animateCamera(cameraUpdate);
}
Step 6: Override the onStart(), onStop(),onDestroy(),onPause(), onResume() and onLowMemory() in the DirectionActivity.Java.
Java:
@Override
protected void onStart() {
super.onStart();
mMapView.onStart();
}
@Override
protected void onStop() {
super.onStop();
mMapView.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
mMapView.onDestroy();
}
@Override
protected void onPause() {
mMapView.onPause();
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
mMapView.onResume();
}
@Override
public void onLowMemory() {
super.onLowMemory();
mMapView.onLowMemory();
}
Now integration of map done.
Result:
Part 3: Location Kit Integration
This kit is being used for getting the current location of the user.
Step 1: Initialize the current location instances.
Java:
private LocationCallback mLocationCallback;
private LocationRequest mLocationRequest;
private FusedLocationProviderClient fusedLocationProviderClient;
private SettingsClient settingsClient;private double latitude;
private double longitude;
Step 2: call getCurrentLocation() inside onCreate() method of DirectionActivity.Java and save it.
Java:
private void getCurrentLocation(){
//create a fusedLocationProviderClient
fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this);
//create a settingsClient
settingsClient = LocationServices.getSettingsClient(this);
mLocationRequest = new LocationRequest();
// set the interval for location updates, in milliseconds.
mLocationRequest.setInterval(10000);
// set the priority of the request
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
if (null == mLocationCallback) {
mLocationCallback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
if (locationResult != null) {
List<Location> locations = locationResult.getLocations();
if (!locations.isEmpty()) {
Location loc = locations.get(0);
latitude = loc.getLatitude();
longitude = loc.getLongitude();
if(count == 0){
count = count + 1;
getRoutes();
}
}
}
}
@Override
public void onLocationAvailability(LocationAvailability locationAvailability) {
if (locationAvailability != null) {
boolean flag = locationAvailability.isLocationAvailable();
Toast.makeText(DirectionActivity.this, "isLocationAvailable:"+flag, Toast.LENGTH_SHORT).show();
}
}
};
}
}
Step 3: call requestLocationUpdatesWithCallback() method after getCurrentLocation()insideonCreate().
Java:
private void requestLocationUpdatesWithCallback() {
try {
LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder();
builder.addLocationRequest(mLocationRequest);
LocationSettingsRequest locationSettingsRequest = builder.build();
// check devices settings before request location updates.
settingsClient.checkLocationSettings(locationSettingsRequest)
.addOnSuccessListener(new OnSuccessListener<LocationSettingsResponse>()
{
@Override
public void onSuccess(LocationSettingsResponse locationSettingsResponse) {
Log.i(TAG, "check location settings success");
// request location updates
fusedLocationProviderClient
.requestLocationUpdates(mLocationRequest, mLocationCallback, Looper.getMainLooper())
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Toast.makeText(DirectionActivity.this,"requestLocationUpdatesWithCallback onFailure:",Toast.LENGTH_SHORT).show();
}
});
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e)
{
Toast.makeText(DirectionActivity.this,"checkLocationSetting onFailure:",Toast.LENGTH_SHORT).show();
int statusCode = ((ApiException) e).getStatusCode();
switch (statusCode) {
case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
try {
ResolvableApiException rae = (ResolvableApiException) e;
rae.startResolutionForResult(DirectionActivity.this, 0);
} catch (IntentSender.SendIntentException sie) {
Log.e(TAG, "PendingIntent unable to execute request.");
}
break;
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
Now getting current location part completed.
Part 4: Direction API Implementation
This API is getting used for getting the routes between source and destination location.
Step 1: Get the destination location from place info and save it inside onCreate()method of DirectionActivity.java.
Java:
// Destination location data
private double destLatitude;
private double destLongitude;destLatitude = getIntent().getDoubleExtra("latitude",0.0);
destLongitude = getIntent().getDoubleExtra("longitude",0.0);
Step 2: Create DirectionService.java for getting the routes between source and destination location.
Java:
public class DirectionService {
public static final String ROOT_URL = "https://mapapi.cloud.huawei.com/mapApi/v1/routeService/";
public static final String conection = "?key=";
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
final MutableLiveData<JsonData> jsonData = new MutableLiveData<>();
private String jsonResponse;
public RouteInfo info;
private static DirectionService directionService;
public static DirectionService getInstance(){
if (directionService == null)
directionService = new DirectionService();
return directionService;
}
public void setRouteInfo(RouteInfo info)
{
this.info = info;
}
public void driving(String serviceName, String apiKey, Route route) throws UnsupportedEncodingException {
JSONObject json = new JSONObject();
JSONObject origin = new JSONObject();
JSONObject destination = new JSONObject();
try {
origin.put("lng",route.getOrigin().getLng());
origin.put("lat", route.getOrigin().getLat());
destination.put("lng", route.getDestination().getLng());
destination.put("lat", route.getDestination().getLat());
json.put("origin", origin);
json.put("destination", destination);
} catch (JSONException e) {
Log.e("error", e.getMessage());
}
RequestBody body = RequestBody.create(JSON, String.valueOf(json));
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder().url(ROOT_URL + serviceName + conection + URLEncoder.encode(apiKey, "UTF-8"))
.post(body)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("driving", e.toString());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
// Log.d("driving", response.body().string());
info.routeInfo(response.body().string());
}
});
}
}
Step 3: Call the getRoute() method inside getCurrentLocation() of DirectionActivity.Java.
Java:
private void getRoutes()
{
// get the routes
Origin origin = new Origin();
origin.setLat(latitude);
origin.setLng(longitude);
Destination dest = new Destination();
dest.setLat(destLatitude);
dest.setLng(destLongitude);
Route route = new Route();
route.setOrigin(origin);
route.setDestination(dest);
try {
DirectionService.getInstance().setRouteInfo(this);
DirectionService.getInstance().driving("driving",MainActivity.MY_API_KEY,route);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
Step 4: Create an Interface RouteInfo.
Java:
public interface RouteInfo {
void routeInfo(String info);
}
Step 5: DirectionActivity.Java will implement RouteInfo interface.
Java:
public class DirectionActivity extends AppCompatActivity implements OnMapReadyCallback,RouteInfo{
}
Step 6: Override routeInfo() method and convert string response to Json object using gson library.
Java:
@Override
public void routeInfo(String info) {
Gson gson = new Gson();
JsonData obj = gson.fromJson(info,JsonData.class);
addPolyline(obj);
addMarker();
animateCameraToCurrentLocation();
}
Step 7: Add polyline from the routes info.
Java:
private void addPolyline(JsonData obj) {
if(hMap == null){
return;
}
if (null != mPolyline) {
mPolyline.remove();
mPolyline = null;
}
PolylineOptions options = new PolylineOptions();
if(obj != null){
ArrayList<Routes> routes = obj.getRoutes();
if(routes != null && routes.size() > 0){
ArrayList<Path> paths = routes.get(0).getPaths();
if(paths != null && paths.size() > 0){
ArrayList<Step> steps = paths.get(0).getSteps();
if(steps != null && steps.size() > 0)
{
for(Step step : steps) {
ArrayList<com.huawei.sitekitsampleapp.model.Polyline> polylines = step.getPolyline();
if(polylines != null && polylines.size() > 0){
for(com.huawei.sitekitsampleapp.model.Polyline polyline : polylines){
// Add lat lng to options
options.add(new LatLng(polyline.getLat(),polyline.getLng()));
}
}
}
}
}
}
}
options.color(Color.GREEN).width(3);
mPolyline = hMap.addPolyline(options);
}
Step 8: Add a Marker at destination location.
Java:
private void addMarker() {
if (null != mMarker) {
mMarker.remove();
}
MarkerOptions options = new MarkerOptions()
.position(new LatLng(destLatitude, destLongitude)).icon(BitmapDescriptorFactory.fromResource(R.drawable.marker));
mMarker = hMap.addMarker(options);
}
Step 9: Add marker.xml to drawable folder.
XML:
<vector android:height="24dp" android:tint="#FF1730"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
</vector>
Step 10: Animate camera to current location.
Java:
private void animateCameraToCurrentLocation()
{
CameraPosition build = new CameraPosition.Builder().target(new LatLng(latitude, longitude)).zoom(13).build();
CameraUpdate cameraUpdate = CameraUpdateFactory.newCameraPosition(build);
hMap.animateCamera(cameraUpdate);
}
Now showing the direction between two place done.
Result:
Tips and Tricks:
Set input properly for origin and Destination for getting the routes.
Conclusion:
This application can help to show the direction between your current location to your favorite place. You can get the restaurants, schools and places and navigate to the same.
Reference:
https://developer.huawei.com/consumer/en/doc/development/HMSCore-References-V5/directions-walking-0000001050161494-V5#EN-US_TOPIC_0000001050161494__section12176172981317
https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/android-sdk-keyword-search-0000001050156630
https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/android-sdk-map-instance-creation-0000001062881706
https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/location-develop-steps-0000001050746143
{
"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"
}
Article Introduction
In this article, we will develop Music Station android app for Huawei Vision S (Smart TV) devices. Huawei Vision S is a brand new large screen category and important part of Huawei's "1+8+N" full-scenario services and Huawei Developer Ecosystem. Since, Huawei Vision S system architecture supports AOSP project framework, we used Leanback Library which offers extensive features for large screens to develop our user experience.
Why an app for Huawei Vision S?
Large screen offers better visibility and enhanced user experience. Due to Covid-19 lockdown, Smart TV has grown to include over 80% more users than it had this time last year. Total distribution of usage for TV is increasing rapidly. As a result of this, total number of TV apps has jumped dramatically including educational and entertainment apps.
Designing App for Huawei Vision S
While designing an app for Huawei Vision S, we have to keep following key points in our mind:
Build Layout for TV: We must design landscape orientation layout that allows users to easily see the screen 10 feet away from the TV.
Management Controller: Our app must support arrow keys and handle offline controllers as well as inputs from multiple controllers.
For this article, we implemented Leanback Library which offers amazing and interactive user experience for apps such as Audio/Video players and so on.
Pre-Requisites
Before getting started, following are the requirements:
Android Studio (During this tutorial, we used version 4.1.1)
Android SDK 24 or later
Huawei Vision S for testing
Development
Following are the major steps of development for this article:
Step 1: Add Dependencies & Permissions
1.1: Add the following dependencies in the app level build.gradle file:
Java:
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
// TV Libs
implementation 'androidx.leanback:leanback:1.0.0'
implementation 'androidx.leanback:leanback-preference:1.0.0'
// General
implementation 'org.greenrobot:eventbus:3.2.0'
implementation 'com.google.code.gson:gson:2.8.7'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.nabinbhandari.android:permissions:3.8'
// Animation
implementation 'com.airbnb.android:lottie:3.7.0'
implementation 'com.gauravk.audiovisualizer:audiovisualizer:0.9.2'
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation ('com.github.bumptech.glide:okhttp3-integration:4.12.0'){
exclude group: 'glide-parent'
}
}
1.2: We are developing this app only for TV. So, we will disable the Touch_Screen requirements and enable the Leanback. For the audio visualizer, we need Record_Audio permission. Add the following permissions and features tag in the AndroidManifest.xml:
Java:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="true" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />
1.3: Huawei Vision S supports Leanback Library but does not supports Leanback Launcher. So, we added the following tag in the SplashActivity inside the AndroidManifest.xml:
Code:
<activity android:name=".activities.SplashActivity"
android:screenOrientation="landscape"
android:banner="@drawable/app_icon"
android:icon="@drawable/app_icon"
android:label="@string/app_name"
android:launchMode="singleTop"
android:logo="@drawable/app_icon"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<!-- HUAWEI Vision S only support CATEGORY_LAUNCHER -->
<category android:name="android.intent.category.LAUNCHER" />
<!-- ADD the below line only if you want to release the same code on Google Play -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
1.4: The banner icon is required when we are developing apps for TV. The size for Huawei Vision S banner icon is 496x280 which must be added in the drawable folder.
Step 2: Generating Supported Language JSON
Since our main goal is playing Music, we restricted data source and generated a JSON file locally to avoid API creation and API calling. In real world scenario, an API can be developed or can be used to get the real-time data.
Step 3: Building Layout
3.1: The most important layout of our application is of PlayerActivity. Add the following activity_player.xml layout file in the layout folder of the res. We developed this layout to enhance user experience and add custom views like Audio Visualizer. The layout has two main sub-layouts, Player Content View and Error View.
XML:
<?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:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_background"
android:keepScreenOn="true">
<ImageView
android:id="@+id/imgBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_background" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/splashAnimation"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:lottie_autoPlay="false"
app:lottie_progress="53"
app:lottie_rawRes="@raw/splash_animation" />
<com.gauravk.audiovisualizer.visualizer.CircleLineVisualizer
android:id="@+id/blastVisualizer"
android:layout_width="match_parent"
android:layout_height="match_parent"
custom:avColor="@color/light_red"
custom:avDensity="0.8"
custom:avSpeed="normal"
custom:avType="fill" />
<ImageView
android:id="@+id/imgOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.5"
android:background="@android:color/black" />
<ProgressBar
android:id="@+id/progressBarLoader"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerInParent="true"
android:indeterminate="true"
android:indeterminateTint="@color/colorPrimaryDark"
android:indeterminateTintMode="src_atop"
android:visibility="gone" />
<RelativeLayout
android:id="@+id/rlContentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:paddingStart="50dp"
android:paddingEnd="50dp">
<ImageView
android:id="@+id/imgIcon"
android:layout_width="80dp"
android:layout_height="80dp" />
<TextView
android:id="@+id/txtSongName"
style="@style/TextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/imgIcon"
android:layout_marginTop="5dp"
android:textSize="@dimen/title_text_size" />
<TextView
android:id="@+id/txtArtistName"
style="@style/TextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/txtSongName"
android:layout_marginTop="5dp"
android:textSize="@dimen/artist_text_size" />
<TextView
android:id="@+id/txtCategoryName"
style="@style/TextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/txtArtistName"
android:layout_marginTop="5dp"
android:textSize="@dimen/category_text_size" />
<ImageButton
android:id="@+id/btnPlayPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txtCategoryName"
android:layout_marginTop="50dp"
android:background="@drawable/ui_selector_bg"
android:focusable="true"
android:src="@drawable/lb_ic_play" />
<SeekBar
android:id="@+id/seekBarAudio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/btnPlayPause"
android:layout_marginTop="10dp"
android:background="@drawable/ui_selector_bg"
android:colorControlActivated="@color/white"
android:focusable="true"
android:progressTint="@color/white"
android:thumbTint="@color/white" />
<TextView
android:id="@+id/txtTotalDuration"
style="@style/TextViewStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/seekBarAudio"
android:layout_alignEnd="@+id/seekBarAudio"
android:layout_marginTop="5dp"
android:text=" / -"
android:textSize="@dimen/time_text_size" />
<TextView
android:id="@+id/txtCurrentTime"
style="@style/TextViewStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/seekBarAudio"
android:layout_marginTop="5dp"
android:layout_toStartOf="@+id/txtTotalDuration"
android:text="-"
android:textSize="@dimen/time_text_size" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rlErrorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:visibility="gone">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<ImageView
android:id="@+id/imgErrorLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:src="@drawable/lb_ic_sad_cloud" />
<TextView
android:id="@+id/txtError"
style="@style/TextViewStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/imgErrorLoading"
android:layout_centerHorizontal="true"
android:text="@string/error_message"
android:textSize="@dimen/time_text_size" />
<Button
android:id="@+id/btnDismiss"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txtError"
android:layout_centerHorizontal="true"
android:layout_marginTop="30dp"
android:focusable="true"
android:text="@string/dismiss_error" />
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>
3.2: In this article, we used Lottie animation in the SplashActivity for better user experience inside the following activity_splash.xml.
XML:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/colorPrimaryDark"
tools:context=".activities.SplashActivity">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/splashAnimation"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:lottie_autoPlay="true"
app:lottie_repeatCount="1"
app:lottie_rawRes="@raw/splash_animation" />
<ImageView
android:id="@+id/imgAppIcon"
android:src="@drawable/img_music_station_logo"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:layout_marginBottom="40dp"
android:scaleX="0.8"
android:scaleY="0.8"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
Step 4: Adding JAVA Classes
4.1: We extended the MainFragment class from BrowseFragment which is Leanback home layout component. This view handles the landing screen containing interactive side menu, main content area with different cards and navigation between them.
Java:
public class MainFragment extends BrowseFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
prepareBackgroundManager();
setupUIElements();
loadRows();
setupEventListeners();
}
private void loadRows() {
ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
CardPresenter cardPresenter = new CardPresenter();
int i;
for (i = 0; i < DataUtil.getData(getActivity()).size(); i++) {
CategoryModel categoryModel = DataUtil.getData(getActivity()).get(i);
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
for (int j = 0; j < categoryModel.getCategorySongs().size(); j++) {
listRowAdapter.add(categoryModel.getCategorySongs().get(j));
}
HeaderItem header = new HeaderItem(i, categoryModel.getCategoryName());
rowsAdapter.add(new ListRow(header, listRowAdapter));
}
setAdapter(rowsAdapter);
}
private void prepareBackgroundManager() {
BackgroundManager mBackgroundManager = BackgroundManager.getInstance(getActivity());
mBackgroundManager.attach(getActivity().getWindow());
mBackgroundManager.setColor(getResources().getColor(R.color.main_background));
DisplayMetrics mMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
}
private void setupUIElements() {
setTitle(getString(R.string.app_name));
setHeadersState(HEADERS_ENABLED);
setHeadersTransitionOnBackEnabled(true);
setBrandColor(ContextCompat.getColor(getActivity(), R.color.colorPrimaryDark));
setSearchAffordanceColor(ContextCompat.getColor(getActivity(), R.color.colorPrimary));
}
private void setupEventListeners() {
setOnItemViewClickedListener(new ItemViewClickedListener());
}
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof Song) {
Song song = (Song) item;
Intent intent = new Intent(getActivity(), PlayerActivity.class);
intent.putExtra(DataUtil.SONG_DETAIL, song);
getActivity().startActivity(intent);
}
}
}
}
4.2: Cards are displayed using CardPresenter which is extended by Presenter from the Leanback library.
Java:
public class CardPresenter extends Presenter {
private static final int CARD_WIDTH = 313;
private static final int CARD_HEIGHT = 176;
private static int sSelectedBackgroundColor;
private static int sDefaultBackgroundColor;
private static void updateCardBackgroundColor(ImageCardView view, boolean selected) {
int color = selected ? sSelectedBackgroundColor : sDefaultBackgroundColor;
view.setBackgroundColor(color);
view.findViewById(R.id.info_field).setBackgroundColor(color);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
sDefaultBackgroundColor = ContextCompat.getColor(parent.getContext(), R.color.background);
sSelectedBackgroundColor = ContextCompat.getColor(parent.getContext(), R.color.colorPrimaryDark);
ImageCardView cardView =
new ImageCardView(parent.getContext()) {
@Override
public void setSelected(boolean selected) {
updateCardBackgroundColor(this, selected);
super.setSelected(selected);
}
};
cardView.setFocusable(true);
cardView.setFocusableInTouchMode(true);
updateCardBackgroundColor(cardView, false);
return new ViewHolder(cardView);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, Object item) {
Song song = (Song) item;
ImageCardView cardView = (ImageCardView) viewHolder.view;
cardView.setTitleText(song.getSongName());
cardView.setContentText(song.getArtistName());
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT);
Glide.with(viewHolder.view.getContext())
.load(song.getImageURL())
.centerCrop()
.placeholder(R.drawable.app_icon)
.error(R.drawable.app_icon)
.into(cardView.getMainImageView());
}
@Override
public void onUnbindViewHolder(ViewHolder viewHolder) {
ImageCardView cardView = (ImageCardView) viewHolder.view;
cardView.setBadgeImage(null);
cardView.setMainImage(null);
}
}
4.3: Whenever user click on any Card Item, PlayerActivity is opened. Following are some of the important functions of the PlayerActivity.java. Please refer to the github link for complete code of this class.
Java:
private void handlePlayer(){
if (currentState == MediaPlayerHolder.PlayerState.PLAYING) {
pauseSong();
} else if (currentState == MediaPlayerHolder.PlayerState.PAUSED ||
currentState == MediaPlayerHolder.PlayerState.COMPLETED ||
currentState == MediaPlayerHolder.PlayerState.RESET) {
playSong();
}
}
private void setUI() {
if (song != null) {
txtSongName.setText(song.getSongName());
txtArtistName.setText(song.getArtistName());
txtCategoryName.setText(song.getCategoryName());
Glide.with(this)
.load(song.getImageURL())
.centerCrop()
.circleCrop()
.placeholder(R.drawable.app_icon)
.error(R.drawable.app_icon)
.into(imgIcon);
}
setupSeekBar();
}
private void pauseSong() {
EventBus.getDefault().post(new LocalEventFromMainActivity.PausePlayback());
}
private void playSong() {
EventBus.getDefault().post(new LocalEventFromMainActivity.StartPlayback());
}
private void resetSong() {
EventBus.getDefault().post(new LocalEventFromMainActivity.ResetPlayback());
}
public void log(StringBuffer formattedMessage) {
Log.d(PlayerActivity.class.getSimpleName(), String.format("log: %s", formattedMessage));
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(LocalEventFromMediaPlayerHolder.UpdateLog event) {
log(event.formattedMessage);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(LocalEventFromMediaPlayerHolder.PlaybackDuration event) {
seekBarAudio.setMax(event.duration);
setTotalDuration(event.duration);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(LocalEventFromMediaPlayerHolder.PlaybackPosition event) {
if (!isUserSeeking) {
seekBarAudio.setProgress(event.position, true);
updateProgressTime(event.position);
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(LocalEventFromMediaPlayerHolder.StateChanged event) {
hideLoader();
currentState = event.currentState;
switch (event.currentState) {
case PLAYING:
btnPlayPause.setImageResource(R.drawable.lb_ic_pause);
if (mMediaPlayerHolder.getAudioSessionId() != -1 && blastVisualizer != null){
blastVisualizer.setAudioSessionId(mMediaPlayerHolder.getAudioSessionId());
blastVisualizer.show();
}
break;
case PAUSED:
case RESET:
case COMPLETED:
btnPlayPause.setImageResource(R.drawable.lb_ic_play);
if (blastVisualizer != null){
blastVisualizer.hide();
}
break;
case ERROR:
showError();
break;
}
}
private void showError() {
if(song != null){
String title = song.getSongName();
String artist = song.getArtistName();
String songName = "Unable to play " + title + " (" + artist + ")";
txtError.setText(songName);
}
rlErrorLayout.setVisibility(View.VISIBLE);
btnDismiss.requestFocus();
}
private void setTotalDuration(int duration) {
long totalSecs = TimeUnit.MILLISECONDS.toSeconds(duration);
long hours = totalSecs / 3600;
long minutes = (totalSecs % 3600) / 60;
long seconds = totalSecs % 60;
String totalDuration = String.format(Locale.ENGLISH, "%02d:%02d:%02d", hours, minutes, seconds);
if (hours == 0) {
totalDuration = String.format(Locale.ENGLISH, "%02d:%02d", minutes, seconds);
}
String text = " / " + totalDuration;
txtTotalDuration.setText(text);
}
private void updateProgressTime(int position){
long currentSecs = TimeUnit.MILLISECONDS.toSeconds(position);
long hours = currentSecs / 3600;
long minutes = (currentSecs % 3600) / 60;
long seconds = currentSecs % 60;
String currentDuration = String.format(Locale.ENGLISH, "%02d:%02d:%02d", hours, minutes, seconds);
if (hours == 0) {
currentDuration = String.format(Locale.ENGLISH, "%02d:%02d", minutes, seconds);
}
txtCurrentTime.setText(currentDuration);
}
4.4: We used EventBus to asynchronously notify the PlayerActivity UI changes. The MediaPlayerHolder.java class handles the MediaPlayer states and manages the functionalities like playing, pause and seekbar position updates. Please refer to the github link for complete code of this class.
Java:
public int getAudioSessionId(){
if(mMediaPlayer != null){
return mMediaPlayer.getAudioSessionId();
} else
return -1;
}
public void release() {
logToUI("release() and mMediaPlayer = null");
mMediaPlayer.release();
EventBus.getDefault().unregister(this);
}
public void stop() {
logToUI("stop() and mMediaPlayer = null");
mMediaPlayer.stop();
}
public void play() {
if (!mMediaPlayer.isPlaying()) {
logToUI(String.format("start() %s", urlPath));
mMediaPlayer.start();
startUpdatingSeekbarWithPlaybackProgress();
EventBus.getDefault()
.post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.PLAYING));
}
}
public void pause() {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
logToUI("pause()");
EventBus.getDefault()
.post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.PAUSED));
}
}
public void reset() {
logToUI("reset()");
mMediaPlayer.reset();
load(urlPath);
stopUpdatingSeekbarWithPlaybackProgress(true);
EventBus.getDefault()
.post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.RESET));
}
public void load(String url) {
this.urlPath = url.replaceAll(" ", "%20");
if (mMediaPlayer != null) {
try {
mMediaPlayer.setAudioAttributes(
new AudioAttributes
.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build());
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer player) {
initSeekbar();
play();
}
});
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
EventBus.getDefault()
.post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.ERROR));
return false;
}
});
mMediaPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
@Override
public void onSeekComplete(MediaPlayer arg0) {
EventBus.getDefault().post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.PLAYING));
SystemClock.sleep(200);
mMediaPlayer.start();
}
});
logToUI("load() {1. setDataSource}");
mMediaPlayer.setDataSource(urlPath);
logToUI("load() {2. prepare}");
mMediaPlayer.prepareAsync();
} catch (Exception e) {
EventBus.getDefault().post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.ERROR));
logToUI(e.toString());
}
}
}
public void seekTo(int duration) {
logToUI(String.format(Locale.ENGLISH, "seekTo() %d ms", duration));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
mMediaPlayer.seekTo(duration, MediaPlayer.SEEK_CLOSEST);
else
mMediaPlayer.seekTo(duration);
}
private void stopUpdatingSeekbarWithPlaybackProgress(boolean resetUIPlaybackPosition) {
if (mExecutor != null) {
mExecutor.shutdownNow();
}
mExecutor = null;
mSeekbarProgressUpdateTask = null;
if (resetUIPlaybackPosition) {
EventBus.getDefault().post(new LocalEventFromMediaPlayerHolder.PlaybackPosition(0));
}
}
private void startUpdatingSeekbarWithPlaybackProgress() {
// Setup a recurring task to sync the mMediaPlayer position with the Seekbar.
if (mExecutor == null) {
mExecutor = Executors.newSingleThreadScheduledExecutor();
}
if (mSeekbarProgressUpdateTask == null) {
mSeekbarProgressUpdateTask = new Runnable() {
@Override
public void run() {
if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
int currentPosition = mMediaPlayer.getCurrentPosition();
EventBus.getDefault().post(
new LocalEventFromMediaPlayerHolder.PlaybackPosition(
currentPosition));
}
}
};
}
mExecutor.scheduleAtFixedRate(
mSeekbarProgressUpdateTask,
0,
SEEKBAR_REFRESH_INTERVAL_MS,
TimeUnit.MILLISECONDS
);
}
public void initSeekbar() {
// Set the duration.
final int duration = mMediaPlayer.getDuration();
EventBus.getDefault().post(new LocalEventFromMediaPlayerHolder.PlaybackDuration(duration));
logToUI(String.format(Locale.ENGLISH, "setting seekbar max %d sec", TimeUnit.MILLISECONDS.toSeconds(duration)));
}
When user click Select button on the Remote Control of Huawei Vision S on any item, the player view is opened and MediaPlayer start loading the song url. Once the song is loaded, it starts playing and the icon of Play changes to Pause. The Audio Visualizer takes AudioSessionId to sync with audio song. By default, seekbar is selected for the user to skip the songs using Right and Left buttons of the Remote Control. The duration of the song updates based on seekbar position.
Step 5: Run the application
We have added all the required code. Now, just build the project, run the application and test on Huawei Vision S.
Conclusion
Using Leanback, developers can develop beautifully crafted android applications with amazing UI/UX experience for Huawei Vision S. They can also enhance their user engagement and behavior. Combining different Huawei Kits supported by Vision S like Account or IAP can yield amazing results.
Tips and Tricks
You must use default Launcher if you are developing app for Huawei Vision S.
Leanback Launcher is not supported by Huawei Vision S.
If you have same code base for Mobile and Vision S devices, you can use TVUtils class to check at run-time about the device and offer functionalities based on it.
Make sure to add all the permissions like RECORD_AUDIO, INTERNET.
Make sure to add run-time permissions check. In this article, we used 3rd party Permission Check library with custom Dialog if user deny any of the required permission.
Always use animation libraries like Lottie or ViewAnimator to enhance UI/UX in your application.
We used AudioVisualizer library to bring Music feel on our Player UI.
References
Android TV Documentation:
https://developer.android.com/training/tv/start
https://developer.android.com/training/tv/start/layouts
https://developer.android.com/training/tv/start/controllers
https://developer.android.com/training/tv/start/navigation
Lottie Android Documentation:
http://airbnb.io/lottie/#/android
Github Code Link:
https://github.com/yasirtahir/MusicStationTV
Original Source
{
"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 will learn how to connect smart gloves from the paired devices. The process is in the smart gloves application. Smart glove application shows all the paired Bluetooth devices. User has to select the gloves and connect to Bluetooth device.
If you are new to the series of articles, follow articles.
Beginner: Integration of Huawei Account kit in Navigation Glove IoT application Using Kotlin - Part 1
Beginner: Integration of Huawei Map kit in Navigation Glove IoT application Using Kotlin - Part 2
Beginner: Integration of Huawei Site kit in Navigation Glove IoT application Using Kotlin - Part 3
Beginner: Integration of Huawei Direction API in Navigation Glove IoT application Using Kotlin - Part 4
In this article, we learn how to display all the paired device in the application. We will also learn how to integrate the Recyclerview in android. Make sure device is already paired to phone.
Note: Application is built in androidx if you are developing application in the older than androidx you need to add the recyclerview dependencies.
Add the following permission in AndroidManifest.xml
XML:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
Follow the steps
Step 1: Add the recyclerview in the activity_connect_smart_gloves.xml
XML:
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/pairedDeviceBtn"
android:layout_below="@id/messageTv"
tools:listitem="@layout/paired_device_list_item"/>
Step 2: If you want to display items let us create one xml file.
paired_device_list_item.xml
XML:
<?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"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_margin="10dp"
app:cardElevation="6dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="5dp"
android:id="@+id/rootLayout">
<ImageView
android:id="@+id/imageview"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="5dp"
android:src="@drawable/gloves_ic"
android:visibility="visible" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="5dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="15dp"
android:text="@string/name"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/deviceName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="15dp"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="15dp"
android:text="@string/mac_address"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/macAddress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="15dp"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
Step 3: Create data class for device model (DeviceModel.kt)
Java:
package com.huawei.navigationglove.model
data class DeviceModel(val name: String, val macAddress: String)
Step 4: declare layout manager in activity.
Java:
private lateinit var linearLayoutManager: LinearLayoutManager
Step 5: Declare the device model variable and the paired device list.
Java:
private var pairedDeviceList = ArrayList<DeviceModel>()
var deviceModel: DeviceModel? = null
Step 6: Initialize the layout manager and set it to recyclerview in onCreate().
Java:
linearLayoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = linearLayoutManager
Step 7: Now create PairedDeviceListAdapter.kt for recycler view.
Java:
package com.huawei.navigationglove.ui.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.huawei.navigationglove.R
import com.huawei.navigationglove.callbacks.OnDeviceClickCallback
import com.huawei.navigationglove.model.DeviceModel
class PairedDevicesAdapter(
private val mList: List<DeviceModel>,
private val onDeviceClickCallback: OnDeviceClickCallback
) :
RecyclerView.Adapter<PairedDevicesAdapter.ViewHolder>() {
// create new views
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.paired_device_list_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val deviceModel = mList[position]
holder.deviceName.text = deviceModel.name
holder.macAddress.text = deviceModel.macAddress
holder.rootLayout.setOnClickListener { onDeviceClickCallback.onDeviceClick(deviceModel) }
}
// return the number of the items in the list
override fun getItemCount(): Int {
return mList.size
}
// Holds the views for adding it to image and text
class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) {
val deviceName: TextView = itemView.findViewById(R.id.deviceName)
val macAddress: TextView = itemView.findViewById(R.id.macAddress)
val rootLayout: LinearLayout = itemView.findViewById(R.id.rootLayout)
}
}
Step 8: Now get all the paired devices, and set the list of devices to adapter.
Java:
private fun pairedDevicesList() {
pairedDeviceList = ArrayList<DeviceModel>()
pairedDevices = myBluetooth!!.bondedDevices
val list: ArrayList<String> = ArrayList<String>()
if ((pairedDevices as MutableSet<BluetoothDevice>?)!!.size > 0) {
for (bt in (pairedDevices as MutableSet<BluetoothDevice>?)!!) {
Log.e(TAG, bt.name + "\n" + bt.address)
list.add(bt.name + "\n" + bt.address) //Get the device's name and the address
deviceModel = DeviceModel(bt.name, bt.address)
pairedDeviceList.add(deviceModel!!)
}
val adapter = PairedDevicesAdapter(pairedDeviceList, this)
recyclerView.adapter = adapter
} else {
Toast.makeText(
applicationContext,
"No Paired Bluetooth Devices Found.",
Toast.LENGTH_LONG
).show()
}
}
Step 9: By now recyclerview will show the list of paired devices. Now when user clicks on the device it has to connect. So now lets create onclick for recyclerview. To do that we will do it using interface callback.
Create OnDeviceClickCallback.kt interface.
Java:
package com.huawei.navigationglove.callbacks
import com.huawei.navigationglove.model.DeviceModel
interface OnDeviceClickCallback {
fun onDeviceClick(deviceModel: DeviceModel)
}
Step 9: Implement this interface in activity.
Java:
class ConnectSmartGlovesActivity : AppCompatActivity(), OnDeviceClickCallback {
Now override the function.
Java:
override fun onDeviceClick(deviceModel: DeviceModel) {
address = deviceModel.macAddress
ConnectBT(this).execute()
}
Step 10: Now create following variables.
Java:
private var progress: ProgressDialog? =null //This progress dialog box while connecting to bluetooth
private var isBtConnected = false
var btSocket: BluetoothSocket? = null //Bridge which sends data to bluetooth from Android app
var myBluetooth: BluetoothAdapter? = null
val myUUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") //Unique
var address: String? = null
Step: 11: Create Async class for connecting.
//This class contains functions that need to be done to connect to the HC-05 module
Java:
open class ConnectBT(val context: Context) : AsyncTask<Void?, Void?, Void?>() // UI thread
{
private var ConnectSuccess = true //if it's here, it's almost connected
override fun onPreExecute() {
progress = ProgressDialog.show(
context,
"Connecting...",
"Please wait!!!"
) //show a progress dialog
}
@SuppressLint("MissingPermission")
override fun doInBackground(vararg devices: Void?): Void? //while the progress dialog is shown, the connection is done in background
{
try {
if (btSocket == null || !isBtConnected) {
myBluetooth =
BluetoothAdapter.getDefaultAdapter() //get the mobile bluetooth device
val dispositivo: BluetoothDevice = myBluetooth!!.getRemoteDevice(address) //connects to the device's address and checks if it's available
Log.e(TAG,"Device: "+Gson().toJson(dispositivo))
btSocket = dispositivo.createInsecureRfcommSocketToServiceRecord(myUUID) //create a RFCOMM (SPP) connection
Log.e(TAG,"Device: "+Gson().toJson(btSocket))
BluetoothAdapter.getDefaultAdapter().cancelDiscovery()
btSocket!!.connect() //start connection
}
} catch (e: IOException) {
ConnectSuccess = false //if the try failed, you can check the exception here
Log.e("MapsActivity","Device not connected" +e.message)
}
return null
}
override fun onPostExecute(result: Void?) //after the doInBackground, it checks if everything went fine
{
super.onPostExecute(result)
if (!ConnectSuccess) {
val toast =
Toast.makeText(context, "Failure", Toast.LENGTH_LONG)
toast.show()
val handler = Handler()
handler.postDelayed({ toast.cancel() }, 300)
//TODO need to check this
//finish()
} else {
val toast = Toast.makeText(context, "Success.", Toast.LENGTH_LONG)
toast.show()
val handler = Handler()
handler.postDelayed({ toast.cancel() }, 300)
//msg("Connected.");
SystemClock.sleep(1000)
isBtConnected = true
}
progress!!.dismiss()
}
}
Step 12: To get the connection and disconnection state create register broadcast receiver in onCreate().
Java:
val filter = IntentFilter()
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED)
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
this.registerReceiver(nReceiver, filter)
Step 13: Create broadcast receiver.
Java:
//This BroadcastReceiver finds listens the status of bluetooth
private val nReceiver: BroadcastReceiver? = object : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.M)
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
if (BluetoothDevice.ACTION_FOUND == action) {
} else if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
co = true
Log.e("mode:", "Connected")
requestLocationUpdatesWithCallback()
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED == action) {
} else if (BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action) {
Log.e("mode:", "disConnected")
} else if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action) {
co = false
Log.e("mode:", "disConnected1")
val vib = context.getSystemService(Vibrator::class.java)
Log.e("Vibe", "" + vib.hasVibrator())
//t1.speak("Your Bluetooth Has Been Disconnected", TextToSpeech.QUEUE_FLUSH, null);
for (i in 0..4) {
vib.vibrate(700)
SystemClock.sleep(1000)
}
i = 0
}
}
}
Result
Tips and Tricks
1. Make sure you have added Bluetooth permission in AndroidManifest.xml
2. Make sure you phone Bluetooth is enabled.
3. If you are building application below androidx, Make sure you have added the recyclerview dependency in build.gradle file.
Conclusion
In this article, we have learnt how to use recyclerview in the android application along with that we have displayed the list of paired devices in the smart gloves application. And also we have learnt how to connect device from the smart gloves application.
Reference
Bluetooth Official document
Recyclerview Document
{
"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