Android Cloud Development Part 1: Bluetooth Sample Setup &. Firebase Realtime Database
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
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 pinSoftwareSerial 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 13int 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.
> 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: 1Warning:
The JKS keystore uses ...
>
👮🏼6. Implement User Login to MainActivity
< === Menu
ActivityResult API — Two contracts:
- Permissions contract
- 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
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
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
}
}