Android Cloud Development Part 2: Android Registration &. Firebase Email Response

In part 2, I’ll get in touch with Bluetooth On/Off switch on the Android phone. For the most part of the story, I focus on Client-Server communication. I will use more commonly with the function ejection. The task is simple: Register a customer and send he/she an email response, like this:

Image for post
Image for post

Part 2 will end at the registration activity, and the backend service will send a welcome email to the customer’s email address.

🐬1. Android-Client Design

< === Menu

Pseudocode:

MainActivity (Part 2)→ Cache: Registered? → WelcomeFragment: Yes: ControlPanel || No: → Registration

ControlPanel (Part 3) → ControlActivity → Bluetooth Detection → Bluetooth Socket

Registration (Part 2) → RegistrationActivity → SearchProductFragment → RegistrationActivity → Found: RegistrationForm || Not Found: SearchProductFragment

File Pattern: MVVM

Image for post
Image for post

Gradle:

Project:

buildscript {
ext.kotlin_version = "1.4.10"
repositories {
google()
jcenter()
mavenCentral()
maven { url "https://oss.jfrog.org/libs-snapshot" }
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.0-alpha14'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// Hilt
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
// google service
classpath 'com.google.gms:google-services:4.3.4'
}
}
...

Module:

plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
id 'com.google.gms.google-services'
}

android {
...
buildFeatures {
dataBinding true
viewBinding true
}
}

dependencies {
// STD

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
// Design
implementation 'com.google.android.material:material:1.2.1'
// Layout
implementation 'androidx.constraintlayout:constraintlayout:2.0.3'

// Activity and Fragment
def activity_version = "1.2.0-beta01"
def fragment_version = "1.3.0-beta01"
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
// Testing Fragments in Isolation
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"

// Test
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
// Assertions
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.ext:truth:1.3.0'
androidTestImplementation 'com.google.truth:truth:1.0'

// Kotlin coroutines
def coroutines_version = "1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
testImplementation 'org.hamcrest:hamcrest-library:1.3'
// Test: Kotlin coroutines
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9'
testImplementation 'org.hamcrest:hamcrest-library:1.3'

// Hilt
def hilt_version = '2.28-alpha'
def hilt_lifecycle_version = '1.0.0-alpha02'
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hilt_lifecycle_version"
kapt "androidx.hilt:hilt-compiler:$hilt_lifecycle_version"
// Tests
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"

// Firebase platform
implementation platform('com.google.firebase:firebase-bom:25.12.0')
implementation 'com.google.firebase:firebase-database-ktx'

// Lifecycle
def lifecycle_version = "2.2.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// Test helpers for LiveData
def arch_version = "2.1.0"
testImplementation "androidx.arch.core:core-testing:$arch_version"
}

👐🏻2. Global Helpers

< === Menu

Image for post
Image for post

ConfigHelper: Serve constant variables for all files

// System
const val REQUEST_SEARCH = "Request Key"
const val SEARCH_KEY = "Search Key"
const val REGISTERED = "Registered"

// Error
const val ERROR = "ERROR"
const val INVALID_EMAIL = "Invalid Email"
const val INVALID_PHONE = "Invalid Phone#"
const val TOO_SHORT = "Too short"
const val UNKNOWN = "UNKNOWN"
const val INTERNAL_CACHE_ERROR = "Internal cache error!"
...

FirebaseHelper: Serve functions for Firebase Realtime database

searchEmail():

