Android Cloud Development Part 1: Bluetooth Sample Setup &. Firebase Realtime Database

Homan Huang
11 min readOct 22, 2020

--

In this plan, I will build a wireless control for these purposes:

  • The communication between Android and Wireless Hardware
  • The communication between Android and Cloud Databases.

Chapter 1 to 3: The setup of hardware is the kids' level: some wires between HC-05, LED, and Arduino board. You can master the code in VS Code instead of Arduino IDE, which is pretty busy with typing or typos if you have tried.

Chapter 5 (Android → Cloud): I will show you the User Management from Firebase Realtime Database.

Chapter 6 to 9 (Android SDK): I create some contracts and loops from ActivityResult, and FragmentResult API. Those are new tools for Android Jetpack.

— === MenU === —

🔀1. Install Arduino IDE, VS Code &. PlatformIO
✏️2
. Create a new project in PlatformIO
🛅3
. Setup BLE4.0 HC-05 & LED Control ← C
____ 👉🏻AT Terminal Setup
____
👉🏻LED Control
📱 4. Android Pseudo Code
🍮5
. Simplified Server-Side User Management
____🔑 SHA1 Key
👮🏼 6
. Implement User Login to MainActivity
____✍🏻 Permissions Contract
____✍🏻 FirebaseAuth Contract
____✍🏻 Rule of Realtime Database
🔀7
. InputActivity
🔂8
. EditActivity and ViewModel
____ 👂🏻setFragmentResultListener
🌐9
. SearchProductFragment and ViewModel
____ 🔊setFragmentResult

🔀1. Install Arduino IDE, VS Code &. PlatformIO

< === Menu

Arduino IDE

Mine is Windows, so I download the Installer version.

VisualStudio Code

PlatformIO

Installed VS Code and go to its market place to download PlatformIO. In VS Code, you’ll have more convenient ways to edit your code.

✏️2. Create a new project in PlatformIO

< === Menu

My project name is Remote RFID. I chose d:\arduino\Project to install this project.

Open Project

Ctrl+o, let’s open my d:\arduino\Project. This is inconvenient. The VS Code doesn’t open the new project for me. Find my project and open it.

🛅3. Setup BLE4.0 HC-05 & LED Control ← C

< === Menu

Wire Connection

Side of digital ports on Arduino UNO

HC-05 has a tricky RX connection: (HC05) RX →Resistor-1K →RX(Port 3) →Resistor-2K →GND. The AT-Configuration is set on transmission speed is 38400 bps. Data flow is set on 9600 bps.

AT Terminal Setup at PlatformIO

> main.cpp: Setup HC-05

#include <Arduino.h>
#include <SoftwareSerial.h>
//HC-05 pin --> port
#define stPin 7
#define rxPin 2 // => HC05: rx
#define txPin 3 // => HC05: tx
// GND pin
// 5V pin
#define enPin 8 //EN pin
SoftwareSerial BTserial(rxPin, txPin); // RX | TX of Arduinochar reading = ' ';boolean BTconnected = false;void setup() {
// set input through EN pin
pinMode(enPin, OUTPUT);
digitalWrite(enPin, HIGH);
//Serial turns on in 1 second. Be patient and wait.
delay(1000);
// wait until the HC-05 has made a connection
while (!BTconnected)
{
if (digitalRead(enPin) == HIGH) { BTconnected = true; };
}
// start th serial communication with the host computer
Serial.begin(9600);
Serial.println("Arduino to Host is ready");

// start communication with the HC-05 using 38400
BTserial.begin(38400);
Serial.println("BTserial started at 38400");
}
void loop() {
// Keep reading from HC-05 and send to Arduino Serial Monitor
if (BTserial.available())
{
reading = BTserial.read();
Serial.write( reading );
}
// Keep reading from Arduino Serial Monitor and send to HC-05
if (Serial.available())
{
reading = Serial.read();
BTserial.write( reading );
}
}
  • BTseria: Tthe communication between Bluetooth and Arduino.
  • Serial: The communication between OS and Arduino.

In PlatformIO, please check the left bottom corner:

Please click ✔️Build & →Upload.

> platformio.ini

Please add:

monitor_speed = 9600
monitor_flags =
--echo

Serial Monitor:

Hardware: Please hold the button on the HC-05 to connect its power. The HC-05 will blink slow, once per 2 seconds.

