feat: add demo mode for testing and review purposes

- Activates by tapping "None (disable)" 7 times.
- Displays a confirmation message when `Demo Mode` is enabled.
- Simulates a connection to a Meshtastic device and allows the app to function without requiring real hardware.
pull/1279/head^2
andrekir 2024-10-02 19:58:11 -03:00
rodzic 16e91c0ebf
commit ffb402acde
4 zmienionych plików z 36 dodań i 17 usunięć

Wyświetl plik

@ -24,6 +24,7 @@ import com.geeksville.mesh.util.anonymize
import com.hoho.android.usbserial.driver.UsbSerialDriver
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
@ -45,24 +46,27 @@ class BTScanModel @Inject constructor(
val devices = MutableLiveData<MutableMap<String, DeviceListEntry>>(mutableMapOf())
val errorText = MutableLiveData<String?>(null)
val isMockInterfaceAddressValid: Boolean by lazy {
radioInterfaceService.isAddressValid(radioInterfaceService.mockInterfaceAddress)
private val showMockInterface = MutableStateFlow(radioInterfaceService.isMockInterface)
fun showMockInterface() {
showMockInterface.value = true
}
init {
combine(
bluetoothRepository.state,
networkRepository.resolvedList,
usbRepository.serialDevicesWithDrivers
) { ble, tcp, usb ->
usbRepository.serialDevicesWithDrivers,
showMockInterface,
) { ble, tcp, usb, showMockInterface ->
devices.value = mutableMapOf<String, DeviceListEntry>().apply {
fun addDevice(entry: DeviceListEntry) { this[entry.fullAddress] = entry }
// Include a placeholder for "None"
addDevice(DeviceListEntry(context.getString(R.string.none), "n", true))
if (isMockInterfaceAddressValid) {
addDevice(DeviceListEntry("Included simulator", "m", true))
if (showMockInterface) {
addDevice(DeviceListEntry("Demo Mode", "m", true))
}
// Include paired Bluetooth devices

Wyświetl plik

@ -1,15 +1,11 @@
package com.geeksville.mesh.repository.radio
import android.app.Application
import com.geeksville.mesh.android.BuildUtils
import com.geeksville.mesh.android.GeeksvilleApplication
import javax.inject.Inject
/**
* Mock interface backend implementation.
*/
class MockInterfaceSpec @Inject constructor(
private val application: Application,
private val factory: MockInterfaceFactory
): InterfaceSpec<MockInterface> {
override fun createInterface(rest: String): MockInterface {
@ -17,6 +13,5 @@ class MockInterfaceSpec @Inject constructor(
}
/** Return true if this address is still acceptable. For BLE that means, still bonded */
override fun addressValid(rest: String): Boolean =
BuildUtils.isEmulator || ((application as GeeksvilleApplication).isInTestLab)
}
override fun addressValid(rest: String): Boolean = true
}

Wyświetl plik

@ -7,6 +7,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.android.BinaryLogFile
import com.geeksville.mesh.android.BuildUtils
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.concurrent.handledLaunch
@ -104,8 +105,8 @@ class RadioInterfaceService @Inject constructor(
return interfaceFactory.toInterfaceAddress(interfaceId, rest)
}
fun isAddressValid(address: String?): Boolean {
return interfaceFactory.addressValid(address)
val isMockInterface: Boolean by lazy {
BuildUtils.isEmulator || (context as GeeksvilleApplication).isInTestLab
}
/** Return the device we are configured to use, or null for none
@ -121,7 +122,7 @@ class RadioInterfaceService @Inject constructor(
var address = prefs.getString(DEVADDR_KEY, null)
// If we are running on the emulator we default to the mock interface, so we can have some data to show to the user
if (address == null && isAddressValid(mockInterfaceAddress)) {
if (address == null && isMockInterface) {
address = mockInterfaceAddress
}

Wyświetl plik

@ -290,6 +290,9 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
.show()
}
private var tapCount = 0
private var lastTapTime: Long = 0
private fun addDeviceButton(device: BTScanModel.DeviceListEntry, enabled: Boolean) {
val b = RadioButton(requireActivity())
b.text = device.name
@ -299,6 +302,19 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
binding.deviceRadioGroup.addView(b)
b.setOnClickListener {
if (device.fullAddress == "n") {
val currentTapTime = System.currentTimeMillis()
if (currentTapTime - lastTapTime > TAP_THRESHOLD) {
tapCount = 0
}
lastTapTime = currentTapTime
tapCount++
if (tapCount >= TAP_TRIGGER) {
model.showSnackbar("Demo Mode enabled")
scanModel.showMockInterface()
}
}
if (!device.bonded) // If user just clicked on us, try to bond
binding.scanStatusText.setText(R.string.starting_pairing)
b.isChecked = scanModel.onSelected(device)
@ -354,7 +370,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// If we are running on an emulator, always leave this message showing so we can test the worst case layout
val curRadio = scanModel.selectedAddress
if (curRadio != null && !scanModel.isMockInterfaceAddressValid) {
if (curRadio != null && curRadio != "m") {
binding.warningNotPaired.visibility = View.GONE
} else if (bluetoothViewModel.enabled.value == true) {
binding.warningNotPaired.visibility = View.VISIBLE
@ -449,6 +465,9 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
companion object {
const val SCAN_PERIOD: Long = 10000 // Stops scanning after 10 seconds
private const val TAP_TRIGGER: Int = 7
private const val TAP_THRESHOLD: Long = 500 // max 500 ms between taps
}
private fun Editable.isIPAddress(): Boolean {