fun searchEmail(
regRef:DatabaseReference, // Firebase
email: String, // email key
refTrue: KFunction2<String, Customer, Unit>, // True
refFalse: KFunction2<String, Customer?, Unit> // False
) {
val tag = "FirebaseHelper: searchEmail():\n"
var fKey = ""
var fCustomer:Customer? = null
val EMAIL_NF = "Email Not Found!"

// search
regRef
.orderByChild("email")
.endAt(email)
.addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(
snapshot: DataSnapshot
) {
val children = snapshot.children
lgd
(tag+"Done gathering children.")

var result = false
for (child in children) {
val data =
child.getValue(Customer::class.java)

lgd(tag+"${child.key} ==> $data ")
if (data?.email == email) {
fKey = child.key!!
fCustomer = data
result = true
break
}
}

if (result) {
lgd(tag+"Work on True Function.")
refTrue(fKey, fCustomer!!)
} else {
lgd(tag+"Work on False Function.")
refFalse(EMAIL_NF, fCustomer)
}
}

override fun onCancelled(error: DatabaseError) {
refFalse(EMAIL_NF, fCustomer)
}
})
}

SearchEmail eject the result to refTrue and refFalse fucntion.

saveCustomerCache():

// Save customer info into cache
fun saveCustomerCache(storage: Storage, customer: Customer) {
storage.setString(CUSTOMER_ID, customer.customerId)
storage.setString(FIRST_NAME, customer.firstName)
storage.setString(LAST_NAME, customer.lastName)
storage.setString(EMAIL_KEY, customer.email)
storage.setString(ADDRESS, customer.address)
storage.setString(CITY, customer.city)
storage.setString(STATE, customer.state)
storage.setInt(Zip, customer.zip)

val tag = "FirebaseHelper: saveCustomerCache():\n"
lgd(tag+"Customer: $customer")
}

LogHelper: Help with Logcat

const val TAG = "MLOG"
fun lgd(s:String) = Log.d(TAG, s)
fun lgi(s:String) = Log.i(TAG, s)
fun lge(s:String) = Log.e(TAG, s)
Image for post
Image for post

I use “MLOG” to track the debug process.

UiHelper: Help UI to process the user input.

// Toast: len: 0-short, 1-long
fun msg(context: Context, s: String, len: Int) =
if (len > 0) Toast.makeText(context, s, LENGTH_LONG).show()
else Toast.makeText(context, s, LENGTH_SHORT).show()

// Flash error for 15 times.
fun flashError(
viewLifecycleOwner: LifecycleOwner,
errorTV: TextView,
duration: Long, // second
delay: Long, // ms
) {...}
// Gather UI data from keyboard, Edittext + rootView
fun isKeyboardShown(
rootView: View,
mEt: EditText,
refSave: KFunction2<Int, Boolean, Unit>
): Boolean {...}

Please check Android: Bottom Side EditText Position Shifting and Android: Coroutines with Blinking TextView for their usages.

ValidateForm.kt: Validate the EditText

private val numRegex = "-?\\d+(\\.\\d+)?".toRegex()
private val hexRegex = "\\p{XDigit}+".toRegex()

class ValidateForm {
...
}

Go ahead, DIY yourself. It’s contains isEtEmplty(), isEtHex(), isEtNum(), isEtEmail(), isEtPhone(), chkMac(), chkLen() and View.OnFocusChangeListener. I just unite all the validations into one class.

➽3. Direct Inject SharedPreference: Storage

< === Menu

Nothing new is in the package. I copy the function from the Dagger-Hilt Codelab and expand the code usage.

Image for post
Image for post

Storage.kt

interface Storage {
fun setString(key: String, value: String)
fun getString(key: String): String

fun setBoolean(key: String, value: Boolean)
fun getBoolean(key: String): Boolean

fun setInt(key: String, value: Int)
fun getInt(key: String): Int

fun delKey(key: String): Boolean
fun clear()
}

SharedPreferenceStorage.kt:

