From 2eb3f70e0c2230908ef5c8f011a3bf0e45be6db7 Mon Sep 17 00:00:00 2001 From: darken Date: Mon, 29 Aug 2022 14:07:07 +0200 Subject: [PATCH] Wear UI wip --- app-wear/build.gradle.kts | 36 ++-- app-wear/src/main/AndroidManifest.xml | 22 ++- app-wear/src/main/java/eu/darken/capod/App.kt | 42 +++++ .../eu/darken/capod/wear/ui/MainActivity.kt | 24 ++- .../eu/darken/capod/wear/ui/MainActivityVM.kt | 34 +++- .../capod/wear/ui/overview/OverviewAdapter.kt | 47 +++++ .../wear/ui/overview/OverviewFragment.kt | 166 +++++++++++++++++ .../wear/ui/overview/OverviewFragmentVM.kt | 173 ++++++++++++++++++ .../ui/overview/cards/BluetoothDisabledVH.kt | 33 ++++ .../ui/overview/cards/MissingMainDeviceVH.kt | 33 ++++ .../ui/overview/cards/PermissionCardVH.kt | 48 +++++ .../ui/overview/cards/pods/DualPodsCardVH.kt | 166 +++++++++++++++++ .../ui/overview/cards/pods/PodDeviceVH.kt | 42 +++++ .../overview/cards/pods/SinglePodsCardVH.kt | 81 ++++++++ .../cards/pods/UnknownPodDeviceCardVH.kt | 48 +++++ .../src/main/res/layout/activity_main.xml | 24 --- .../src/main/res/layout/main_activity.xml | 22 +++ .../src/main/res/navigation/nav_graph.xml | 24 +++ app-wear/src/main/res/values-night/themes.xml | 32 ++++ app-wear/src/main/res/values/colors.xml | 63 +++++++ app-wear/src/main/res/values/themes.xml | 48 +++++ 21 files changed, 1156 insertions(+), 52 deletions(-) create mode 100644 app-wear/src/main/java/eu/darken/capod/App.kt create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewAdapter.kt create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragment.kt create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragmentVM.kt create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/BluetoothDisabledVH.kt create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/MissingMainDeviceVH.kt create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/PermissionCardVH.kt create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/DualPodsCardVH.kt create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/PodDeviceVH.kt create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/SinglePodsCardVH.kt create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt delete mode 100644 app-wear/src/main/res/layout/activity_main.xml create mode 100644 app-wear/src/main/res/layout/main_activity.xml create mode 100644 app-wear/src/main/res/navigation/nav_graph.xml create mode 100644 app-wear/src/main/res/values-night/themes.xml create mode 100644 app-wear/src/main/res/values/colors.xml create mode 100644 app-wear/src/main/res/values/themes.xml diff --git a/app-wear/build.gradle.kts b/app-wear/build.gradle.kts index 2e09b70c..5d6df4d8 100644 --- a/app-wear/build.gradle.kts +++ b/app-wear/build.gradle.kts @@ -111,11 +111,11 @@ android { kotlinOptions { jvmTarget = "1.8" freeCompilerArgs = freeCompilerArgs + listOf( - "-Xopt-in=kotlin.ExperimentalStdlibApi", - "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", - "-Xopt-in=kotlinx.coroutines.FlowPreview", - "-Xopt-in=kotlin.time.ExperimentalTime", - "-Xopt-in=kotlin.RequiresOptIn" + "-opt-in=kotlin.ExperimentalStdlibApi", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlinx.coroutines.FlowPreview", + "-opt-in=kotlin.time.ExperimentalTime", + "-opt-in=kotlin.RequiresOptIn" ) } @@ -140,30 +140,28 @@ android { } dependencies { - implementation(project(":app-common")) - - implementation("androidx.wear:wear:1.2.0") - implementation("androidx.wear.tiles:tiles-material:1.1.0") - implementation("com.google.android.horologist:horologist-tiles:0.1.5") - implementation("com.google.android.gms:play-services-wearable:17.1.0") - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") + implementation(project(":app-common")) + addBaseKotlin() addDagger() - // Debugging - implementation("com.bugsnag:bugsnag-android:5.9.2") - implementation("com.getkeepsafe.relinker:relinker:1.4.3") - - implementation("androidx.core:core-splashscreen:1.0.0-alpha02") - addBaseWorkManager() addBaseAndroid() addBaseAndroidUi() - implementation("com.google.android.material:material:1.5.0-rc01") + + addNavigation() addTesting() + + implementation("com.bugsnag:bugsnag-android:5.9.2") + implementation("com.getkeepsafe.relinker:relinker:1.4.3") + + implementation("androidx.wear:wear:1.2.0") + implementation("androidx.wear.tiles:tiles-material:1.1.0") + implementation("com.google.android.horologist:horologist-tiles:0.1.5") + implementation("com.google.android.gms:play-services-wearable:17.1.0") } \ No newline at end of file diff --git a/app-wear/src/main/AndroidManifest.xml b/app-wear/src/main/AndroidManifest.xml index bc67a4b9..67ecc46f 100644 --- a/app-wear/src/main/AndroidManifest.xml +++ b/app-wear/src/main/AndroidManifest.xml @@ -3,6 +3,26 @@ package="eu.darken.capod"> + + + + + + + + + + @@ -11,6 +31,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" + android:name=".App" android:theme="@android:style/Theme.DeviceDefault"> - log(TAG) { "ReLinker: $message" } } + .loadLibrary(this, "bugsnag-plugin-android-anr") + + autoReporting.setup() + + log(TAG) { "onCreate() done! ${Exception().asLog()}" } + } + + override fun getWorkManagerConfiguration(): Configuration = Configuration.Builder() + .setMinimumLoggingLevel(android.util.Log.VERBOSE) + .setWorkerFactory(workerFactory) + .build() + + companion object { + internal val TAG = logTag("CAP") + } +} diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/MainActivity.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/MainActivity.kt index 24a9759f..1a0ce1ba 100644 --- a/app-wear/src/main/java/eu/darken/capod/wear/ui/MainActivity.kt +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/MainActivity.kt @@ -1,17 +1,27 @@ package eu.darken.capod.wear.ui -import android.app.Activity import android.os.Bundle -import eu.darken.capod.databinding.ActivityMainBinding +import androidx.activity.viewModels +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import dagger.hilt.android.AndroidEntryPoint +import eu.darken.capod.R +import eu.darken.capod.common.navigation.findNavController +import eu.darken.capod.common.uix.Activity2 +import eu.darken.capod.databinding.MainActivityBinding -class MainActivity : Activity() { +@AndroidEntryPoint +class MainActivity : Activity2() { - private lateinit var binding: ActivityMainBinding + private val vm: MainActivityVM by viewModels() + private lateinit var ui: MainActivityBinding + private val navController by lazy { supportFragmentManager.findNavController(R.id.nav_host) } override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) + ui = MainActivityBinding.inflate(layoutInflater) + setContentView(ui.root) } -} \ No newline at end of file +} diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/MainActivityVM.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/MainActivityVM.kt index 4ae95b6a..6bfb929d 100644 --- a/app-wear/src/main/java/eu/darken/capod/wear/ui/MainActivityVM.kt +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/MainActivityVM.kt @@ -3,7 +3,12 @@ package eu.darken.capod.wear.ui import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.permissions.Permission import eu.darken.capod.common.uix.ViewModel2 +import eu.darken.capod.main.core.PermissionTool +import eu.darken.capod.monitor.core.PodMonitor +import eu.darken.capod.pods.core.PodDevice +import kotlinx.coroutines.flow.combine import javax.inject.Inject @@ -11,4 +16,31 @@ import javax.inject.Inject class MainActivityVM @Inject constructor( handle: SavedStateHandle, dispatcherProvider: DispatcherProvider, -) : ViewModel2(dispatcherProvider = dispatcherProvider) \ No newline at end of file + private val podMonitor: PodMonitor, + private val permissionTool: PermissionTool, +) : ViewModel2(dispatcherProvider = dispatcherProvider) { + + sealed class State { + data class PermissionRequired( + val permissions: List, + ) : State() + + data class Devices( + val devices: List + ) : State() + } + + val state = combine( + podMonitor.devices, + permissionTool.missingPermissions, + ) { devices, missingPermissions -> + when { + missingPermissions.isNotEmpty() -> State.PermissionRequired( + permissions = missingPermissions.toList() + ) + else -> State.Devices( + devices = devices + ) + } + } +} \ No newline at end of file diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewAdapter.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewAdapter.kt new file mode 100644 index 00000000..4928f1d3 --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewAdapter.kt @@ -0,0 +1,47 @@ +package eu.darken.capod.wear.ui.overview + +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.viewbinding.ViewBinding +import eu.darken.capod.common.lists.BindableVH +import eu.darken.capod.common.lists.differ.AsyncDiffer +import eu.darken.capod.common.lists.differ.DifferItem +import eu.darken.capod.common.lists.differ.HasAsyncDiffer +import eu.darken.capod.common.lists.differ.setupDiffer +import eu.darken.capod.common.lists.modular.ModularAdapter +import eu.darken.capod.common.lists.modular.mods.DataBinderMod +import eu.darken.capod.common.lists.modular.mods.TypedVHCreatorMod +import eu.darken.capod.wear.ui.overview.cards.BluetoothDisabledVH +import eu.darken.capod.wear.ui.overview.cards.MissingMainDeviceVH +import eu.darken.capod.wear.ui.overview.cards.PermissionCardVH +import eu.darken.capod.wear.ui.overview.cards.pods.DualPodsCardVH +import eu.darken.capod.wear.ui.overview.cards.pods.SinglePodsCardVH +import eu.darken.capod.wear.ui.overview.cards.pods.UnknownPodDeviceCardVH +import javax.inject.Inject + +class OverviewAdapter @Inject constructor() : + ModularAdapter>(), + HasAsyncDiffer { + + override val asyncDiffer: AsyncDiffer<*, Item> = setupDiffer() + + init { + modules.add(DataBinderMod(data)) + modules.add(TypedVHCreatorMod({ data[it] is PermissionCardVH.Item }) { PermissionCardVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is DualPodsCardVH.Item }) { DualPodsCardVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is SinglePodsCardVH.Item }) { SinglePodsCardVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is MissingMainDeviceVH.Item }) { MissingMainDeviceVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is BluetoothDisabledVH.Item }) { BluetoothDisabledVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(it) }) + } + + override fun getItemCount(): Int = data.size + + abstract class BaseVH( + @LayoutRes layoutId: Int, + parent: ViewGroup + ) : ModularAdapter.VH(layoutId, parent), BindableVH + + interface Item : DifferItem + +} \ No newline at end of file diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragment.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragment.kt new file mode 100644 index 00000000..f58c7a38 --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragment.kt @@ -0,0 +1,166 @@ +package eu.darken.capod.wear.ui.overview + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.text.SpannableStringBuilder +import android.view.View +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import eu.darken.capod.BuildConfig +import eu.darken.capod.R +import eu.darken.capod.common.colorString +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.lists.differ.update +import eu.darken.capod.common.lists.setupDefaults +import eu.darken.capod.common.permissions.Permission +import eu.darken.capod.common.uix.Fragment3 +import eu.darken.capod.common.upgrade.UpgradeRepo +import eu.darken.capod.common.viewbinding.viewBinding +import eu.darken.capod.databinding.MainFragmentBinding +import javax.inject.Inject + + +@AndroidEntryPoint +class OverviewFragment : Fragment3(R.layout.main_fragment) { + + override val vm: OverviewFragmentVM by viewModels() + override val ui: MainFragmentBinding by viewBinding() + + @Inject + lateinit var adapter: OverviewAdapter + + lateinit var permissionLauncher: ActivityResultLauncher + var awaitingPermission = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + awaitingPermission = savedInstanceState?.getBoolean("awaitingPermission") ?: false + + permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + log { "Request for $id was granted=$granted" } + vm.onPermissionResult(granted) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + ui.apply { + list.setupDefaults(adapter, dividers = false) + } + + ui.toolbar.apply { +// setOnMenuItemClickListener { +// when (it.itemId) { +// R.id.menu_item_settings -> { +// vm.goToSettings() +// true +// } +// R.id.menu_item_donate -> { +// vm.onUpgrade() +// true +// } +// R.id.menu_item_upgrade -> { +// vm.onUpgrade() +// true +// } +// else -> false +// } +// } + } + + vm.listItems.observe2(ui) { + if (BuildConfig.DEBUG) toolbar.subtitle = "${it.size} items" + adapter.update(it) + } + + vm.workerAutolaunch.observe2 { + // While UI is active, subscribe to the autolaunch routine + } + + vm.requestPermissionEvent.observe2(ui) { + when (it) { + Permission.IGNORE_BATTERY_OPTIMIZATION -> { + awaitingPermission = true + startActivity( + Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:${requireContext().packageName}") + ) + ) + } + Permission.SYSTEM_ALERT_WINDOW -> { + awaitingPermission = true + startActivity( + Intent( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:${requireContext().packageName}") + ) + ) + } + else -> { + permissionLauncher.launch(it.permissionId) + } + } + } + + vm.upgradeState.observe2(ui) { info -> +// val gplay = toolbar.menu.findItem(R.id.menu_item_upgrade) +// val donate = toolbar.menu.findItem(R.id.menu_item_donate) +// gplay.isVisible = false +// donate.isVisible = false +// +// val baseTitle = when (info.type) { +// UpgradeRepo.Type.GPLAY -> { +// if (info.isPro) { +// getString(R.string.app_name_pro) +// } else { +// gplay.isVisible = true +// getString(R.string.app_name) +// } +// } +// UpgradeRepo.Type.FOSS -> { +// if (info.isPro) { +// getString(R.string.app_name_foss) +// } else { +// donate.isVisible = true +// getString(R.string.app_name) +// } +// } +// }.split(" ".toRegex()) +// .dropLastWhile { it.isEmpty() } +// .toTypedArray() +// +// toolbar.title = if (baseTitle.size == 2) { +// val builder = SpannableStringBuilder(baseTitle[0] + " ") +// val color = when (info.type) { +// UpgradeRepo.Type.FOSS -> R.color.brand_secondary +// else -> R.color.brand_tertiary +// } +// builder.append(colorString(requireContext(), color, baseTitle[1])) +// } else { +// getString(R.string.app_name) +// } + } + vm.launchUpgradeFlow.observe2 { + it(requireActivity()) + } + super.onViewCreated(view, savedInstanceState) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean("awaitingPermission", awaitingPermission) + super.onSaveInstanceState(outState) + } + + override fun onResume() { + super.onResume() + if (awaitingPermission) { + awaitingPermission = false + log { "awaitingPermission=true" } + vm.onPermissionResult(true) + } + } +} diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragmentVM.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragmentVM.kt new file mode 100644 index 00000000..5918bf1c --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragmentVM.kt @@ -0,0 +1,173 @@ +package eu.darken.capod.wear.ui.overview + +import android.app.Activity +import androidx.lifecycle.LiveData +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import eu.darken.capod.common.bluetooth.BluetoothManager2 +import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.debug.autoreport.DebugSettings +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.flow.combine +import eu.darken.capod.common.livedata.SingleLiveEvent +import eu.darken.capod.common.navigation.navVia +import eu.darken.capod.common.permissions.Permission +import eu.darken.capod.common.uix.ViewModel3 +import eu.darken.capod.common.upgrade.UpgradeRepo +import eu.darken.capod.main.core.GeneralSettings +import eu.darken.capod.main.core.MonitorMode +import eu.darken.capod.main.core.PermissionTool +import eu.darken.capod.monitor.core.PodMonitor +import eu.darken.capod.pods.core.DualPodDevice +import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.SinglePodDevice +import eu.darken.capod.wear.ui.overview.cards.BluetoothDisabledVH +import eu.darken.capod.wear.ui.overview.cards.MissingMainDeviceVH +import eu.darken.capod.wear.ui.overview.cards.PermissionCardVH +import eu.darken.capod.wear.ui.overview.cards.pods.DualPodsCardVH +import eu.darken.capod.wear.ui.overview.cards.pods.SinglePodsCardVH +import eu.darken.capod.wear.ui.overview.cards.pods.UnknownPodDeviceCardVH +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.isActive +import java.time.Instant +import javax.inject.Inject + +@HiltViewModel +class OverviewFragmentVM @Inject constructor( + @Suppress("UNUSED_PARAMETER") handle: SavedStateHandle, + dispatcherProvider: DispatcherProvider, +// private val monitorControl: MonitorControl, + private val podMonitor: PodMonitor, + private val permissionTool: PermissionTool, + private val generalSettings: GeneralSettings, + debugSettings: DebugSettings, + private val upgradeRepo: UpgradeRepo, + private val bluetoothManager: BluetoothManager2, +) : ViewModel3(dispatcherProvider = dispatcherProvider) { + + val upgradeState = upgradeRepo.upgradeInfo.asLiveData2() + val launchUpgradeFlow = SingleLiveEvent<(Activity) -> Unit>() + + private val updateTicker = channelFlow { + while (isActive) { + trySend(Unit) + delay(3000) + } + } + + val workerAutolaunch: LiveData = permissionTool.missingPermissions + .onEach { + if (it.isNotEmpty()) { + log(TAG) { "Missing permissions: $it" } + return@onEach + } + + val shouldStartMonitor = when (generalSettings.monitorMode.value) { + MonitorMode.MANUAL -> false + MonitorMode.AUTOMATIC -> bluetoothManager.connectedDevices().first().isNotEmpty() + MonitorMode.ALWAYS -> true + } + if (shouldStartMonitor) { + log(TAG) { "Starting monitor" } +// monitorControl.startMonitor() + } + } + .map { } + .asLiveData2() + + val requestPermissionEvent = SingleLiveEvent() + + private val pods: Flow> = permissionTool.missingPermissions + .flatMapLatest { permissions -> + if (permissions.isNotEmpty()) { + return@flatMapLatest flowOf(emptyList()) + } + + generalSettings.showAll.flow.flatMapLatest { showAll -> + if (showAll) { + podMonitor.devices + } else { + podMonitor.mainDevice.map { mainDevice -> + mainDevice?.let { listOf(it) } ?: emptyList() + } + } + } + } + .catch { errorEvents.postValue(it) } + + val listItems: LiveData> = combine( + updateTicker, + permissionTool.missingPermissions, + pods, + debugSettings.isDebugModeEnabled.flow, + generalSettings.showAll.flow, + bluetoothManager.isBluetoothEnabled, + podMonitor.mainDevice, + ) { _, permissions, pods, isDebugMode, showAll, isBluetoothEnabled, mainPod -> + val items = mutableListOf() + + permissions + .map { perm -> + PermissionCardVH.Item( + permission = perm, + onRequest = { requestPermissionEvent.postValue(it) }, + ) + } + .run { items.addAll(this) } + + if (permissions.isEmpty()) { + if (!isBluetoothEnabled) { + items.add(0, BluetoothDisabledVH.Item) + } else if (mainPod == null) { + items.add(0, MissingMainDeviceVH.Item) + } + } + + if (permissions.isEmpty() && isBluetoothEnabled) { + pods.map { + val now = Instant.now() + when (it) { + is DualPodDevice -> DualPodsCardVH.Item( + now = now, + device = it, + showDebug = isDebugMode, + isMainPod = it == mainPod, + ) + is SinglePodDevice -> SinglePodsCardVH.Item( + now = now, + device = it, + showDebug = isDebugMode, + isMainPod = it == mainPod, + ) + else -> UnknownPodDeviceCardVH.Item( + now = now, + device = it, + showDebug = isDebugMode, + isMainPod = it == mainPod, + ) + } + }.run { items.addAll(this) } + } + + items + } + .catch { errorEvents.postValue(it) } + .asLiveData2() + + fun onPermissionResult(granted: Boolean) { + if (granted) permissionTool.recheck() + } + + fun goToSettings() = launch { + OverviewFragmentDirections.actionOverviewFragmentToSettingsFragment().navVia(this@OverviewFragmentVM) + } + + fun onUpgrade() = launch { + val call: (Activity) -> Unit = { + upgradeRepo.launchBillingFlow(it) + } + launchUpgradeFlow.postValue(call) + } + +} \ No newline at end of file diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/BluetoothDisabledVH.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/BluetoothDisabledVH.kt new file mode 100644 index 00000000..41b77519 --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/BluetoothDisabledVH.kt @@ -0,0 +1,33 @@ +package eu.darken.capod.wear.ui.overview.cards + +import android.view.ViewGroup +import eu.darken.capod.R +import eu.darken.capod.common.lists.binding +import eu.darken.capod.common.lists.differ.DifferItem +import eu.darken.capod.databinding.OverviewBluetoothDisabledItemBinding +import eu.darken.capod.wear.ui.overview.OverviewAdapter + +class BluetoothDisabledVH(parent: ViewGroup) : + OverviewAdapter.BaseVH( + R.layout.overview_bluetooth_disabled_item, + parent + ) { + + override val viewBinding = lazy { + OverviewBluetoothDisabledItemBinding.bind(itemView) + } + + override val onBindData: OverviewBluetoothDisabledItemBinding.( + item: Item, + payloads: List + ) -> Unit = binding(payload = true) { item -> + + } + + object Item : OverviewAdapter.Item { + override val stableId: Long = Item::class.hashCode().toLong() + + override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?) + get() = { old, new -> if (new::class.isInstance(old)) new else null } + } +} \ No newline at end of file diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/MissingMainDeviceVH.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/MissingMainDeviceVH.kt new file mode 100644 index 00000000..71dddf65 --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/MissingMainDeviceVH.kt @@ -0,0 +1,33 @@ +package eu.darken.capod.wear.ui.overview.cards + +import android.view.ViewGroup +import eu.darken.capod.R +import eu.darken.capod.common.lists.binding +import eu.darken.capod.common.lists.differ.DifferItem +import eu.darken.capod.databinding.OverviewNomaindeviceItemBinding +import eu.darken.capod.wear.ui.overview.OverviewAdapter + +class MissingMainDeviceVH(parent: ViewGroup) : + OverviewAdapter.BaseVH( + R.layout.overview_nomaindevice_item, + parent + ) { + + override val viewBinding = lazy { + OverviewNomaindeviceItemBinding.bind(itemView) + } + + override val onBindData: OverviewNomaindeviceItemBinding.( + item: Item, + payloads: List + ) -> Unit = binding(payload = true) { item -> + + } + + object Item : OverviewAdapter.Item { + override val stableId: Long = Item::class.hashCode().toLong() + + override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?) + get() = { old, new -> if (new::class.isInstance(old)) new else null } + } +} \ No newline at end of file diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/PermissionCardVH.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/PermissionCardVH.kt new file mode 100644 index 00000000..be2345c6 --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/PermissionCardVH.kt @@ -0,0 +1,48 @@ +package eu.darken.capod.wear.ui.overview.cards + +import android.text.Html +import android.text.method.LinkMovementMethod +import android.view.ViewGroup +import eu.darken.capod.R +import eu.darken.capod.common.PrivacyPolicy +import eu.darken.capod.common.lists.binding +import eu.darken.capod.common.lists.differ.DifferItem +import eu.darken.capod.common.permissions.Permission +import eu.darken.capod.databinding.OverviewPermissionItemBinding +import eu.darken.capod.wear.ui.overview.OverviewAdapter + +class PermissionCardVH(parent: ViewGroup) : + OverviewAdapter.BaseVH( + R.layout.overview_permission_item, + parent + ) { + + override val viewBinding = lazy { + OverviewPermissionItemBinding.bind(itemView) + } + + override val onBindData: OverviewPermissionItemBinding.( + item: Item, + payloads: List + ) -> Unit = binding(payload = true) { item -> + permissionLabel.setText(item.permission.labelRes) + permissionDescription.setText(item.permission.descriptionRes) + grantAction.setOnClickListener { item.onRequest(item.permission) } + privacyPolicy.apply { + movementMethod = LinkMovementMethod.getInstance() + val ppText = getString(R.string.settings_privacy_policy_label) + val ppLink = PrivacyPolicy.URL + text = Html.fromHtml("$ppText", 0) + } + } + + data class Item( + val permission: Permission, + val onRequest: (Permission) -> Unit + ) : OverviewAdapter.Item { + override val stableId: Long = permission.hashCode().toLong() + + override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?) + get() = { old, new -> if (new::class.isInstance(old)) new else null } + } +} \ No newline at end of file diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/DualPodsCardVH.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/DualPodsCardVH.kt new file mode 100644 index 00000000..d2ee8ca8 --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/DualPodsCardVH.kt @@ -0,0 +1,166 @@ +package eu.darken.capod.wear.ui.overview.cards.pods + +import android.graphics.Typeface +import android.view.ViewGroup +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import eu.darken.capod.R +import eu.darken.capod.common.lists.binding +import eu.darken.capod.databinding.OverviewPodsDualItemBinding +import eu.darken.capod.pods.core.* +import eu.darken.capod.pods.core.apple.DualAirPods +import eu.darken.capod.pods.core.apple.DualAirPods.LidState +import java.time.Duration +import java.time.Instant + +class DualPodsCardVH(parent: ViewGroup) : + PodDeviceVH( + R.layout.overview_pods_dual_item, + parent + ) { + + override val viewBinding = lazy { OverviewPodsDualItemBinding.bind(itemView) } + + override val onBindData = binding(payload = true) { item: Item -> + val device = item.device + name.apply { + val sb = StringBuilder(device.getLabel(context)) + if (device is HasPodStyle && item.showDebug) { + val style = device.podStyle + sb.append(" (${style.getColor(context)})") + } + text = sb + + if (item.isMainPod) setTypeface(typeface, Typeface.BOLD) + else setTypeface(typeface, Typeface.NORMAL) + + if (device is DualAirPods && item.showDebug) { + append(" [${device.primaryPod.name}]") + } + } + deviceIcon.setImageResource(device.iconRes) + + lastSeen.text = context.getString(R.string.last_seen_x, device.lastSeenFormatted(item.now)) + firstSeen.text = context.getString(R.string.first_seen_x, device.firstSeenFormatted(item.now)) + firstSeen.isGone = Duration.between(device.seenFirstAt, device.seenLastAt).toMinutes() < 1 + + reception.text = item.getReceptionText() + + // Pods battery state + device.apply { + podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent)) + podLeftBatteryLabel.text = getBatteryLevelLeftPod(context) + + podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent)) + podRightBatteryLabel.text = getBatteryLevelRightPod(context) + } + + // Pods charging state + device.apply { + if (this is HasChargeDetectionDual) { + podLeftChargingIcon.isInvisible = !isLeftPodCharging + podLeftChargingLabel.isInvisible = !isLeftPodCharging + + podRightChargingIcon.isInvisible = !isRightPodCharging + podRightChargingLabel.isInvisible = !isRightPodCharging + } else { + podLeftChargingIcon.isGone = true + podLeftChargingLabel.isGone = true + + podRightChargingIcon.isGone = true + podRightChargingLabel.isGone = true + } + } + + // Microphone state + device.apply { + if (this is HasDualMicrophone) { + podLeftMicrophoneIcon.isInvisible = !isLeftPodMicrophone + podLeftMicrophoneLabel.isInvisible = !isLeftPodMicrophone + + podRightMicrophoneIcon.isInvisible = !isRightPodMicrophone + podRightMicrophoneLabel.isInvisible = !isRightPodMicrophone + } else { + podLeftMicrophoneIcon.isGone = true + podLeftMicrophoneLabel.isGone = true + + podRightMicrophoneIcon.isGone = true + podRightMicrophoneLabel.isGone = true + } + } + + // Pods wear state + device.apply { + if (this is HasEarDetectionDual) { + podLeftWearIcon.isInvisible = !isLeftPodInEar + podLeftWearLabel.isInvisible = !isLeftPodInEar + + podRightWearIcon.isInvisible = !isRightPodInEar + podRightWearLabel.isInvisible = !isRightPodInEar + } else { + podLeftWearIcon.isGone = true + podLeftWearLabel.isGone = true + + podRightWearIcon.isGone = true + podRightWearLabel.isGone = true + } + } + + // Case charge state + device.apply { + if (this is HasCase) { + podCaseBatteryIcon.isGone = false + podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent)) + podCaseBatteryLabel.text = getBatteryLevelCase(context) + + podCaseChargingIcon.isInvisible = !isCaseCharging + podCaseChargingLabel.isInvisible = !isCaseCharging + } else { + podCaseBatteryIcon.isGone = true + podCaseBatteryLabel.isGone = true + + podCaseChargingIcon.isGone = true + podCaseChargingLabel.isGone = true + } + } + + // Case lid state + device.apply { + if (this is DualAirPods) { + podCaseLidLabel.text = when (caseLidState) { + LidState.OPEN -> context.getString(R.string.pods_case_status_open_label) + LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label) + else -> context.getString(R.string.pods_case_unknown_state) + } + + val hideInfo = !listOf(LidState.OPEN, LidState.CLOSED).contains(caseLidState) + podCaseLidIcon.isInvisible = hideInfo + podCaseLidLabel.isInvisible = hideInfo + } else { + podCaseLidIcon.isGone = true + podCaseLidLabel.isGone = true + } + } + + // Connection state + device.apply { + val sb = StringBuilder() + if (this is HasStateDetection) { + sb.append(state.getLabel(context)) + } + if (item.showDebug) { + sb.append("\n\n").append("---Debug---") + sb.append("\n").append(rawDataHex) + } + status.text = sb + status.isGone = sb.isEmpty() + } + } + + data class Item( + override val now: Instant, + override val device: DualPodDevice, + override val showDebug: Boolean, + override val isMainPod: Boolean, + ) : PodDeviceVH.Item +} \ No newline at end of file diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/PodDeviceVH.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/PodDeviceVH.kt new file mode 100644 index 00000000..b5de2298 --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/PodDeviceVH.kt @@ -0,0 +1,42 @@ +package eu.darken.capod.wear.ui.overview.cards.pods + +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.viewbinding.ViewBinding +import eu.darken.capod.R +import eu.darken.capod.common.lists.BindableVH +import eu.darken.capod.common.lists.differ.DifferItem +import eu.darken.capod.common.lists.modular.ModularAdapter +import eu.darken.capod.wear.ui.overview.OverviewAdapter +import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.getSignalQuality +import java.time.Instant + +abstract class PodDeviceVH( + @LayoutRes layoutId: Int, + parent: ViewGroup +) : ModularAdapter.VH(layoutId, parent), BindableVH { + + fun Item.getReceptionText(): String = device.getSignalQuality(context) + .let { if (showDebug) "$it ${device.seenCounter}" else it } + .let { if (isMainPod) "$it\n(${getString(R.string.pods_yours)})" else it } + + interface Item : OverviewAdapter.Item { + + val now: Instant + + val device: PodDevice + + val showDebug: Boolean + + val isMainPod: Boolean + + override val stableId: Long get() = device.identifier.hashCode().toLong() + + override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?)? + get() = { old, new -> + if (new::class.isInstance(old)) new else null + } + + } +} \ No newline at end of file diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/SinglePodsCardVH.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/SinglePodsCardVH.kt new file mode 100644 index 00000000..b9bd2ec9 --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/SinglePodsCardVH.kt @@ -0,0 +1,81 @@ +package eu.darken.capod.wear.ui.overview.cards.pods + +import android.graphics.Typeface +import android.view.ViewGroup +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import eu.darken.capod.R +import eu.darken.capod.common.lists.binding +import eu.darken.capod.databinding.OverviewPodsSingleItemBinding +import eu.darken.capod.pods.core.* +import java.time.Instant + +class SinglePodsCardVH(parent: ViewGroup) : + PodDeviceVH( + R.layout.overview_pods_single_item, + parent + ) { + + override val viewBinding = lazy { OverviewPodsSingleItemBinding.bind(itemView) } + + override val onBindData = binding(payload = true) { item: Item -> + val device = item.device + + name.apply { + text = device.getLabel(context) + if (item.isMainPod) setTypeface(typeface, Typeface.BOLD) + else setTypeface(typeface, Typeface.NORMAL) + } + + deviceIcon.setImageResource(device.iconRes) + + lastSeen.text = device.lastSeenFormatted(item.now) + + reception.text = item.getReceptionText() + + // Battery level + device.apply { + batteryLabel.text = getBatteryLevelHeadset(context) + batteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent)) + } + + // Charge state + device.apply { + if (this is HasChargeDetection) { + chargingIcon.isInvisible = isHeadsetBeingCharged + chargingLabel.isInvisible = isHeadsetBeingCharged + } else { + chargingIcon.isGone = true + chargingLabel.isGone = true + } + } + + // Has ear detection + device.apply { + if (this is HasEarDetection) { + wearIcon.isInvisible = isBeingWorn + wearLabel.isInvisible = isBeingWorn + } else { + wearIcon.isGone = true + wearLabel.isGone = true + } + } + + status.apply { + val sb = StringBuilder() + if (item.showDebug) { + sb.append("--- Debug ---") + sb.append("\n").append(device.rawDataHex) + } + text = sb + isGone = !item.showDebug + } + } + + data class Item( + override val now: Instant, + override val device: SinglePodDevice, + override val showDebug: Boolean, + override val isMainPod: Boolean, + ) : PodDeviceVH.Item +} \ No newline at end of file diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt new file mode 100644 index 00000000..7cb3f64e --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt @@ -0,0 +1,48 @@ +package eu.darken.capod.wear.ui.overview.cards.pods + +import android.graphics.Typeface +import android.view.ViewGroup +import eu.darken.capod.R +import eu.darken.capod.common.lists.binding +import eu.darken.capod.databinding.OverviewPodsUnknownItemBinding +import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.ApplePods +import eu.darken.capod.pods.core.lastSeenFormatted +import java.time.Instant + +class UnknownPodDeviceCardVH(parent: ViewGroup) : + PodDeviceVH( + R.layout.overview_pods_unknown_item, + parent + ) { + + override val viewBinding = lazy { + OverviewPodsUnknownItemBinding.bind(itemView) + } + + override val onBindData = binding(payload = true) { item -> + val device = item.device + name.apply { + text = device.getLabel(context) + if (item.isMainPod) setTypeface(typeface, Typeface.BOLD) + else setTypeface(typeface, Typeface.NORMAL) + } + + lastSeen.text = device.lastSeenFormatted(item.now) + reception.text = item.getReceptionText() + + details.text = when (item.device) { + is ApplePods -> getString(R.string.pods_unknown_contact_dev) + else -> getString(R.string.pods_unknown_label) + } + + rawdata.text = device.rawDataHex.joinToString("\n") + } + + data class Item( + override val now: Instant, + override val device: PodDevice, + override val showDebug: Boolean = false, + override val isMainPod: Boolean = false, + ) : PodDeviceVH.Item +} \ No newline at end of file diff --git a/app-wear/src/main/res/layout/activity_main.xml b/app-wear/src/main/res/layout/activity_main.xml deleted file mode 100644 index d10d150b..00000000 --- a/app-wear/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app-wear/src/main/res/layout/main_activity.xml b/app-wear/src/main/res/layout/main_activity.xml new file mode 100644 index 00000000..68418b8c --- /dev/null +++ b/app-wear/src/main/res/layout/main_activity.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app-wear/src/main/res/navigation/nav_graph.xml b/app-wear/src/main/res/navigation/nav_graph.xml new file mode 100644 index 00000000..7a3fe0f3 --- /dev/null +++ b/app-wear/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app-wear/src/main/res/values-night/themes.xml b/app-wear/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..d69a4b5b --- /dev/null +++ b/app-wear/src/main/res/values-night/themes.xml @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/app-wear/src/main/res/values/colors.xml b/app-wear/src/main/res/values/colors.xml new file mode 100644 index 00000000..77c8e5d2 --- /dev/null +++ b/app-wear/src/main/res/values/colors.xml @@ -0,0 +1,63 @@ + + + #3F7AFF + #FFFFFF + #DAE2FF + #00174B + #715C00 + #FFFFFF + #FFE16C + #231B00 + #006D39 + #FFFFFF + #5CFFA0 + #00210D + #BA1B1B + #FFDAD4 + #FFFFFF + #410001 + #FEFBFF + #1B1B1F + #FEFBFF + #1B1B1F + #E2E2EC + #44464E + #75767F + #F2F0F5 + #303033 + #B1C5FF + + #3F7AFF + #002A78 + #003EA7 + #DAE2FF + #E9C426 + #3B2F00 + #564500 + #FFE16C + #33E286 + #00391B + #005229 + #5CFFA0 + #FFB4A9 + #930006 + #680003 + #FFDAD4 + #1B1B1F + #E3E1E6 + #1B1B1F + #E3E1E6 + #44464E + #C6C6D0 + #8F909A + #1B1B1F + #E3E1E6 + #0054D9 + + #3F7AFF + #BA1B1B + + #3f7aff + #ffd83c + #41eb8e + \ No newline at end of file diff --git a/app-wear/src/main/res/values/themes.xml b/app-wear/src/main/res/values/themes.xml new file mode 100644 index 00000000..b9adb822 --- /dev/null +++ b/app-wear/src/main/res/values/themes.xml @@ -0,0 +1,48 @@ + + + + + + + + + \ No newline at end of file