[Huawei Developers] Freemind R:
For our App, users will be able to send messages to others if they know the nickname of the destination user. In this part we will allow the user to choose or edit his nickname, and add the business logic of the user management on our backend.

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/002/383/434/2640052000002383434.20201102014702.23154116017260885591943158413372:50511119004815:2800:9832EC81B12D146A2CA6EF010D21B03E6E9F572B7DEFF515F9B5E21C090E8E4C.png"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/002/383/434/2640052000002383434.20201102014702.23154116017260885591943158413372:50511119004815:2800:9832EC81B12D146A2CA6EF010D21B03E6E9F572B7DEFF515F9B5E21C090E8E4C.png[/IMG]​

Checking the user's nickname

Upon the startup, if the user is already registered, the app will check if the user has already registered his nickname. To achieve this, we will use our REST client built with HQUIC.

Note: The next class is based on my previous article about Building an Android QUIC REST Client with HQUIC [kotlin], so is highly recomendable to read it first.

class HQUICClient(context: Context, private val requestId:Int=0) : UrlRequest.Callback() {

var hquicService: HQUICService? = null
val CAPACITY = 10240
val TAG="QUICClient"
val response=ByteArrayOutputStream()
var listener: HQUICClientListener?=null

init {
hquicService = HQUICService(context)
hquicService?.setCallback(this)
}

fun makeRequest(url: String, method: String, headers: HashMap<String, String>?=null,body:ByteArray?=null){
hquicService?.sendRequest(url,method,headers,body)
}

override fun onRedirectReceived(
request: UrlRequest?,
info: UrlResponseInfo?,
newLocationUrl: String?
) {
request?.followRedirect()
}

override fun onResponseStarted(request: UrlRequest?, info: UrlResponseInfo?) {
val byteBuffer = ByteBuffer.allocateDirect(CAPACITY)
request?.read(byteBuffer)
}

override fun onReadCompleted(
request: UrlRequest?,
info: UrlResponseInfo?,
byteBuffer: ByteBuffer?
) {
byteBuffer?.apply {
response.write(array(),arrayOffset(),position())
response.flush()
}
request?.read(ByteBuffer.allocateDirect(CAPACITY))
}

override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
listener?.onSuccess(requestId,response.toByteArray())
}

override fun onFailed(request: UrlRequest?, info: UrlResponseInfo?, error: CronetException?) {
listener?.apply { onFailure(requestId,error.toString()) }
}

interface HQUICClientListener{
fun onSuccess(requestId: Int,response: ByteArray)
fun onFailure(requestId: Int,error: String)
}
}

Now, we will create a ProfileUtils class which will be responsible for the related nickname operations.

class ProfileUtils(private val callback:ProfileCallback) :HQUICClient.HQUICClientListener{
companion object {
const val TAG="ProfileUtils"
const val PROFILE_URL = "https://qz41hkiav2.execute-api.us-east-2.amazonaws.com/Prod/profile/"
const val NICKNAME_RESOURCE = "nickname/"
const val UID = "uid"
const val NICKNAME="nickname"
const val CHECK_NICKNAME=200
const val UPDATE_NICKNAME=300
}

fun checkNickname(context: Context,uid: String) {
Log.e(TAG,"Checking Nickname")
publishUid(context,uid)
val apiURL = "$PROFILE_URL?$UID=$uid"
CoroutineScope(Dispatchers.IO).launch {
context.let {
HQUICClient(it, CHECK_NICKNAME).apply{
listener=this@ProfileUtils
makeRequest(apiURL,"GET")
}

}
}
}

private fun publishUid(context: Context, uid: String) {
val intent= Intent().apply {
action = AccountPushReceiver.ACTION
putExtra(AccountPushReceiver.PARAM,AccountPushReceiver.UID)
putExtra(AccountPushReceiver.UID,uid)
}
context.sendBroadcast(intent)
}

fun updateNickname(context:Context,uid:String,nickname:String){
Log.e(TAG,"Updating Nickname")
//publishUid(context,uid)
val url="$PROFILE_URL$NICKNAME_RESOURCE"
val params=JSONObject().apply {
put(UID,uid)
put(NICKNAME,nickname)
}
val headers=HashMap<String,String>()
headers["Content-Type"]= "application/json"
CoroutineScope(Dispatchers.IO).launch {
context.let {
HQUICClient(it, UPDATE_NICKNAME).apply{
listener=this@ProfileUtils
makeRequest(url,"POST",headers,params.toString().toByteArray())
}

}
}
}

override fun onSuccess(requestId:Int,response: ByteArray) {
val result= String(response)
Log.e(TAG,"RequestID: $requestId $result")
val json=JSONObject(result)
when(requestId){
CHECK_NICKNAME ->handleNicknameCheck(json)

UPDATE_NICKNAME ->handleNicknameUpdate(json)
}
}

private fun handleNicknameCheck(json:JSONObject){
val body=json.getJSONObject("body")
if(body.has("nickname")){
val nickname=body.getString("nickname")
callback.onNicknameResult(ProfileCallback.NICKNAME_RETRIEVED,nickname)
}else callback.onNicknameResult(ProfileCallback.NICKNAME_EMPTY,"")

}

private fun handleNicknameUpdate(json:JSONObject){
val body=json.getJSONObject("body")
when(json.getInt("responseCode")){
ProfileCallback.NICKNAME_USED ->{
callback.onNicknameResult(ProfileCallback.NICKNAME_USED,body.getString("message"))
}
ProfileCallback.NICKNAME_INSERTED->{
callback.onNicknameResult(ProfileCallback.NICKNAME_USED,body.getString("nickname"))
}
}
}

override fun onFailure(requestId:Int,error: String) {
}

interface ProfileCallback{
companion object{
const val NICKNAME_USED=1
const val NICKNAME_INSERTED=2
const val NICKNAME_RETRIEVED=3
const val NICKNAME_EMPTY=4
}
fun onNicknameResult(resultCode:Int,data: String)
}

}