// @Inject tells Dagger how to provide instances of this type
class SharedPreferencesStorage @Inject constructor(
@ApplicationContext context: Context,
private val fileName: String
) : Storage {

private val sharedPreferences = context
.getSharedPreferences(fileName, Context.MODE_PRIVATE)

// String Value
override fun setString(key: String, value: String) {
with(sharedPreferences.edit()) {
putString(key, value)
apply()
}
}

override fun getString(key: String): String {
return sharedPreferences.getString(key, UNKNOWN)!!
}

// Boolean Value
override fun setBoolean(key: String, value: Boolean) {
with(sharedPreferences.edit()) {
putBoolean(key, value)
apply()
}
}

override fun getBoolean(key: String): Boolean {
return sharedPreferences.getBoolean(key, false)
}

// Integer Value
override fun setInt(key: String, value: Int) {
with(sharedPreferences.edit()) {
putInt(key, value)
apply()
}
}

override fun getInt(key: String): Int {
return sharedPreferences.getInt(key, 0)
}

// Remove value
override fun delKey(key: String): Boolean {
with(sharedPreferences.edit()) {
remove(key)
apply()
}
return !sharedPreferences.contains(key)
}

// Clear all data
override fun clear() {
with(sharedPreferences.edit()) {
clear()
apply()
}
}
}

BluetoothAppModule.kt:

@Module
@InstallIn(ApplicationComponent::class)
class BluetoothAppModule {

@Provides
fun provideFileName(): String = "bleDevice"
}

StorageModule.kt:

@InstallIn(ApplicationComponent::class)
@Module
abstract class StorageModule {

@Binds abstract fun provideStorage(
storage: SharedPreferencesStorage): Storage

}

🐙4. Android: MainActivity & ViewModel

< === Menu

Image for post
Image for post
A: What?! It looks pretty empty.
Homan: The UI depends on everntContainer to dispatch the fragment.

Coding Path

MainActivity detects new user and old user.

New user => WelcomeFragment

Old user => ControlFragment

Contracts

In the client app, I need to deal with permissions and BluetoothOnOff. So I need two contracts.

1. Permission Contract:

AndroidManifest.xml:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="com.google.android.things.permission.MANAGE_BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />

MainActivity.kt:

private val REQUIRED_PERMISSIONS = arrayOf(
BLUETOOTH,
BLUETOOTH_ADMIN,
ACCESS_FINE_LOCATION,
ACCESS_COARSE_LOCATION

)

private const val BLUETOOTH_REQUEST_CODE = 9191

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var bluetoothHelper: BluetoothHelper private val reqMultiplePermissions = registerForActivityResult(
RequestMultiplePermissions()
) { permissions ->
permissions.entries.forEach {
lgd("Permission: ${it.key} = ${it.value}")
}
checkBluetoothStatus()
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
...
// check permission
reqMultiplePermissions.launch(REQUIRED_PERMISSIONS)
// load bluetooth functions
bluetoothHelper = BluetoothHelper()
...
}
...
companion object {
const val tag = "MainAct: "
}
}

After the app gets permission from the owner, it will detect Bluetooth On/Off status.

Bluetooth OnOff Contract

Image for post
Image for post

BluetoothHelper.kt: My Bluetooth library

class BluetoothHelper {
private var bluetoothAdapter =
BluetoothAdapter.getDefaultAdapter()

// check bluetooth switch
fun isEnabled(): Boolean {
return !bluetoothAdapter.isEnabled
}
}

BluetoothOnOffContract.kt: It contols Bluetooth switch on the cell phone.

class BluetoothOnOffContract: 
ActivityResultContract<Int, String?>() {

override fun createIntent(p0: Context, p1: Int?):
Intent {
return Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
}

override fun parseResult(resultCode: Int, intent: Intent?):
String? {
return if (resultCode == RESULT_OK)
BLUETOOTH_ON
else
BLUETOOTH_OFF
}
}

MainActivity.kt: bluetoothContract

private val bluetoothContract = registerForActivityResult(
BluetoothOnOffContract()
) { result ->
if (result.equals(BLUETOOTH_ON)) {
lgd(tag+BLUETOOTH_ON)
msg(this, BLUETOOTH_READY, 1)
}
else {
lgd(tag+BLUETOOTH_OFF)
msg(this, BLUETOOTH_NOT_ON, 0)
finish()
}
}

After the permission check, the app will check the Bluetooth switch.