PlatformIO: Click on the plug image to turn on the serial monitor.

> Executing task: C:\Users\Homan\.platformio\penv\Scripts\platformio.exe device monitor <--- Available filters and text transformations: colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at http://bit.ly/pio-monitor-filters
--- Miniterm on COM5 9600,8,N,1 ---
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
Arduino to Host is ready
BTserial started at 38400
at
OK

Run these commands for the Bluetooth terminal.

at+name=HomanBle01
OK
at+pswd=4343
OK
at+addr
+ADDR:98d3:32:70b1c9
OK

HC-05 Setup is finished. Unplug its power, and plug it back. It’s ready to hook up (blinking twice per second).

LED Control

The setup code needs to save as somewhere else. Or you can open a new project for LED control. We need to input the LED control code in the main.cpp.

> main.cpp: setup()

#include <Arduino.h>// LED switch
#define ledPin 13
int state = 0;void setup() {
// set led pin
pinMode( ledPin, OUTPUT );
digitalWrite( ledPin, LOW );
Serial.begin(9600);
}

> main.cpp: LED flashes per second

void loop() {
digitalWrite(ledPin, HIGH); // set the LED on
delay(1000); // wait for a second
digitalWrite(ledPin, LOW); // set the LED off
delay(1000);
}

Upload: Port 13 is working fine.

Working on Pin 13

> main.cpp: LED control

void loop() {
if(Serial.available() > 0) {
// read data
state = Serial.read();
}
// state: 0 - LED ON
// 1 - OFF
if (state == '0') {
// OFF
digitalWrite(ledPin, LOW);
// Send back
Serial.println("LED: OFF");
state = 0;
} else {
// ON
digitalWrite( ledPin, HIGH);
Serial.println("LED: ON");;
state = 1;
}
}

Upload. We test later on Android.

📱4. Android Pseudo Code

< === Menu

We need to see the production line, marketing, and warranty service as a chain. This the lifecycle of a product in the modern world. A new product may need two apps: One is for the company and one is for the customer.

Server Side

  • User: Who can add product information?
  • User: Manage product information I/O
  • User: Handle Payment and customer registration
  • User: Who can manage warranty service?

Client Side

  • Use user-inputted productId to get the MAC address of the Bluetooth Module on the Cloud database, such as Firebase.
  • Store the MAC address as SharedPreferences.
  • Pair the Bluetooth Module
  • Start LED control activity

🍮5. Simplified Server-Side User Management

< === Menu

For this demo, I will assign a user to take the job of data entry on Firebase.

The server can verify the customer has purchased a device by Product-Id. In return, the server will post a MAC-Address to the client-side. After that, the client can communicate with the remote device.

Firebase Prebuilt UI

I like prebuilt UI so I can have more time on coding instead of UI.

Let’s add Firebase Prebuilt UI into the Gradle.

// Firebase Auth UI
implementation 'com.firebaseui:firebase-ui-auth:6.2.0'

> Firebase console: Sign-In methods

>User: Add user

>Google Account User

Use SHA1 key to register your app, so you can use Google account to login directly. To get SHA1 key in the terminal:

> gradlew signingReport...
Certificate fingerprints:
SHA1: ...
SHA256: ...
Signature algorithm name: SHA1withRSA
Subject Public Key Algorithm: 2048-bit RSA key
Version: 1
Warning:
The JKS keystore uses ...
>

👮🏼6. Implement User Login to MainActivity

< === Menu

ActivityResult API — Two contracts:

  1. Permissions contract
  2. FirebaseAuth contract

Permissions Contract

Google team has given you already, called ActivityResultContracts.RequestMultiplePermissions().

AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

MainActivity.kt:

private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.INTERNET
)
...
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

...

private val reqMultiplePermissions = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
)
{ permissions ->
permissions.entries.forEach {
lgd("Permission: ${it.key} = ${it.value}")
}
}


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

