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

Homan Huang
17 min readNov 13, 2020

--

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:

  • The customer needs to enter the product ID to download its operational code in the app.
  • The server searches product ID and returns one if it is found.
  • The customer fills out the registration form and sends it to the server. Or just an email if he/she is a return customer, he/she needs to transfer the app to a new phone.
  • The server saves the data into the online database and sends a welcome email to the customer’s email. We need a cloud function in this section.

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

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

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)

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.

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

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

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

WelcomeFragment.kt:

The fragment manages the new customer and returned customer.

  • New customer => RegistrationActivity => RegisterControlActivity
  • Return customer => RegisterControlActivity

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

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.

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.

Open a CMD,

> d:
> md firebase

Or you can do it in the windows.

Firebase-tools

> npm install -g firebase-tools

Login:

> firebase login

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:

Allow

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.

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

index.ts

functions.https: helloWorld

Ctrl + /
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.

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:

=> undefined

?text=HelloWorld

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:

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

Open your browser; enter: snippet-generator.app

Right Hand Side:

Copy snippet

VS Code:

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:

fdb → TAB

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

< === Backend Menu

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.

Next,

Next,

Create Key

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

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.

SendGrid:

In SendGrid.

Run the app.

Realtime Database:

GCP:

Check function log:

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

Received Email

The onCreate has triggered. The response email has appeared.

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.

--

--

Homan Huang
Homan Huang

No responses yet