private fun checkBluetoothStatus() {
lgd(tag+"check bluetooth status")
if (bluetoothHelper.isEnabled()) {
lgd(tag+"Turning Bluetooth ON...")
bluetoothContract.launch(BLUETOOTH_REQUEST_CODE)
}
}

MainViewModel: Check cache and offer registration

MainViewModel.kt:

class MainViewModel @ViewModelInject constructor(
private val storage: Storage
) : ViewModel() {

// Registration control
val regControls = MutableLiveData<Int>()
// Cache var
var registered = false

init {
//storage.clear() // test purpose
// Check cache
registered = storage.getBoolean(REGISTERED)

if (registered)
regControls.value = -1
else
regControls.value = storage.getInt(CTRL_COUNT)
lgd(tag+"Controls count: ${regControls.value}")
}

companion object {
const val tag = "MainVM: "
}
}

Monitor the registration control at MainActivity.kt:

override fun onCreate(savedInstanceState: Bundle?) {
...
// Welcome
mainViewModel.regControls.observe(
this,
{ controlCount ->
if (controlCount < 1) {
showWelcomeFragment()
} else {
//showControlPanel()
}
}
)
}
fun showWelcomeFragment() {
val fragment = WelcomeFragment.newInstance()
lgd(tag+"Show Welcome Fragment.")
val mTransaction = fragmentManager.beginTransaction()
mTransaction.add(R.id.eventContainer, fragment)
mTransaction.commit()
}

🎉5. WelcomeFragemnt and ViewModel

< === Menu

WelcomeFragement and its ViewModel serves for MainActivity. So it stays in the same package.

fragment_welcome.xml

Image for post
Image for post

WelcomeFragment.kt:

The fragment manages the new customer and returned customer.

Vars

private var diffGap = 0
private var adjustEditText = false
private var mGlobalLayoutListener: OnGlobalLayoutListener? = null

UI

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

welcomeCL = view.findViewById(R.id.welcomeCL)

// EditText
customerEmailEt = view.findViewById(R.id.customerEmailEt)
customerEmailEt.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
checkCustomer()
}
false
}

// Search indicator

progressBar = view.findViewById(R.id.progressBar)
progressBar.visibility = View.GONE
lgd
(ftag+"ProgressBar is GONE.")

// EditText adjustment
mGlobalLayoutListener = OnGlobalLayoutListener {
resolvePosition()
}
rootView = view.findViewById(R.id.rootFL)
rootView // check keyboard and editText distance
.viewTreeObserver
.addOnGlobalLayoutListener(mGlobalLayoutListener)

// Registration FAB
regFab = view.findViewById(R.id.regFab)
regFab.setOnClickListener {
progressBar.visibility = View.GONE
val intent = Intent(activity,
RegistrationActivity::class.java)
startActivity(intent)
}

// Email FAB

emailFab = view.findViewById(R.id.emailFab)
emailFab.setOnClickListener {
checkCustomer()
}

// Email error

emailNotFoundTV = view.findViewById(R.id.emailNotFoundTV)
emailNotFoundTV.visibility = View.GONE

// ImageButton
clearIB = view.findViewById(R.id.clearIBT)
clearIB.setOnClickListener{
customerEmailEt.setText("")
emailNotFoundTV.visibility = View.GONE
}

// LiveData

welcomeVM.emailError.observe(
viewLifecycleOwner,
{ message ->
if (message != UNKNOWN)
showErrorMsg(message)
}
)

// LiveData
welcomeVM.emailKeyFound.observe(
viewLifecycleOwner,
{ customerKey ->
lgd(ftag+"emailKeyFound: $customerKey")
if (customerKey != UNKNOWN)
toRegisteredControl(customerKey)
}
)
}

SoftKeyboard