// check permission
reqMultiplePermissions.launch(REQUIRED_PERMISSIONS)
}
...
companion object {
private const val tag = "MYLOG MainAct"
fun lgd(s:String) = Log.d(tag, s)

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

FirebaseAuth Contract

Request Code:

const val FirebaseLoginCode = 3133

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

New Contract → FirebaseAuthContract.kt:

class FirebaseAuthContract:
ActivityResultContract<Int, FirebaseUser?>() {

private val providers = arrayListOf(
AuthUI.IdpConfig.EmailBuilder().build(),
AuthUI.IdpConfig.PhoneBuilder().build(),
AuthUI.IdpConfig.GoogleBuilder().build()

)


override fun createIntent(
context: Context, code: Int?
): Intent {
return AuthUI.getInstance()
.createSignInIntentBuilder()
.setAvailableProviders(providers)
.build()
}

override fun parseResult(
resultCode: Int, intent: Intent?
): FirebaseUser? {
return if (resultCode == RESULT_OK) {
FirebaseAuth.getInstance().currentUser
} else {
null
}
}
}

Declaration at MainActivity.kt:

private val firebaseAuthContract = registerForActivityResult(
FirebaseAuthContract()
) { result ->
if (result != null) {
lgd("Updating User: $result")
mainViewModel.updateUser(result)
}
else {
lgd("Login error: please come back later.")
msg(this,
"Login Error! Please contact system administrator.",
1)
finish()
}
}

Run the contract:

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

// check permission
reqMultiplePermissions.launch(REQUIRED_PERMISSIONS)

firebaseAuthContract.launch(FirebaseLoginCode)

Test Run:

Working.

Rule of Realtime Database

You need to give the user some rights for editing.

UIDs are from your Authentication.

🔀7. InputActivity

< === Menu

I use MVC in this activity. Simple, it is.

@AndroidEntryPoint
class InputActivity : AppCompatActivity() {
// Firebase
private var user: FirebaseUser? = null
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_input)

user = FirebaseAuth.getInstance().currentUser

...

}

fun submitData(view: View) {
if (!validateForm()) return
...
saveToFirebase(product)
}

private fun validateForm(): Boolean {...}

private fun saveToFirebase(product: Product) {
val dbRef = Firebase.database.reference
dbRef.child("product")
.push()
.setValue(product)
.addOnSuccessListener {
lgd("Saved the data.")
finish()
}
.addOnFailureListener{
lge("Error: ${it.message}")
}
}


companion object {...}
}

🔂8. EditActivity and ViewModel

< === Menu

I have input some products and try to edit some of them. In this activity, I use the MVVM pattern and open fragment for the result.

EditActivity

Code:

@AndroidEntryPoint
class EditActivity : AppCompatActivity() {

// Firebase
private var user: FirebaseUser? = null

private val editViewModel: EditViewModel by viewModels()
private lateinit var editBinding: ActivityEditBinding
private val fragmentManager = supportFragmentManager

...


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

editBinding = DataBindingUtil.setContentView(
this, R.layout.activity_edit
)
editBinding.apply {
editVM = editViewModel
lifecycleOwner = this@EditActivity
}

// Firebase user
user = FirebaseAuth.getInstance().currentUser

// UI
...

→ This is your fish line for the fragment result:

        // open fragment for result
fragmentManager.setFragmentResultListener(
REQUEST_SEARCH, this
) { _, bundle ->
val result = bundle.getString(SEARCH_WORDS)
if (result != null) {
editViewModel.setSearchKey(result)
}
}

        // Search Actions
editViewModel.found.observe(this, {
when (it!!) {
SearchType.FOUND -> {
editCL.visibility = View.VISIBLE
}
SearchType.PROCESSING -> {
lgd("Processing search key...")
searchProduct(null)
}
SearchType.NOT_FOUND -> {
lgd("Search Key Not Found")
searchProduct( "Not Found, Retry!\n" +
"Product Key:")
}
}
})

// Update UI
editViewModel.fProduct.observe(this, {
productEt.setText(it.name)
serialEt.setText(it.serial.toString())
pidEt.setText(it.pid)
macEt1.setText(it.macAddr.substring(0,4))
macEt2.setText(it.macAddr.substring(5,7))
macEt3.setText(it.macAddr.substring(8,14))
})

cancelBt.setOnClickListener { finish() }
}

private fun searchProduct(title: String?) {
editCL.visibility = View.GONE

val sFragment = SearchProductFragment.newInstance()
title.let {
sFragment.arguments = Bundle().apply {
putString(SEARCH_TITLE, it)
}
}

lgd("Load search product fragment.")
val mTransaction = fragmentManager.beginTransaction()
mTransaction.add(R.id.searchFL, sFragment)
mTransaction.commit()
}