As you can see, the HQUICClient receives 2 parameters, the first is the ClientListener which will listen for the response. The second parameter is the request id, as we will use the same listener lo listen the result of different operations, the request id will helps us to know which operation is giving us the reported response.

Server side configuration

As you may remember, we are using AWS to manage our backend, the services used are:

API Gateway: To build our REST API

Lambda: To perform the backend operations

DynamoDB: To store the users data

First, lets check the DynamoDB table structure

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/002/383/434/2640052000002383434.20201109052419.71212372751884745790243602249821:50511119004815:2800:896E1E08834B4B63A3FD079CEDEFDBBB99615925622B0B120118016E912B3796.png"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/002/383/434/2640052000002383434.20201109052419.71212372751884745790243602249821:50511119004815:2800:896E1E08834B4B63A3FD079CEDEFDBBB99615925622B0B120118016E912B3796.png[/IMG]

We have 3 fields

uid: The user id provided by Auth Service

token: The push token provided by the Huawei Push Kit

nickname: The unique nickname choosen by the user, this value will be shared with other users in order to send and receive messages.

Registering the user

The push token may change with the time or if the user clears the app date, so it must be reported upon each app startup. The lambda function to handle this operation is the next:

'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('InsertSuccesful'),
};
const result=await writeDB(event,documentClient).catch(function(){
response.statusCode=400;
response.body="Insert"
});
return response;
};

function writeDB(event, documentClient){
const entry = {
TableName: "MessengerUsers",
Item: {
uid: event.uid
}
};
if(event.token){
entry.Item.token=event.token;
}
console.log(JSON.stringify(entry));
return documentClient.put(entry).promise();

}

Note: The function above must be modified to update the token if the uid is already registered. For now it will overwrite the existin registry with the given values.

Querying the user's data

The app will ask if there is an already registered nickname for the given UID, this operation will be performed by sending a GET request to our API. The function responsible for that is the next:

'use strict'

const AWS = require('aws-sdk');
AWS.config.update({ region: "us-east-2" });

exports.handler = async (event) => {

console.log(JSON.stringify(event));

const ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" });
const documentClient = new AWS.DynamoDB.DocumentClient({ region: "us-east-2" });

var params = {
Key: {
"uid": event.uid

},
TableName: "MessengerUsers"
};

// TODO implement
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
var data;
try{
data=await documentClient.get(params).promise();
console.log("Query succeeded.");
response.body=data.Item;
}catch(err){
console.error("Unable to query. Error:", JSON.stringify(err, null, 2));
response.body=JSON.stringify(err, null, 2);
}

return response;
};

Updating the Nickname

When the user enters his nickname, we must validate first if the nickname is available for using, if is it, we will update the user registry with the new nickname. If not, we must notify the user to choose another one.

'use strict'

const AWS = require('aws-sdk');
AWS.config.update({ region: "us-east-2" });

exports.handler = async(event) => {

console.log(JSON.stringify(event));

const ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" });
const documentClient = new AWS.DynamoDB.DocumentClient({ region: "us-east-2" });

var data;

try{
data =await checkNickname(documentClient, event.nickname);
}catch(err){
return response(400,err);
}

if(data.Count>0){
return response(1,{message:event.nickname+" is already used"});
}

try {
const result=await update(documentClient,event.nickname,event.uid);
return response(2,result.Attributes);
} catch (e) {
return response(400,e);
}

};

function response(code, data){
return{
responseCode:code,
body:data
}
}

const checkNickname =(documentClient, nickname)=> {
var params = {
FilterExpression: 'nickname = :hkey',
ExpressionAttributeValues: {
':hkey': nickname
},
TableName: "MessengerUsers"
};

return documentClient.scan(params).promise();

};

const update=(documentClient,nickname,uid)=>{
const entry = {
TableName: "MessengerUsers",
Key:{
uid:uid
},
UpdateExpression: "set nickname = :nickname",
ExpressionAttributeValues:{
":nickname":nickname,
},
ReturnValues:"UPDATED_NEW"
};
return documentClient.update(entry).promise();
};

More details, you can visit https://forums.developer.huawei.com/forumPortal/en/topic/0204400884797260110