fun resolvePosition() {
// achieve keyboard and UI ratio
val kbStatus = isKeyboardShown(
customerEmailEt.rootView,
customerEmailEt,
::getKeyboardData
)

if (kbStatus) {
if (adjustEditText)
welcomeCL.translationY = diffGap.toFloat()
} else {
welcomeCL.translationY = 0f
}
}

private fun getKeyboardData(
diffGap: Int, adjustEditText: Boolean
) {
this.diffGap = diffGap
this.adjustEditText = adjustEditText
}
override fun onDestroy() {
super.onDestroy()
rootView.viewTreeObserver
.removeOnGlobalLayoutListener(mGlobalLayoutListener)
}

Email Form Check

private fun checkCustomer() {
if (!validateForm()) return
lgd(ftag + "Data passed")

val email = customerEmailEt.text.toString()
progressBar.visibility = View.VISIBLE
welcomeVM.searchCustomer(email)
}

private fun validateForm(): Boolean {
var emailVf = vf.isEtEmpty(customerEmailEt)
if (emailVf) {
if (!vf.isEtEmail(customerEmailEt)) {
customerEmailEt.error = "Invalid Email"
customerEmailEt.requestFocus()
emailVf = false
}
customerEmailEt.error = null
}

return emailVf
}

// Blink the error message
private fun showErrorMsg(message: String) {
customerEmailEt.requestFocus()
progressBar.visibility = View.GONE
emailNotFoundTV.text = message

flashError(
viewLifecycleOwner,
emailNotFoundTV,
10,
300
)
}

To RegisterControlActivity

fun toRegisterControl(emailKey: String) {
progressBar.visibility = View.GONE
val intent = Intent(
activity,
RegisterControlActivity::class.java
)
startActivity(intent)
}

👨‍💻6. Registration and ViewModel

< === Menu

This activity will save the user data on the Firebase.

activity_registration.xml

Image for post
Image for post

RegistartionActivity.kt

onCreate(): If email is detected, the activity will redirect to RegisterControlActivity.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_registration)

// verified form for empty
lastNameEt.onFocusChangeListener = vf.mFocusChangeListener
firstNameEt.onFocusChangeListener = vf.mFocusChangeListener
emailEt.onFocusChangeListener = vf.mFocusChangeListener
phoneEt.onFocusChangeListener = vf.mFocusChangeListener
addrEt.onFocusChangeListener = vf.mFocusChangeListener
cityEt.onFocusChangeListener = vf.mFocusChangeListener
stateEt.onFocusChangeListener = vf.mFocusChangeListener
zipEt.onFocusChangeListener = vf.mFocusChangeListener

//fillTestData()

// Search Actions
registrationVM.emailKeyFound.observe(
this, { addRegisterControl() })
}

submitData():

fun submitData(view: View) {
if (!validateForm()) return
lgd(tag + "Data passed")

// Combine data to form a customer
val customer = Customer(
"",
firstNameEt.text.toString(),
lastNameEt.text.toString(),
emailEt.text.toString(),
phoneEt.text.toString(),
addrEt.text.toString(),
cityEt.text.toString(),
stateEt.text.toString(),
zipEt.text.toString().toInt()
)

registrationVM.saveRgistrationData(customer)
}

validateFrom():

