Expert: Directory App MVVM Jetpack (Video Call with Webrtc & HMS CloudDB) in Android using Kotlin- Part-5
Published: 2022-06-17 07:16:49

Overview

In this article, I will create a Directory android application using Webrtc Video Calling App in which I will integrate HMS Core kits such as HMS Account, AuthService, Identity Kit, Firebase Auth, Firebase Realtime DB and CloudDB .

App will make use of android MVVM clean architecture using Jetpack components such as DataBinding, AndroidViewModel, Observer, LiveData and much more.

In this article we are going to implement DataBinding using Observable pattern.

Huawei Cloud DB Kit Introduction

Huawei Cloud DB is a device-cloud synergy database product that enables seamless data synchronization between the device and cloud and between devices, and supports offline application operations, helping you quickly develop device-cloud and multi-device synergy applications.

Flexible synchronization modes:

Cloud DB supports cache and local data synchronization modes. In cache mode, data on the device is a subset of data on the cloud. If persistent cache is allowed, query results will be automatically cached on the device. In local mode, data is stored locally and is not synchronized to the cloud.

Powerful query capability:

Cloud DB supports various predicate query methods. Multiple chain filtering conditions can be used to filter and sort returned results, and limit the number of objects in the returned result set. In cache mode, data can be queried from the Cloud DB zone on the cloud or that on the local device. In local mode, data is directly queried from the Cloud DB zone on the local device.

Real-time update:

In cache mode of Cloud DB, you can listen on data as needed and use the data synchronization function of Cloud DB to update changed data between the device and cloud in real time.

Offline operations:

In cache mode of Cloud DB, if persistent cache is allowed, the application query is automatically changed from Cloud DB to the local host after the device gets offline. All data modified locally will be automatically synchronized to Cloud DB after the device gets online.

Scalability:

Cloud DB provides powerful HUAWEI CLOUD infrastructure functions, including automatic multi-region data replication, consistency assurance, atomic batch operations, and transaction support.

Security level:

Cloud DB supports device-cloud data full encryption management, triple authentication by app, user, and service, and role-based permission management to ensure data security.

WebRTC Service Introduction

WebRTC is a free and open-source project providing web browsers and mobile applications with real-time communication via application programming interfaces.

Prerequisite

Huawei Phone EMUI 3.0 or later.
Non-Huawei phones Android 4.4 or later (API level 19 or higher).
HMS Core APK 4.0.0.300 or later
Android Studio
AppGallery Account
App Gallery Integration process

1. Sign In and Create or Choose a project on AppGallery Connect portal.​

2. Navigate to Project settings and download the configuration file.​

3. Navigate to General Information, and then provide Data Storage location.

4. Navigate to Build then Enable Cloud DB

5. Navigate to Cloud DB and Create DB:


App Development

Add Required Dependencies:

Launch Android studio and create a new project. Once the project is ready.
Add following dependency for HMS Kits
```
//HMS Kits
implementation 'com.huawei.agconnect:agconnect-core:1.5.0.300'
implementation 'com.huawei.hms:hwid:5.3.0.302'
implementation 'com.huawei.hms:identity:5.3.0.300'
implementation 'com.huawei.agconnect:agconnect-cloud-database:1.5.0.300'
//Google Firebase    
implementation platform('com.google.firebase:firebase-bom:28.4.1')
    implementation 'com.google.firebase:firebase-analytics'
    implementation 'com.google.firebase:firebase-auth'
    implementation 'com.google.firebase:firebase-database'
    implementation 'com.google.android.gms:play-services-auth:19.2.0'
    implementation 'com.airbnb.android:lottie:4.1.0'
    implementation 'com.mikhaellopez:circularimageview:4.3.0'
    implementation 'com.kaopiz:kprogresshud:1.2.0'
    implementation 'com.google.android.gms:play-services-ads:20.4.0'
    implementation 'com.github.bumptech.glide:glide:4.12.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
```
Navigate to the Gradle scripts folder and open build.gradle (project: app)
```
repositories {
        google()
        jcenter()
        maven {url 'https://developer.huawei.com/repo/'}
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.0.1"
        classpath 'com.huawei.agconnect:agcp:1.4.2.300'
```
Configure AndroidManifest.xml.
```
<meta-data
            android:name="install_channel"
            android:value="AppGallery" />
        <meta-data
            android:name="com.huawei.hms.client.appid"
            android:value="104460711" />
    </application>
```
Code Implementation