fun submitData(view: View) {
if (!validateForm()) return
...

lgd("save to firebase")
editViewModel.updateFirebase(this, product)
}

private fun validateForm(): Boolean {...}

companion object {
private const val tag = "MYLOG EditAct"
fun lgd(s: String) = Log.d(tag, s)

const val REQUEST_SEARCH = "Request Key"
const val SEARCH_WORDS = "Search Key"
const val SEARCH_TITLE = "Search Title"
}
}

All actions are controlled by found← LiveData and setFragmentResultListener. The effect will be

Not Found
Found

EditViewModel

class EditViewModel @ViewModelInject constructor(
) : ViewModel() {

// Search status
val found = MutableLiveData<SearchType>()

// Found product
val fProduct = MutableLiveData<Product>()

// Found key of product
var fKey:String = ""

...

init {
found.value = SearchType.PROCESSING
}

fun searchProductKey() {
lgd("Processing $searchKey")
if (searchKey != "") {
val productRef = dbRef.child("product")

val productSearch = searchKey
productRef
.orderByChild("pid")
.endAt(productSearch)
.addListenerForSingleValueEvent
(
object : ValueEventListener {

override fun onDataChange(
snapshot: DataSnapshot
) {
val children = snapshot.children

var result = false
for (child in children) {
val data =
child.getValue(Product::class.java)
lgd("${child.key} ==> $data ")
if (data?.pid == productSearch) {
fKey = child.key!!
fProduct.value = data
result = true
break
}

}

lgd("search result: $result")
if (result) {
found.value = SearchType.FOUND
} else {
found.value = SearchType.NOT_FOUND
}
}

override fun onCancelled(error: DatabaseError) {
lge(error.message)
}
})
} else {
lgd("Empty Search Key!!!")
}

}

fun setSearchKey(str: String) {
searchKey = str
searchProductKey()
}

fun updateFirebase(activity: EditActivity, data: Product) {
if (data != fProduct.value) {
val productRef = dbRef.child("product").child(fKey)
productRef.setValue(data)
.addOnSuccessListener {
lgd("Saved the data.")
activity.finish()
}
.addOnFailureListener{
lge("Error: ${it.message}")
activity.finish()
}
} else {
lgd("Nothing to save. Bye!")
activity.finish()
}
}


companion object {
private const val tag = "MYLOG EditVM"
fun lgd(s:String) = Log.d(tag, s)
fun lge(s:String) = Log.e(tag, s)
var searchKey: String = ""

const val ZEROMAC = "0000:00:000000"
}
}

enum class SearchType {
FOUND, PROCESSING, NOT_FOUND
}

Just follow the plotted plan, you’ll understand.

🌐9. SearchProductFragment and ViewModel

< === Menu

SearchProductFragment

Simple UI

Code:

@AndroidEntryPoint
class SearchProductFragment : Fragment() {
...

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

arguments?.let {
title = it.getString(SEARCH_TITLE)
if (title != null) searchViewModel.setTitle(title!!)
}
}

override fun onCreateView(...){...}

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

→ This is your fish to return to the activity.

        searchBt.setOnClickListener {
if (!validateForm(pidEt)) return@setOnClickListener

val pid = pidEt.text.toString()
// save result
setFragmentResult(
REQUEST_SEARCH,
bundleOf(SEARCH_WORDS to pid))

close()
}

    searchViewModel.title.observe(
viewLifecycleOwner,
{ enterTitleTV.text = it }
)
}

// close fragment
private fun close() {
val manager = requireActivity().supportFragmentManager

if (manager.backStackEntryCount == 0) {
manager.beginTransaction().remove(this).commit()
} else {
manager.popBackStack();
}
}

private fun validateForm(et: EditText):
Boolean = vF.isEtEmpty(et)

companion object {
fun newInstance() = SearchProductFragment()
private const val tag = "MYLOG SearchPF"
fun lgd(s:String) = Log.d(tag, s)
}
}

SearchProductViewModel

class SearchProductViewModel@ViewModelInject constructor(
) : ViewModel() {

val title = MutableLiveData<String>()

init {
title.value = "Product Key:"
}

fun setTitle(title: String) {
this.title.value = title
}
}

Done! ✌️ See you in part 2!

--

--

Homan Huang
Homan Huang

Written by Homan Huang

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.

No responses yet