private var click = 1 //click effect
private fun validateForm() : Boolean {
lgd(tag + "validate form...")
click = if (click == 1) {
submit.setBackgroundResource(R.drawable.frame_red_filled)
submitTV.setTextColor(Color.parseColor(M_RED))
0
} else {
submit.setBackgroundResource(R.drawable.frame_green_filled)
submitTV.setTextColor(Color.parseColor(M_GREEN))
1
}

val fnameVf = vf.isEtEmpty(firstNameEt)
if (fnameVf) firstNameEt.error = null

val lnameVf = vf.isEtEmpty(lastNameEt)
if (lnameVf) lastNameEt.error = null

var emailVf = vf.isEtEmpty(emailEt)
if (emailVf) {
if (!vf.isEtEmail(emailEt)) {
emailEt.error = INVALID_EMAIL
emailVf = false
}
emailEt.error = null
}

var phoneVf = vf.isEtEmpty(phoneEt)
if (phoneVf) {
if (vf.chkLen(phoneEt, 10)) {
if (!vf.isEtPhone(phoneEt)) {
phoneEt.error = INVALID_PHONE
phoneVf = false
}
phoneEt.error = null
} else {
phoneEt.error = TOO_SHORT
phoneVf = false
}
}

val addrVf = vf.isEtEmpty(addrEt)
if (lnameVf) addrEt.error = null

val cityVf = vf.isEtEmpty(cityEt)
if (lnameVf) cityEt.error = null

val stateVf = vf.isEtEmpty(stateEt)
if (lnameVf) stateEt.error = null

var zipVf = vf.isEtEmpty(zipEt)
if (zipVf) {
if (vf.chkLen(zipEt, 5)) {
if (!vf.isEtNum(zipEt)) {
zipEt.error = "Invalid Zipcode"
zipVf = false
}
zipEt.error = null
} else {
zipEt.error = "Too short"
zipVf = false
}
}

return fnameVf && lnameVf && emailVf && phoneVf
&& addrVf && cityVf && stateVf && zipVf
}

addRegisterControl():

fun addRegisterControl() {
val intent = Intent(this,
RegisterControlActivity::class.java)
startActivity(intent)
}

ValidateFrom.kt:

val mFocusChangeListener = View.OnFocusChangeListener {
v, hasFocus ->
val et = v as EditText
if (!hasFocus) {
isEtEmpty(et)
}
}

RegistrationViewModel.kt

class RegistrationViewModel@ViewModelInject constructor(
private val storage: Storage
) : ViewModel() {

// customer exist?
val emailKeyFound = MutableLiveData<String>()
// firebase
private val dbRef = Firebase.database.reference
private var fCustomer: Customer? = null


companion object {
const val tag = "RegVM:"
}
}

saveRegistrationData():

fun saveRgistrationData(customer: Customer) {
lgd(tag+"Register customer's info on Firebase.")
fCustomer = customer

// Search existed customer
val regRef = dbRef.child("registration")
searchEmail(
regRef,
customer.email,
::showProductList, // true
::saveCustomer // false
)
}

True function — showProductList():

// Shows the control list
// refTrue: KFunction1<String, Customer?, Unit>
fun showProductList(customerKey: String, customer: Customer) {
lgd(tag+"Save customer($customerKey) to cache: $customer")
saveCustomerCache(storage, customer)
emailKeyFound.postValue(customerKey)
}

False function — saveCustomer():

// Register a new customer (parameters useless)
// refFalse: KFunction2<String, Customer?, Unit>
// extra is null
// customer is null
private fun saveCustomer(extra: String, customer: Customer?) {
val regRef = dbRef.child("registration")

val customerKey = regRef.push().key
fCustomer!!.customerId = customerKey!!
regRef.child(customerKey)
.setValue(fCustomer)
.addOnSuccessListener {
lgd(tag+"Saved the data.")
saveCustomerCache(storage, fCustomer!!)
showProductList(fCustomer!!.email, fCustomer!!)
}
.addOnFailureListener {
lge(tag+"Save Customer Error: ${it.message}")
}
}

Firebase Realtime Database Rule

I need to change the rule to accept the new data category.

{
"rules": {
"product": {
".read": true,
".write": "auth != null && auth.uid == 'YOUR USER ID'"
},

"registration": {
".read": true,
".write": true
}
}
}

The “registration” sector allows customer registration.

👾7. Google Cloud Functions: HelloWorld

< === Backend Menu

At the backend, I can create a GCF to respond to the new registration. If you have a RestAPI server, you can do it, too. This is a serverless example.

Image for post
Image for post

In Google Cloud Platform, you need to enable Cloud Functions API.

Install Firebase CLI

New Folder

I will put everything in a folder for local editing.

Image for post
Image for post

Open a CMD,