Created following package model, event, viewmodel.
ViewModel: The ViewModel makes it easy to update data changes on the UI.Create a package named viewmodel in your main folder.Then create a new file and name it LoginViewModel.kt along with their FactoryViewModelProviders.

MainActivity.kt:
```
package com.hms.directory
class MainActivity : AppCompatActivity(), ActivityNavigation {
    private lateinit var viewModel: LoginViewModel
    private lateinit var dataBinding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        val viewModel: LoginViewModel by lazy {
            val activity = requireNotNull(this) {}
            ViewModelProviders.of(this, LoginViewModelFactory(activity.application))
                .get(LoginViewModel::class.java)
        }
        dataBinding.loginViewModel = viewModel
        dataBinding.lifecycleOwner = this
        viewModel.startActivityForResultEvent.setEventReceiver(this, this)
    }
    public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        viewModel.onResultFromActivity(requestCode, data)
        super.onActivityResult(requestCode, resultCode, data)
    }
}
```
LoginViewModel.kt:
```
package com.hms.directory.viewmodel
@SuppressLint("StaticFieldLeak")
class LoginViewModel(application: Application) : AndroidViewModel(application), Observable {
    private val context = getApplication<Application>().applicationContext
    private var mAuthManager: AccountAuthService? = null
    private var mAuthParam: AccountAuthParams? = null
    val startActivityForResultEvent = LiveMessageEvent<ActivityNavigation>()
    fun login() {
        val intent = Intent(context, OrderActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        context.startActivity(intent)
       /* mAuthParam = AccountAuthParamsHelper(AccountAuthParams.DEFAULT_AUTH_REQUEST_PARAM)
            .setIdToken()
            .setAccessToken()
            .createParams()
        mAuthManager = AccountAuthManager.getService(Activity(), mAuthParam)
        startActivityForResultEvent.sendEvent {
            startActivityForResult(
                mAuthManager?.signInIntent,
                HMS_SIGN_IN
            )
        }*/
    }
    fun onResultFromActivity(requestCode: Int, data: Intent?) {
        when (requestCode) {
            HMS_SIGN_IN -> {
                val authAccountTask = AccountAuthManager.parseAuthResultFromIntent(data)
                onCompleteLogin(authAccountTask)
            }
        }
    }
    private fun onCompleteLogin(doneTask: Task<AuthAccount>) {
        if (doneTask.isSuccessful) {
            val authAccount = doneTask.result
            Log.d("LoginViewModel", "SigIn Success")
            context.startActivity(Intent(context, ContactListActivity::class.java))
        } else {
            Log.d("LoginViewModel", "SigIn Error")
        }
    }
    override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
    }
    override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
    }
}
```
ContactActivity.kt:
```
public class ContactListActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact_list);
        // Load contacts from file
        Contacts.loadData(this);
        // Set up recycler view and fill it with all the contacts
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.contact_list);
        recyclerView.setAdapter(new ContactListAdapter(this, Contacts.LIST));
    }
```
LoginFireBaseActivity.java
```
package com.hms.directory.app.call;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.FirebaseApp;
import com.google.firebase.auth.AuthCredential;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.GoogleAuthProvider;
import com.google.firebase.database.FirebaseDatabase;
import com.hms.corrierapp.R;
import com.hms.directory.app.call.models.User;
import org.jetbrains.annotations.NotNull;
public class LoginActivity extends AppCompatActivity {
    GoogleSignInClient mGoogleSignInClient;
    int RC_SIGN_IN = 11;
    FirebaseAuth mAuth;
    FirebaseDatabase database;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login_goole);
        mAuth = FirebaseAuth.getInstance();
        if (mAuth.getCurrentUser() != null) {
            goToNextActivity();
        }
        database = FirebaseDatabase.getInstance();
        GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken("1016048264402-439a9aamtpiajbgqeqg24qkum2bb7fmh.apps.googleusercontent.com")
                .requestEmail()
                .build();
        mGoogleSignInClient = GoogleSignIn.getClient(this, gso);
        findViewById(R.id.loginBtn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = mGoogleSignInClient.getSignInIntent();
                startActivityForResult(intent, RC_SIGN_IN);
                //startActivity(new Intent(LoginActivity.this, MainActivity.class));
            }
        });
    }
    void goToNextActivity() {
        startActivity(new Intent(LoginActivity.this, MainActivity.class));
        finish();
    }
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable @org.jetbrains.annotations.Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == RC_SIGN_IN) {
            Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
            GoogleSignInAccount account = task.getResult();
            authWithGoogle(account.getIdToken());
        }
    }
    void authWithGoogle(String idToken) {
        AuthCredential credential = GoogleAuthProvider.getCredential(idToken, null);
        mAuth.signInWithCredential(credential)
                .addOnCompleteListener(new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull @NotNull Task<AuthResult> task) {
                        if (task.isSuccessful()) {
                            FirebaseUser user = mAuth.getCurrentUser();
                            User firebaseUser = new User(user.getUid(), user.getDisplayName(), user.getPhotoUrl().toString(), "Unknown", 500);
                            database.getReference()
                                    .child("profiles")
                                    .child(user.getUid())
                                    .setValue(firebaseUser).addOnCompleteListener(new OnCompleteListener<Void>() {
                                @Override
                                public void onComplete(@NonNull @NotNull Task<Void> task) {
                                    if (task.isSuccessful()) {
                                        startActivity(new Intent(LoginActivity.this, MainActivity.class));
                                        finishAffinity();
                                    } else {
                                        Toast.makeText(LoginActivity.this, task.getException().getLocalizedMessage(), Toast.LENGTH_SHORT).show();
                                    }
                                }
                            });
                            //Log.e("profile", user.getPhotoUrl().toString());
                        } else {
                            Log.e("err", task.getException().getLocalizedMessage());
                        }
                    }
                });
    }
}
```
CallConnectingActivity.java
```
public class ConnectingActivity extends AppCompatActivity {
    ActivityConnectingBinding binding;
    FirebaseAuth auth;
    FirebaseDatabase database;
    boolean isOkay = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityConnectingBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        auth = FirebaseAuth.getInstance();
        database = FirebaseDatabase.getInstance();
        String profile = getIntent().getStringExtra("profile");
        Glide.with(this)
                .load(profile)
                .into(binding.profile);
        String username = auth.getUid();
        database.getReference().child("users")
                .orderByChild("status")
                .equalTo(0).limitToFirst(1)
                .addListenerForSingleValueEvent(new ValueEventListener() {
                    @Override
                    public void onDataChange(@NonNull @NotNull DataSnapshot snapshot) {
                        if (snapshot.getChildrenCount() > 0) {
                            isOkay = true;
                            // Room Available
                            for (DataSnapshot childSnap : snapshot.getChildren()) {
                                database.getReference()
                                        .child("users")
                                        .child(childSnap.getKey())
                                        .child("incoming")
                                        .setValue(username);
                                database.getReference()
                                        .child("users")
                                        .child(childSnap.getKey())
                                        .child("status")
                                        .setValue(1);
                                Intent intent = new Intent(ConnectingActivity.this, CallActivity.class);
                                String incoming = childSnap.child("incoming").getValue(String.class);
                                String createdBy = childSnap.child("createdBy").getValue(String.class);
                                boolean isAvailable = childSnap.child("isAvailable").getValue(Boolean.class);
                                intent.putExtra("username", username);
                                intent.putExtra("incoming", incoming);
                                intent.putExtra("createdBy", createdBy);
                                intent.putExtra("isAvailable", isAvailable);
                                startActivity(intent);
                                finish();
                            }
                        } else {
                            // Not Available
                            HashMap<String, Object> room = new HashMap<>();
                            room.put("incoming", username);
                            room.put("createdBy", username);
                            room.put("isAvailable", true);
                            room.put("status", 0);
                            database.getReference()
                                    .child("users")
                                    .child(username)
                                    .setValue(room).addOnSuccessListener(new OnSuccessListener<Void>() {
                                @Override
                                public void onSuccess(Void unused) {
                                    database.getReference()
                                            .child("users")
                                            .child(username).addValueEventListener(new ValueEventListener() {
                                        @Override
                                        public void onDataChange(@NonNull @NotNull DataSnapshot snapshot) {
                                            if (snapshot.child("status").exists()) {
                                                if (snapshot.child("status").getValue(Integer.class) == 1) {
                                                    if (isOkay)
                                                        return;
                                                    isOkay = true;
                                                    Intent intent = new Intent(ConnectingActivity.this, CallActivity.class);
                                                    String incoming = snapshot.child("incoming").getValue(String.class);
                                                    String createdBy = snapshot.child("createdBy").getValue(String.class);
                                                    boolean isAvailable = snapshot.child("isAvailable").getValue(Boolean.class);
                                                    intent.putExtra("username", username);
                                                    intent.putExtra("incoming", incoming);
                                                    intent.putExtra("createdBy", createdBy);
                                                    intent.putExtra("isAvailable", isAvailable);
                                                    startActivity(intent);
                                                    finish();
                                                }
                                            }
                                        }
                                        @Override
                                        public void onCancelled(@NonNull @NotNull DatabaseError error) {
                                        }
                                    });
                                }
                            });
                        }
                    }
                    @Override
                    public void onCancelled(@NonNull @NotNull DatabaseError error) {
                    }
                });
    }
}
```
CloudDB:
```
import android.content.Context;
import android.util.Log;
import com.huawei.agconnect.AGCRoutePolicy;
import com.huawei.agconnect.AGConnectInstance;
import com.huawei.agconnect.AGConnectOptionsBuilder;
import com.huawei.agconnect.auth.AGConnectAuth;
import com.huawei.agconnect.cloud.database.AGConnectCloudDB;
import com.huawei.agconnect.cloud.database.CloudDBZone;
import com.huawei.agconnect.cloud.database.CloudDBZoneConfig;
import com.huawei.agconnect.cloud.database.CloudDBZoneQuery;
import com.huawei.agconnect.cloud.database.exceptions.AGConnectCloudDBException;
import com.huawei.hmf.tasks.OnFailureListener;
import com.huawei.hmf.tasks.OnSuccessListener;
import com.huawei.hmf.tasks.Task;
import static android.content.ContentValues.TAG;
public class CloudDB {
    private Context context;
    private static CloudDB instance;
    private AGConnectCloudDB mCloudDB;
    private CloudDBZoneConfig mConfig;
    private CloudDBZone mCloudDBZone;
    private CloudDB(Context context) {
        this.context=context;
    }
    public static CloudDB getInstance(Context context) {
        if (instance==null)instance=new CloudDB(context);
        return instance;
    }
    public CloudDB initAGConnectCloudDB() {
        AGConnectCloudDB.initialize(context);
        return this;
    }
    public CloudDB createCloudDb(){
        AGConnectInstance instance = AGConnectInstance.buildInstance(new AGConnectOptionsBuilder().setRoutePolicy(AGCRoutePolicy.CHINA).build(mContext));
        mCloudDB = AGConnectCloudDB.getInstance(instance, AGConnectAuth.getInstance(instance));
        mCloudDB.createObjectType(ObjectTypeInfoHelper.getObjectTypeInfo());
        return this;
    }
    public void configCloudDb(){
        mConfig = new CloudDBZoneConfig("QuickStartDemo",
                CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE,
                CloudDBZoneConfig.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC);
        mConfig.setPersistenceEnabled(true);
        Task<CloudDBZone> openDBZoneTask = mCloudDB.openCloudDBZone2(mConfig, true);
        openDBZoneTask.addOnSuccessListener(new OnSuccessListener<CloudDBZone>() {
            @Override
            public void onSuccess(CloudDBZone cloudDBZone) {
                Log.i(TAG, "open cloudDBZone success");
                mCloudDBZone = cloudDBZone;
                // Add subscription after opening cloudDBZone success
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(Exception e) {
                Log.w(TAG, "open cloudDBZone failed for " + e.getMessage());
            }
        });
    }
    public void upsertBookInfos(BookInfo bookInfo) {
        if (mCloudDBZone == null) {
            Log.w(TAG, "CloudDBZone is null, try re-open it");
            return;
        }
        Task<Integer> upsertTask = mCloudDBZone.executeUpsert(bookInfo);
        upsertTask.addOnSuccessListener(new OnSuccessListener<Integer>() {
            @Override
            public void onSuccess(Integer cloudDBZoneResult) {
                Log.i(TAG, "Upsert " + cloudDBZoneResult + " records");
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(Exception e) {
                mUiCallBack.updateUiOnError("Insert book info failed");
            }
        });
    }
    public void viewCloudDbData(){
        private OnSnapshotListener<BookInfo> mSnapshotListener = new OnSnapshotListener<BookInfo>() {
            @Override
            public void onSnapshot(CloudDBZoneSnapshot<BookInfo> cloudDBZoneSnapshot, AGConnectCloudDBException e) {
                if (e != null) {
                    Log.w(TAG, "onSnapshot: " + e.getMessage());
                    return;
                }
                CloudDBZoneObjectList<BookInfo> snapshotObjects = cloudDBZoneSnapshot.getSnapshotObjects();
                List<BookInfo> bookInfos = new ArrayList<>();
                try {
                    if (snapshotObjects != null) {
                        while (snapshotObjects.hasNext()) {
                            BookInfo bookInfo = snapshotObjects.next();
                            bookInfos.add(bookInfo);
                            updateBookIndex(bookInfo);
                        }
                    }
                    mUiCallBack.onSubscribe(bookInfos);
                } catch (AGConnectCloudDBException snapshotException) {
                    Log.w(TAG, "onSnapshot:(getObject) " + snapshotException.getMessage());
                } finally {
                    cloudDBZoneSnapshot.release();
                }
            }
        };
    }
    public void addSubscription() {
        if (mCloudDBZone == null) {
            Log.w(TAG, "CloudDBZone is null, try re-open it");
            return;
        }
        try {
            CloudDBZoneQuery<BookInfo> snapshotQuery = CloudDBZoneQuery.where(BookInfo.class)
                    .equalTo(BookEditFields.SHADOW_FLAG, true);
            mRegister = mCloudDBZone.subscribeSnapshot(snapshotQuery,
                    CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY, mSnapshotListener);
        } catch (AGConnectCloudDBException e) {
            Log.w(TAG, "subscribeSnapshot: " + e.getMessage());
        }
    }
}
```
Xml layout DataBinding
To include data binding in the UI, enclose all content with <layout></layout>.

The ViewModel is introduced to the layout in the <data></data> section, as shown. Ensure that the type value points to the specific folder that has the required ViewModel.

App Build Result



Tips and Tricks

Identity Kit displays the HUAWEI ID registration or sign-in page first. The user can use the functions provided by Identity Kit only after signing in using a registered HUAWEI ID.

Conclusion

In this article, we have learned how to integrate Huawei Cloud DB using Webrtc Video Call in Android application. After completely read this article user can easily implement Huawei CloudDB in the Directory App android application using Kotlin.

Thanks for reading this article. Be sure to like and comment to this article, if you found it helpful. It means a lot to me.

References

HMS Docs:

https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/introduction-0000001050048870

https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-clouddb-introduction-0000001054212760

Cloud DB Kit Training Video:

https://developer.huawei.com/consumer/en/training/course/video/101628259491513909