More information like this, you can visit HUAWEI Developer Forum
Original link: https://forums.developer.huawei.com/forumPortal/en/topicview?tid=0202352465220930189&fid=0101187876626530001
QUIC is a new transport protocol based on UDP wich allows improve the speed of the network connecctions due to the low latency of UDP. QUIC means Quick UDP Internet Connections and is planned to become a standar.
The key features of QUIC are:
Dramatically reduced connection establishment time
Improved congestion control
Multiplexing without head of line blocking
Connection migration
Some companies are starting supporting QUIC on their servers, now from the client side, is time to be prepared and adding QUIC support to our apps. This time we will buid a News App with QUIC support using the HQUIC kit.
HQUIC allow us connect with web services over the QUIC protocol, if the remote does not supports QUIC, the kit will use HTTP 2 instead automatically.
Previous Requirements
An android studio project
A developer account on newsapi.org
Adding the required dependencies
This example will require the next dependencies:
RecyclerView: To show all the news in a list.
SwipeRefreshLayout: To allw the user refreshing the screen using a swipe gesture.
HQUIC: The kit which allow us connect with services over QUIC
To use HQUIC you must add the Huawei's repository to your top level build.gradle.
Code:
buildscript {
buildscript {
repositories {
maven { url 'https://developer.huawei.com/repo/' }// This line
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
}
}
}
allprojects {
repositories {
maven { url 'https://developer.huawei.com/repo/' }// and this one
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Now add the required dependencies to your app level build.gradle
Code:
implementation 'com.huawei.hms:hquic-provider:5.0.0.300'
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
Designing the UI
We will show a list of items inside a RecyclerView, so you must design the item layout which will be displayed for each article. This basic view will only display the title, a brief introduction and the date of publication:
Code:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="TextView"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Content"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="TextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/content" />
</androidx.constraintlayout.widget.ConstraintLayout>
For the Activity Layout, the design will contain a SwipeRefresLayout with the RecyclerView inside
Code:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="@+id/swipeRefreshLayout">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerNews"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.recyclerview.widget.RecyclerView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>
To show the articles on a recycler view you will need an adapter, the adapter will report the click event of any item to the given listener.
Code:
class NewsAdapter(val news:ArrayList<Article>,var listener: NewsViewHolder.onNewsClickListener?): RecyclerView.Adapter<NewsAdapter.NewsViewHolder>() {
class NewsViewHolder(itemView: View, var listener:onNewsClickListener?) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
public fun init(article: Article){
itemView.title.text=article.title
itemView.content.text=article.description
val date= SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()).parse(article.time)
itemView.time.text=date?.toString()
itemView.setOnClickListener(this)
}
interface onNewsClickListener{
fun onClickedArticle(position: Int)
}
override fun onClick(v: View?) {
listener?.onClickedArticle(adapterPosition)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
val view=LayoutInflater.from(parent.context)
.inflate(R.layout.item_view,parent,false)
return NewsViewHolder(view,listener)
}
override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
holder.init(news[position])
}
override fun getItemCount(): Int {
return news.size
}
}
HQUIC Service
We will use the HQUICService class provided by the HQUIC demo project. This class abstracts the HQUIC kit capabilities and allows us to perform a request easily.
Code:
class HQUICService (val context: Context){
private val TAG = "HQUICService"
private val DEFAULT_PORT = 443
private val DEFAULT_ALTERNATEPORT = 443
private val executor: Executor = Executors.newSingleThreadExecutor()
private var cronetEngine: CronetEngine? = null
private var callback: UrlRequest.Callback? = null
/**
* Asynchronous initialization.
*/
init {
HQUICManager.asyncInit(
context,
object : HQUICManager.HQUICInitCallback {
override fun onSuccess() {
Log.i(TAG, "HQUICManager asyncInit success")
}
override fun onFail(e: Exception?) {
Log.w(TAG, "HQUICManager asyncInit fail")
}
})
}
/**
* Create a Cronet engine.
*
* @param url URL.
* @return cronetEngine Cronet engine.
*/
private fun createCronetEngine(url: String): CronetEngine? {
if (cronetEngine != null) {
return cronetEngine
}
val builder= CronetEngine.Builder(context)
builder.enableQuic(true)
builder.addQuicHint(getHost(url), DEFAULT_PORT, DEFAULT_ALTERNATEPORT)
cronetEngine = builder.build()
return cronetEngine
}
/**
* Construct a request
*
* @param url Request URL.
* @param method method Method type.
* @return UrlRequest urlrequest instance.
*/
private fun builRequest(url: String, method: String): UrlRequest? {
val cronetEngine: CronetEngine? = createCronetEngine(url)
val requestBuilder= cronetEngine?.newUrlRequestBuilder(url, callback, executor)
requestBuilder?.apply {
setHttpMethod(method)
return build()
}
return null
}
/**
* Send a request to the URL.
*
* @param url Request URL.
* @param method Request method type.
*/
fun sendRequest(url: String, method: String) {
Log.i(TAG, "callURL: url is " + url + "and method is " + method)
val urlRequest: UrlRequest? = builRequest(url, method)
urlRequest?.apply { urlRequest.start() }
}
/**
* Parse the domain name to obtain the host name.
*
* @param url Request URL.
* @return host Host name.
*/
private fun getHost(url: String): String? {
var host: String? = null
try {
val url1 = URL(url)
host = url1.host
} catch (e: MalformedURLException) {
Log.e(TAG, "getHost: ", e)
}
return host
}
fun setCallback(mCallback: UrlRequest.Callback?) {
callback = mCallback
}
}
Downloading the news
We will define a helper class to handle the request and parses the response into an ArrayList. The HQUIC kit will read certain number of bytes per time, for big responses the onReadCompleted method will be called more than once.
Code:
data class Article(val author:String,
val title:String,
val description:String,
val url:String,
val time:String)
class NewsClient(context: Context): UrlRequest.Callback() {
var hquicService: HQUICService? = null
val CAPACITY = 10240
val TAG="NewsDownloader"
var response:StringBuilder=java.lang.StringBuilder()
var listener:NewsClientListener?=null
init {
hquicService = HQUICService(context)
hquicService?.setCallback(this)
}
fun getNews(url: String, method:String){
hquicService?.sendRequest(url,method)
}
override fun onRedirectReceived(
request: UrlRequest,
info: UrlResponseInfo,
newLocationUrl: String
) {
request.followRedirect()
}
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
Log.i(TAG, "onResponseStarted: ")
val byteBuffer = ByteBuffer.allocateDirect(CAPACITY)
request.read(byteBuffer)
}
override fun onReadCompleted(
request: UrlRequest,
info: UrlResponseInfo,
byteBuffer: ByteBuffer
) {
Log.i(TAG, "onReadCompleted: method is called")
val readed=String(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.position())
response.append(readed)
request.read(ByteBuffer.allocateDirect(CAPACITY))
}
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
//If everything is ok you can read the response body
val json=JSONObject(response.toString())
val array=json.getJSONArray("articles")
val list=ArrayList<Article>()
for (i in 0 until array.length()){
val article=array.getJSONObject(i)
val author=article.getString("author")
val title=article.getString("title")
val description=article.getString("description")
val time=article.getString("publishedAt")
val url=article.getString("url")
list.add(Article(author, title, description, url, time))
}
listener?.onSuccess(list)
}
override fun onFailed(request: UrlRequest, info: UrlResponseInfo, error: CronetException) {
//If someting fails you must report the error
listener?.onFailure(error.toString())
}
public interface NewsClientListener{
fun onSuccess(news:ArrayList<Article>)
fun onFailure(error: String)
}
}
Making the request
Define tue request properties
Code:
private val API_KEY="YOUR_API_KEY"
private val URL = "https://newsapi.org/v2/top-headlines?apiKey=$API_KEY"
private val METHOD = "GET"
Call the getNews function to start the request, if everything goes well, the list of news will be delivered on the onSuccess callback. If an error ocurs, the onFailure callback will be triggered.
Code:
private fun getNews() {
val country=Locale.getDefault().country
val url= "$URL&country=$country"
Log.e("URL",url)
val downloader=NewsClient(this)
downloader.apply {
[email protected]
getNews(url,METHOD)
}
}
override fun onSuccess(news: ArrayList<Article>) {
this.news.apply {
clear()
addAll(news)
}
runOnUiThread{
swipeRefreshLayout.isRefreshing=false
loadingDialog?.dismiss()
adapter?.notifyDataSetChanged()
}
}
override fun onFailure(error: String) {
val errorDialog=AlertDialog.Builder(this).apply {
setTitle(R.string.error_title)
val message="${getString(R.string.error_message)} \n $error"
setMessage(message)
setPositiveButton(R.string.ok){ dialog, _ ->
dialog.dismiss()
}
setCancelable(false)
create()
show()
}
}
{
"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"
}
Conclusion
Is just a question of time for the QUIC protocol to become in the new standard of internet connections. For now you can get your apps ready to suppot it and provide the best user experience.
Test this demo: https://github.com/danms07/HQUICNews
Reference
Official Document
HQUIC sample code
what is it difference between QUIC and retrofit ?
Related
More information like this, you can visit HUAWEI Developer Forum
Introduction
In this article I would like to address the Game Service topic by doing a practical example in which we will implement the kit. The goal is to achieve a simple application where our user has the possibility to log in with his Huawei ID and obtain information regarding his player on Game Service. In the Huawei repositories we can find projects with all the implementation but in my opinion it is better to create a new project where we have the opportunity to do our own development.
Steps:
1. Create an App in AGC
2. Add the necessary libraries and repositories
3. Permissions in our Application
4. Building the user interface
5. Create the Signing class
6. Write the code in our MainActity
7. Test the App
Create an App in AGC
If you already have experience implementing HMS you will have noticed that the creation of an App in the AGC console is regularly required. If you are in this case, I recommend that you skip this step and go to step 3.
In this link you can find a detailed guide on how to create an App in AGC, generate your finger print and download the json services.
https://developer.huawei.com/consumer/en/codelab/HMSPreparation/index.html#0
In case you do not have Android Studio Configured on your device, I also share a guide with the requirements.
https://developer.huawei.com/consumer/en/codelab/HMSAccounts-Kotlin/index.html#1
Once your App is created in AGC, it is important that you activate the Game Service, Account Kit and In App purchases services in case you require it. We can go to the Apis Management tab
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
What have we accomplished so far?
We have an App in AGC connected to our project and with activated services.
Add the necessary libraries and repositories
Once our project is created in Android Studio it will be necessary to add the following lines of code
Let's add the following lines to the project gradle
Code:
buildscript {
repositories {
google()
jcenter()
//Esta linea
maven { url 'http://developer.huawei.com/repo/' }
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.0"
//Tambien esta esta linea
classpath 'com.huawei.agconnect:agcp:1.3.1.300'
}
}
allprojects {
repositories {
google()
jcenter()
//Repositorio de Maven
maven {url 'http://developer.huawei.com/repo/'}
}
}
Now let's add the necessary dependencies in the app gradle.
Code:
implementation'com.huawei.agconnect:agconnect-core:1.3.1.300' //HMSCore
implementation 'com.huawei.hms:hwid:4.0.4.300' //Huawei Id
implementation 'com.huawei.hms:game:4.0.3.301' //Game Service
implementation 'com.huawei.hms:base:4.0.4.301'
implementation 'com.squareup.picasso:picasso:2.71828'
Do not forget to add the plugin.
Code:
apply plugin:'com.huawei.agconnect'
Permissions in our Application
Now let's add the permissions of our Application because we want to use the Game Service services. So in the Android Manifest File, let's add the necessary permissions.
Code:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Building the user interface
The time has come to build the user interface, what we will do is use the powerful tool that Android Studio from Constraint offers us, basically what we are looking for is to achieve something like this. Where we will place the elements that we want to show when obtaining the data of our user. Of course you can create the interface that you like the most, but if you want to use this simple distribution of elements this is the code
Code:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="68dp"
android:text="Welcome"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/avatarImg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/textView9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="28dp"
android:text="Player Id"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/idtv" />
<TextView
android:id="@+id/textView10"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginEnd="51dp"
android:text="Player Level"
app:layout_constraintEnd_toStartOf="@+id/leveltv"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintTop_toBottomOf="@+id/textView9" />
<TextView
android:id="@+id/idtv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="24dp"
android:text="TextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.082"
app:layout_constraintStart_toEndOf="@+id/textView9"
app:layout_constraintTop_toBottomOf="@+id/avatarImg" />
<TextView
android:id="@+id/leveltv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="TextView"
app:layout_constraintStart_toStartOf="@+id/idtv"
app:layout_constraintTop_toBottomOf="@+id/idtv" />
<Button
android:id="@+id/loginButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Login"
app:layout_constraintBottom_toBottomOf="parent"
tools:layout_editor_absoluteX="0dp" />
<Button
android:id="@+id/btnInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="88dp"
android:text="PlayerInfo"
app:layout_constraintBottom_toTopOf="@+id/loginButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.475"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView10"
app:layout_constraintVertical_bias="0.178" />
</androidx.constraintlayout.widget.ConstraintLayout>
Create the SignInCenter class
In this class we will make the instance of the class and we will be able to manage the Huawei of Authentication
Code:
public class SignInCenter {
private static SignInCenter INS = new SignInCenter();
private static AuthHuaweiId currentAuthHuaweiId;
public static SignInCenter get() {
return INS;
}
public void updateAuthHuaweiId(AuthHuaweiId AuthHuaweiId) {
currentAuthHuaweiId = AuthHuaweiId;
}
public AuthHuaweiId getAuthHuaweiId() {
return currentAuthHuaweiId;
}
}
Write the code in our MainActity
Let's work on our Main Activity is the time to get our hands dirty, I will put the source code and within the add comments to each method this for reading is easier
Code:
/**
*Variable Declarations
* Buttons, TextViews
* AuthHuaweiId to store retrived elements
* ImageView to show avatar
* Handler to use a service
*/
private Button loginButton;
private Button infoButton;
private TextView welcomeTv,idtv,leveltv;
private AuthHuaweiId mAuthid;
private final static int SIGN_IN_INTENT = 3000;
private String playerId;
private String sessionId = null;
private ImageView avatar;
private Handler handler;
private final static int HEARTBEAT_TIME = 15 * 60 * 1000;
private static final String TAG = "TAG1";
private boolean hasInit = false;
//---------------------------------------------------------------------------------------------
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Call UI elements we can use Butterknife or if you are using Kotlin use the extensions to get rid of this part
loginButton = findViewById(R.id.loginButton);
loginButton.setOnClickListener(this);
welcomeTv = findViewById(R.id.textView);
avatar = findViewById(R.id.avatarImg);
infoButton = findViewById(R.id.btnInfo);
infoButton.setOnClickListener(this);
idtv = findViewById(R.id.idtv);
leveltv = findViewById(R.id.leveltv);
}
//---------------------------------------------------------------------------------------------
/*
* Overide onclick method and by using elements id call the proper methods, dont forget to
* implement View.Onclick Listener Interface
*/
//---------------------------------------------------------------------------------------------
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.loginButton:
signIn();
break;
case R.id.btnInfo:
init();
getCurrentPlayer();
break;
}
}
//---------------------------------------------------------------------------------------------
/*
* This is the method where we will initialize the service by passing the Authentification ID
* if the user has logged in correctly
*/
//---------------------------------------------------------------------------------------------
public void init() {
JosAppsClient appsClient = JosApps.getJosAppsClient(this,mAuthid);
appsClient.init();
Log.d(TAG,"init success");
hasInit = true;
}
//---------------------------------------------------------------------------------------------
/*
*Get the current player by using the ID there are many ways to achive this but for this example
* we will keep it simple
**/
//---------------------------------------------------------------------------------------------
private void getCurrentPlayer() {
PlayersClientImpl client = (PlayersClientImpl) Games.getPlayersClient(this,mAuthid);
//Create a Task to get the Player
Task<Player> task = client.getCurrentPlayer();
task.addOnSuccessListener(new OnSuccessListener<Player>() {
@Override
public void onSuccess(Player player) {
String result = "display:" + player.getDisplayName() + "\n" + "playerId:" + player.getPlayerId() + "\n"
+ "playerLevel:" + player.getLevel() + "\n" + "timestamp:" + player.getSignTs() + "\n"
+ "playerSign:" + player.getPlayerSign();
Log.d("TAG1",result);
idtv.setText(player.getPlayerId());
leveltv.setText(player.getLevel() + "");
playerId = player.getPlayerId();
gameBegin();
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//gamePlayExtra();
}
};
new Timer().schedule(new TimerTask() {
@Override
public void run() {
Message message = new Message();
handler.sendMessage(message);
}
}, HEARTBEAT_TIME, HEARTBEAT_TIME);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof ApiException) {
String result = "rtnCode:" + ((ApiException) e).getStatusCode();
Log.d("TAG1",result);
}
}
});
}
//---------------------------------------------------------------------------------------------
/*
*On this method the Game will begin with the obtained ID
*/
//---------------------------------------------------------------------------------------------
public void gameBegin() {
if (TextUtils.isEmpty(playerId)) {
Log.d("TAG1","GetCurrentPlayer first.");
return;
}
String uid = UUID.randomUUID().toString();
PlayersClient client = Games.getPlayersClient(this,mAuthid);
Task<String> task = client.submitPlayerEvent(playerId, uid, "GAMEBEGIN");
task.addOnSuccessListener(new OnSuccessListener<String>() {
@Override
public void onSuccess(String jsonRequest) {
if (jsonRequest == null) {
Log.d("TAG1","jsonRequest is null");
return;
}
try {
JSONObject data = new JSONObject(jsonRequest);
sessionId = data.getString("transactionId");
} catch (JSONException e) {
Log.d("TAG1","parse jsonArray meet json exception");
return;
}
Log.d("TAG1","submitPlayerEvent traceId: " + jsonRequest);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof ApiException) {
String result = "rtnCode:" + ((ApiException) e).getStatusCode();
Log.d("TAG1",result);
}
}
});
}
//---------------------------------------------------------------------------------------------
/*
* This is the method to sign in i have commented most of the lines to understa what we have been doing
*/
//---------------------------------------------------------------------------------------------
private void signIn() {
//Create a task with the Type of AuthHuaweiId
Task<AuthHuaweiId> authHuaweiIdTask = HuaweiIdAuthManager.getService(this, getHuaweiIdParams()).silentSignIn();
//Add the proper Listener so we can track the response of the Task
authHuaweiIdTask.addOnSuccessListener(new OnSuccessListener<AuthHuaweiId>() {
//Must overide the Osuccess Method which will return an AuthHuaweiId Object
@Override
public void onSuccess(AuthHuaweiId authHuaweiId) {
//Logs to track the information
Log.d("TAG1","signIn success");
Log.d("TAG1","Id" + authHuaweiId.getDisplayName());
Log.d("TAG1", "Picture" + authHuaweiId.getAvatarUriString());
//Handle the user interface
welcomeTv.setVisibility(View.VISIBLE);
welcomeTv.setText("Welcome back " + authHuaweiId.getDisplayName());
Picasso.get().load(authHuaweiId.getAvatarUriString()).into(avatar);
loginButton.setVisibility(View.INVISIBLE);
Log.d("TAG1","AT:" + authHuaweiId.getAccessToken());
mAuthid = authHuaweiId;
//Sign in Center update
SignInCenter.get().updateAuthHuaweiId(authHuaweiId);
infoButton.setVisibility(View.VISIBLE);
}
}).addOnFailureListener(new OnFailureListener() {
//Something went wrong, use this method to inform your users
@Override
public void onFailure(Exception e) {
if (e instanceof ApiException) {
ApiException apiException = (ApiException) e;
Log.d("TAG1","signIn failed:" + apiException.getStatusCode());
Log.d("TAG1","start getSignInIntent");
signInNewWay();
}
}
});
}
//---------------------------------------------------------------------------------------------
private void signInNewWay() {
Intent intent = HuaweiIdAuthManager.getService(MainActivity.this, getHuaweiIdParams()).getSignInIntent();
startActivityForResult(intent, SIGN_IN_INTENT);
}
//---------------------------------------------------------------------------------------------
/*
*Create the HuaweiIdParams so we can send it to the service
*/
//---------------------------------------------------------------------------------------------
public HuaweiIdAuthParams getHuaweiIdParams() {
return new HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM_GAME).createParams();
}
//---------------------------------------------------------------------------------------------
/*
*Dont Forget to overide onActivity Result otherwise we wont have any functionality working
* */
//---------------------------------------------------------------------------------------------
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (SIGN_IN_INTENT == requestCode) {
handleSignInResult(data);
} else {
Log.d("TAG1","unknown requestCode in onActivityResult");
}
}
//---------------------------------------------------------------------------------------------
/*
*Handle the result of signin we can take some desicion here
* */
//---------------------------------------------------------------------------------------------
private void handleSignInResult(Intent data) {
if (null == data) {
Log.d("TAG1","signIn inetnt is null");
return;
}
// HuaweiIdSignIn.getSignedInAccountFromIntent(data);
String jsonSignInResult = data.getStringExtra("HUAWEIID_SIGNIN_RESULT");
if (TextUtils.isEmpty(jsonSignInResult)) {
Log.d("TAG1","SignIn result is empty");
return;
}
try {
HuaweiIdAuthResult signInResult = new HuaweiIdAuthResult().fromJson(jsonSignInResult);
if (0 == signInResult.getStatus().getStatusCode()) {
Log.d("TAG1","Sign in success.");
Log.d("TAG1","Sign in result: " + signInResult.toJson());
SignInCenter.get().updateAuthHuaweiId(signInResult.getHuaweiId());
// getCurrentPlayer();
} else {
Log.d("TAG1","Sign in failed: " + signInResult.getStatus().getStatusCode());
}
} catch (JSONException var7) {
Log.d("TAG1","Failed to convert json from signInResult.");
}
}
Conclusion
Well! Well we have achieved it we have a small implementation of a simple Game Service but that we could add to our video games. Game Service has many very useful methods, check the documentation if you would like to go deeper into this topic.
https://developer.huawei.com/consumer/en/hms/huawei-game
QUIC is a new transport protocol based on UDP wich allows improve the speed of the network connecctions due to the low latency of UDP. QUIC means Quick UDP Internet Connections and is planned to become a standar.
The key features of QUIC are:
Dramatically reduced connection establishment time
Improved congestion control
Multiplexing without head of line blocking
Connection migration
Some companies are starting supporting QUIC on their servers, now from the client side, is time to be prepared and adding QUIC support to our apps. This time we will buid a News App with QUIC support using the HQUIC kit.
HQUIC allow us connect with web services over the QUIC protocol, if the remote does not supports QUIC, the kit will use HTTP 2 instead automatically.
Previous Requirements
An android studio project
A developer account on newsapi.org
Adding the required dependencies
This example will require the next dependencies:
RecyclerView: To show all the news in a list.
SwipeRefreshLayout: To allw the user refreshing the screen using a swipe gesture.=
HQUIC: The kit which allow us connect with services over QUIC
To use HQUIC you must add the Huawei's repository to your top level build.gradle.
Code:
buildscript {
buildscript {
repositories {
maven { url 'https://developer.huawei.com/repo/' }// This line
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
}
}
}
allprojects {
repositories {
maven { url 'https://developer.huawei.com/repo/' }// and this one
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Now add the required dependencies to your app level build.gradle
Code:
implementation 'com.huawei.hms:hquic-provider:5.0.0.300'
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
Designing the UI
We will show a list of items inside a RecyclerView, so you must design the item layout which will be displayed for each article. This basic view will only display the title, a brief introduction and the date of publication:
Code:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="TextView"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Content"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="TextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/content" />
</androidx.constraintlayout.widget.ConstraintLayout>
For the Activity Layout, the design will contain a SwipeRefresLayout with the RecyclerView inside
Code:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="@+id/swipeRefreshLayout">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerNews"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.recyclerview.widget.RecyclerView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>
To show the articles on a recycler view you will need an adapter, the adapter will report the click event of any item to the given listener.
Code:
class NewsAdapter(val news:ArrayList<Article>,var listener: NewsViewHolder.onNewsClickListener?): RecyclerView.Adapter<NewsAdapter.NewsViewHolder>() {
class NewsViewHolder(itemView: View, var listener:onNewsClickListener?) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
public fun init(article: Article){
itemView.title.text=article.title
itemView.content.text=article.description
val date= SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()).parse(article.time)
itemView.time.text=date?.toString()
itemView.setOnClickListener(this)
}
interface onNewsClickListener{
fun onClickedArticle(position: Int)
}
override fun onClick(v: View?) {
listener?.onClickedArticle(adapterPosition)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
val view=LayoutInflater.from(parent.context)
.inflate(R.layout.item_view,parent,false)
return NewsViewHolder(view,listener)
}
override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
holder.init(news[position])
}
override fun getItemCount(): Int {
return news.size
}
}
HQUIC Service
We will use the HQUICService class provided by the HQUIC demo project. This class abstracts the HQUIC kit capabilities and allows us to perform a request easily.
Code:
class HQUICService (val context: Context){
private val TAG = "HQUICService"
private val DEFAULT_PORT = 443
private val DEFAULT_ALTERNATEPORT = 443
private val executor: Executor = Executors.newSingleThreadExecutor()
private var cronetEngine: CronetEngine? = null
private var callback: UrlRequest.Callback? = null
/**
* Asynchronous initialization.
*/
init {
HQUICManager.asyncInit(
context,
object : HQUICManager.HQUICInitCallback {
override fun onSuccess() {
Log.i(TAG, "HQUICManager asyncInit success")
}
override fun onFail(e: Exception?) {
Log.w(TAG, "HQUICManager asyncInit fail")
}
})
}
/**
* Create a Cronet engine.
*
* @param url URL.
* @return cronetEngine Cronet engine.
*/
private fun createCronetEngine(url: String): CronetEngine? {
if (cronetEngine != null) {
return cronetEngine
}
val builder= CronetEngine.Builder(context)
builder.enableQuic(true)
builder.addQuicHint(getHost(url), DEFAULT_PORT, DEFAULT_ALTERNATEPORT)
cronetEngine = builder.build()
return cronetEngine
}
/**
* Construct a request
*
* @param url Request URL.
* @param method method Method type.
* @return UrlRequest urlrequest instance.
*/
private fun builRequest(url: String, method: String): UrlRequest? {
val cronetEngine: CronetEngine? = createCronetEngine(url)
val requestBuilder= cronetEngine?.newUrlRequestBuilder(url, callback, executor)
requestBuilder?.apply {
setHttpMethod(method)
return build()
}
return null
}
/**
* Send a request to the URL.
*
* @param url Request URL.
* @param method Request method type.
*/
fun sendRequest(url: String, method: String) {
Log.i(TAG, "callURL: url is " + url + "and method is " + method)
val urlRequest: UrlRequest? = builRequest(url, method)
urlRequest?.apply { urlRequest.start() }
}
/**
* Parse the domain name to obtain the host name.
*
* @param url Request URL.
* @return host Host name.
*/
private fun getHost(url: String): String? {
var host: String? = null
try {
val url1 = URL(url)
host = url1.host
} catch (e: MalformedURLException) {
Log.e(TAG, "getHost: ", e)
}
return host
}
fun setCallback(mCallback: UrlRequest.Callback?) {
callback = mCallback
}
}
Downloading the news
We will define a helper class to handle the request and parses the response into an ArrayList. The HQUIC kit will read certain number of bytes per time, for big responses the onReadCompleted method will be called more than once.
Code:
data class Article(val author:String,
val title:String,
val description:String,
val url:String,
val time:String)
class NewsClient(context: Context): UrlRequest.Callback() {
var hquicService: HQUICService? = null
val CAPACITY = 10240
val TAG="NewsDownloader"
var response:StringBuilder=java.lang.StringBuilder()
var listener:NewsClientListener?=null
init {
hquicService = HQUICService(context)
hquicService?.setCallback(this)
}
fun getNews(url: String, method:String){
hquicService?.sendRequest(url,method)
}
override fun onRedirectReceived(
request: UrlRequest,
info: UrlResponseInfo,
newLocationUrl: String
) {
request.followRedirect()
}
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
Log.i(TAG, "onResponseStarted: ")
val byteBuffer = ByteBuffer.allocateDirect(CAPACITY)
request.read(byteBuffer)
}
override fun onReadCompleted(
request: UrlRequest,
info: UrlResponseInfo,
byteBuffer: ByteBuffer
) {
Log.i(TAG, "onReadCompleted: method is called")
val readed=String(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.position())
response.append(readed)
request.read(ByteBuffer.allocateDirect(CAPACITY))
}
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
//If everything is ok you can read the response body
val json=JSONObject(response.toString())
val array=json.getJSONArray("articles")
val list=ArrayList<Article>()
for (i in 0 until array.length()){
val article=array.getJSONObject(i)
val author=article.getString("author")
val title=article.getString("title")
val description=article.getString("description")
val time=article.getString("publishedAt")
val url=article.getString("url")
list.add(Article(author, title, description, url, time))
}
listener?.onSuccess(list)
}
override fun onFailed(request: UrlRequest, info: UrlResponseInfo, error: CronetException) {
//If someting fails you must report the error
listener?.onFailure(error.toString())
}
public interface NewsClientListener{
fun onSuccess(news:ArrayList<Article>)
fun onFailure(error: String)
}
}
Making the request
Define tue request properties
Code:
private val API_KEY="YOUR_API_KEY"
private val URL = "https://newsapi.org/v2/top-headlines?apiKey=$API_KEY"
private val METHOD = "GET"
Call the getNews function to start the request, if everything goes well, the list of news will be delivered on the onSuccess callback. If an error ocurs, the onFailure callback will be triggered.
Code:
private fun getNews() {
val country=Locale.getDefault().country
val url= "$URL&country=$country"
Log.e("URL",url)
val downloader=NewsClient(this)
downloader.apply {
[email protected]
getNews(url,METHOD)
}
}
override fun onSuccess(news: ArrayList<Article>) {
this.news.apply {
clear()
addAll(news)
}
runOnUiThread{
swipeRefreshLayout.isRefreshing=false
loadingDialog?.dismiss()
adapter?.notifyDataSetChanged()
}
}
override fun onFailure(error: String) {
val errorDialog=AlertDialog.Builder(this).apply {
setTitle(R.string.error_title)
val message="${getString(R.string.error_message)} \n $error"
setMessage(message)
setPositiveButton(R.string.ok){ dialog, _ ->
dialog.dismiss()
}
setCancelable(false)
create()
show()
}
}
{
"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"
}
Conclusion
Is just a question of time for the QUIC protocol to become in the new standard of internet connections. For now you can get your apps ready to suppot it and provide the best user experience.
Test this demo: https://github.com/danms07/HQUICNews
Reference
Official Document
HQUIC sample code
Sometimes you may need to show an alert to an specific user, for example, as a result of the interaction of another user (someone else commented your post), or to notify an event caused by your own business logic (Sign In detected in another device). In this article I'm going to show you how to bind a push token to a user id, so you can send personalized push notifications.
Previous requirements
A verified developer account
An app project with the HMS core SDK
Enabling the requred services
Go to your project in AppGallery Connect and make sure to enable Push Kit and Account Kit on the Manage APIs tab.
{
"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"
}
Now go to Push Kit on the left side panel to properly enabling the Push Service.
Integrting the SDKs
First, you must add the related dependencies for Account kit and Push kit. Add the next lines under dependencies of your app level build.gradle.
Code:
implementation 'com.huawei.hms:hwid:5.0.1.301'
implementation 'com.huawei.hms:push:5.0.1.300'
Note: please refer to the related document to obtain the most recent version of each SDK
Solution proposal
We want to bind a push token to the user's email (you can choose other user ID in your own implementation). We know we can get both in our app, but we don't know exaclty when. So, my suggestion is registering a BroadcastReceiver upon the app startup, which will wait for the token and the email. When the receiver has both, it will perform a POST operation to our server to save/update the binding. Once the operation is complete, the Receiver will perform a self unregister operation.
Code:
class MyBroadcastReceiver: BroadcastReceiver() {
companion object {
private val TAG="Receiver"
val PARAM="param"
val MAIL="mail"
val TOKEN="token"
val ACTION="ACTION_REGISTER_USER"
private val SERVER="https://a8a7ycwh8b.execute-api.us-east-2.amazonaws.com/Prod"
}
private var email:String?=null
private var token:String?=null
private var context:Context?=null
override fun onReceive(context: Context?, intent: Intent?) {
this.context=context
intent?.extras?.let {
val key=it.getString(PARAM)
val value=it.getString(key)
checkParamValues(key!!,value!!)
}
}
fun register(context:Context){
Log.d(TAG,"Registering Receiver")
context.registerReceiver(this,IntentFilter(ACTION))
}
private fun unregister(){
Log.d(TAG,"UnregisteringReceiver")
context?.unregisterReceiver(this)
}
private fun checkParamValues(key: String, value: String) {
Log.e(TAG,"Received $key")
when(key){
MAIL -> {
email=value
token?.let{registerInServer(value,it)}
}
TOKEN -> {
token=value
email?.let { registerInServer(it,value) }
}
}
}
private fun registerInServer(mail:String,token:String){
//perform registerOperation
CoroutineScope(Dispatchers.IO).launch {
val connection= URL(SERVER).openConnection() as HttpURLConnection
val outputStream=connection.apply{
requestMethod="POST"
doInput=true
doOutput=true
}.outputStream
val jsonObject=JSONObject().apply {
put(MAIL,mail)
put(TOKEN,token)
}
outputStream.write(jsonObject.toString().toByteArray())
Log.d("Response","${connection.responseCode} ${connection.responseMessage}")
//unregisterReceiver
unregister()
}
}
}
To ensure the receiver will be registered before we get the push token, we can define our own Application class and register the Receiver from the onCreate method. If we register a BroadcastReceiver by using the ApplicationContext, it will be alive as long as the app is running, that's very convenient for our purpose.
Code:
class MyApplication: Application() {
override fun onCreate() {
super.onCreate()
MyBroadcastReceiver().register(this)
}
}
Note: Don't forget to add your custom application class to your AndroidManifest.
Code:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hms.demo.accountpush">
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
If you already know how to implement Account kit and Push kit, maybe your problem is solved with this example. If not, please keep reading.
Integrating Push Kit
Create a class which inherits from HmsMessageService and override the methoods onNewToken and onMessageReceived.
Code:
class MyHmsMessageService: HmsMessageService() {
override fun onNewToken(token: String?) {
super.onNewToken(token)
token?.let {
Log.d("newToken",it)
publishToken(it)
}
}
private fun publishToken(token:String) {
val intent= Intent().apply {
action = MyBroadcastReceiver.ACTION
putExtra(MyBroadcastReceiver.PARAM,MyBroadcastReceiver.TOKEN)
putExtra(MyBroadcastReceiver.TOKEN,token)
}
sendBroadcast(intent)
}
override fun onMessageReceived(message: RemoteMessage?) {
message?.let {
Log.i("OnNewMessage",it.data)
val map=it.dataOfMap
for( key in map.keys){
Log.d("onNewMessage",map[key]!!)
}
}
}
}
Once the push token is delivered, we must report it to our BroadcastReceiver by using the publishToken funcion.
Add the next settings to your Android Manifest under <application>
Code:
<!--Enables the push automatic initialization-->
<meta-data
android:name="push_kit_auto_init_enabled"
android:value="true"/>
<!--Registers the custom HmsMessageService-->
<service
android:name=".MyHmsMessageService"
android:exported="false">
<intent-filter>
<action android:name="com.huawei.push.action.MESSAGING_EVENT"/>
</intent-filter>
</service>
This configuration will enable the automatic initialization, so the push token will be always delivered to the onNewToken method automatically.
Integrating Account Kit
First, we will ask if the app has been authorized previously. For this we can use the Silent Sign In API.
Code:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//We will try to use the silent sign in
val authParams = HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM)
.setScopeList(LoginActivity.SCOPES)
.createParams()
val service = HuaweiIdAuthManager.getService([email protected], authParams)
val task = service.silentSignIn()
task.addOnSuccessListener {//The sign in is successful, we can jump to the profile activity
//Apply for a token and register in the server side
val intent=Intent([email protected],ProfileActivity::class.java)
intent.putExtra(ProfileActivity.ACCOUNT,it)
startActivity(intent)
}
task.addOnFailureListener{//Redirect to the login activity
startActivity(Intent([email protected],LoginActivity::class.java))
finish()
}
}
}
If a user has authorized our app before, he will be logged in automatically. By other way he will be redirected to our login activity.
Create the layout of your login activity by using the Huawei Sign In button, provided on the Account kit SDK. This app will support Auth Code and ID Token.
Code:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LoginActivity">
<com.huawei.hms.support.hwid.ui.HuaweiIdAuthButton
android:id="@+id/code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<com.huawei.hms.support.hwid.ui.HuaweiIdAuthButton
android:id="@+id/idtoken"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
app:hwid_color_policy="hwid_color_policy_blue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:text="Authorization Code"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:text="Id Token"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/code" />
</androidx.constraintlayout.widget.ConstraintLayout>
Let's define the logic of the LoginActivity, as we want to get the user's email, we must apply for it by using the apropiated Scope. The user must approve the email permission manually at the authorization page, if not, the sign in response wont include it. In this case we are displayung a message to the user, asking for the email approval.
Code:
class LoginActivity : AppCompatActivity(), View.OnClickListener {
companion object {
private val AUTH_CODE = 100
private val AUTH_TOKEN = 200
private val TAG = "LoginActivity"
val SCOPES = listOf(Scope("email"))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
code.setOnClickListener(this)
idtoken.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.code -> signInCode()
R.id.idtoken -> signInToken()
else -> {
}
}
}
private fun signInCode() {
val authParams = HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM)
.setAuthorizationCode()
.setScopeList(SCOPES)
.createParams()
val service = HuaweiIdAuthManager.getService(this, authParams)
startActivityForResult(service.getSignInIntent(), AUTH_CODE)
}
private fun signInToken() {
val authParams = HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM)
.setIdToken()
.setScopeList(SCOPES)
.createParams()
val service = HuaweiIdAuthManager.getService(this, authParams)
startActivityForResult(service.getSignInIntent(), AUTH_TOKEN)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
AUTH_CODE -> handleAuthCodeResult(data)
AUTH_TOKEN -> handleAuthTokenResult(data)
}
}
private fun handleAuthTokenResult(data: Intent?) {
val authHuaweiIdTask = HuaweiIdAuthManager.parseAuthResultFromIntent(data)
if (authHuaweiIdTask.isSuccessful) {
// The sign-in is successful, and the user's HUAWEI ID information and ID token are obtained.
val huaweiAccount = authHuaweiIdTask.result
checkAccount(huaweiAccount)
} else {
// The sign-in failed. No processing is required. Logs are recorded to facilitate fault locating.
Log.e(
TAG,
"sign in failed : " + (authHuaweiIdTask.exception as ApiException).message
)
}
}
private fun handleAuthCodeResult(data: Intent?) {
val authHuaweiIdTask = HuaweiIdAuthManager.parseAuthResultFromIntent(data)
if (authHuaweiIdTask.isSuccessful) {
//The sign-in is successful, and the user's HUAWEI ID information and authorization code are obtained.
val huaweiAccount = authHuaweiIdTask.result
checkAccount(huaweiAccount)
} else {
// The sign-in failed.
Log.e(TAG, "sign in failed : " + (authHuaweiIdTask.exception as ApiException).message)
}
}
private fun jump(huaweiAccount: AuthHuaweiId) {
val intent = Intent(this, ProfileActivity::class.java)
intent.putExtra("account", huaweiAccount)
startActivity(intent)
finish()
}
private fun checkAccount(huaweiAccount: AuthHuaweiId) {
if (huaweiAccount.email.isNullOrEmpty()) {
Toast.makeText(
this, "Please allow email access in the authorization page", Toast.LENGTH_LONG
).show()
return
}
jump(huaweiAccount)
}
}
Now, from the profile activity (or the activity with the core functionality of your app) we must report the user's email to our Receiver.
Code:
companion object{
private val TAG="ProfileActivity"
val ACCOUNT="account"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profile)
val huaweiId=intent.extras?.getParcelable<AuthHuaweiId>(ACCOUNT)
huaweiId?.apply {
name.text=displayName
mail.text=email
publishMail(email)
loadProfilePic(avatarUriString)
}
}
Bonus: Server side configuration
The BroadcastReceiver in this example is targeting a Lambda function on AWS, this is the code which makes the data insertion in a DynamoDB table.
Code:
'use strict'
const AWS = require('aws-sdk');
AWS.config.update({ region: "us-east-2" });
exports.handler = async (event) => {
// TODO implement
console.log(event);
const ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" });
const documentClient = new AWS.DynamoDB.DocumentClient({ region: "us-east-2" });
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
const result=await writeDB(event,documentClient).catch(function(){
response.statusCode=400;
});
return response;
};
function writeDB(event, documentClient){
const entry = {
TableName: "PushBinding",
Item: {
mail: event.mail,
token: event.token
}
};
console.log(JSON.stringify(entry));
return documentClient.put(entry).promise();
}
Conclusion
Now you know how to associate a user id with a push token to send personalized push notifications. The Broadcast Receiver is used here to avoid stopping the app navigation by waiting for the push token or the user's email.
How many messages can the HUAWEI Push Kit server send at a time?
Most android applications download it's content from the cloud (commonly a REST API) getting ready to parse and display that information with lists and menus in order to display dynamic content or provide a personalized experience. There are some third party libraries designed to consume a REST API (like Retrofit) or to download media content (like Glide and Picasso). This time, let me introduce you the new Huawei Network Kit.
What is nework kit?
Network kit is the new Huawei's System SDK designed to simplify the communications with web services by providing 2 main connection modes:
Rest Client
HTTP Client
Network kit supports QUIC connections automatically, that means if the Web service supports QUIC or migrates to QUIC, your app will keep working without require any change. In addition, this kit is pretty similar to the well known Retrofit, so, if you have previous experience with Retrofit, you will be able to integrate Network Kit withount complications.
Previously, we made a News client by using the HQUIC kit. In this article we are going to develop a news client application by using the new Huawei Network Kit.
Previous requirements
A developer account in newsapi.org
Android Studio 4.0 or later and the kotlin plugin
Setting up the project
Network kit doesn't require to setup a project in AGC, but you still need to add the Huawei Maven repositories to your project-level build.gradle:
Java:
buildscript {
ext.kotlin_version = "1.4.31"
repositories {
...
maven {url 'https://developer.huawei.com/repo/'}
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
...
maven {url 'https://developer.huawei.com/repo/'}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Go to the official documents and look for the Network kit latest version under version change history. Once you have found the latest version available, add it to yout app-level build.gradle as follows
Java:
implementation 'com.huawei.hms:network-embedded:5.0.1.301'
We will use Moshi to parse the response from the web service, let's add the related dependencies and the kapt plugin to proccess the annotations.
Java:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
android{
...
}
dependencies {
...
implementation 'com.huawei.hms:network-embedded:5.0.1.301'
implementation 'com.squareup.moshi:moshi:1.11.0'
implementation "com.squareup.moshi:moshi-kotlin:1.11.0"
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.11.0'
...
}
To display the news in a list, we must add RecyclerView and CardView to our project and must enable the DataBinding library to make our job easier.
Java:
android {
...
//Enabling DataBinding and ViewBinding
buildFeatures{
viewBinding true
dataBinding true
}
...
}
dependencies {
...
//MVVM dependencies
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"
//DataBinding dependency
kapt "com.android.databinding:compiler:3.1.4"
//Layout dependencies
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
...
}
We are ready to start the project.
Building the request
First of all, Network kit must be initialized, let's create an Application class to do this job
NetworkApplication.kt
Java:
class NetworkApplication: Application() {
companion object {
const val TAG="Network Application"
}
override fun onCreate() {
super.onCreate()
initNetworkKit()
}
private fun initNetworkKit() {
// Initialize the object only once, upon the first call.
NetworkKit.init(this ,object : NetworkKit.Callback() {
override fun onResult(result: Boolean) {
if (result) {
Log.i(TAG, "Networkkit init success")
} else {
Log.i(TAG, "Networkkit init failed")
}
}
})
}
}
To make sure this code will be excecuted upon each startup, we must specify this class inside the application element in our AndroidManifest.xml. Let's add the required permissions too.
XML:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".NetworkApplication"
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NetworkKitDemo"
android:usesCleartextTraffic="true">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
Now, we must create the data models wich Moshi will use to parse the response
NewsResponse.kt
Java:
@JsonClass(generateAdapter = true)
data class NewsResponse(
@Json(name = "status") val status: String?,
@Json(name = "totalResults") val totalResults: Int?,
@Json(name = "articles") val articles: List<Article>
)
@JsonClass(generateAdapter = true)
data class Article(
@Json(name = "source") val source: Source?,
@Json(name = "author") val author: String?,
@Json(name = "title") val title: String?,
@Json(name = "description") val description: String?,
@Json(name = "url") val url: String?,
@Json(name = "urlToImage") val urlToImage: String?,
@Json(name = "publishedAt") val publishedAt: String?,
@Json(name = "content") val content: String?
)
@JsonClass(generateAdapter = true)
data class Source(
@Json(name = "id") val id: String?,
@Json(name = "name") val name: String?
)
Network kit porvides 2 operation modes, we will use the REST Client to get the Top headlines in the user's country and the HTTP Client mode to download the picture of each Article. We will create a singleton class called NetworkKitHelper.
Let's take a look to the REST Client mode:
NetworkKitHelper.kt
Java:
object NetworkKitHelper {
const val TAG: String = "HTTPClient"
//Your API key from newsapi.org
val apiKey = Keys.readApiKey()
fun createNewsClient(): NewsService {
val restClient = RestClient.Builder()
.httpClient(HttpClient.Builder().build())
.baseUrl("https://newsapi.org/v2/")//Specify the API base URL, this is useful if you will consume multiple paths of the same API
.build()
return restClient.create(NewsService::class.java)
}
//Declare a Request API
interface NewsService {
//Use the GET annotation to specify the path
@GET("top-headlines/")
fun getTopHeadlines(/* use the Query annotation to specify a query parameter in the request*/
@Query("apiKey") apiKey: String? = "",
@Query("country") country: String
): Submit<String?>?
}
fun loadTopHeadlines(sampleService: NewsService, listener: NewsClientListener?,country:String=Locale.getDefault().country) {
sampleService.getTopHeadlines(apiKey,country)?.enqueue(object : Callback<String?>() {
@Throws(IOException::class)
override fun onResponse(submit: Submit<String?>?, response: Response<String?>) {
// Obtain the response. This method will be called if the request is successful.
val body = response.body
body?.let {
try {
val moshi = Moshi.Builder().build()
val adapter = moshi.adapter(NewsResponse::class.java)
val news = adapter.fromJson(it)
news?.let { myNews ->
listener?.onNewsDownloaded(myNews.articles)
}
} catch (e: JSONException) {
Log.e("excepion", e.toString())
}
}
}
override fun onFailure(submit: Submit<String?>?, exception: Throwable?) {
// Obtain the response. This method will be called if the request fails.
Log.e("LoadTopHeadlines", "response onFailure = " + exception?.message)
}
})
}
interface NewsClientListener {
fun onNewsDownloaded(news: List<Article>)
}
}
Put special attention to the loadTopHeadlines function. As you can see, there aren't coroutines or threads defined, we are using the enqueue API instead. By this way Network Kit will handle the request in asynchronous mode for us.
If the API call is successfull, we will use Moshi to parse the response into data objects. By other way, we will be notified about the error in the onFailure callback. Once the response has been parsed, NetworkKitHelper will repor the news to the specified NewsClientListener.
Let's add the code to download the preview pics:
NetworkKitHelper.kt (Adding)
Java:
object NetworkKitHelper {
private val httpClient: HttpClient = createClient()
private fun createClient(): HttpClient {
return HttpClient.Builder()
.callTimeout(1000)
.connectTimeout(10000)
.build()
}
fun createRequest(url: String): Request {
return httpClient.newRequest()
.url(url)
.method("GET")
.build()
}
fun httpClientEnqueue(request: Request, listener: HttpClientListener? = null) {
httpClient.newSubmit(request).enqueue(object : Callback<ResponseBody?>() {
@Throws(IOException::class)
override fun onResponse(
submit: Submit<ResponseBody?>?,
response: Response<ResponseBody?>
) {
// Process the response if the request is successful.
Log.i(TAG, "response code:" + response.code)
response.body?.let {
listener?.onSuccess(it.bytes())
}
}
override fun onFailure(submit: Submit<ResponseBody?>?, throwable: Throwable?) {
// Process the exception if the request fails.
Log.w(TAG, "response onFailure = ${throwable?.message}")
}
})
}
interface HttpClientListener {
fun onSuccess(body: ByteArray)
}
}
As well as with the REST Client mode, we are able to enqueue HTTP Requests and define a callback for each one. In this case, we are receiving a byte array which will be used to create and display a bitmap.
Here we will face a complication. If we try to store the bitmap in the same data class as the Article, Moshi will cause a reflection error at compilation time. To solve this, we will define a new class to store the article and be responsible to load the bitmap, by doing so, we will be able to load the news as soon as we get them and then using the observer pattern, the bitmap will be added to the view as soon as it's ready.
ArticleModel.kt
Java:
class ArticleModel(val article: Article) : NetworkKitHelper.HttpClientListener {
private val _bitmap= MutableLiveData<Bitmap?>().apply{postValue(null)}
val bitmap: LiveData<Bitmap?> =_bitmap
init {
loadBitmap()
}
fun loadBitmap() {
article.urlToImage?.let{
val request=NetworkKitHelper.createRequest(it)
NetworkKitHelper.httpClientEnqueue(request, this)
}
}
override fun onSuccess(body: ByteArray) {
val bitmap= BitmapFactory.decodeByteArray(body, 0, body.size)
val resizedBitmap = Bitmap.createScaledBitmap(bitmap, 1000, 600, true)
_bitmap.postValue(resizedBitmap)
}
}
As soon as any instance of ArticleModel is created, it will enqueue an HTTP request async for the preview pic. If the call is successfull, we will receive a ByteArray in the onSuccess callback to create our bitmap from it and let the observer know the bitmap is ready to be displayed.
Sending the request
Let's create a ViewModel which will be responsible to invoke the API and store the data. Here we will use the observer pattern to let the observer know the Articles are ready to be displayed.
MainViewModel.kt
Java:
class MainViewModel : ViewModel(), NetworkKitHelper.NewsClientListener {
private val _articles = MutableLiveData<ArrayList<ArticleModel>>().apply { value = ArrayList() }
val articles: LiveData<ArrayList<ArticleModel>> = _articles
fun loadTopHeadlines(){
articles.value?.let{
if(it.isEmpty()) getTopHeadlines()
else return
}
}
private fun getTopHeadlines() {
NetworkKitHelper.loadTopHeadlines(NetworkKitHelper.createNewsClient(),this)
}
override fun onNewsDownloaded(news: List<Article>) {
val list=ArrayList<ArticleModel>()
for (article: Article in news) {
list.add(ArticleModel(article))
}
_articles.postValue(list)
}
}
To avoid downloading the news again when the user rotates the screen, we are defining the loadTopHeadlines function. It will only make the request if the list of articles is empty.
Displaying the Articles
We will use DataBinding to quicly display our news in a RecyclerView on the MainActivity, let's take a look to the main layout
activity_main.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<layout
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"
>
<data class="MainBinding"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_height="match_parent"
android:layout_width="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Now we must define the card wich will be rendered for each article
{
"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_card.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data class="ArticleBinding">
<variable
name="item"
type="com.hms.demo.networkkitdemo.ArticleModel" />
</data>
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="5dp"
android:layout_marginVertical="5dp"
card_view:cardCornerRadius="15dp"
card_view:cardElevation="20dp"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/pic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:contentDescription="@string/desc"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintHorizontal_bias="1.0"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/articleTitle"
card_view:shapeAppearanceOverlay="@style/roundedImageView"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/articleTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.article.title}"
android:textAlignment="viewStart"
android:textSize="24sp"
android:textStyle="bold"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintHorizontal_bias="0.498"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@{item.article.description}"
android:textSize="20sp"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/pic" />
<TextView
android:id="@+id/source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@{item.article.source.name}"
card_view:layout_constraintBottom_toBottomOf="parent"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/desc"
card_view:layout_constraintVertical_bias="0.09" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>
The ArticleBinding class will be responsible to fill the view with the values in it's ArticleModel instance for us. That's the magic of DataBinding.
As you may know, to display elements in a RecyclerView we need an Adapter, so let's define it
NewsAdapter.kt
Java:
class NewsAdapter: RecyclerView.Adapter<NewsAdapter.NewsViewHolder>() {
var articles:List<ArticleModel> =ArrayList()
class NewsViewHolder(private val binding:ArticleBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(item:ArticleModel){
binding.item=item
item.bitmap.observe(binding.root.context as LifecycleOwner){
it?.let{
binding.pic.setImageBitmap(it)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
val inflater=LayoutInflater.from(parent.context)
val binding=ArticleBinding.inflate(inflater,parent,false)
return NewsViewHolder(binding)
}
override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
holder.bind(articles[position])
}
override fun getItemCount(): Int {
return articles.size
}
}
Put special attention to the bind function of the NewsViewHolder class, from here we are telling to the ArticleBinding instance what is the information we want to display in the view. Also, we are using the observer pattern to update the ImageView once the article's preview pic has been downloaded.
Finally, is time to join everything through the MainActivity
MainActivity.kt
Java:
class MainActivity : AppCompatActivity() {
companion object {
const val TAG="Main"
}
private lateinit var binding:MainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding= MainBinding.inflate(layoutInflater)
binding.lifecycleOwner = this
setContentView(binding.root)
val viewModel:MainViewModel=ViewModelProvider(this).get(MainViewModel::class.java)
val adapter=NewsAdapter()
viewModel.articles.observe(this){
adapter.articles=it
adapter.notifyDataSetChanged()
}
binding.recycler.adapter=adapter
viewModel.loadTopHeadlines()
}
}
Final result
Tips & Tricks
If your app will consume a REST API with Kotlin, is better to use Moshi instead of gson because Moshi can understand the kotlin's not-nullable types.
If you will use API keys to authenticate your client with the server, is better to use the NDK to hide your KEY and prevent it from being obtained by using reverse engineering. Let's use the Rahul Sharma's hidding method. (Make sure to download the Android NDK from the SDK Manager)
1. Swithch to the Project view and create a jni directory under the main directory.
2. Under the jni directory add the next 3 files:
Android.mk
Code:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := keys
LOCAL_SRC_FILES := keys.c
include $(BUILD_SHARED_LIBRARY)
Application.mk
Code:
APP_ABI := all
Keys.c (Put here your API key)
Code:
#include <jni.h>
JNIEXPORT jstring JNICALL
Java_com_hms_demo_networkkitdemo_Keys_getApiKey(JNIEnv *env, jclass instance) {
return (*env)->NewStringUTF(env, "PUT_HERE_YOUR_API_KEY");
}
3. Switch back to the Android View and create a Keys kotlin object
Keys.kt
Java:
object Keys {
init {
System.loadLibrary("keys")
}
private external fun getApiKey(): String?
public fun readApiKey(): String? { //use this method for String
return getApiKey()
}
}
4. Tell gradle you will use NDK by adding the next code inside android
build.gradle (app-level)
Java:
plugins {
...
}
android {
...
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
}
dependencies {
...
}
Finally, modify the NetworkKitHelper object to read the API key from the native library.
NetworkKitHelper.kt (Modifying)
Code:
object NetworkKitHelper {
val apiKey = Keys.readApiKey()
}
Conclusion
By using Network kit your app will be ready to perform requests over QUIC or HTTP/2 without writting extra code. The REST Client mode and it's annotations are helpful to to quickly consume a REST API without taking care about Threads or Coroutines. And finally, the HTTP Client mode is useful to download preview images or any other stuff which is not a JSON.
References
Read In Forum
Network Kit official Docs
Hiding Secret/Api key from reverse engineering in Android using NDK
Moshi
Hi, i have one question if we use network kit then we no need to use any third-party like volley, Retrofit?
Is it faster and easy to use than Retrofit library?
The new Huawei Network kit provides you with convenient and easy to use APIs to perform HTTP operations. You can use it as HTTP Client to quickly download many kind of files from internet. In this article we are going to use it to download files from Huawei Drive by using the Public APIs, by this way we will be able to browse and download files, even from non-huawei devices.
Note: This article is a continuation of my previous post called "Accessing to Huawei Drive by using AppAuth and REST APIs". Is highly recommended to read it first before continue with this one.
Integrating Drive Kit
We will use the new Huawei Drive kit to download the user's files stored in Huawei Drive, by doing so, the kit will handle the HTTP calls in asynchronous mode for us. Add the Network Kit SDK under dependencies in your app-level build.gradle. We will also use CardView to display the user's files in a list, so let us add both dependencies.
Code:
implementation 'com.huawei.hms:network-embedded:5.0.1.301'
implementation "androidx.cardview:cardview:1.0.0"
Network Kit must be initialized before calling it's APIs, we can ensure the initialization upon each startup by defining our own Application class.
MyApplication.kt
Code:
class MyApplication : Application(){
override fun onCreate() {
super.onCreate()
initNetworkKit()
}
fun initNetworkKit() {
// Initialize the object only once, upon the first call.
NetworkKit.init(this, object : NetworkKit.Callback() {
override fun onResult(result: Boolean) {
val TAG = "NetworkKit"
if (result) {
Log.i(TAG, "Networkkit init success")
} else {
Log.i(TAG, "Networkkit init failed")
}
}
})
}
}
Do not forget to register your Application class in your AndroidManifest.xml
AndroidManifest.xml (Modifying)
Code:
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppAuthDriveKit">
Modifying the Contract
Let us add the file management related methods to our abstract class, this will be useful if we want to add more Drive providers in the future (like Microsoft's OneDrive).
DriveService.kt (Modifying)
Code:
abstract class DriveService (val service: AuthorizationService,
val authState: AuthState){
var driveListener:OnDriveEventListener?=null
abstract fun startService()
abstract fun listFiles()
abstract fun downloadFile(context: Context, cloudFile: CloudFile)
abstract fun uploadFile(file: File)
interface OnDriveEventListener{
fun onServiceReady(storageData: String)
fun onServiceFailure(info: String)
fun onFilesListed(files: List<CloudFile>)
fun onFileUploaded()
fun onDownloadStarted(cloudFile: CloudFile)
fun onDownloadProgress(cloudFile: CloudFile, progress: Int)
fun onFileDownloaded(cloudFile: CloudFile)
}
}
Listing the Files
We will call the Files.List API to get the details of all the stored user's files on his Root directory. First, we need a class to store all the information of a file in the cloud. A data class will be useful to achieve this.
Note: A file object in the cloud has a lot of information, for demonstration purposes we will just keep the most important properties.
Code:
data class CloudFile(
val filename: String,
val fileSuffix: String,
val createdTime: String,
val downloadLink: String,
val mimeType:String,
val fileSize:Int
){
val stringSizeInKb="${fileSize/1024} KB"
}
Previously, we have called he About API to get some general information about the user's Drive, into that information, it came the "domain", that's the name of the host which is able to dispatch all the Drive requests related to the current user. We will use the "domain" to query the file list. This information will be obtained by using a normal HTTP call.
Note: The next code can be improved by using the REST Client mode of Network Kit, but is not the focus of this article.
HuaweiDriveService.kt (modifying)
Code:
override fun listFiles() {
CoroutineScope(Dispatchers.IO).launch {
authState.performActionWithFreshTokens(service){accessToken,_,_->
val url="$domain/drive/v1/files?fields=*"
val conn=URL(url).openConnection() as HttpURLConnection
conn.addRequestProperty("Authorization","Bearer $accessToken")
conn.addRequestProperty("Cache-Control","no-cache")
val response=if(conn.responseCode<400) convertStreamToString(conn.inputStream)
else convertStreamToString(conn.errorStream)
if(conn.responseCode==200){
val json=JSONObject(response)
buildList(json)
}else driveListener?.onServiceFailure(response)
}
}
}
As you can see, the registered OnDriveEventListener will receive the list of available files in the Cloud. In this case, the DriveViewModel, which will store the files list into a MutableLiveData.
DriveViewModel.kt(Modifying)
Code:
var list=MutableLiveData<ArrayList<CloudFile>>()
override fun onFilesListed(files: List<CloudFile>) {
list.apply {
value?.run {
clear()
addAll(files)
}
postValue(value)
}
}
Let us define the view which will be used to display our file items.
{
"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"
}
file_item.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<layout
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"
xmlns:card_view="http://schemas.android.com/apk/res-auto">
<data class="FileItemBinding"
><variable
name="item"
type="com.hms.demo.appauthdrivekit.CloudFile" /></data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
card_view:cardElevation="20dp"
card_view:cardCornerRadius="15dp"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:padding="5dp"
android:layout_height="wrap_content"
>
<ImageView
android:id="@+id/icon"
android:layout_width="60dp"
android:layout_height="60dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/ic_insert_drive_file_black_18dp"
android:src="@drawable/ic_insert_drive_file_black_18dp"
android:layout_marginEnd="5dp"/>
<TextView
android:id="@+id/fileName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginTop="5dp"
android:text="@{item.filename}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/fileType"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@{item.fileSuffix}"
app:layout_constraintBottom_toTopOf="@+id/fileSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toBottomOf="@+id/fileName" />
<TextView
android:id="@+id/fileSize"
android:layout_width="0dp"
android:layout_height="19dp"
android:layout_marginStart="5dp"
android:layout_marginBottom="5dp"
android:text="@{item.stringSizeInKb}"
android:textAlignment="textEnd"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>
Is time to create the adapter, a download request will be triggered if the user performs a long click on any item.
FileAdapter.kt
Code:
class FileAdapter : RecyclerView.Adapter<FileAdapter.FileVH>() {
lateinit var items:ArrayList<CloudFile>
var cloudFileEventListener:CloudFileEventListener?=null
class FileVH(private val binding: FileItemBinding): RecyclerView.ViewHolder(binding.root),View.OnLongClickListener {
var fileEventListener:CloudFileEventListener?=null
fun bind(item:CloudFile){
binding.item=item
binding.root.setOnLongClickListener(this)
}
override fun onLongClick(v: View?): Boolean {
binding.item?.let {
fileEventListener?.onDownloadRequest(it)
}
return true
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileVH {
val layoutInflater=LayoutInflater.from(parent.context)
val binding=FileItemBinding.inflate(layoutInflater,parent,false)
return FileVH(binding).apply { fileEventListener= cloudFileEventListener}
}
override fun onBindViewHolder(holder: FileVH, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int {
return items.size
}
interface CloudFileEventListener{
fun onDownloadRequest(item:CloudFile)
}
}
Downloading a file
Before performing any File operation, we must make sure to have the related permissions granted, by othe way we will fall in a SecurityException. the DriveViewModel will listen for the download requests from the Recycler Adapter, and this Viewmodel is connected to our activity by the DriveNavigator interface, by using that connections, we can ask to the activity to check if the storage permissions are granted upon any download request.
DriveVM.kt (Modifying)
Code:
override fun onDownloadRequest(item:CloudFile) {
navigator?.apply {
if(checkStoragePermissions()){
val context=navigator as Context
driveService?.downloadFile(context,item)
} else requestStoragePermissions()
}
}
From the DriveActivity, we must check if the permissions are granted and asking for them if not.
DriveActivity.kt (Modifying)
Code:
override fun checkStoragePermissions(): Boolean {
val read=ContextCompat.checkSelfPermission(this,Manifest.permission.READ_EXTERNAL_STORAGE)
val write=ContextCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)
return read==PackageManager.PERMISSION_GRANTED&&write==PackageManager.PERMISSION_GRANTED
}
override fun requestStoragePermissions() {
val permissions= arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE)
requestPermissions(permissions, STORAGE_PERMISSIONS)
}
Since Android 10, the access to the user's storage must be performed by using MediaStore. As the file creation process is the same no matter the Drive provider (Huawei, Microsoft, Google, etc.), let's add to our DriveService class the capability of creating new Files, so any subclass of this will be able to create new files in the local storage.
DriveService.kt (Modifying)
Code:
open fun createFileUri(context: Context, item: CloudFile):Uri?{
val resolver=context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, item.filename)
put(MediaStore.MediaColumns.MIME_TYPE, item.mimeType)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, "Documents")
}
}
return resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)
}
Is time to download the file, at this point you may think we can easily download files by using the Network Kit file downloading capability. The problem here is the files.get web API of Drive Kit, doesn't provide you a file by definition, it provides only the file content and we must use that content as well as the file metadata to build the file by ourselves.
Let's suppose the APIs from other Drive providers have the same behavior (provide only the file content), in that case, we can add the download capability to the DriveService class. to achieve our goal, we must create an HTTP Client and a request.
DriveService.kt (Modifying)
Code:
private val httpClient:HttpClient by lazy { HttpClient.Builder()
.connectTimeout(10000)
.build() }
open fun buildGetRequest(url: String, headers: Map<String, String>):Request{
return httpClient.newRequest().apply {
url(url)
method("GET")
for(key in headers.keys){
addHeader(key,headers[key])
}
}.build()
}
Now we are ready to develop the file downloading capability.
DrveService.kt (Modifying)
Code:
open fun enqueueDownload(output:OutputStream,cloudFile: CloudFile, request: Request) {
httpClient.newSubmit(request).enqueue(object : Callback<ResponseBody?>() {
@Throws(IOException::class)
override fun onResponse(
submit: Submit<ResponseBody?>?,
response: Response<ResponseBody?>
) {
if(response.isOK){
driveListener?.onDownloadStarted(cloudFile)
response.body?.inputStream?.let {input->
val fileWriter= DataOutputStream(output)
val bytes=ByteArray(1024)
var downloaded=0
var readed=0
while (input.read(bytes,0,bytes.size).also { readed=it }>0){
downloaded+=readed
fileWriter.apply {
write(bytes,0,readed)
flush()
val progress=(downloaded*100)/cloudFile.fileSize
driveListener?.onDownloadProgress(cloudFile,progress)
}
}
fileWriter.close()
driveListener?.onFileDownloaded(cloudFile)
}
}
}
override fun onFailure(submit: Submit<ResponseBody?>?, throwable: Throwable) {
// Process the exception if the request fails.
Log.w("Download", "response onFailure = " + throwable.message)
}
})
}
As you can see, we are using the OnDriveEventListener interface to notify the download state and the download progress. Is time to override the downloadFile function in our HuaweiDriveService class.
HuaweiDriveService.kt (Modifying)
Code:
override fun downloadFile(context: Context, cloudFile: CloudFile) {
authState.performActionWithFreshTokens(service){ accessToken, _, _->
//Creating the file in the local storage
createFileUri(context,cloudFile)?.let {
val output=context.contentResolver.openOutputStream(it)
//Adding the authorization information for the request
val headers= mapOf("Content-Type" to "application/json","Authorization" to "Bearer $accessToken")
//Building the request
val request=buildGetRequest(cloudFile.downloadLink,headers)
//Starting the download
output?.let { stream -> enqueueDownload(stream,cloudFile,request) }
}
}
}
Starting and listening the download
Let's go back to the DriveVM class, from here, if a download request is received and the proper permissions are granted, it will call the driveService.downloadFile function to start the download task. The ViewModel will be notified about the download status changes thorugh the DriveService.OnDriveEventListener interface.
DriveVM.kt (Modifying)
Code:
override fun onDownloadStarted(cloudFile: CloudFile) {
Log.i("File","onDownloadStarted ${cloudFile.filename}")
}
override fun onDownloadProgress(cloudFile: CloudFile, progress: Int) {
navigator?.onDownloadProgress(cloudFile,progress)
}
override fun onFileDownloaded(cloudFile: CloudFile) {
navigator?.onFileDownloaded(cloudFile)
}
From this callbacks we will notify the activity (DriveNavigator) to update the user interface upon any download update.
DriveActivity.kt (Modifying)
Code:
private var progressDialog:AlertDialog?=null
override fun onDownloadProgress(cloudFile: CloudFile, progress: Int) {
runOnUiThread{
if(progressDialog==null) setupDialog(cloudFile.filename)
progressDialog?.apply {
setMessage("Progress: ${progress}% of ${cloudFile.fileSize/1000}KB")
show()
}
}
}
override fun onFileDownloaded(cloudFile: CloudFile) {
runOnUiThread {
progressDialog?.let {
if(it.isShowing) it.dismiss()
it.cancel()
}
progressDialog=null
AlertDialog.Builder(this)
.setTitle("Download Complete")
.setMessage("File ${cloudFile.filename} downloaded successfully!")
.setPositiveButton("Ok"){dialogInterface,_->
dialogInterface.dismiss()
}
.create().show()
}
}
private fun setupDialog(filename:String) {
progressDialog=AlertDialog.Builder(this).setTitle("Downloading: $filename").setCancelable(false).create()
}
Final Result
Conclusion
We have used Network Kit to download files from Huawei Drive without depending on the HMSCore APK, by this way, our app will be able to work even in non-Huawei Devices.
Network kit provides powerful APIs as it's File Upload/Download feature or it's REST Client mode, but is also useful to perform lower-level operations if is used in HttpClient mode, this versatility can help you to build any HTTP operation from your app with support for QUIC and Asynchronous mode.
Reference
Network Kit: URL Request
Drive Kit: Files.get
Original Source