> d:
> md firebase

Or you can do it in the windows.

Firebase-tools

> npm install -g firebase-tools

Login:

> firebase login
Image for post
Image for post

You’d better logout and login back in because your login authentication may be outdated.

> firebase logout
+ Logged out from homanhuang@gmail.com
> firebase login
i Firebase optionally collects CLI usage and error reporting information to help improve our products. Data is collected in accordance with Google's privacy policy (https://policies.google.com/privacy) and is not used to identify you.
? Allow Firebase to collect CLI usage and error reporting information? (Y/n) yVisit this URL on this device to log in:
https://accounts.google.com/...
Waiting for authentication...

A browser will ask you to login like this:

Image for post
Image for post
Image for post
Image for post
Allow
Image for post
Image for post

Close the browser and go back to CLI:

+  Success! Logged in as YOUR@EMAIL.COM

Now, you can check your projects in Firebase.

> firebase projects:list

This is mine.

Image for post
Image for post

It’s ready to input.

D:\firebase\firebase init

“init” parameter will automatically create some function file for you.

####### ### #######  ####### #######     ##     #####  #######
# # # ## # # ## # ## # #
##### # ####### ##### ####### ######## ##### #####
# # # ## # # ## # ## # #
# ### # ## ####### ####### # ## ##### #######
You're about to initialize a Firebase project in this directory:D:\firebase\? Are you ready to proceed? Yes
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices.
(space & enter)
(*) Functions: Configure and deploy Cloud Functions
=== Project SetupFirst, let's associate this project directory with a Firebase project. You can create multiple project aliases by running firebase use --add, but for now we'll just set up a default project.? Please select an option: Use an existing project
? Select a default Firebase project for this directory:
(*) bluetoothfirebase-27440 (BluetoothFirebase)
i Using project bluetoothfirebase-27440 (BluetoothFirebase)
=== Functions SetupA functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.
? What language would you like to use to write Cloud Functions? TypeScript
? Do you want to use ESLint to catch probable bugs and enforce style? Yes
+ Wrote functions/package.json
+ Wrote functions/.eslintrc.js
+ Wrote functions/tsconfig.json
+ Wrote functions/src/index.ts
+ Wrote functions/.gitignore
? Do you want to install dependencies with npm now? Yes
> protobufjs@6.10.1 postinstall D:\firebase\email\functions\node_modules\protobufjs
> node scripts/postinstall
npm notice created a lockfile as package-lock.json. You should commit this file.
added 417 packages from 274 contributors and audited 417 packages in 12.626s
38 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilitiesi Writing configuration info to firebase.json...
i Writing project information to .firebaserc...
i Writing gitignore file to .gitignore...
+ Firebase initialization complete!

📡8. HTTP functions in VS Code

< === Backend Menu

Image for post
Image for post
Image for post
Image for post

index.ts

Image for post
Image for post

functions.https: helloWorld

Image for post
Image for post
Ctrl + /
Image for post
Image for post
Ctrl + S

Ctrl + S and deploy:

D:\firebase>firebase deploy...
+ functions[helloWorld(us-central1)]: Successful create operation.
Function URL (helloWorld): https://us-central1-bluetoothfirebase-27440.cloudfunctions.net/helloWorld
+ Deploy complete!...D:\firebase>

It takes 3 to 5 minutes to upload. Slow, be patient. The server will give you a link to run this function.

Image for post
Image for post
Postman

functions.https: Query Test

Remove helloWorld; let’s test HTTP query.

export const stringLength = functions.https.onRequest(
(request, response) => {
var text = request.query.text;
var textLength = text?.length;
response.send(textLength+""); //attach with null
});

Ctrl + S and deploy:

+  functions[stringLength(us-central1)]: Successful create operation.
Function URL (stringLength): https://us-central1-bluetoothfirebase-27440.cloudfunctions.net/stringLength
+ Deploy complete!...D:\firebase>

Nothing at the end:

