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
|
package com.github.nacabaro.vbhelper
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.nfc.NfcAdapter
|
|
||||||
import android.nfc.Tag
|
|
||||||
import android.nfc.tech.NfcA
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@ -13,17 +10,10 @@ import androidx.activity.enableEdgeToEdge
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.runtime.Composable
|
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 androidx.lifecycle.lifecycleScope
|
||||||
import com.github.cfogrady.vb.dim.card.DimReader
|
import com.github.cfogrady.vb.dim.card.DimReader
|
||||||
import com.github.nacabaro.vbhelper.navigation.AppNavigation
|
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.be.BENfcCharacter
|
||||||
import com.github.cfogrady.vbnfc.data.DeviceType
|
|
||||||
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
import com.github.cfogrady.vbnfc.data.NfcCharacter
|
||||||
import com.github.nacabaro.vbhelper.di.VBHelper
|
import com.github.nacabaro.vbhelper.di.VBHelper
|
||||||
import com.github.nacabaro.vbhelper.domain.Dim
|
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.BECharacterData
|
||||||
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
|
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
|
||||||
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
|
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 com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private lateinit var nfcAdapter: NfcAdapter
|
|
||||||
private lateinit var deviceToCryptographicTransformers: Map<UShort, CryptographicTransformer>
|
|
||||||
|
|
||||||
private var nfcCharacter = MutableStateFlow<NfcCharacter?>(null)
|
private var nfcCharacter = MutableStateFlow<NfcCharacter?>(null)
|
||||||
|
|
||||||
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
|
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
private val onActivityLifecycleListeners = HashMap<String, ActivityLifecycleListener>()
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
deviceToCryptographicTransformers = getMapOfCryptographicTransformers()
|
|
||||||
|
|
||||||
|
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()
|
registerFileActivityResult()
|
||||||
|
|
||||||
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
val application = applicationContext as VBHelper
|
||||||
if (maybeNfcAdapter == null) {
|
val settingsScreenController = SettingsScreenController.Factory(this, ApkSecretsImporter(), application.container.dataStoreSecretsRepository)
|
||||||
Toast.makeText(this, "No NFC on device!", Toast.LENGTH_SHORT).show()
|
.buildSettingScreenHandlers()
|
||||||
finish()
|
val scanScreenController = ScanScreenControllerImpl(
|
||||||
return
|
application.container.dataStoreSecretsRepository.secretsFlow,
|
||||||
}
|
this::handleReceivedNfcCharacter,
|
||||||
nfcAdapter = maybeNfcAdapter
|
this,
|
||||||
|
this::registerActivityLifecycleListener,
|
||||||
|
this::unregisterActivityLifecycleListener)
|
||||||
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
VBHelperTheme {
|
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() {
|
private fun registerFileActivityResult() {
|
||||||
@ -154,26 +176,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainApplication() {
|
private fun MainApplication(settingsScreenController: SettingsScreenController, scanScreenController: ScanScreenControllerImpl) {
|
||||||
var isDoneReadingCharacter by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
AppNavigation(
|
AppNavigation(
|
||||||
isDoneReadingCharacter = isDoneReadingCharacter,
|
applicationNavigationHandlers = AppNavigationHandlers(settingsScreenController, scanScreenController),
|
||||||
onClickRead = {
|
|
||||||
handleTag {
|
|
||||||
val character = it.receiveCharacter()
|
|
||||||
nfcCharacter.value = character
|
|
||||||
|
|
||||||
val importStatus = addCharacterScannedIntoDatabase()
|
|
||||||
|
|
||||||
isDoneReadingCharacter = true
|
|
||||||
|
|
||||||
importStatus
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClickScan = {
|
|
||||||
isDoneReadingCharacter = false
|
|
||||||
},
|
|
||||||
onClickImportCard = {
|
onClickImportCard = {
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
@ -184,73 +190,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
private fun handleReceivedNfcCharacter(character: NfcCharacter): String {
|
||||||
private fun getMapOfCryptographicTransformers(): Map<UShort, CryptographicTransformer> {
|
nfcCharacter.value = character
|
||||||
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)))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
val importStatus = addCharacterScannedIntoDatabase()
|
||||||
private fun showWirelessSettings() {
|
|
||||||
Toast.makeText(this, "NFC must be enabled", Toast.LENGTH_SHORT).show()
|
|
||||||
startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXTRACTED DIRECTLY FROM EXAMPLE APP
|
return importStatus
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@ -12,17 +12,19 @@ import com.github.nacabaro.vbhelper.screens.BattlesScreen
|
|||||||
import com.github.nacabaro.vbhelper.screens.DexScreen
|
import com.github.nacabaro.vbhelper.screens.DexScreen
|
||||||
import com.github.nacabaro.vbhelper.screens.DiMScreen
|
import com.github.nacabaro.vbhelper.screens.DiMScreen
|
||||||
import com.github.nacabaro.vbhelper.screens.HomeScreen
|
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.SettingsScreen
|
||||||
|
import com.github.nacabaro.vbhelper.screens.SettingsScreenController
|
||||||
import com.github.nacabaro.vbhelper.screens.SpriteViewer
|
import com.github.nacabaro.vbhelper.screens.SpriteViewer
|
||||||
import com.github.nacabaro.vbhelper.screens.StorageScreen
|
import com.github.nacabaro.vbhelper.screens.StorageScreen
|
||||||
|
|
||||||
|
data class AppNavigationHandlers(val settingsScreenController: SettingsScreenController, val scanScreenController: ScanScreenControllerImpl)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavigation(
|
fun AppNavigation(
|
||||||
onClickRead: () -> Unit,
|
applicationNavigationHandlers: AppNavigationHandlers,
|
||||||
onClickScan: () -> Unit,
|
|
||||||
onClickImportCard: () -> Unit,
|
onClickImportCard: () -> Unit,
|
||||||
isDoneReadingCharacter: Boolean
|
|
||||||
) {
|
) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
@ -49,11 +51,9 @@ fun AppNavigation(
|
|||||||
StorageScreen()
|
StorageScreen()
|
||||||
}
|
}
|
||||||
composable(BottomNavItem.Scan.route) {
|
composable(BottomNavItem.Scan.route) {
|
||||||
onClickScan()
|
|
||||||
ScanScreen(
|
ScanScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onClickRead = onClickRead,
|
scanScreenController = applicationNavigationHandlers.scanScreenController,
|
||||||
isDoneReadingCharacter = isDoneReadingCharacter
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(BottomNavItem.Dex.route) {
|
composable(BottomNavItem.Dex.route) {
|
||||||
@ -64,6 +64,7 @@ fun AppNavigation(
|
|||||||
composable(BottomNavItem.Settings.route) {
|
composable(BottomNavItem.Settings.route) {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
|
settingsScreenController = applicationNavigationHandlers.settingsScreenController,
|
||||||
onClickImportCard = onClickImportCard
|
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
|
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.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -22,6 +26,7 @@ import com.github.nacabaro.vbhelper.components.TopBanner
|
|||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
|
settingsScreenController: SettingsScreenController,
|
||||||
onClickImportCard: () -> Unit
|
onClickImportCard: () -> Unit
|
||||||
) {
|
) {
|
||||||
Scaffold (
|
Scaffold (
|
||||||
@ -42,12 +47,10 @@ fun SettingsScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
SettingsSection("General")
|
SettingsSection("NFC Communication")
|
||||||
SettingsEntry(title = "Import VB key", description = "Import standard vital bracelet keys") { }
|
SettingsEntry(title = "Import APK", description = "Import Secrets From Vital Arean 2.1.0 APK") {
|
||||||
SettingsEntry(title = "Import VB Characters key", description = "Import standard vital bracelet keys") { }
|
settingsScreenController.apkFilePickLauncher.launch(arrayOf("*/*"))
|
||||||
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("DiM/BEm management")
|
SettingsSection("DiM/BEm management")
|
||||||
SettingsEntry(title = "Import DiM card", description = "Import DiM/BEm card file", onClick = onClickImportCard)
|
SettingsEntry(title = "Import DiM card", description = "Import DiM/BEm card file", onClick = onClickImportCard)
|
||||||
SettingsEntry(title = "Rename DiM/BEm", description = "Set card name") { }
|
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
|
@Composable
|
||||||
fun SettingsEntry(
|
fun SettingsEntry(
|
||||||
title: String,
|
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.VitalCharactersDeviceType, CryptographicTransformer(vbcHmacKeys.hmacKey1, vbcHmacKeys.hmacKey2, this.aesKey, cipher)),
|
||||||
Pair(DeviceType.VitalBraceletBEDeviceType, CryptographicTransformer(beHmacKeys.hmacKey1, beHmacKeys.hmacKey2, this.aesKey, beCipher)),
|
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