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:
- 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.
— === Menu === —
Frontend ==>
🐬1. Android-Client Design
👐🏻2. Global Helpers
🏵🏵🏵🏵 ConfigHelper
🏵🏵🏵🏵 FirebaseHelper
🏵🏵🏵🏵 LogHelper
🏵🏵🏵🏵 UiHelper
🏵🏵🏵🏵 ValidateForm
➽ 3. Direct Inject SharedPreference: Storage
🏵🏵🏵🏵 Storage.kt
🏵🏵🏵🏵 SharedPreferenceStorage.kt
🏵🏵🏵🏵 BluetoothAppModule.kt
🏵🏵🏵🏵 StorageModule.kt
🐙4. Android: MainActivity & ViewModel
🏵🏵🏵🏵 Permission Contract
🏵🏵🏵🏵 Bluetooth OnOff Contract
🎉5. WelcomeFragemnt and ViewModel
👨💻6. Registration and ViewModel
🏵🏵🏵🏵 Firebase Realtime Database Rule
Backend ==>
👾7. Google Cloud Functions: HelloWorld
📡8. HTTP functions in VS Code
🐱🏍9. Snippet in VS Code
🏄♀️10. Realtime Database Functions in VS Code
🏵🏵🏵🏵 SendGrid Email Service
🐬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
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:
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/postinstallnpm 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.626s38 packages are looking for funding
run `npm fund` for detailsfound 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
index.ts
functions.https: helloWorld
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.
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:
?text=HelloWorld
Passed.
🐱🏍9. Snippet in VS Code
functions.database: sample
export const myDbFun = functions.database.ref('PATH')
.onCreate((snapshot, context) => {});
Create Live Template or Snippet.
VS Code:
Open your browser; enter: snippet-generator.app
Right Hand Side:
VS Code:
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:
🏄♀️10. Realtime Database Functions in VS Code
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,
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:
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.