Image for post
Image for post
=> undefined

?text=HelloWorld

Image for post
Image for post

Passed.

🐱‍🏍9. Snippet in VS Code

< === Backend Menu

functions.database: sample

export const myDbFun = functions.database.ref('PATH')
.onCreate((snapshot, context) => {
});

Create Live Template or Snippet.

VS Code:

Image for post
Image for post
Ctrl+Shift+P → snippet
Image for post
Image for post
The typescript.json will open.

Open your browser; enter: snippet-generator.app

Image for post
Image for post

Right Hand Side:

Image for post
Image for post
Copy snippet

VS Code:

Image for post
Image for post
Ctrl + S

You can save multiple snippets in typescript.json separated by “{} , {}”, for example:

{
"Firebase Realtime Database Function onCreate": {
...
},
"HTTPS Query text": {
...
}
}

Let’s use the snippet in index.ts:

Image for post
Image for post
fdb → TAB

🏄‍♀️10. Realtime Database Functions in VS Code

< === Backend Menu

Image for post
Image for post
https://firebase.google.com/docs/functions/database-events

functions.database: onCreate Trigger

export const newRegistrationDetected = functions
.region('europe-west2')
.database.ref('registration/{$regId}/email')
.onCreate((snapshot, context) => {
});

Before I input more code, I need a domain service and email service to deliver emails.

Domain Service

You can get any Domain service within $12/year. Some domains even provide free email accounts, except Google.

Email Service

I choose SendGrid. It’s basically free for everyone. After I registered an account, I need to add an API key.

Image for post
Image for post

Next,

Image for post
Image for post

Next,

Image for post
Image for post
Create Key

Click the “Create Key” button, you’ll have your password.

Image for post
Image for post

Don’t verify yet, it has a time limit!

VC Code:

const mailOptions = {
from: 'manager@homanandroidworld.com',
to: regEmail,
subject: 'Welcome to Homan Wifi Control Service.',
html: '<b>Our company will help you to manage all of your wireless controls.!</b>'
};
const transporter = nodeMailer.createTransport({
host: "smtp.sendgrid.net",
port: 465,
secure: true,
auth: {
user: "apikey",
pass: "YOUR PASSWORD FROM SendGrid"
}
});
try {
transporter.sendMail(mailOptions)
console.log("email sent")
} catch(e) {
console.log("Something broke!"+e)
}

Now, deploy the function:

>firebase deploy
...
+ functions: Finished running predeploy script.
i functions: ensuring required API cloudfunctions.googleapis.com is enabled...
i functions: ensuring required API cloudbuild.googleapis.com is enabled...
+ functions: required API cloudbuild.googleapis.com is enabled
+ functions: required API cloudfunctions.googleapis.com is enabled
i functions: preparing functions directory for uploading...
i functions: packaged functions (45.08 KB) for uploading
+ functions: functions folder uploaded successfully
i functions: creating Node.js 12 function newRegistrationDetected(europe-west2)...
+ functions[newRegistrationDetected(europe-west2)]: Successful create operation.
+ Deploy complete!Project Console: https://console.firebase.google.com/project/bluetoothfirebase-27440/overview

My bad: I sent my function to the European region. My region is “us-west2”. So I delete the old one and deploy again.

Image for post
Image for post

SendGrid:

Image for post
Image for post
In SendGrid.

Run the app.

Image for post
Image for post

Realtime Database:

Image for post
Image for post

GCP:

Image for post
Image for post

Check function log:

Image for post
Image for post

“Email sent,” my SendGrid account has verified the API key.

Received Email

Image for post
Image for post

The onCreate has triggered. The response email has appeared.

Image for post
Image for post

Great. I have my email response from GCF.

🤬Don’t try the Email service of Google Domain!

👹Google will automatically charge you $12 per email per month.

Computer Science BS from SFSU. I studied and worked on Android system since 2017. If you are interesting in my past works, please go to my LinkedIn.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store