mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-01-27 16:05:32 +00:00
Integrate SecretsRepository and ApkImporter with app
Refactor methods out of MainActivity into Controllers for ScanScreen and SettingsScreen
This commit is contained in:
parent
c456d455ef
commit
4529925906
@ -0,0 +1,16 @@
|
||||
package com.github.nacabaro.vbhelper
|
||||
|
||||
interface ActivityLifecycleListener {
|
||||
fun onPause()
|
||||
fun onResume()
|
||||
|
||||
companion object {
|
||||
fun noOpInstance(): ActivityLifecycleListener {
|
||||
return object: ActivityLifecycleListener {
|
||||
override fun onPause() {}
|
||||
|
||||
override fun onResume() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,8 @@
|
||||
package com.github.nacabaro.vbhelper
|
||||
|
||||
import android.content.Intent
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.Tag
|
||||
import android.nfc.tech.NfcA
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@ -13,17 +10,10 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.cfogrady.vb.dim.card.DimReader
|
||||
import com.github.nacabaro.vbhelper.navigation.AppNavigation
|
||||
import com.github.cfogrady.vbnfc.CryptographicTransformer
|
||||
import com.github.cfogrady.vbnfc.TagCommunicator
|
||||
import com.github.cfogrady.vbnfc.be.BENfcCharacter
|
||||
import com.github.cfogrady.vbnfc.data.DeviceType
|
||||
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||
import com.github.nacabaro.vbhelper.di.VBHelper
|
||||
import com.github.nacabaro.vbhelper.domain.Dim
|
||||
@ -32,39 +22,71 @@ import com.github.nacabaro.vbhelper.domain.Character
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
|
||||
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
|
||||
import com.github.nacabaro.vbhelper.navigation.AppNavigationHandlers
|
||||
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.screens.SettingsScreenController
|
||||
import com.github.nacabaro.vbhelper.source.ApkSecretsImporter
|
||||
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private lateinit var nfcAdapter: NfcAdapter
|
||||
private lateinit var deviceToCryptographicTransformers: Map<UShort, CryptographicTransformer>
|
||||
|
||||
private var nfcCharacter = MutableStateFlow<NfcCharacter?>(null)
|
||||
|
||||
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
deviceToCryptographicTransformers = getMapOfCryptographicTransformers()
|
||||
private val onActivityLifecycleListeners = HashMap<String, ActivityLifecycleListener>()
|
||||
|
||||
private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {
|
||||
if( onActivityLifecycleListeners[key] != null) {
|
||||
throw IllegalStateException("Key is already in use")
|
||||
}
|
||||
onActivityLifecycleListeners[key] = activityLifecycleListener
|
||||
}
|
||||
|
||||
private fun unregisterActivityLifecycleListener(key: String) {
|
||||
onActivityLifecycleListeners.remove(key)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
registerFileActivityResult()
|
||||
|
||||
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
||||
if (maybeNfcAdapter == null) {
|
||||
Toast.makeText(this, "No NFC on device!", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
nfcAdapter = maybeNfcAdapter
|
||||
val application = applicationContext as VBHelper
|
||||
val settingsScreenController = SettingsScreenController.Factory(this, ApkSecretsImporter(), application.container.dataStoreSecretsRepository)
|
||||
.buildSettingScreenHandlers()
|
||||
val scanScreenController = ScanScreenControllerImpl(
|
||||
application.container.dataStoreSecretsRepository.secretsFlow,
|
||||
this::handleReceivedNfcCharacter,
|
||||
this,
|
||||
this::registerActivityLifecycleListener,
|
||||
this::unregisterActivityLifecycleListener)
|
||||
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
VBHelperTheme {
|
||||
MainApplication()
|
||||
MainApplication(settingsScreenController, scanScreenController)
|
||||
}
|
||||
}
|
||||
Log.i("MainActivity", "Activity onCreated")
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.i("MainActivity", "onPause")
|
||||
for(activityListener in onActivityLifecycleListeners) {
|
||||
activityListener.value.onPause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Log.i("MainActivity", "Resume")
|
||||
for(activityListener in onActivityLifecycleListeners) {
|
||||
activityListener.value.onResume()
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerFileActivityResult() {
|
||||
@ -154,26 +176,10 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainApplication() {
|
||||
var isDoneReadingCharacter by remember { mutableStateOf(false) }
|
||||
private fun MainApplication(settingsScreenController: SettingsScreenController, scanScreenController: ScanScreenControllerImpl) {
|
||||
|
||||
AppNavigation(
|
||||
isDoneReadingCharacter = isDoneReadingCharacter,
|
||||
onClickRead = {
|
||||
handleTag {
|
||||
val character = it.receiveCharacter()
|
||||
nfcCharacter.value = character
|
||||
|
||||
val importStatus = addCharacterScannedIntoDatabase()
|
||||
|
||||
isDoneReadingCharacter = true
|
||||
|
||||
importStatus
|
||||
}
|
||||
},
|
||||
onClickScan = {
|
||||
isDoneReadingCharacter = false
|
||||
},
|
||||
applicationNavigationHandlers = AppNavigationHandlers(settingsScreenController, scanScreenController),
|
||||
onClickImportCard = {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
@ -184,73 +190,12 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||
private fun getMapOfCryptographicTransformers(): Map<UShort, CryptographicTransformer> {
|
||||
return mapOf(
|
||||
Pair(DeviceType.VitalBraceletBEDeviceType,
|
||||
CryptographicTransformer(readableHmacKey1 = resources.getString(R.string.password1),
|
||||
readableHmacKey2 = resources.getString(R.string.password2),
|
||||
aesKey = resources.getString(R.string.decryptionKey),
|
||||
substitutionCipher = resources.getIntArray(R.array.substitutionArray))),
|
||||
// Pair(DeviceType.VitalSeriesDeviceType,
|
||||
// CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
|
||||
// hmacKey2 = resources.getString(R.string.password2),
|
||||
// decryptionKey = resources.getString(R.string.decryptionKey),
|
||||
// substitutionCipher = resources.getIntArray(R.array.substitutionArray))),
|
||||
// Pair(DeviceType.VitalCharactersDeviceType,
|
||||
// CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
|
||||
// hmacKey2 = resources.getString(R.string.password2),
|
||||
// decryptionKey = resources.getString(R.string.decryptionKey),
|
||||
// substitutionCipher = resources.getIntArray(R.array.substitutionArray)))
|
||||
)
|
||||
}
|
||||
private fun handleReceivedNfcCharacter(character: NfcCharacter): String {
|
||||
nfcCharacter.value = character
|
||||
|
||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||
private fun showWirelessSettings() {
|
||||
Toast.makeText(this, "NFC must be enabled", Toast.LENGTH_SHORT).show()
|
||||
startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
|
||||
}
|
||||
val importStatus = addCharacterScannedIntoDatabase()
|
||||
|
||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||
private fun buildOnReadTag(handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
|
||||
return { tag->
|
||||
val nfcData = NfcA.get(tag)
|
||||
if (nfcData == null) {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
nfcData.connect()
|
||||
nfcData.use {
|
||||
val tagCommunicator = TagCommunicator.getInstance(nfcData, deviceToCryptographicTransformers)
|
||||
val successText = handlerFunc(tagCommunicator)
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, successText, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||
private fun handleTag(handlerFunc: (TagCommunicator)->String) {
|
||||
if (!nfcAdapter.isEnabled) {
|
||||
showWirelessSettings()
|
||||
} else {
|
||||
val options = Bundle()
|
||||
// Work around for some broken Nfc firmware implementations that poll the card too fast
|
||||
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
|
||||
nfcAdapter.enableReaderMode(this, buildOnReadTag(handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (nfcAdapter.isEnabled) {
|
||||
nfcAdapter.disableReaderMode(this)
|
||||
}
|
||||
return importStatus
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@ -12,17 +12,19 @@ import com.github.nacabaro.vbhelper.screens.BattlesScreen
|
||||
import com.github.nacabaro.vbhelper.screens.DexScreen
|
||||
import com.github.nacabaro.vbhelper.screens.DiMScreen
|
||||
import com.github.nacabaro.vbhelper.screens.HomeScreen
|
||||
import com.github.nacabaro.vbhelper.screens.ScanScreen
|
||||
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreen
|
||||
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.screens.SettingsScreen
|
||||
import com.github.nacabaro.vbhelper.screens.SettingsScreenController
|
||||
import com.github.nacabaro.vbhelper.screens.SpriteViewer
|
||||
import com.github.nacabaro.vbhelper.screens.StorageScreen
|
||||
|
||||
data class AppNavigationHandlers(val settingsScreenController: SettingsScreenController, val scanScreenController: ScanScreenControllerImpl)
|
||||
|
||||
@Composable
|
||||
fun AppNavigation(
|
||||
onClickRead: () -> Unit,
|
||||
onClickScan: () -> Unit,
|
||||
applicationNavigationHandlers: AppNavigationHandlers,
|
||||
onClickImportCard: () -> Unit,
|
||||
isDoneReadingCharacter: Boolean
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
@ -49,11 +51,9 @@ fun AppNavigation(
|
||||
StorageScreen()
|
||||
}
|
||||
composable(BottomNavItem.Scan.route) {
|
||||
onClickScan()
|
||||
ScanScreen(
|
||||
navController = navController,
|
||||
onClickRead = onClickRead,
|
||||
isDoneReadingCharacter = isDoneReadingCharacter
|
||||
scanScreenController = applicationNavigationHandlers.scanScreenController,
|
||||
)
|
||||
}
|
||||
composable(BottomNavItem.Dex.route) {
|
||||
@ -64,6 +64,7 @@ fun AppNavigation(
|
||||
composable(BottomNavItem.Settings.route) {
|
||||
SettingsScreen(
|
||||
navController = navController,
|
||||
settingsScreenController = applicationNavigationHandlers.settingsScreenController,
|
||||
onClickImportCard = onClickImportCard
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,108 +0,0 @@
|
||||
package com.github.nacabaro.vbhelper.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.github.nacabaro.vbhelper.components.TopBanner
|
||||
import com.github.nacabaro.vbhelper.navigation.BottomNavItem
|
||||
import com.github.nacabaro.vbhelper.screens.scanScreen.ReadingCharacterScreen
|
||||
|
||||
@Composable
|
||||
fun ScanScreen(
|
||||
navController: NavController,
|
||||
onClickRead: () -> Unit,
|
||||
isDoneReadingCharacter: Boolean
|
||||
) {
|
||||
var readingScreen by remember { mutableStateOf(false) }
|
||||
|
||||
if (isDoneReadingCharacter) {
|
||||
readingScreen = false
|
||||
navController.navigate(BottomNavItem.Home.route)
|
||||
}
|
||||
|
||||
if (readingScreen) {
|
||||
ReadingCharacterScreen { readingScreen = false }
|
||||
} else {
|
||||
ChooseConnectOption(
|
||||
onClickRead = {
|
||||
readingScreen = true
|
||||
onClickRead()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChooseConnectOption(
|
||||
onClickRead: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { TopBanner(text = "Scan a Vital Bracelet") }
|
||||
) { contentPadding ->
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(contentPadding)
|
||||
) {
|
||||
ScanButton(
|
||||
text = "Vital Bracelet to App",
|
||||
onClick = onClickRead
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ScanButton(
|
||||
text = "App to Vital Bracelet",
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ScanButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ScanScreenPreview() {
|
||||
ScanScreen(
|
||||
navController = rememberNavController(),
|
||||
onClickRead = { },
|
||||
isDoneReadingCharacter = false
|
||||
)
|
||||
}
|
||||
@ -1,5 +1,9 @@
|
||||
package com.github.nacabaro.vbhelper.screens
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -22,6 +26,7 @@ import com.github.nacabaro.vbhelper.components.TopBanner
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
navController: NavController,
|
||||
settingsScreenController: SettingsScreenController,
|
||||
onClickImportCard: () -> Unit
|
||||
) {
|
||||
Scaffold (
|
||||
@ -42,12 +47,10 @@ fun SettingsScreen(
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SettingsSection("General")
|
||||
SettingsEntry(title = "Import VB key", description = "Import standard vital bracelet keys") { }
|
||||
SettingsEntry(title = "Import VB Characters key", description = "Import standard vital bracelet keys") { }
|
||||
SettingsEntry(title = "Import VB BE key", description = "Import standard vital bracelet keys") { }
|
||||
SettingsEntry(title = "Import transform functions", description = "Import standard vital bracelet keys") { }
|
||||
SettingsEntry(title = "Import decryption key", description = "Import standard vital bracelet keys") { }
|
||||
SettingsSection("NFC Communication")
|
||||
SettingsEntry(title = "Import APK", description = "Import Secrets From Vital Arean 2.1.0 APK") {
|
||||
settingsScreenController.apkFilePickLauncher.launch(arrayOf("*/*"))
|
||||
}
|
||||
SettingsSection("DiM/BEm management")
|
||||
SettingsEntry(title = "Import DiM card", description = "Import DiM/BEm card file", onClick = onClickImportCard)
|
||||
SettingsEntry(title = "Rename DiM/BEm", description = "Set card name") { }
|
||||
@ -58,6 +61,12 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
fun buildFilePickLauncher(activity: ComponentActivity, onItemPicked: (Uri?) -> Unit): ActivityResultLauncher<Array<String>> {
|
||||
return activity.registerForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
onItemPicked.invoke(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsEntry(
|
||||
title: String,
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
package com.github.nacabaro.vbhelper.screens
|
||||
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.nacabaro.vbhelper.source.SecretsImporter
|
||||
import com.github.nacabaro.vbhelper.source.SecretsRepository
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class SettingsScreenController(val apkFilePickLauncher: ActivityResultLauncher<Array<String>>) {
|
||||
|
||||
class Factory(private val componentActivity: ComponentActivity, private val secretsImporter: SecretsImporter, private val secretsRepository: SecretsRepository) {
|
||||
|
||||
fun buildSettingScreenHandlers(): SettingsScreenController {
|
||||
return SettingsScreenController(
|
||||
apkFilePickLauncher = buildFilePickerActivityLauncher(this::importApk)
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildFilePickerActivityLauncher(onResult : (Uri?) ->Unit): ActivityResultLauncher<Array<String>> {
|
||||
return componentActivity.registerForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
onResult.invoke(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun importApk(uri: Uri?) {
|
||||
if(uri == null) {
|
||||
componentActivity.runOnUiThread {
|
||||
Toast.makeText(componentActivity, "APK Import Cancelled", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
return
|
||||
}
|
||||
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
|
||||
componentActivity.contentResolver.openInputStream(uri).use {
|
||||
if(it == null) {
|
||||
componentActivity.runOnUiThread {
|
||||
Toast.makeText(
|
||||
componentActivity,
|
||||
"Selected file is empty!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
var secrets: Secrets? = null
|
||||
try {
|
||||
secrets = secretsImporter.importSecrets(it)
|
||||
} catch (e: Exception) {
|
||||
componentActivity.runOnUiThread {
|
||||
Toast.makeText(componentActivity, "Secrets import failed. Please only select the official Vital Arena App 2.1.0 APK.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
|
||||
secretsRepository.updateSecrets(secrets)
|
||||
}.invokeOnCompletion {
|
||||
componentActivity.runOnUiThread {
|
||||
Toast.makeText(componentActivity, "Secrets successfully imported. Connections with devices are now possible.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
package com.github.nacabaro.vbhelper.screens.scanScreen
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
|
||||
import com.github.nacabaro.vbhelper.components.TopBanner
|
||||
import com.github.nacabaro.vbhelper.navigation.BottomNavItem
|
||||
import com.github.nacabaro.vbhelper.source.isMissingSecrets
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
const val SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER = "SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER"
|
||||
|
||||
@Composable
|
||||
fun ScanScreen(
|
||||
navController: NavController,
|
||||
scanScreenController: ScanScreenController,
|
||||
) {
|
||||
val secrets by scanScreenController.secretsFlow.collectAsState(null)
|
||||
var readingScreen by remember { mutableStateOf(false) }
|
||||
var isDoneReadingCharacter by remember { mutableStateOf(false) }
|
||||
|
||||
DisposableEffect(readingScreen) {
|
||||
if(readingScreen) {
|
||||
scanScreenController.registerActivityLifecycleListener(SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER, object: ActivityLifecycleListener {
|
||||
override fun onPause() {
|
||||
scanScreenController.cancelRead()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
scanScreenController.onClickRead(secrets!!) {
|
||||
isDoneReadingCharacter = true
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
scanScreenController.onClickRead(secrets!!) {
|
||||
isDoneReadingCharacter = true
|
||||
}
|
||||
}
|
||||
onDispose {
|
||||
if(readingScreen) {
|
||||
scanScreenController.unregisterActivityLifecycleListener(SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER)
|
||||
scanScreenController.cancelRead()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDoneReadingCharacter) {
|
||||
readingScreen = false
|
||||
navController.navigate(BottomNavItem.Home.route)
|
||||
}
|
||||
|
||||
if (readingScreen) {
|
||||
ReadingCharacterScreen {
|
||||
readingScreen = false
|
||||
scanScreenController.cancelRead()
|
||||
}
|
||||
} else {
|
||||
val context = LocalContext.current
|
||||
ChooseConnectOption(
|
||||
onClickRead = {
|
||||
if(secrets == null) {
|
||||
Toast.makeText(context, "Secrets is not yet initialized. Try again.", Toast.LENGTH_SHORT).show()
|
||||
} else if(secrets?.isMissingSecrets() == true) {
|
||||
Toast.makeText(context, "Secrets not yet imported. Go to Settings and Import APK", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
readingScreen = true // kicks off nfc adapter in DisposableEffect
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChooseConnectOption(
|
||||
onClickRead: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { TopBanner(text = "Scan a Vital Bracelet") }
|
||||
) { contentPadding ->
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(contentPadding)
|
||||
) {
|
||||
ScanButton(
|
||||
text = "Vital Bracelet to App",
|
||||
onClick = onClickRead,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ScanButton(
|
||||
text = "App to Vital Bracelet",
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ScanButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ScanScreenPreview() {
|
||||
ScanScreen(
|
||||
navController = rememberNavController(),
|
||||
scanScreenController = object: ScanScreenController {
|
||||
override val secretsFlow = MutableStateFlow<Secrets>(Secrets.getDefaultInstance())
|
||||
override fun unregisterActivityLifecycleListener(key: String) { }
|
||||
override fun registerActivityLifecycleListener(
|
||||
key: String,
|
||||
activityLifecycleListener: ActivityLifecycleListener
|
||||
) {
|
||||
|
||||
}
|
||||
override fun onClickRead(secrets: Secrets, onComplete: ()->Unit) {}
|
||||
override fun cancelRead() {}
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.github.nacabaro.vbhelper.screens.scanScreen
|
||||
|
||||
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ScanScreenController {
|
||||
val secretsFlow: Flow<Secrets>
|
||||
fun onClickRead(secrets: Secrets, onComplete: ()->Unit)
|
||||
fun cancelRead()
|
||||
|
||||
fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener)
|
||||
fun unregisterActivityLifecycleListener(key: String)
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
package com.github.nacabaro.vbhelper.screens.scanScreen
|
||||
|
||||
import android.content.Intent
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.Tag
|
||||
import android.nfc.tech.NfcA
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.cfogrady.vbnfc.TagCommunicator
|
||||
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
|
||||
import com.github.nacabaro.vbhelper.source.getCryptographicTransformerMap
|
||||
import com.github.nacabaro.vbhelper.source.isMissingSecrets
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ScanScreenControllerImpl(
|
||||
override val secretsFlow: Flow<Secrets>,
|
||||
private val nfcHandler: (NfcCharacter)->String,
|
||||
private val context: ComponentActivity,
|
||||
private val registerActivityLifecycleListener: (String, ActivityLifecycleListener)->Unit,
|
||||
private val unregisterActivityLifecycleListener: (String)->Unit,
|
||||
): ScanScreenController {
|
||||
|
||||
private val nfcAdapter: NfcAdapter
|
||||
|
||||
init {
|
||||
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(context)
|
||||
if (maybeNfcAdapter == null) {
|
||||
Toast.makeText(context, "No NFC on device!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
nfcAdapter = maybeNfcAdapter
|
||||
checkSecrets()
|
||||
}
|
||||
|
||||
override fun onClickRead(secrets: Secrets, onComplete: ()->Unit) {
|
||||
handleTag(secrets) { tagCommunicator ->
|
||||
val character = tagCommunicator.receiveCharacter()
|
||||
val resultMessage = nfcHandler(character)
|
||||
onComplete.invoke()
|
||||
resultMessage
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelRead() {
|
||||
if(nfcAdapter.isEnabled) {
|
||||
nfcAdapter.disableReaderMode(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerActivityLifecycleListener(
|
||||
key: String,
|
||||
activityLifecycleListener: ActivityLifecycleListener
|
||||
) {
|
||||
registerActivityLifecycleListener.invoke(key, activityLifecycleListener)
|
||||
}
|
||||
|
||||
override fun unregisterActivityLifecycleListener(key: String) {
|
||||
unregisterActivityLifecycleListener.invoke(key)
|
||||
}
|
||||
|
||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||
private fun handleTag(secrets: Secrets, handlerFunc: (TagCommunicator)->String) {
|
||||
if (!nfcAdapter.isEnabled) {
|
||||
showWirelessSettings()
|
||||
} else {
|
||||
val options = Bundle()
|
||||
// Work around for some broken Nfc firmware implementations that poll the card too fast
|
||||
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
|
||||
nfcAdapter.enableReaderMode(context, buildOnReadTag(secrets, handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||
private fun buildOnReadTag(secrets: Secrets, handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
|
||||
return { tag->
|
||||
val nfcData = NfcA.get(tag)
|
||||
if (nfcData == null) {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
nfcData.connect()
|
||||
nfcData.use {
|
||||
val tagCommunicator = TagCommunicator.getInstance(nfcData, secrets.getCryptographicTransformerMap())
|
||||
val successText = handlerFunc(tagCommunicator)
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, successText, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkSecrets() {
|
||||
context.lifecycleScope.launch(Dispatchers.IO) {
|
||||
if(secretsFlow.stateIn(context.lifecycleScope).value.isMissingSecrets()) {
|
||||
context.runOnUiThread {
|
||||
Toast.makeText(context, "Missing Secrets. Go to settings and import Vital Arena APK", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
||||
private fun showWirelessSettings() {
|
||||
Toast.makeText(context, "NFC must be enabled", Toast.LENGTH_SHORT).show()
|
||||
context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
|
||||
}
|
||||
}
|
||||
@ -43,4 +43,17 @@ fun Secrets.getCryptographicTransformerMap(): Map<UShort, CryptographicTransform
|
||||
Pair(DeviceType.VitalCharactersDeviceType, CryptographicTransformer(vbcHmacKeys.hmacKey1, vbcHmacKeys.hmacKey2, this.aesKey, cipher)),
|
||||
Pair(DeviceType.VitalBraceletBEDeviceType, CryptographicTransformer(beHmacKeys.hmacKey1, beHmacKeys.hmacKey2, this.aesKey, beCipher)),
|
||||
)
|
||||
}
|
||||
|
||||
fun Secrets.isMissingSecrets(): Boolean {
|
||||
return this.aesKey.length != 24 ||
|
||||
this.vbCipherList.size != 16 ||
|
||||
this.beCipherList.size != 16 ||
|
||||
this.vbdmHmacKeys.isMissingKey() ||
|
||||
this.vbcHmacKeys.isMissingKey() ||
|
||||
this.beHmacKeys.isMissingKey()
|
||||
}
|
||||
|
||||
fun HmacKeys.isMissingKey(): Boolean {
|
||||
return this.hmacKey1.length != 24 || this.hmacKey2.length != 24
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user