diff --git a/.gitignore b/.gitignore
index 55b25c74..e7cc6cfc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,4 +21,5 @@ wear/google-services.json
wearsettings/debug
wearsettings/release
releases
-allowed_wearsettings_callers.xml
\ No newline at end of file
+allowed_wearsettings_callers.xml
+/ImportTranslations.bat
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 6a141a3d..afcd69f4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,67 +1,17 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext {
- compileSdkVersion = 35
- minSdkVersion = 24
- targetSdkVersion = 34
-
- kotlin_version = '2.1.10'
- kotlinx_version = '1.10.2'
-
- desugar_version = '2.1.5'
-
- firebase_version = '33.13.0'
-
- activity_version = '1.10.1'
- appcompat_version = '1.7.0'
- constraintlayout_version = '2.2.1'
- core_version = '1.16.0'
- fragment_version = '1.8.6'
- lifecycle_version = '2.9.0'
- preference_version = '1.2.1'
- recyclerview_version = '1.4.0'
- coresplash_version = '1.0.1'
- work_version = '2.10.1'
- navigation_version = '2.9.0'
- datastore_version = '1.1.7'
-
- test_core_version = '1.6.1'
- test_runner_version = '1.6.2'
- test_rules_version = '1.6.1'
- junit_version = '1.2.1'
- androidx_truth_version = '1.6.0'
- google_truth_version = '1.4.4'
-
- material_version = '1.12.0'
-
- compose_bom_version = '2025.05.00'
- compose_compiler_version = '1.5.15'
- wear_compose_version = '1.4.1'
- wear_tiles_version = '1.4.1'
- wear_watchface_version = '1.2.1'
- horologist_version = '0.6.23'
- accompanist_version = '0.37.3'
-
- gson_version = '2.13.1'
- timber_version = '5.0.1'
-
- // Shizuku
- shizuku_version = '13.1.5'
- refine_version = '4.4.0'
- }
-
repositories {
mavenCentral()
google()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.10.0'
- classpath 'com.google.gms:google-services:4.4.2'
- classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.3'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlin_version"
+ classpath libs.gradle
+ classpath libs.google.services
+ classpath libs.firebase.crashlytics.gradle
+ classpath libs.kotlin.gradle.plugin
+ classpath libs.compose.compiler.gradle.plugin
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
diff --git a/common/.gitignore b/common/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/common/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/common/build.gradle b/common/build.gradle
new file mode 100644
index 00000000..414bc942
--- /dev/null
+++ b/common/build.gradle
@@ -0,0 +1,43 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ namespace 'com.thewizrd.common'
+ compileSdk = libs.versions.compileSdkVersion.get().toInteger()
+
+ defaultConfig {
+ minSdkVersion libs.versions.minSdkVersion.get().toInteger()
+ targetSdkVersion libs.versions.targetSdkVersion.get().toInteger()
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ debug {
+ minifyEnabled false
+ }
+ release {
+ // Let the app module take care of this
+ minifyEnabled false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ kotlin {
+ jvmToolchain(17)
+ }
+}
+
+dependencies {
+ implementation project(":shared_resources")
+
+ implementation libs.appcompat
+ implementation libs.core.ktx
+ implementation libs.core.splashscreen
+ implementation libs.material
+}
\ No newline at end of file
diff --git a/common/consumer-rules.pro b/common/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/common/proguard-rules.pro b/common/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/common/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..44008a43
--- /dev/null
+++ b/common/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/common/src/main/res/drawable/preference_round_background.xml b/common/src/main/res/drawable/preference_round_background.xml
new file mode 100644
index 00000000..edfdb463
--- /dev/null
+++ b/common/src/main/res/drawable/preference_round_background.xml
@@ -0,0 +1,30 @@
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/src/main/res/drawable/preference_round_background_bottom.xml b/common/src/main/res/drawable/preference_round_background_bottom.xml
new file mode 100644
index 00000000..208a469b
--- /dev/null
+++ b/common/src/main/res/drawable/preference_round_background_bottom.xml
@@ -0,0 +1,32 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/src/main/res/drawable/preference_round_background_center.xml b/common/src/main/res/drawable/preference_round_background_center.xml
new file mode 100644
index 00000000..66cbcce3
--- /dev/null
+++ b/common/src/main/res/drawable/preference_round_background_center.xml
@@ -0,0 +1,28 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/src/main/res/drawable/preference_round_background_top.xml b/common/src/main/res/drawable/preference_round_background_top.xml
new file mode 100644
index 00000000..053faa4c
--- /dev/null
+++ b/common/src/main/res/drawable/preference_round_background_top.xml
@@ -0,0 +1,32 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/src/main/res/values-de/strings.xml b/common/src/main/res/values-de/strings.xml
new file mode 100644
index 00000000..4a25a30d
--- /dev/null
+++ b/common/src/main/res/values-de/strings.xml
@@ -0,0 +1,8 @@
+
+
+ "Bluetooth"
+ "Bluetooth-Berechtigung aktiviert"
+ "Bluetooth-Berechtigung deaktiviert"
+ "Bitte-nicht-stören-Zugriff aktiviert"
+ "Bitte-nicht-stören-Zugriff deaktiviert. Bitte hier klicken, um die Änderung der Bitte-nicht-stören-Einstellung von Ihrem WearOS-Gerät aus zu aktivieren."
+
\ No newline at end of file
diff --git a/common/src/main/res/values-es/strings.xml b/common/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..04862204
--- /dev/null
+++ b/common/src/main/res/values-es/strings.xml
@@ -0,0 +1,8 @@
+
+
+ "Bluetooth"
+ "Permiso de Bluetooth activado"
+ "Permiso de Bluetooth desactivado"
+ "Acceso a No Molestar activado"
+ "Acceso a No Molestar desactivado. Por favor, haga clic para activar el cambio de la configuración de No Molestar desde su dispositivo WearOS"
+
\ No newline at end of file
diff --git a/common/src/main/res/values-fr/strings.xml b/common/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..31c01d7f
--- /dev/null
+++ b/common/src/main/res/values-fr/strings.xml
@@ -0,0 +1,8 @@
+
+
+ "Bluetooth"
+ "Autorisation Bluetooth activée"
+ "Autorisation Bluetooth désactivée"
+ "Accès Ne pas déranger activé"
+ "Accès Ne pas déranger désactivé. Veuillez cliquer pour autoriser la modification du paramètre Ne pas déranger depuis votre appareil WearOS"
+
\ No newline at end of file
diff --git a/wearsettings/src/main/res/values-night/themes.xml b/common/src/main/res/values-night/styles.xml
similarity index 90%
rename from wearsettings/src/main/res/values-night/themes.xml
rename to common/src/main/res/values-night/styles.xml
index d846e296..c872be59 100644
--- a/wearsettings/src/main/res/values-night/themes.xml
+++ b/common/src/main/res/values-night/styles.xml
@@ -1,6 +1,7 @@
+
-
-
\ No newline at end of file
+
+
diff --git a/mobile/src/main/res/values/dimens.xml b/common/src/main/res/values/dimens.xml
similarity index 100%
rename from mobile/src/main/res/values/dimens.xml
rename to common/src/main/res/values/dimens.xml
diff --git a/common/src/main/res/values/preference_styles.xml b/common/src/main/res/values/preference_styles.xml
new file mode 100644
index 00000000..7384c520
--- /dev/null
+++ b/common/src/main/res/values/preference_styles.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
new file mode 100644
index 00000000..40f703f4
--- /dev/null
+++ b/common/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+
+ Bluetooth
+ Bluetooth permission enabled
+ Bluetooth permission disabled
+
+ Do not Disturb access enabled
+ Do not Disturb access disabled. Please click to enable changing Do not Disturb setting from your WearOS device
+
\ No newline at end of file
diff --git a/common/src/main/res/values/styles.xml b/common/src/main/res/values/styles.xml
new file mode 100644
index 00000000..46d6d7f5
--- /dev/null
+++ b/common/src/main/res/values/styles.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/settings-helper.md b/docs/settings-helper.md
index 0319e45b..39bc358c 100644
--- a/docs/settings-helper.md
+++ b/docs/settings-helper.md
@@ -8,11 +8,11 @@ permalink: /settings-helper
Companion app for SimpleWear
-Latest version: [SimpleWear Settings v1.3.1]({{
-site.github.repository_url}}/releases/download/v1.16.1_release/wearsettings-release-1.3.1.apk)
+Latest version: [SimpleWear Settings v1.4.0]({{
+site.github.repository_url}}/releases/download/v1.17.0_release/wearsettings-release-1.4.0.apk)
-Previous version: [SimpleWear Settings v1.3.0]({{
-site.github.repository_url}}/releases/download/v1.16.0_beta/wearsettings-release-1.3.0.apk)
+Previous version: [SimpleWear Settings v1.3.1]({{
+site.github.repository_url}}/releases/download/v1.16.1_release/wearsettings-release-1.3.1.apk)
## WiFi and Location Toggle
diff --git a/gradle.properties b/gradle.properties
index a9a76950..9a9a9a02 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
+org.gradle.jvmargs=-Xmx2048m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 00000000..1c391fe1
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,160 @@
+[versions]
+compileSdkVersion = "36"
+minSdkVersion = "24"
+targetSdkVersion = "36"
+
+gradleVersion = "8.13.0"
+kotlin_version = '2.1.20'
+kotlinx_version = '1.10.2'
+
+desugar_version = '2.1.5'
+
+firebase_version = "34.5.0"
+firebaseCrashlyticsGradleVersion = "3.0.6"
+googleServicesVersion = "4.4.4"
+play_services_wearable_version = "19.0.0"
+app_update_ktx_version = "2.1.0"
+
+activity_version = '1.11.0'
+annotation_version = "1.9.1"
+appcompat_version = '1.7.1'
+constraintlayout_version = '2.2.1'
+core_version = '1.17.0'
+fragment_version = '1.8.9'
+lifecycle_version = '2.9.4'
+preference_version = '1.2.1'
+recyclerview_version = '1.4.0'
+recyclerview_selection_version = "1.2.0"
+coresplash_version = "1.2.0"
+work_version = '2.11.0'
+navigation_version = "2.9.6"
+datastore_version = '1.1.7'
+media_version = "1.7.1"
+palette_version = "1.0.0"
+
+test_core_version = '1.7.0'
+test_runner_version = '1.7.0'
+test_rules_version = '1.7.0'
+junit_version = '1.3.0'
+androidx_truth_version = '1.7.0'
+google_truth_version = '1.4.5'
+
+material_version = '1.14.0-alpha06'
+material_compose_version = "1.5.0-alpha08"
+
+compose_bom_version = "2025.11.00"
+compose_compiler_version = '1.5.15'
+
+wearableVersion = "2.9.0"
+wear_version = "1.3.0"
+wear_ongoing_version = "1.1.0"
+wear_phone_interactions_version = "1.1.0"
+wear_remote_interactions_version = "1.1.0"
+wear_tooling_preview_version = "1.0.0"
+reorderable_version = "3.0.0"
+
+wear_compose_version = "1.5.5"
+wear_tiles_version = '1.5.0'
+wear_watchface_version = '1.2.1'
+horologist_version = '0.7.15'
+accomapnist_version = '0.37.3'
+wear_protolayout_version = "1.3.0"
+
+gson_version = '2.13.2'
+timber_version = '5.0.1'
+
+# Shizuku
+shizuku_version = '13.1.5'
+refine_version = '4.4.0'
+
+libsu_version = "3.1.2"
+dexmaker_version = "2.28.6"
+hiddenapibypassVersion = "6.1"
+
+[libraries]
+accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accomapnist_version" }
+androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "annotation_version" }
+androidx-compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose_compiler_version" }
+androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material_compose_version" }
+androidx-compose-material-icons-extended-android = { module = "androidx.compose.material:material-icons-extended-android" }
+androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
+androidx-compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics" }
+androidx-compose-ui = { module = "androidx.compose.ui:ui" }
+androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
+androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
+androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
+wear-tiles-tooling-preview = { module = "androidx.wear.tiles:tiles-tooling-preview", version.ref = "wear_tiles_version" }
+androidx-watchface-complications-data = { module = "androidx.wear.watchface:watchface-complications-data", version.ref = "wear_watchface_version" }
+refine-annotation = { module = "dev.rikka.tools.refine:annotation", version.ref = "refine_version" }
+refine-annotation-processor = { module = "dev.rikka.tools.refine:annotation-processor", version.ref = "refine_version" }
+hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypassVersion" }
+horologist-audio-ui-material3 = { module = "com.google.android.horologist:horologist-audio-ui-material3", version.ref = "horologist_version" }
+libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu_version" }
+refine-runtime = { module = "dev.rikka.tools.refine:runtime", version.ref = "refine_version" }
+shizuki-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku_version" }
+shizuki-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku_version" }
+compose-compiler-gradle-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin_version" }
+wear-compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "wear_compose_version" }
+firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsGradleVersion" }
+google-services = { module = "com.google.gms:google-services", version.ref = "googleServicesVersion" }
+gradle = { module = "com.android.tools.build:gradle", version.ref = "gradleVersion" }
+horologist-compose-tools = { module = "com.google.android.horologist:horologist-compose-tools", version.ref = "horologist_version" }
+horologist-tiles = { module = "com.google.android.horologist:horologist-tiles", version.ref = "horologist_version" }
+kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
+desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_version" }
+test-core = { module = "androidx.test:core", version.ref = "test_core_version" }
+test-runner = { module = "androidx.test:runner", version.ref = "test_runner_version" }
+test-rules = { module = "androidx.test:rules", version.ref = "test_rules_version" }
+androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junit_version" }
+androidx-truth = { module = "androidx.test.ext:truth", version.ref = "androidx_truth_version" }
+google-truth = { module = "com.google.truth:truth", version.ref = "google_truth_version" }
+kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx_version" }
+kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx_version" }
+kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinx_version" }
+core-ktx = { module = "androidx.core:core-ktx", version.ref = "core_version" }
+fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment_version" }
+preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preference_version" }
+lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" }
+lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle_version" }
+wearable = { module = "com.google.android.wearable:wearable", version.ref = "wearableVersion" }
+work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work_version" }
+core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coresplash_version" }
+play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "play_services_wearable_version" }
+app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "app_update_ktx_version" }
+firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase_version" }
+firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
+firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
+firebase-config = { module = "com.google.firebase:firebase-config" }
+appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat_version" }
+constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout_version" }
+recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview_version" }
+recyclerview-selection = { module = "androidx.recyclerview:recyclerview-selection", version.ref = "recyclerview_selection_version" }
+media = { module = "androidx.media:media", version.ref = "media_version" }
+material = { module = "com.google.android.material:material", version.ref = "material_version" }
+timber = { module = "com.jakewharton.timber:timber", version.ref = "timber_version" }
+gson = { module = "com.google.code.gson:gson", version.ref = "gson_version" }
+dexmaker = { module = "com.linkedin.dexmaker:dexmaker", version.ref = "dexmaker_version" }
+wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wear_compose_version" }
+wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wear_compose_version" }
+wear-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "wear_compose_version" }
+wear-protolayout-material3 = { module = "androidx.wear.protolayout:protolayout-material3", version.ref = "wear_protolayout_version" }
+horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist_version" }
+horologist-media-ui-material3 = { module = "com.google.android.horologist:horologist-media-ui-material3", version.ref = "horologist_version" }
+wear-tiles = { module = "androidx.wear.tiles:tiles", version.ref = "wear_tiles_version" }
+wear-tiles-testing = { module = "androidx.wear.tiles:tiles-testing", version.ref = "wear_tiles_version" }
+wear-tiles-renderer = { module = "androidx.wear.tiles:tiles-renderer", version.ref = "wear_tiles_version" }
+wear-tiles-tooling = { module = "androidx.wear.tiles:tiles-tooling", version.ref = "wear_tiles_version" }
+wear-watchface-complications-data-source-ktx = { module = "androidx.wear.watchface:watchface-complications-data-source-ktx", version.ref = "wear_watchface_version" }
+palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "palette_version" }
+activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity_version" }
+compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom_version" }
+navigation-runtime-ktx = { module = "androidx.navigation:navigation-runtime-ktx", version.ref = "navigation_version" }
+datastore = { module = "androidx.datastore:datastore", version.ref = "datastore_version" }
+wear = { module = "androidx.wear:wear", version.ref = "wear_version" }
+wear-ongoing = { module = "androidx.wear:wear-ongoing", version.ref = "wear_ongoing_version" }
+wear-phone-interactions = { module = "androidx.wear:wear-phone-interactions", version.ref = "wear_phone_interactions_version" }
+wear-remote-interactions = { module = "androidx.wear:wear-remote-interactions", version.ref = "wear_remote_interactions_version" }
+wear-tooling-preview = { module = "androidx.wear:wear-tooling-preview", version.ref = "wear_tooling_preview_version" }
+reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable_version" }
+
+[plugins]
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index cfd35bc0..2558dbe0 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
diff --git a/hidden-api/build.gradle b/hidden-api/build.gradle
index 8cec97d3..69eaba75 100644
--- a/hidden-api/build.gradle
+++ b/hidden-api/build.gradle
@@ -4,11 +4,11 @@ plugins {
}
android {
- compileSdk rootProject.compileSdkVersion
+ compileSdk = libs.versions.compileSdkVersion.get().toInteger()
defaultConfig {
- minSdkVersion rootProject.minSdkVersion
- targetSdkVersion rootProject.targetSdkVersion
+ minSdkVersion libs.versions.minSdkVersion.get().toInteger()
+ targetSdkVersion libs.versions.targetSdkVersion.get().toInteger()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -38,7 +38,7 @@ android {
}
dependencies {
- annotationProcessor 'dev.rikka.tools.refine:annotation-processor:4.4.0'
- compileOnly 'dev.rikka.tools.refine:annotation:4.4.0'
- implementation 'androidx.annotation:annotation:1.9.1'
+ annotationProcessor libs.refine.annotation.processor
+ compileOnly libs.refine.annotation
+ implementation libs.androidx.annotation
}
\ No newline at end of file
diff --git a/hidden-api/src/main/java/android/app/INotificationManager.java b/hidden-api/src/main/java/android/app/INotificationManager.java
new file mode 100644
index 00000000..2da9979b
--- /dev/null
+++ b/hidden-api/src/main/java/android/app/INotificationManager.java
@@ -0,0 +1,24 @@
+package android.app;
+
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+
+import androidx.annotation.DeprecatedSinceApi;
+import androidx.annotation.RequiresApi;
+
+public interface INotificationManager {
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ @DeprecatedSinceApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ void setInterruptionFilter(String pkg, int interruptionFilter);
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ void setInterruptionFilter(String pkg, int interruptionFilter, boolean fromUser);
+
+ abstract class Stub extends Binder implements INotificationManager {
+ public static INotificationManager asInterface(IBinder obj) {
+ throw new RuntimeException("Stub!");
+ }
+ }
+}
diff --git a/hidden-api/src/main/java/android/nfc/INfcAdapter.java b/hidden-api/src/main/java/android/nfc/INfcAdapter.java
new file mode 100644
index 00000000..97a7fba7
--- /dev/null
+++ b/hidden-api/src/main/java/android/nfc/INfcAdapter.java
@@ -0,0 +1,29 @@
+package android.nfc;
+
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.IInterface;
+
+import androidx.annotation.DeprecatedSinceApi;
+import androidx.annotation.RequiresApi;
+
+public interface INfcAdapter extends IInterface {
+ @DeprecatedSinceApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ boolean enable();
+
+ @DeprecatedSinceApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ boolean disable(boolean saveState);
+
+ @RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ boolean enable(String pkg);
+
+ @RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ boolean disable(boolean saveState, String pkg);
+
+ abstract class Stub extends Binder implements INfcAdapter {
+ public static INfcAdapter asInterface(IBinder obj) {
+ throw new RuntimeException("Stub!");
+ }
+ }
+}
\ No newline at end of file
diff --git a/hidden-api/src/main/java/android/os/IPowerManager.java b/hidden-api/src/main/java/android/os/IPowerManager.java
index 9e0c249a..eadb878a 100644
--- a/hidden-api/src/main/java/android/os/IPowerManager.java
+++ b/hidden-api/src/main/java/android/os/IPowerManager.java
@@ -13,6 +13,12 @@ public interface IPowerManager extends IInterface {
void goToSleep(long time, int reason, int flags);
+ @DeprecatedSinceApi(api = Build.VERSION_CODES.Q)
+ boolean setPowerSaveMode(boolean mode);
+
+ @RequiresApi(api = Build.VERSION_CODES.Q)
+ boolean setPowerSaveModeEnabled(boolean mode);
+
abstract class Stub extends Binder implements IPowerManager {
public static IPowerManager asInterface(IBinder obj) {
throw new RuntimeException("Stub!");
diff --git a/mobile/build.gradle b/mobile/build.gradle
index fd7b6b96..705f7ec3 100644
--- a/mobile/build.gradle
+++ b/mobile/build.gradle
@@ -5,7 +5,7 @@ apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
android {
- compileSdk rootProject.compileSdkVersion
+ compileSdk = libs.versions.compileSdkVersion.get().toInteger()
defaultConfig {
applicationId "com.thewizrd.simplewear"
@@ -18,12 +18,12 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
- minSdkVersion rootProject.minSdkVersion
- targetSdkVersion rootProject.targetSdkVersion
+ minSdkVersion libs.versions.minSdkVersion.get().toInteger()
+ targetSdkVersion libs.versions.targetSdkVersion.get().toInteger()
// NOTE: Version Code Format [TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1)]
// NOTE: update SUPPORTED_VERSION_CODE if needed
- versionCode 341916050
- versionName "1.16.1"
+ versionCode 361917020
+ versionName "1.17.0"
vectorDrawables.useSupportLibrary = true
}
@@ -33,13 +33,13 @@ android {
applicationIdSuffix ".debug"
debuggable true
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
shrinkResources true
crunchPngs true
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
@@ -65,48 +65,49 @@ android {
dependencies {
implementation project(":shared_resources")
+ implementation project(":common")
- coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_version"
+ coreLibraryDesugaring libs.desugar.jdk.libs
// Unit Testing
- androidTestImplementation "androidx.test:core:$test_core_version"
+ androidTestImplementation libs.test.core
// AndroidJUnitRunner and JUnit Rules
- androidTestImplementation "androidx.test:runner:$test_runner_version"
- androidTestImplementation "androidx.test:rules:$test_rules_version"
+ androidTestImplementation libs.test.runner
+ androidTestImplementation libs.test.rules
// Assertions
- androidTestImplementation "androidx.test.ext:junit:$junit_version"
- androidTestImplementation "androidx.test.ext:truth:$androidx_truth_version"
- androidTestImplementation "com.google.truth:truth:$google_truth_version"
+ androidTestImplementation libs.androidx.junit
+ androidTestImplementation libs.androidx.truth
+ androidTestImplementation libs.google.truth
// Kotlin
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_version"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_version"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_version"
-
- implementation "androidx.core:core-ktx:$core_version"
- implementation "androidx.preference:preference-ktx:$preference_version"
- implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
- implementation "androidx.work:work-runtime-ktx:$work_version"
- implementation "androidx.core:core-splashscreen:$coresplash_version"
-
- implementation 'com.google.android.gms:play-services-wearable:19.0.0'
- implementation 'com.google.android.play:app-update-ktx:2.1.0'
-
- implementation platform("com.google.firebase:firebase-bom:$firebase_version")
- implementation 'com.google.firebase:firebase-analytics'
- implementation 'com.google.firebase:firebase-crashlytics'
- implementation 'com.google.firebase:firebase-config'
-
- implementation "androidx.appcompat:appcompat:$appcompat_version"
- implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version"
- implementation "androidx.recyclerview:recyclerview-selection:1.1.0"
- implementation 'androidx.media:media:1.7.0'
-
- implementation "com.google.android.material:material:$material_version"
-
- implementation "com.jakewharton.timber:timber:$timber_version"
- implementation "com.google.code.gson:gson:$gson_version"
- implementation 'com.linkedin.dexmaker:dexmaker:2.28.4'
-}
\ No newline at end of file
+ implementation libs.kotlinx.coroutines.core
+ implementation libs.kotlinx.coroutines.android
+ implementation libs.kotlinx.coroutines.play.services
+
+ implementation libs.core.ktx
+ implementation libs.core.splashscreen
+ implementation libs.preference.ktx
+ implementation libs.lifecycle.runtime.ktx
+ implementation libs.work.runtime.ktx
+
+ implementation libs.play.services.wearable
+ implementation libs.app.update.ktx
+
+ implementation platform(libs.firebase.bom)
+ implementation libs.firebase.analytics
+ implementation libs.firebase.crashlytics
+ implementation libs.firebase.config
+
+ implementation libs.appcompat
+ implementation libs.constraintlayout
+ implementation libs.recyclerview.selection
+ implementation libs.media
+
+ implementation libs.material
+
+ implementation libs.timber
+ implementation libs.gson
+ implementation libs.dexmaker
+}
diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
index 917340b6..52123578 100644
--- a/mobile/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -32,6 +32,7 @@
+
@@ -72,6 +73,7 @@
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme.Launcher"
@@ -141,6 +143,9 @@
+
{
+ WearableWorker.sendActionUpdate(context, Actions.NFC)
+ }
+ PowerManager.ACTION_POWER_SAVE_MODE_CHANGED -> {
+ WearableWorker.sendActionUpdate(context, Actions.BATTERYSAVER)
+ }
}
}
}
@@ -194,6 +202,8 @@ class App : Application(), ActivityLifecycleCallbacks, Configuration.Provider {
addAction(WifiManager.WIFI_STATE_CHANGED_ACTION)
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
addAction("android.media.VOLUME_CHANGED_ACTION")
+ addAction(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED)
+ addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)
}
// Receiver exported for system broadcasts
ContextCompat.registerReceiver(
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/FirebaseConfigurator.kt b/mobile/src/main/java/com/thewizrd/simplewear/FirebaseConfigurator.kt
index 1de9676d..a061b0c1 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/FirebaseConfigurator.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/FirebaseConfigurator.kt
@@ -4,6 +4,10 @@ import android.annotation.SuppressLint
import android.content.Context
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
+import com.google.firebase.remoteconfig.ConfigUpdate
+import com.google.firebase.remoteconfig.ConfigUpdateListener
+import com.google.firebase.remoteconfig.FirebaseRemoteConfig
+import com.google.firebase.remoteconfig.FirebaseRemoteConfigException
import com.thewizrd.shared_resources.utils.AnalyticsProps
import com.thewizrd.shared_resources.utils.ContextUtils.isLargeTablet
import com.thewizrd.shared_resources.utils.ContextUtils.isSmallestWidth
@@ -14,15 +18,19 @@ import com.thewizrd.shared_resources.utils.Logger
object FirebaseConfigurator {
@SuppressLint("MissingPermission")
fun initialize(context: Context) {
- FirebaseAnalytics.getInstance(context).setUserProperty(
- AnalyticsProps.DEVICE_TYPE, if (context.isTv()) {
- "tv"
- } else if (context.isLargeTablet() || context.isSmallestWidth(600)) {
- "tablet"
- } else {
- "mobile"
- }
- )
+ FirebaseAnalytics.getInstance(context).run {
+ setUserProperty(
+ AnalyticsProps.DEVICE_TYPE, if (context.isTv()) {
+ "tv"
+ } else if (context.isLargeTablet() || context.isSmallestWidth(600)) {
+ "tablet"
+ } else {
+ "mobile"
+ }
+ )
+
+ setUserProperty(AnalyticsProps.PLATFORM, "Android")
+ }
FirebaseCrashlytics.getInstance().apply {
isCrashlyticsCollectionEnabled = true
@@ -32,5 +40,23 @@ object FirebaseConfigurator {
if (!BuildConfig.DEBUG) {
Logger.registerLogger(CrashlyticsLoggingTree())
}
+
+ // Add Firebase RemoteConfig real-time listener
+ FirebaseRemoteConfig.getInstance().run {
+ addOnConfigUpdateListener(object : ConfigUpdateListener {
+ override fun onUpdate(configUpdate: ConfigUpdate) {
+ Logger.verbose("FirebaseConfigurator", "Remote update received")
+ this@run.activate()
+ }
+
+ override fun onError(error: FirebaseRemoteConfigException) {
+ Logger.error(
+ "FirebaseConfigurator",
+ message = "Error on real-time update",
+ t = error
+ )
+ }
+ })
+ }
}
}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/MainActivity.kt b/mobile/src/main/java/com/thewizrd/simplewear/MainActivity.kt
index 1b84fcec..461cf213 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/MainActivity.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/MainActivity.kt
@@ -2,16 +2,12 @@ package com.thewizrd.simplewear
import android.content.Intent
import android.os.Bundle
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
import android.view.View
import android.view.ViewTreeObserver
+import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.core.view.MenuProvider
import androidx.lifecycle.lifecycleScope
-import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.color.DynamicColors
import com.thewizrd.simplewear.updates.InAppUpdateManager
import kotlinx.coroutines.launch
@@ -26,10 +22,11 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
// Note: needed due to splash screen theme
- DynamicColors.applyIfAvailable(this)
+ DynamicColors.applyToActivityIfAvailable(this)
inAppUpdateManager = InAppUpdateManager.create(applicationContext)
@@ -63,54 +60,6 @@ class MainActivity : AppCompatActivity() {
}
}
}
-
- val appBarLayout = findViewById(R.id.app_bar)
- appBarLayout.liftOnScrollTargetViewId = R.id.scrollView
- appBarLayout.isLiftOnScroll = true
-
- setSupportActionBar(findViewById(R.id.toolbar))
- addMenuProvider(object : MenuProvider {
- override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
- menuInflater.inflate(R.menu.actions, menu)
- }
-
- override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
- when (menuItem.itemId) {
- R.id.timed_actions -> {
- supportFragmentManager.beginTransaction()
- .replace(R.id.fragment_container, TimedActionsFragment())
- .addToBackStack("timedActions")
- .commit()
- return true
- }
- }
-
- return false
- }
-
- override fun onPrepareMenu(menu: Menu) {
- menu.setGroupVisible(
- R.id.action_group,
- supportFragmentManager.backStackEntryCount == 0
- )
- }
- })
- supportActionBar?.setDefaultDisplayHomeAsUpEnabled(true)
-
- supportFragmentManager.addOnBackStackChangedListener {
- supportActionBar?.setDisplayHomeAsUpEnabled(supportFragmentManager.backStackEntryCount > 0)
- invalidateMenu()
- }
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- android.R.id.home -> {
- supportFragmentManager.popBackStack()
- }
- }
-
- return super.onOptionsItemSelected(item)
}
override fun onResume() {
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/PermissionCheckFragment.kt b/mobile/src/main/java/com/thewizrd/simplewear/PermissionCheckFragment.kt
index 6b4f66d6..ac4de1fc 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/PermissionCheckFragment.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/PermissionCheckFragment.kt
@@ -1,6 +1,7 @@
package com.thewizrd.simplewear
import android.Manifest
+import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Activity
import android.app.admin.DevicePolicyManager
@@ -16,8 +17,9 @@ import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
+import android.content.res.Configuration.UI_MODE_NIGHT_MASK
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.graphics.Color
-import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
@@ -29,8 +31,15 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
+import androidx.core.graphics.ColorUtils
+import androidx.core.net.toUri
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.children
import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.thewizrd.shared_resources.helpers.WearSettingsHelper
@@ -62,6 +71,11 @@ import java.util.regex.Pattern
class PermissionCheckFragment : LifecycleAwareFragment() {
companion object {
private const val TAG = "PermissionCheckFragment"
+
+ private const val CORNERS_FULL = 0
+ private const val CORNERS_TOP = 1
+ private const val CORNERS_CENTER = 2
+ private const val CORNERS_BOTTOM = 3
}
private lateinit var binding: FragmentPermcheckBinding
@@ -163,7 +177,7 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
)
.show()
- binding.companionPairProgress.visibility = View.GONE
+ binding.companionPairProgress.hide()
}
}
@@ -172,7 +186,7 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
if (it.resultCode == Activity.RESULT_OK) {
pairDevice()
} else {
- binding.companionPairProgress.visibility = View.GONE
+ binding.companionPairProgress.hide()
}
}
}
@@ -184,6 +198,36 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
): View {
// Inflate the layout for this fragment
binding = FragmentPermcheckBinding.inflate(inflater, container, false)
+
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
+ binding.bottom.updateLayoutParams {
+ val sysBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ height = sysBarInsets.top + sysBarInsets.bottom
+ }
+
+ insets
+ }
+
+ binding.toolbar.setOnMenuItemClickListener { menuItem ->
+ when (menuItem.itemId) {
+ R.id.timed_actions -> {
+ parentFragmentManager.beginTransaction()
+ .replace(R.id.fragment_container, TimedActionsFragment())
+ .addToBackStack("timedActions")
+ .commit()
+ true
+ }
+ }
+
+ false
+ }
+ binding.scrollView.children.firstOrNull().let { root ->
+ val parent = root as ViewGroup
+
+ parent.viewTreeObserver.addOnGlobalLayoutListener {
+ updateRoundedBackground(parent)
+ }
+ }
binding.torchPref.setOnClickListener {
if (!isCameraPermissionEnabled(requireContext())) {
permissionRequestLauncher.launch(arrayOf(Manifest.permission.CAMERA))
@@ -326,7 +370,7 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
binding.wearsettingsPref.setOnClickListener {
if (!WearSettingsHelper.isWearSettingsInstalled() || !WearSettingsHelper.isWearSettingsUpToDate()) {
val i = Intent(Intent.ACTION_VIEW).apply {
- data = Uri.parse(getString(R.string.url_wearsettings_helper))
+ data = getString(R.string.url_wearsettings_helper).toUri()
}
if (i.resolveActivity(requireContext().packageManager) != null) {
startActivity(i)
@@ -344,7 +388,7 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
if (!PhoneStatusHelper.isWriteSystemSettingsPermissionEnabled(ctx)) {
runCatching {
startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply {
- data = Uri.parse("package:${ctx.packageName}")
+ data = "package:${ctx.packageName}".toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}
@@ -387,7 +431,7 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !canScheduleExactAlarms(ctx)) {
runCatching {
startActivity(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
- data = Uri.parse("package:${ctx.packageName}")
+ data = "package:${ctx.packageName}".toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}
@@ -410,11 +454,12 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
}
}
- binding.companionPairProgress.visibility = View.VISIBLE
+ binding.companionPairProgress.show()
enqueueAction(requireContext(), WearableWorker.ACTION_REQUESTBTDISCOVERABLE)
pairDevice()
}
+ @SuppressLint("WrongConstant", "UseRequiresApi")
@TargetApi(Build.VERSION_CODES.Q)
private fun pairDevice() {
runWithView {
@@ -483,7 +528,7 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
context?.let { _ ->
runCatching {
lifecycleScope.launch {
- binding.companionPairProgress.visibility = View.GONE
+ binding.companionPairProgress.hide()
}
companionDeviceResultLauncher.launch(
IntentSenderRequest.Builder(it)
@@ -499,7 +544,7 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
context?.let { ctx ->
lifecycleScope.launch {
- binding.companionPairProgress.visibility = View.GONE
+ binding.companionPairProgress.hide()
Toast.makeText(
ctx,
R.string.message_nodevices_found,
@@ -591,32 +636,57 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
private fun updateCamPermText(enabled: Boolean) {
binding.torchPrefSummary.setText(if (enabled) R.string.permission_camera_enabled else R.string.permission_camera_disabled)
- binding.torchPrefSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.torchPrefSummary.setTextColor(
+ getTextColor(
+ binding.torchPrefSummary.context,
+ enabled
+ )
+ )
}
private fun updateDeviceAdminText(enabled: Boolean) {
binding.lockscreenSummary.setText(if (enabled) R.string.permission_admin_enabled else R.string.permission_lockscreen_disabled)
- binding.lockscreenSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.lockscreenSummary.setTextColor(
+ getTextColor(
+ binding.lockscreenSummary.context,
+ enabled
+ )
+ )
}
private fun updateLockScreenText(enabled: Boolean) {
binding.lockscreenSummary.setText(if (enabled) R.string.permission_lockscreen_enabled else R.string.permission_lockscreen_disabled)
- binding.lockscreenSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.lockscreenSummary.setTextColor(
+ getTextColor(
+ binding.lockscreenSummary.context,
+ enabled
+ )
+ )
}
private fun updateDNDAccessText(enabled: Boolean) {
binding.dndSummary.setText(if (enabled) R.string.permission_dnd_enabled else R.string.permission_dnd_disabled)
- binding.dndSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.dndSummary.setTextColor(getTextColor(binding.dndSummary.context, enabled))
}
private fun updatePairPermText(enabled: Boolean) {
binding.companionPairSummary.setText(if (enabled) R.string.permission_pairdevice_enabled else R.string.permission_pairdevice_disabled)
- binding.companionPairSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.companionPairSummary.setTextColor(
+ getTextColor(
+ binding.companionPairSummary.context,
+ enabled
+ )
+ )
}
private fun updateNotifListenerText(enabled: Boolean) {
binding.notiflistenerSummary.setText(if (enabled) R.string.prompt_notifservice_enabled else R.string.prompt_notifservice_disabled)
- binding.notiflistenerSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.notiflistenerSummary.setTextColor(
+ getTextColor(
+ binding.notiflistenerSummary.context,
+ enabled
+ )
+ )
}
private fun updateUninstallText(enabled: Boolean) {
@@ -626,7 +696,12 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
private fun updateManageCallsText(enabled: Boolean) {
binding.callcontrolSummary.setText(if (enabled) R.string.permission_callmanager_enabled else R.string.permission_callmanager_disabled)
- binding.callcontrolSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.callcontrolSummary.setTextColor(
+ getTextColor(
+ binding.callcontrolSummary.context,
+ enabled
+ )
+ )
}
private fun updateWearSettingsHelperPref(installed: Boolean, upToDate: Boolean) {
@@ -643,36 +718,50 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
)
binding.wearsettingsPrefSummary.setTextColor(
if (installed) {
- if (upToDate) Color.GREEN else Color.argb(0xFF, 0xFF, 0xA5, 0)
+ if (upToDate) {
+ getTextColor(binding.wearsettingsPrefSummary.context, true)
+ } else {
+ Color.argb(0xFF, 0xFF, 0xA5, 0)
+ }
} else {
- Color.RED
+ getTextColor(binding.wearsettingsPrefSummary.context, false)
}
)
}
private fun updateSystemSettingsPref(enabled: Boolean) {
binding.systemsettingsPrefSummary.setText(if (enabled) R.string.permission_systemsettings_enabled else R.string.permission_systemsettings_disabled)
- binding.systemsettingsPrefSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.systemsettingsPrefSummary.setTextColor(
+ getTextColor(
+ binding.systemsettingsPrefSummary.context,
+ enabled
+ )
+ )
}
private fun updateNotificationPref(enabled: Boolean) {
binding.notifPrefSummary.setText(if (enabled) R.string.permission_notifications_enabled else R.string.permission_notifications_disabled)
- binding.notifPrefSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.notifPrefSummary.setTextColor(
+ getTextColor(
+ binding.notifPrefSummary.context,
+ enabled
+ )
+ )
}
private fun updateBTPref(enabled: Boolean) {
binding.btPrefSummary.setText(if (enabled) R.string.permission_bt_enabled else R.string.permission_bt_disabled)
- binding.btPrefSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.btPrefSummary.setTextColor(getTextColor(binding.btPrefSummary.context, enabled))
}
private fun updateGesturesPref(enabled: Boolean) {
binding.gesturesSummary.setText(if (enabled) R.string.permission_gestures_enabled else R.string.permission_gestures_disabled)
- binding.gesturesSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.gesturesSummary.setTextColor(getTextColor(binding.gesturesSummary.context, enabled))
}
private fun updateExactAlarmsPref(enabled: Boolean) {
binding.alarmsSummary.setText(if (enabled) R.string.permission_exact_alarms_enabled else R.string.permission_exact_alarms_disabled)
- binding.alarmsSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.alarmsSummary.setTextColor(getTextColor(binding.alarmsSummary.context, enabled))
}
@Suppress("DEPRECATION")
@@ -681,14 +770,14 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
runCatching {
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
- data = Uri.parse("package:${ctx.packageName}")
+ data = "package:${ctx.packageName}".toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}
} else {
runCatching {
startActivity(Intent(Intent.ACTION_UNINSTALL_PACKAGE).apply {
- data = Uri.parse("package:${ctx.packageName}")
+ data = "package:${ctx.packageName}".toUri()
})
}
}
@@ -740,4 +829,60 @@ class PermissionCheckFragment : LifecycleAwareFragment() {
.setNegativeButton(android.R.string.cancel, null)
.show()
}
+
+ @ColorInt
+ private fun getTextColor(context: Context, enabled: Boolean): Int {
+ return when (enabled) {
+ true -> if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) {
+ Color.GREEN
+ } else {
+ ColorUtils.blendARGB(Color.GREEN, Color.BLACK, 0.25f)
+ }
+
+ false -> if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) {
+ ColorUtils.blendARGB(Color.RED, Color.WHITE, 0.25f)
+ } else {
+ Color.RED
+ }
+ }
+ }
+
+ private fun updateRoundedBackground(parent: ViewGroup) {
+ val permissionsPreferences =
+ parent.children.filter { it.tag == "permissions" && it.isVisible }.toList()
+ val settingsPreferences =
+ parent.children.filter { it.tag == "settings" && it.isVisible }.toList()
+
+ permissionsPreferences.forEachIndexed { index, view ->
+ val cornerType = when {
+ permissionsPreferences.size <= 1 -> CORNERS_FULL
+ index == 0 -> CORNERS_TOP
+ index == permissionsPreferences.size - 1 -> CORNERS_BOTTOM
+ else -> CORNERS_CENTER
+ }
+
+ when (cornerType) {
+ CORNERS_FULL -> view.setBackgroundResource(R.drawable.preference_round_background)
+ CORNERS_TOP -> view.setBackgroundResource(R.drawable.preference_round_background_top)
+ CORNERS_BOTTOM -> view.setBackgroundResource(R.drawable.preference_round_background_bottom)
+ CORNERS_CENTER -> view.setBackgroundResource(R.drawable.preference_round_background_center)
+ }
+ }
+
+ settingsPreferences.forEachIndexed { index, view ->
+ val cornerType = when {
+ settingsPreferences.size <= 1 -> CORNERS_FULL
+ index == 0 -> CORNERS_TOP
+ index == settingsPreferences.size - 1 -> CORNERS_BOTTOM
+ else -> CORNERS_CENTER
+ }
+
+ when (cornerType) {
+ CORNERS_FULL -> view.setBackgroundResource(R.drawable.preference_round_background)
+ CORNERS_TOP -> view.setBackgroundResource(R.drawable.preference_round_background_top)
+ CORNERS_BOTTOM -> view.setBackgroundResource(R.drawable.preference_round_background_bottom)
+ CORNERS_CENTER -> view.setBackgroundResource(R.drawable.preference_round_background_center)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/TimedActionsFragment.kt b/mobile/src/main/java/com/thewizrd/simplewear/TimedActionsFragment.kt
index 504ef6fa..a28114f6 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/TimedActionsFragment.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/TimedActionsFragment.kt
@@ -1,27 +1,34 @@
package com.thewizrd.simplewear
+import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
-import android.view.ActionMode
import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.SelectionPredicates
import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.selection.StableIdKeyProvider
import androidx.recyclerview.selection.StorageStrategy
+import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import androidx.transition.TransitionManager
+import com.google.android.material.transition.MaterialFade
import com.thewizrd.shared_resources.actions.Actions
+import com.thewizrd.shared_resources.utils.ContextUtils.dpToPx
+import com.thewizrd.simplewear.adapters.SpacerAdapter
import com.thewizrd.simplewear.adapters.TimedActionsAdapter
import com.thewizrd.simplewear.databinding.FragmentTimedActionsBinding
import com.thewizrd.simplewear.helpers.AlarmStateManager
@@ -39,49 +46,6 @@ class TimedActionsFragment : Fragment(), SharedPreferences.OnSharedPreferenceCha
private lateinit var onBackPressedCallback: OnBackPressedCallback
- private var actionMode: ActionMode? = null
- private val actionModeCallback = object : ActionMode.Callback {
- override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
- activity?.menuInflater?.inflate(R.menu.selectable_list, menu)
- actionMode = mode
- return true
- }
-
- override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
- return false
- }
-
- override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
- return when (item?.itemId) {
- R.id.action_selectAll -> {
- selectionTracker.setItemsSelected(
- actionAdapter.currentList.map { it.action.actionType.value.toLong() },
- true
- )
- true
- }
-
- R.id.action_delete -> {
- selectionTracker.selection.forEach { itemId ->
- deleteAction(actionId = itemId)
- }
- true
- }
-
- else -> false
- }
- }
-
- override fun onDestroyActionMode(mode: ActionMode?) {
- selectionTracker.clearSelection()
- actionMode = null
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- }
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -89,11 +53,38 @@ class TimedActionsFragment : Fragment(), SharedPreferences.OnSharedPreferenceCha
): View {
binding = FragmentTimedActionsBinding.inflate(inflater, container, false)
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
+ binding.recyclerView.updateLayoutParams {
+ val sysBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ bottomMargin = sysBarInsets.top + sysBarInsets.bottom
+ }
+
+ insets
+ }
+
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
actionAdapter = TimedActionsAdapter()
actionAdapter.setHasStableIds(true)
- binding.recyclerView.adapter = actionAdapter
+ binding.recyclerView.adapter = ConcatAdapter(
+ ConcatAdapter.Config.Builder()
+ .setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
+ .build(),
+ actionAdapter, SpacerAdapter(requireContext().dpToPx(64f).toInt())
+ )
+
+ binding.actionDelete.setOnClickListener {
+ selectionTracker.selection.toList().forEach { itemId ->
+ deleteAction(actionId = itemId)
+ }
+ }
+
+ binding.actionSelectAll.setOnClickListener {
+ selectionTracker.setItemsSelected(
+ actionAdapter.currentList.map { it.action.actionType.value.toLong() },
+ true
+ )
+ }
val keyProvider = StableIdKeyProvider(binding.recyclerView)
val sBuilder = SelectionTracker.Builder(
@@ -128,26 +119,47 @@ class TimedActionsFragment : Fragment(), SharedPreferences.OnSharedPreferenceCha
selectionTracker.addObserver(object : SelectionTracker.SelectionObserver() {
override fun onSelectionChanged() {
- if (selectionTracker.selection.isEmpty) {
+ if (!selectionTracker.hasSelection()) {
onBackPressedCallback.isEnabled = false
- actionMode?.finish()
+ animateToolbar(false)
} else {
onBackPressedCallback.isEnabled = true
- if (actionMode == null) {
- activity?.startActionMode(actionModeCallback)
- }
+ animateToolbar(true)
}
- actionMode?.title = selectionTracker.selection.size().toString()
+ binding.selectionCount.text = selectionTracker.selection.size().toString()
}
+ @SuppressLint("RestrictedApi")
override fun onSelectionCleared() {
onBackPressedCallback.isEnabled = false
- actionMode?.finish()
+ animateToolbar(false)
}
})
val swipeToDeleteHandler = object : SwipeToDeleteCallback(requireContext()) {
+ override fun getSwipeDirs(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder
+ ): Int {
+ return if (viewHolder is TimedActionsAdapter.TimedActionViewHolder) {
+ super.getSwipeDirs(recyclerView, viewHolder)
+ } else {
+ 0
+ }
+ }
+
+ override fun getDragDirs(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder
+ ): Int {
+ return if (viewHolder is TimedActionsAdapter.TimedActionViewHolder) {
+ super.getDragDirs(recyclerView, viewHolder)
+ } else {
+ 0
+ }
+ }
+
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (viewHolder is TimedActionsAdapter.TimedActionViewHolder) {
val ctx = viewHolder.itemView.context
@@ -192,5 +204,13 @@ class TimedActionsFragment : Fragment(), SharedPreferences.OnSharedPreferenceCha
val action = Actions.valueOf(actionId.toInt())
PhoneStatusHelper.removedScheduledTimedAction(ctx, action)
WearableWorker.enqueueAction(ctx, WearableWorker.ACTION_SENDTIMEDACTIONSUPDATE)
+ selectionTracker.deselect(actionId)
+ }
+
+ private fun animateToolbar(visible: Boolean) {
+ TransitionManager.beginDelayedTransition(binding.root, MaterialFade().apply {
+ duration = if (visible) 150 else 84
+ })
+ binding.floatingToolbar.isVisible = visible
}
}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/adapters/SpacerAdapter.kt b/mobile/src/main/java/com/thewizrd/simplewear/adapters/SpacerAdapter.kt
new file mode 100644
index 00000000..eac0593b
--- /dev/null
+++ b/mobile/src/main/java/com/thewizrd/simplewear/adapters/SpacerAdapter.kt
@@ -0,0 +1,42 @@
+package com.thewizrd.simplewear.adapters
+
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.Space
+import androidx.annotation.Px
+import androidx.recyclerview.widget.RecyclerView
+import com.thewizrd.simplewear.R
+
+class SpacerAdapter(@Px private val spacerSize: Int) :
+ RecyclerView.Adapter() {
+
+ init {
+ setHasStableIds(true)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ return object : RecyclerView.ViewHolder(
+ Space(parent.context).apply {
+ layoutParams = RecyclerView.LayoutParams(
+ MATCH_PARENT, spacerSize
+ )
+ }
+ ) {}
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ // no-op
+ }
+
+ override fun getItemCount(): Int {
+ return 1
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return R.id.spacer
+ }
+
+ override fun getItemId(position: Int): Long {
+ return R.id.spacer.toLong()
+ }
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/adapters/TimedActionsAdapter.kt b/mobile/src/main/java/com/thewizrd/simplewear/adapters/TimedActionsAdapter.kt
index aa34fca0..a2e9da80 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/adapters/TimedActionsAdapter.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/adapters/TimedActionsAdapter.kt
@@ -3,6 +3,8 @@ package com.thewizrd.simplewear.adapters
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails
import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.widget.DiffUtil
@@ -49,7 +51,13 @@ class TimedActionsAdapter : ListAdapter {
+ val action: ListChangedAction
+ val newStartingIndex: Int
+ val oldStartingIndex: Int
+ val oldItems: List?
+ val newItems: List?
+
+ constructor(action: ListChangedAction, newStartingIndex: Int, oldStartingIndex: Int) {
+ this.action = action
+ this.newStartingIndex = newStartingIndex
+ this.oldStartingIndex = oldStartingIndex
+ this.oldItems = null
+ this.newItems = null
+ }
+
+ constructor(
+ action: ListChangedAction,
+ newStartingIndex: Int,
+ oldStartingIndex: Int,
+ oldItems: List?,
+ newItems: List?
+ ) {
+ this.action = action
+ this.newStartingIndex = newStartingIndex
+ this.oldStartingIndex = oldStartingIndex
+ this.oldItems = oldItems
+ this.newItems = newItems
+ }
+}
+
+
+interface OnListChangedListener {
+ /**
+ * Called whenever a change of unknown type has occurred, such as the entire list being
+ * set to new values.
+ *
+ * @param sender The changing list.
+ * @param args The data for the onChanged event.
+ */
+ fun onChanged(sender: ArrayList, args: ListChangedArgs)
+}
+
+class CallbackList {
+ private val mCallbacks: MutableList> = mutableListOf()
+
+ fun add(callback: OnListChangedListener) {
+ mCallbacks.add(callback)
+ }
+
+ fun remove(callback: OnListChangedListener) {
+ mCallbacks.remove(callback)
+ }
+
+ fun notifyChange(sender: ArrayList, args: ListChangedArgs) {
+ for (i in mCallbacks.indices) {
+ mCallbacks[i].onChanged(sender, args)
+ }
+ }
+}
+
+open class ObservableArrayList : ArrayList {
+ @delegate:Transient
+ protected val mListeners: CallbackList by lazy { CallbackList() }
+
+ constructor() : super()
+
+ constructor(initialCapacity: Int) : super(initialCapacity)
+
+ constructor(c: MutableCollection) : super(c)
+
+ fun addOnListChangedCallback(listChangedListener: OnListChangedListener) {
+ mListeners.add(listChangedListener)
+ }
+
+ fun removeOnListChangedCallback(listChangedListener: OnListChangedListener) {
+ mListeners.remove(listChangedListener)
+ }
+
+ fun move(oldIndex: Int, newIndex: Int) {
+ super.set(oldIndex, super.set(newIndex, super.get(oldIndex)))
+
+ mListeners.notifyChange(
+ this,
+ ListChangedArgs(ListChangedAction.MOVE, newIndex, oldIndex)
+ )
+ }
+
+ override fun set(index: Int, element: T): T {
+ val oldVal = super.set(index, element)
+ mListeners.notifyChange(
+ this,
+ ListChangedArgs(
+ ListChangedAction.REPLACE,
+ index,
+ index,
+ listOf(oldVal),
+ listOf(element)
+ )
+ )
+ return oldVal
+ }
+
+ override fun add(t: T): Boolean {
+ super.add(t)
+ mListeners.notifyChange(
+ this,
+ ListChangedArgs(ListChangedAction.ADD, size - 1, -1, null, listOf(t))
+ )
+ return true
+ }
+
+ override fun add(index: Int, element: T) {
+ super.add(index, element)
+ mListeners.notifyChange(
+ this,
+ ListChangedArgs(ListChangedAction.ADD, index, -1, null, listOf(element))
+ )
+ }
+
+ override fun removeAt(index: Int): T {
+ val `val` = super.removeAt(index)
+ mListeners.notifyChange(
+ this,
+ ListChangedArgs(ListChangedAction.REMOVE, -1, index, listOf(`val`), null)
+ )
+ return `val`
+ }
+
+ override fun remove(o: T): Boolean {
+ val index = indexOf(o)
+ if (index >= 0) {
+ removeAt(index)
+ return true
+ } else {
+ return false
+ }
+ }
+
+ override fun clear() {
+ val oldSize = size
+ super.clear()
+ if (oldSize != 0) {
+ mListeners.notifyChange(
+ this,
+ ListChangedArgs(ListChangedAction.RESET, -1, -1)
+ )
+ }
+ }
+
+ override fun addAll(c: Collection): Boolean {
+ val oldSize = size
+ val added = super.addAll(c)
+ if (added) {
+ mListeners.notifyChange(
+ this,
+ ListChangedArgs(ListChangedAction.ADD, oldSize - 1, -1, null, LinkedList(c))
+ )
+ }
+ return added
+ }
+
+ override fun addAll(index: Int, c: Collection): Boolean {
+ val added = super.addAll(index, c)
+ if (added) {
+ mListeners.notifyChange(
+ this,
+ ListChangedArgs(ListChangedAction.ADD, index, -1, null, LinkedList(c))
+ )
+ }
+ return added
+ }
+
+ override fun removeRange(fromIndex: Int, toIndex: Int) {
+ val oldItems = this.subList(fromIndex, toIndex)
+ super.removeRange(fromIndex, toIndex)
+ mListeners.notifyChange(
+ this,
+ ListChangedArgs(ListChangedAction.REMOVE, fromIndex, -1, oldItems, null)
+ )
+ }
+
+ override fun removeAll(c: Collection): Boolean {
+ val value = super.removeAll(c)
+ mListeners.notifyChange(
+ this,
+ ListChangedArgs(
+ ListChangedAction.REMOVE,
+ -1,
+ -1,
+ LinkedList(c),
+ null
+ )
+ )
+ return value
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ override fun removeIf(filter: Predicate): Boolean {
+ val value = super.removeIf(filter)
+ mListeners.notifyChange(
+ this,
+ ListChangedArgs(ListChangedAction.REMOVE, -1, -1)
+ )
+ return value
+ }
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/helpers/PhoneStatusHelper.kt b/mobile/src/main/java/com/thewizrd/simplewear/helpers/PhoneStatusHelper.kt
index a82571d9..26da6da6 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/helpers/PhoneStatusHelper.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/helpers/PhoneStatusHelper.kt
@@ -17,8 +17,11 @@ import android.content.IntentFilter
import android.content.pm.PackageManager
import android.hardware.camera2.CameraManager
import android.location.LocationManager
+import android.media.AudioFocusRequest
import android.media.AudioManager
import android.net.wifi.WifiManager
+import android.nfc.NfcAdapter
+import android.nfc.NfcManager
import android.os.BatteryManager
import android.os.Build
import android.os.Handler
@@ -584,6 +587,34 @@ object PhoneStatusHelper {
return if (musicActive) ActionStatus.SUCCESS else ActionStatus.FAILURE
}
+ suspend fun sendPauseMusicCommand(context: Context): ActionStatus {
+ // Send pause event to which ever player has audio focus
+ val audioMan = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ if (audioMan.isMusicActive) {
+ val event = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE)
+ audioMan.dispatchMediaKeyEvent(event)
+ }
+
+ // Use AudioFocus as a fallback
+ if (audioMan.isMusicActive) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+ .build()
+ audioMan.requestAudioFocus(request)
+ audioMan.abandonAudioFocusRequest(request)
+ } else {
+ audioMan.requestAudioFocus(
+ null,
+ AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN
+ )
+ audioMan.abandonAudioFocus(null)
+ }
+ }
+
+ return if (!audioMan.isMusicActive) ActionStatus.SUCCESS else ActionStatus.FAILURE
+ }
+
suspend fun isMusicActive(context: Context, delay: Boolean = true): ActionStatus {
val audioMan = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
@@ -696,6 +727,19 @@ object PhoneStatusHelper {
}
}
+ fun openWirelessSettings(context: Context): ActionStatus {
+ return try {
+ context.startActivity(
+ Intent(Settings.ACTION_WIRELESS_SETTINGS)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ ActionStatus.SUCCESS
+ } catch (e: Exception) {
+ Logger.writeLine(Log.ERROR, e)
+ ActionStatus.FAILURE
+ }
+ }
+
fun isWriteSystemSettingsPermissionEnabled(context: Context): Boolean {
return Settings.System.canWrite(context)
}
@@ -914,6 +958,12 @@ object PhoneStatusHelper {
}
}
+ Actions.NFC -> {
+ if (!checkSecureSettingsPermission(context)) {
+ return ActionStatus.PERMISSION_DENIED
+ }
+ }
+
else -> {}
}
@@ -995,4 +1045,63 @@ object PhoneStatusHelper {
fun setWifiApEnabled(context: Context, enable: Boolean): ActionStatus =
TetherHelper.setWifiApEnabled(context, enable)
+
+ fun checkSecureSettingsPermission(context: Context): Boolean {
+ return ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.WRITE_SECURE_SETTINGS
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+
+ fun isNfcEnabled(context: Context): Boolean {
+ val nfcAdapter = NfcAdapter.getDefaultAdapter(context)
+ return nfcAdapter?.isEnabled ?: false
+ }
+
+ fun setNfcEnabled(context: Context, enable: Boolean): ActionStatus {
+ if (checkSecureSettingsPermission(context)) {
+ val nfcService = context.getSystemService(Context.NFC_SERVICE) as NfcManager
+ return nfcService.defaultAdapter?.let {
+ try {
+ val success = if (enable) it.enable() else it.disable()
+ if (success) {
+ ActionStatus.SUCCESS
+ } else {
+ ActionStatus.FAILURE
+ }
+ } catch (e: SecurityException) {
+ Logger.writeLine(Log.ERROR, e)
+ ActionStatus.PERMISSION_DENIED
+ }
+ } ?: ActionStatus.FAILURE
+ } else {
+ return ActionStatus.PERMISSION_DENIED
+ }
+ }
+
+ fun isBatterySaverEnabled(context: Context): Boolean {
+ val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+ return powerManager.isPowerSaveMode
+ }
+
+ fun setBatterySaverEnabled(context: Context, enable: Boolean): ActionStatus {
+ return if (checkSecureSettingsPermission(context)) {
+ try {
+ val success = Settings.Global.putInt(
+ context.contentResolver, "low_power", if (enable) 1 else 0
+ )
+
+ if (success) {
+ ActionStatus.SUCCESS
+ } else {
+ ActionStatus.FAILURE
+ }
+ } catch (e: SecurityException) {
+ Logger.writeLine(Log.ERROR, e)
+ ActionStatus.PERMISSION_DENIED
+ }
+ } else {
+ ActionStatus.PERMISSION_DENIED
+ }
+ }
}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/helpers/SwipeToDeleteCallback.kt b/mobile/src/main/java/com/thewizrd/simplewear/helpers/SwipeToDeleteCallback.kt
index e3c12f70..83a0a753 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/helpers/SwipeToDeleteCallback.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/helpers/SwipeToDeleteCallback.kt
@@ -5,9 +5,9 @@ import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
-import android.graphics.drawable.ColorDrawable
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.graphics.drawable.toDrawable
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.thewizrd.shared_resources.utils.ContextUtils.getAttrColor
@@ -18,7 +18,7 @@ abstract class SwipeToDeleteCallback(context: Context) :
private val deleteIcon = DrawableCompat.wrap(
ContextCompat.getDrawable(context, R.drawable.ic_delete_outline)!!.mutate()
)
- private val deleteBackground = ColorDrawable(context.getAttrColor(R.attr.colorError))
+ private val deleteBackground = context.getAttrColor(R.attr.colorError).toDrawable()
private val clearPaint = Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/helpers/TetherHelper.kt b/mobile/src/main/java/com/thewizrd/simplewear/helpers/TetherHelper.kt
index 6db29340..cddfacb9 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/helpers/TetherHelper.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/helpers/TetherHelper.kt
@@ -15,8 +15,14 @@ import com.android.dx.stock.ProxyBuilder
import com.thewizrd.shared_resources.actions.ActionStatus
import com.thewizrd.shared_resources.utils.Logger
import com.thewizrd.simplewear.helpers.PhoneStatusHelper.isWriteSystemSettingsPermissionEnabled
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
import java.lang.reflect.Proxy
import java.util.concurrent.Executor
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
object TetherHelper {
private const val TAG = "TetherHelper"
@@ -182,35 +188,7 @@ object TetherHelper {
return false
}
- val codeCacheDir = context.applicationContext.codeCacheDir
- val proxy = try {
- ProxyBuilder.forClass(getConnMgrOnStartTetheringCallbackClass())
- .dexCache(codeCacheDir)
- .handler { proxy, method, args ->
- when (method?.name) {
- "onTetheringStarted" -> {
- Logger.info("Proxy", "onTetheringStarted")
- }
-
- "onTetheringFailed" -> {
- Logger.error(
- "Proxy",
- "onTetheringFailed: args = ${args.contentToString()}"
- )
- }
-
- else -> {
- ProxyBuilder.callSuper(proxy, method, args)
- }
- }
-
- null
- }.build()
- } catch (e: Exception) {
- Logger.error(TAG, e, "startTethering: Error ProxyBuilder")
- return@runCatching false
- }
-
+ val startTetherCallbackClass = getConnMgrOnStartTetheringCallbackClass()
val cm =
context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@@ -218,11 +196,57 @@ object TetherHelper {
"startTethering",
Int::class.java, /* type */
Boolean::class.java, /* showProvisioningUi */
- getConnMgrOnStartTetheringCallbackClass(),
+ startTetherCallbackClass,
Handler::class.java
)
- method.invoke(cm, TETHERING_WIFI, false, proxy, null)
- true
+
+ runBlocking {
+ withTimeout(10000) {
+ suspendCancellableCoroutine { continuation ->
+ val codeCacheDir = context.applicationContext.codeCacheDir
+ val proxy = try {
+ ProxyBuilder.forClass(startTetherCallbackClass)
+ .dexCache(codeCacheDir)
+ .handler { _, method, args ->
+ when (method?.name) {
+ "onTetheringStarted" -> {
+ Logger.info("Proxy", "onTetheringStarted")
+ if (isActive) {
+ continuation.resume(true)
+ }
+ }
+
+ "onTetheringFailed" -> {
+ Logger.error(
+ "Proxy",
+ "onTetheringFailed: args = ${args.contentToString()}"
+ )
+ if (isActive) {
+ continuation.resume(false)
+ }
+ }
+
+ else -> {
+ if (isActive) {
+ continuation.resume(false)
+ }
+ }
+ }
+
+ null
+ }.build()
+ } catch (e: Exception) {
+ Logger.error(TAG, e, "startTethering: Error ProxyBuilder")
+ if (isActive) {
+ continuation.resumeWithException(e)
+ } else {
+ }
+ }
+
+ method.invoke(cm, TETHERING_WIFI, false, proxy, null)
+ }
+ }
+ }
}.getOrElse {
if (it is SecurityException || it.cause is SecurityException) {
Logger.error(TAG, it, "Permission denied starting tethering")
@@ -294,7 +318,8 @@ object TetherHelper {
private fun startTethering(
context: Context,
exemptFromEntitlementCheck: Boolean = true,
- shouldShowEntitlementUi: Boolean = false
+ shouldShowEntitlementUi: Boolean = false,
+ retry: Boolean = true
): Boolean {
Logger.info(TAG, "entering startTethering...")
@@ -305,35 +330,6 @@ object TetherHelper {
}
val tetherCallbackIface = getTetherMgrStartTetheringCallbackInterface()
- val proxy = try {
- Proxy.newProxyInstance(
- tetherCallbackIface.classLoader,
- arrayOf(tetherCallbackIface)
- ) { _, method, args ->
- when (method?.name) {
- "onTetheringStarted" -> {
- Logger.info("Proxy", "onTetheringStarted")
- }
-
- "onTetheringFailed" -> {
- val resultCode = args[0] as Int
-
- if (resultCode == TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION) {
- // retry
- startTethering(context, false, shouldShowEntitlementUi)
- } else {
- Logger.error("Proxy", "onTetheringFailed: code = $resultCode")
- }
- }
- }
-
- null
- }
- } catch (e: Exception) {
- Logger.error(TAG, e, "startTethering: Error Proxy")
- return@runCatching false
- }
-
val tetheringMgr = context.applicationContext.getSystemService(TETHERING_SERVICE)
val tetheringMgrClass = Class.forName("android.net.TetheringManager")
@@ -341,15 +337,70 @@ object TetherHelper {
"startTethering",
Class.forName("android.net.TetheringManager\$TetheringRequest"), /* request */
Executor::class.java,
- getTetherMgrStartTetheringCallbackInterface()
- )
- method.invoke(
- tetheringMgr,
- createTetheringRequest(exemptFromEntitlementCheck, shouldShowEntitlementUi),
- Executor { it.run() },
- proxy
+ tetherCallbackIface
)
- true
+
+ runBlocking {
+ withTimeout(10000) {
+ suspendCancellableCoroutine { continuation ->
+ val proxy = try {
+ Proxy.newProxyInstance(
+ tetherCallbackIface.classLoader,
+ arrayOf(tetherCallbackIface)
+ ) { _, method, args ->
+ when (method?.name) {
+ "onTetheringStarted" -> {
+ Logger.info("Proxy", "onTetheringStarted")
+ if (isActive) {
+ continuation.resume(true)
+ }
+ }
+
+ "onTetheringFailed" -> {
+ val resultCode = args[0] as Int
+
+ if (resultCode == TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION && retry) {
+ // retry
+ startTethering(
+ context,
+ exemptFromEntitlementCheck = false,
+ shouldShowEntitlementUi = shouldShowEntitlementUi,
+ retry = false
+ )
+ } else {
+ Logger.error(
+ "Proxy",
+ "onTetheringFailed: code = $resultCode"
+ )
+ if (isActive) {
+ continuation.resume(false)
+ }
+ }
+ }
+ }
+
+ null
+ }
+ } catch (e: Exception) {
+ Logger.error(TAG, e, "startTethering: Error Proxy")
+ if (isActive) {
+ continuation.resumeWithException(e)
+ } else {
+ }
+ }
+
+ method.invoke(
+ tetheringMgr,
+ createTetheringRequest(
+ exemptFromEntitlementCheck,
+ shouldShowEntitlementUi
+ ),
+ Executor { it.run() },
+ proxy
+ )
+ }
+ }
+ }
}.getOrElse {
if (it is SecurityException || it.cause is SecurityException) {
Logger.error(TAG, it, "Permission denied starting tethering")
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/media/MediaControllerService.kt b/mobile/src/main/java/com/thewizrd/simplewear/media/MediaControllerService.kt
index 080918ee..e5696427 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/media/MediaControllerService.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/media/MediaControllerService.kt
@@ -34,6 +34,7 @@ import android.util.Log
import android.util.TypedValue
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
+import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.scale
@@ -187,7 +188,7 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
private fun createForegroundNotification(context: Context): Notification {
val notif = NotificationCompat.Builder(context, NOT_CHANNEL_ID).apply {
- setSmallIcon(R.drawable.ic_baseline_music_note_24)
+ setSmallIcon(R.drawable.ic_music_note_white_24dp)
setContentTitle(context.getString(R.string.not_title_mediacontroller_running))
setOnlyAlertOnce(true)
setSilent(true)
@@ -270,18 +271,18 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
val isAutoLaunch = intent.getBooleanExtra(EXTRA_AUTOLAUNCH, false)
val isSoftLaunch = intent.getBooleanExtra(EXTRA_SOFTLAUNCH, false)
- scope.launch {
- if ((isAutoLaunch || selectedPackageName == mSelectedMediaApp?.packageName) && mController != null) return@launch
-
- if (!selectedPackageName.isNullOrBlank()) {
- mSelectedMediaApp = mAvailableMediaApps.find {
- it.packageName == selectedPackageName
+ if (!(isAutoLaunch && mController != null && mSelectedMediaApp?.packageName != mController?.packageName) && !(selectedPackageName != null && selectedPackageName == mSelectedMediaApp?.packageName && mController?.packageName == selectedPackageName)) {
+ scope.launch {
+ if (!selectedPackageName.isNullOrBlank()) {
+ mSelectedMediaApp = mAvailableMediaApps.find {
+ it.packageName == selectedPackageName
+ }
+ mSelectedPackageName = mSelectedMediaApp?.packageName
+ connectMediaSession(isSoftLaunch)
+ } else {
+ mSelectedPackageName = null
+ findActiveMediaSession()
}
- mSelectedPackageName = mSelectedMediaApp?.packageName
- connectMediaSession(isSoftLaunch)
- } else {
- mSelectedPackageName = null
- findActiveMediaSession()
}
}
}
@@ -311,7 +312,7 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
disconnectMedia(invalidateData = true)
- stopForeground(true)
+ ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
scope.cancel()
super.onDestroy()
}
@@ -375,8 +376,7 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
// Check if active session has changed
if (mSelectedPackageName == null) {
// If so reset
- disconnectMedia(invalidateData = true)
- mSelectedPackageName = firstActiveCtrlr.packageName
+ mSelectedPackageName = null
mSelectedMediaApp = null
}
}
@@ -390,8 +390,11 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
activeSessions, packageManager, resources
)
- val activeMediaApp =
+ val activeMediaApp = if (mSelectedPackageName != null) {
actionSessionDetails.find { it.packageName == mSelectedPackageName }
+ } else {
+ actionSessionDetails.find { it.packageName == firstActiveCtrlr?.packageName }
+ }
if (activeMediaApp?.sessionToken != null || mBrowser?.isConnected == true) {
if (activeMediaApp != null) mSelectedMediaApp = activeMediaApp
@@ -422,7 +425,7 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
if (!isActive) return@launch
- if (!mediaBrowserServices.isNullOrEmpty()) {
+ if (mediaBrowserServices.isNotEmpty()) {
mediaBrowserServices.forEach { s ->
mAvailableMediaApps.add(
MediaAppDetails(
@@ -435,7 +438,7 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
}
if (!mSelectedPackageName.isNullOrBlank()) {
- mAvailableMediaApps.find {
+ mSelectedMediaApp = mAvailableMediaApps.find {
it.packageName == mSelectedPackageName
}
@@ -621,7 +624,11 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
private fun onUpdateQueue() {
mController?.let {
- mQueueItemsAdapter.setQueueItems(it, it.queue)
+ val queueItems = runCatching {
+ it.queue
+ }.getOrDefault(emptyList())
+
+ mQueueItemsAdapter.setQueueItems(it, queueItems)
} ?: run {
Logger.error(TAG, "Failed to update queue info, null MediaController.")
mQueueItemsAdapter.clear()
@@ -631,7 +638,7 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
private fun sendControllerUnavailable() {
scope.launch {
- sendMediaPlayerState()
+ disconnectMedia(invalidateData = true)
}
}
@@ -1070,7 +1077,7 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
runCatching {
val iconDrawable = ContextCompat.getDrawable(
applicationContext,
- R.drawable.ic_baseline_play_circle_filled_24
+ R.drawable.ic_play_circle_filled_white_24dp
)
actions.add(
ActionItem(
@@ -1198,7 +1205,7 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
if (!isActive) return@launch
- if (mNodes.size == 0 || mItems.isNullOrEmpty()) {
+ if (mNodes.isEmpty() || mItems.isNullOrEmpty()) {
// Remove all items (datamap)
sendDataByChannel(itemNodePath, null, BrowseMediaItems::class.java)
return@launch
@@ -1206,10 +1213,13 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
// Send media items to datamap
val mediaItems = mItems?.map {
+ val size = dpToPx(48f).toInt()
+
MediaItem(
mediaId = it.mediaId ?: "",
title = it.description.title.toString(),
- icon = it.description.iconBitmap?.toByteArray()
+ subTitle = it.description.subtitle?.toString(),
+ icon = it.description.iconBitmap?.scale(size, size)?.toByteArray()
)
}
@@ -1274,7 +1284,7 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
}
protected open fun unsubscribe() {
- if (mNodes.size > 0) {
+ if (mNodes.isNotEmpty()) {
mBrowser!!.unsubscribe(mNodes.peek(), callback)
}
updateItems(null)
@@ -1318,10 +1328,13 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene
// Send action items to datamap
val queueItems = mQueueItems.map {
+ val size = dpToPx(48f).toInt()
+
QueueItem(
queueId = it.queueId,
title = it.description.title.toString(),
- icon = it.description.iconBitmap?.toByteArray()
+ subTitle = it.description.subtitle?.toString(),
+ icon = it.description.iconBitmap?.scale(size, size)?.toByteArray()
)
}
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/services/CallControllerService.kt b/mobile/src/main/java/com/thewizrd/simplewear/services/CallControllerService.kt
index b865c321..b1f8a817 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/services/CallControllerService.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/services/CallControllerService.kt
@@ -16,6 +16,7 @@ import android.os.Handler
import android.os.Looper
import android.telecom.CallAudioState
import android.telecom.TelecomManager
+import android.telecom.VideoProfile
import android.telephony.PhoneStateListener
import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
@@ -23,7 +24,9 @@ import android.util.Log
import android.view.KeyEvent
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
+import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.toBitmapOrNull
import androidx.lifecycle.LifecycleService
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.MessageEvent
@@ -31,6 +34,7 @@ import com.google.android.gms.wearable.Wearable
import com.thewizrd.shared_resources.data.CallState
import com.thewizrd.shared_resources.helpers.InCallUIHelper
import com.thewizrd.shared_resources.helpers.toImmutableCompatFlag
+import com.thewizrd.shared_resources.utils.ImageUtils.toByteArray
import com.thewizrd.shared_resources.utils.JSONParser
import com.thewizrd.shared_resources.utils.Logger
import com.thewizrd.shared_resources.utils.booleanToBytes
@@ -38,10 +42,13 @@ import com.thewizrd.shared_resources.utils.bytesToBool
import com.thewizrd.shared_resources.utils.bytesToChar
import com.thewizrd.shared_resources.utils.stringToBytes
import com.thewizrd.simplewear.R
+import com.thewizrd.simplewear.helpers.ContactsHelper
import com.thewizrd.simplewear.helpers.PhoneStatusHelper
import com.thewizrd.simplewear.preferences.Settings
+import com.thewizrd.simplewear.services.OngoingCall.callStateCompat
import com.thewizrd.simplewear.wearable.WearableManager
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
@@ -49,6 +56,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
class CallControllerService : LifecycleService(), MessageClient.OnMessageReceivedListener,
@@ -86,6 +94,7 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
const val ACTION_DISCONNECTCONTROLLER = "SimpleWear.Droid.action.DISCONNECT_CONTROLLER"
private const val ACTION_TOGGLEMUTE = "SimpleWear.Droid.action.TOGGLE_MUTE"
private const val ACTION_TOGGLESPEAKER = "SimpleWear.Droid.action.TOGGLE_SPEAKER"
+ private const val ACTION_ANSWERCALL = "SimpleWear.Droid.action.ANSWER_CALL"
private const val ACTION_HANGUPCALL = "SimpleWear.Droid.action.HANGUP_CALL"
private const val EXTRA_TOGGLESTATE = "SimpleWear.Droid.extra.TOGGLE_STATE"
@@ -162,7 +171,9 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
return mForegroundNotification!!
}
- private fun updateNotification(context: Context, callActive: Boolean) {
+ private fun updateNotification(context: Context, callState: Int) {
+ val callActive = callState != TelephonyManager.CALL_STATE_IDLE
+
val notif = NotificationCompat.Builder(context, NOT_CHANNEL_ID).apply {
setSmallIcon(R.drawable.ic_settings_phone_24dp)
setContentTitle(context.getString(if (callActive) R.string.message_callactive else R.string.not_title_callcontroller_running))
@@ -202,6 +213,18 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
)
)
}
+ if (callState == TelephonyManager.CALL_STATE_RINGING) {
+ addAction(
+ 0,
+ context.getString(R.string.call_notification_answer_action),
+ PendingIntent.getService(
+ context, ACTION_ANSWERCALL.hashCode(),
+ Intent(context, CallControllerService::class.java)
+ .setAction(ACTION_ANSWERCALL),
+ PendingIntent.FLAG_UPDATE_CURRENT.toImmutableCompatFlag()
+ )
+ )
+ }
addAction(
0,
context.getString(R.string.action_hangup),
@@ -253,6 +276,11 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
registerMediaControllerListener()
registerPhoneStateListener()
+ OngoingCall.callLiveData.observe(this) {
+ scope.launch {
+ sendCallState(it?.callStateCompat)
+ }
+ }
OngoingCall.callState.observe(this) {
scope.launch {
onCallStateChanged(it)
@@ -274,6 +302,13 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
}
}
}
+ OngoingCall.callNotificationLiveData.observe(this) {
+ scope.launch {
+ if (isInCall()) {
+ sendCallState(it?.callState)
+ }
+ }
+ }
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -298,7 +333,7 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
}
// Send call state
- sendCallState(mTelephonyManager.callState, "")
+ sendCallState(mTelephonyManager.callStateCompat)
mWearableManager.sendMessage(
null,
InCallUIHelper.MuteMicStatusPath,
@@ -358,7 +393,7 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
mMessageClient.removeListener(this)
mWearableManager.unregister()
- stopForeground(true)
+ ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
scope.cancel()
super.onDestroy()
}
@@ -374,13 +409,14 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
}
}
+ @Suppress("DEPRECATION")
private fun registerPhoneStateListener() {
mTelephonyManager?.let { tm ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
mTelephonyCallback =
object : TelephonyCallback(), TelephonyCallback.CallStateListener {
override fun onCallStateChanged(state: Int) {
- this@CallControllerService.onCallStateChanged(state, "")
+ this@CallControllerService.onCallStateChanged(state)
}
}
@@ -403,14 +439,12 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
mPhoneStateListener = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
object : PhoneStateListener(Executors.newSingleThreadExecutor()) {
override fun onCallStateChanged(state: Int, phoneNumber: String?) {
- super.onCallStateChanged(state, phoneNumber)
this@CallControllerService.onCallStateChanged(state, phoneNumber)
}
}
} else {
object : PhoneStateListener() {
override fun onCallStateChanged(state: Int, phoneNumber: String?) {
- super.onCallStateChanged(state, phoneNumber)
this@CallControllerService.onCallStateChanged(state, phoneNumber)
}
}
@@ -432,6 +466,7 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
}
}
+ @Suppress("DEPRECATION")
private fun unregisterPhoneStateListener() {
mTelephonyManager?.let { tm ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -483,7 +518,7 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
scope.launch {
sendCallState(newState, phoneNo)
}
- updateNotification(this, newState != TelephonyManager.CALL_STATE_IDLE)
+ updateNotification(this, newState)
}
private suspend fun sendCallState(state: Int? = null, phoneNo: String? = null) {
@@ -491,17 +526,22 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
OngoingCall.call?.details?.contactDisplayName
} else {
null
- } ?: OngoingCall.call?.details?.callerDisplayName ?: phoneNo ?: ""
+ } ?: OngoingCall.call?.details?.callerDisplayName
+ ?: OngoingCall.currentCallNotification?.callerName
+ ?: getContactName(phoneNo) ?: ""
- val callState = state ?: OngoingCall.call?.let {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- it.details.state
- } else {
- it.state
- }
- } ?: TelephonyManager.CALL_STATE_IDLE
+ val callStartTime = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ OngoingCall.call?.details?.creationTimeMillis
+ } else {
+ OngoingCall.call?.details?.connectTimeMillis
+ } ?: OngoingCall.currentCallNotification?.notifWhen ?: -1L
- val callActive = callState != TelephonyManager.CALL_STATE_IDLE
+ val callState = state
+ ?: OngoingCall.call?.callStateCompat
+ ?: OngoingCall.currentCallNotification?.callState
+ ?: TelephonyManager.CALL_STATE_IDLE
+
+ val callActive = callState != TelephonyManager.CALL_STATE_IDLE || isInCall()
var supportedFeatures = 0
if (supportsSpeakerToggle()) {
@@ -511,18 +551,43 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
supportedFeatures += InCallUIHelper.INCALL_FEATURE_DTMF
}
+ var callerBitmap: ByteArray? = null
+
+ runCatching {
+ val contactPhotoUri = OngoingCall.call?.details?.contactPhotoUri
+
+ callerBitmap = if (contactPhotoUri != null) {
+ withContext(Dispatchers.IO) {
+ ContactsHelper.getContactPhotoData(applicationContext, contactPhotoUri)
+ }
+ } else {
+ OngoingCall.currentCallNotification?.callerPhotoIcon?.let {
+ withContext(Dispatchers.IO) {
+ it.loadDrawable(applicationContext)
+ ?.toBitmapOrNull()
+ ?.toByteArray()
+ }
+ }
+ }
+ }
+
val callStateData = CallState(
callerName = callerName,
+ callerBitmap = callerBitmap,
callActive = callActive,
+ callState = callState,
+ callStartTime = callStartTime,
supportedFeatures = supportedFeatures
)
sendCallState(nodeID = null, callStateData)
if (Settings.isBridgeCallsEnabled()) {
+ val callStateJson = JSONParser.serializer(callStateData, CallState::class.java)
+
mWearableManager.sendMessage(
null,
InCallUIHelper.CallStateBridgePath,
- callActive.booleanToBytes()
+ callActive.booleanToBytes() + callStateJson.stringToBytes()
)
}
}
@@ -539,9 +604,12 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
when (messageEvent.path) {
InCallUIHelper.CallStatePath -> {
scope.launch {
- sendCallState(mTelephonyManager.callState, "")
+ sendCallState(mTelephonyManager.callStateCompat)
}
}
+ InCallUIHelper.AnswerCallPath -> {
+ sendAnswerEvent()
+ }
InCallUIHelper.EndCallPath -> {
sendHangupEvent()
}
@@ -593,6 +661,25 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
}
}
+ private fun sendAnswerEvent() {
+ OngoingCall.call?.let {
+ it.answer(it.details?.videoState ?: VideoProfile.STATE_AUDIO_ONLY)
+ } ?: run {
+ mTelecomMediaCtrlr?.dispatchMediaButtonEvent(
+ KeyEvent(
+ KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_HEADSETHOOK
+ )
+ )
+ mTelecomMediaCtrlr?.dispatchMediaButtonEvent(
+ KeyEvent(
+ KeyEvent.ACTION_UP,
+ KeyEvent.KEYCODE_HEADSETHOOK
+ )
+ )
+ }
+ }
+
private fun sendHangupEvent() {
OngoingCall.call?.disconnect() ?: run {
mTelecomMediaCtrlr?.dispatchMediaButtonEvent(
@@ -623,7 +710,7 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
isMicrophoneMute().booleanToBytes()
)
}
- updateNotification(this, isInCall())
+ updateNotification(this, mTelephonyManager.callStateCompat)
}
private fun isMicrophoneMute(): Boolean {
@@ -634,7 +721,6 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
}
}
- @RequiresApi(Build.VERSION_CODES.S)
private fun toggleSpeakerphone(nodeID: String? = null, on: Boolean) {
mInCallManagerAdapter.setSpeakerPhoneEnabled(on)
scope.launch {
@@ -644,10 +730,9 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
isSpeakerPhoneEnabled().booleanToBytes()
)
}
- updateNotification(this, isInCall())
+ updateNotification(this, mTelephonyManager.callStateCompat)
}
- @RequiresApi(Build.VERSION_CODES.S)
private fun isSpeakerPhoneEnabled(): Boolean {
return mInCallManagerAdapter.getAudioState()?.route == CallAudioState.ROUTE_SPEAKER
}
@@ -662,7 +747,7 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
private fun isInCall(): Boolean {
return runCatching {
mTelecomManager.isInCall
- }.getOrDefault(mTelephonyManager.callState != TelephonyManager.CALL_STATE_IDLE)
+ }.getOrDefault(mTelephonyManager.callStateCompat != TelephonyManager.CALL_STATE_IDLE)
}
@SuppressLint("MissingPermission")
@@ -671,4 +756,25 @@ class CallControllerService : LifecycleService(), MessageClient.OnMessageReceive
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && mInCallManagerAdapter.isInCallServiceAvailable()
}.getOrDefault(false)
}
+
+ private fun getContactName(phoneNo: String? = null): String? {
+ Logger.debug(TAG, "Contact name: $phoneNo")
+
+ return phoneNo
+ }
+
+ @Suppress("DEPRECATION")
+ @get:SuppressLint("MissingPermission")
+ private val TelephonyManager.callStateCompat: Int
+ get() {
+ return runCatching {
+ /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ this.callStateForSubscription
+ } else {*/
+ this.callState
+ //}
+ }.onFailure {
+ Logger.error(TAG, it)
+ }.getOrDefault(TelephonyManager.CALL_STATE_IDLE)
+ }
}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/services/InCallManagerService.kt b/mobile/src/main/java/com/thewizrd/simplewear/services/InCallManagerService.kt
index b1d5d43d..f8fdc7c5 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/services/InCallManagerService.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/services/InCallManagerService.kt
@@ -8,15 +8,20 @@ import android.os.IBinder
import android.os.Looper
import android.telecom.Call
import android.telecom.CallAudioState
+import android.telecom.CallEndpoint
import android.telecom.InCallService
import android.telecom.TelecomManager
import android.telephony.TelephonyManager
+import androidx.annotation.DeprecatedSinceApi
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.lifecycle.MutableLiveData
import com.thewizrd.shared_resources.utils.Logger
+import com.thewizrd.simplewear.helpers.ListChangedArgs
+import com.thewizrd.simplewear.helpers.ObservableArrayList
+import com.thewizrd.simplewear.helpers.OnListChangedListener
+import java.util.concurrent.Executors
-@RequiresApi(Build.VERSION_CODES.S)
class InCallManagerService : InCallService() {
companion object {
@RequiresApi(Build.VERSION_CODES.S)
@@ -27,6 +32,9 @@ class InCallManagerService : InCallService() {
}
private var previousAudioRoute: Int? = null
+ private var previousCallEndpoint: CallEndpoint? = null
+ private val availableCallEndpoints: MutableList = mutableListOf()
+ private var isMuted = false
override fun onCallAdded(call: Call?) {
OngoingCall.call = call
@@ -37,6 +45,8 @@ class InCallManagerService : InCallService() {
OngoingCall.call = null
}
+ @Deprecated("Deprecated in Java")
+ @DeprecatedSinceApi(api = 34)
override fun onCallAudioStateChanged(audioState: CallAudioState?) {
if (audioState?.route != CallAudioState.ROUTE_SPEAKER) {
previousAudioRoute = audioState?.route
@@ -44,14 +54,61 @@ class InCallManagerService : InCallService() {
OngoingCall.callAudioState.postValue(audioState)
}
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun onCallEndpointChanged(callEndpoint: CallEndpoint) {
+ if (callEndpoint.endpointType != CallEndpoint.TYPE_SPEAKER) {
+ previousAudioRoute = CallAudioState.ROUTE_SPEAKER
+ previousCallEndpoint = callEndpoint
+ }
+ OngoingCall.callAudioState.postValue(createAudioState())
+ }
+
+ override fun onMuteStateChanged(isMuted: Boolean) {
+ this.isMuted = isMuted
+ }
+
+ override fun onAvailableCallEndpointsChanged(availableEndpoints: List) {
+ availableCallEndpoints.clear()
+ availableCallEndpoints.addAll(availableEndpoints)
+ }
+
+ @Suppress("DEPRECATION")
fun setSpeakerPhoneEnabled(enabled: Boolean) {
- setAudioRoute(
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (enabled) {
- CallAudioState.ROUTE_SPEAKER
+ val speakerEndpoint =
+ availableCallEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_SPEAKER }
+
+ if (speakerEndpoint != null) {
+ requestCallEndpointChange(
+ speakerEndpoint,
+ Executors.newSingleThreadExecutor()
+ ) {}
+ } else {
+ setAudioRoute(CallAudioState.ROUTE_SPEAKER)
+ }
} else {
- previousAudioRoute ?: CallAudioState.ROUTE_EARPIECE
+ val targetEndpoint = previousCallEndpoint
+ ?: availableCallEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_EARPIECE }
+
+ if (targetEndpoint != null) {
+ requestCallEndpointChange(
+ targetEndpoint,
+ Executors.newSingleThreadExecutor()
+ ) {}
+ } else {
+ setAudioRoute(previousAudioRoute ?: CallAudioState.ROUTE_EARPIECE)
+ }
}
- )
+ } else {
+ setAudioRoute(
+ if (enabled) {
+ CallAudioState.ROUTE_SPEAKER
+ } else {
+ previousAudioRoute ?: CallAudioState.ROUTE_EARPIECE
+ }
+ )
+ }
}
override fun onBind(intent: Intent?): IBinder? {
@@ -66,9 +123,36 @@ class InCallManagerService : InCallService() {
return result
}
+
+ @Suppress("DEPRECATION")
+ internal fun createAudioState(): CallAudioState? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ currentCallEndpoint?.let { endpoint ->
+ CallAudioState(
+ isMuted,
+ endpoint.getAudioRoute(),
+ availableCallEndpoints.map { it.getAudioRoute() }
+ .reduceOrNull { acc, i -> acc or i } ?: 0
+ )
+ }
+ } else {
+ callAudioState
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private fun CallEndpoint.getAudioRoute(): Int {
+ return when (this.endpointType) {
+ CallEndpoint.TYPE_BLUETOOTH -> CallAudioState.ROUTE_BLUETOOTH
+ CallEndpoint.TYPE_EARPIECE -> CallAudioState.ROUTE_EARPIECE
+ CallEndpoint.TYPE_SPEAKER -> CallAudioState.ROUTE_SPEAKER
+ CallEndpoint.TYPE_STREAMING -> CallAudioState.ROUTE_STREAMING
+ CallEndpoint.TYPE_WIRED_HEADSET -> CallAudioState.ROUTE_WIRED_HEADSET
+ else -> -1
+ }
+ }
}
-@RequiresApi(Build.VERSION_CODES.S)
class InCallManagerAdapter private constructor() {
private var mInCallService: InCallManagerService? = null
@@ -121,15 +205,15 @@ class InCallManagerAdapter private constructor() {
}
fun getAudioState(): CallAudioState? {
- return mInCallService?.callAudioState
+ return mInCallService?.createAudioState()
}
}
-@RequiresApi(Build.VERSION_CODES.S)
object OngoingCall {
val callLiveData = MutableLiveData()
val callState = MutableLiveData()
val callAudioState = MutableLiveData()
+ val callNotificationLiveData = MutableLiveData()
private val callback = object : Call.Callback() {
override fun onStateChanged(call: Call?, state: Int) {
@@ -138,26 +222,59 @@ object OngoingCall {
}
}
+ @Suppress("DEPRECATION")
var call: Call? = null
internal set(value) {
field?.unregisterCallback(callback)
callLiveData.postValue(value)
if (value != null) {
value.registerCallback(callback)
- callState.postValue(
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- value.details.state
- } else {
- value.state
- }
- )
+ callState.postValue(value.callStateCompat)
} else {
callState.postValue(TelephonyManager.CALL_STATE_IDLE)
}
field = value
}
- fun hangup() {
- call?.disconnect()
+ internal val callNotifications =
+ ObservableArrayList()
+
+ var currentCallNotification: NotificationListener.CallNotificationData? = null
+ internal set(value) {
+ if (value?.key != field?.key) {
+ callNotificationLiveData.postValue(value)
+ field = value
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ val Call.callStateCompat: Int
+ get() {
+ val state = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ this.details.state
+ } else {
+ this.state
+ }
+
+ return when (state) {
+ Call.STATE_DIALING,
+ Call.STATE_HOLDING,
+ Call.STATE_ACTIVE -> TelephonyManager.CALL_STATE_OFFHOOK
+
+ Call.STATE_RINGING -> TelephonyManager.CALL_STATE_RINGING
+ else -> TelephonyManager.CALL_STATE_IDLE
+ }
+ }
+
+ init {
+ callNotifications.addOnListChangedCallback(object :
+ OnListChangedListener {
+ override fun onChanged(
+ sender: ArrayList,
+ args: ListChangedArgs
+ ) {
+ currentCallNotification = sender.lastOrNull()
+ }
+ })
}
}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/services/NotificationListener.kt b/mobile/src/main/java/com/thewizrd/simplewear/services/NotificationListener.kt
index 3e06fa6a..11566e4e 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/services/NotificationListener.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/services/NotificationListener.kt
@@ -1,11 +1,18 @@
package com.thewizrd.simplewear.services
+import android.app.Notification
import android.app.NotificationManager
+import android.app.Person
import android.content.ComponentName
import android.content.Context
+import android.graphics.drawable.Icon
import android.os.Build
import android.service.notification.NotificationListenerService
+import android.service.notification.StatusBarNotification
+import android.telephony.TelephonyManager
import androidx.core.app.NotificationManagerCompat
+import androidx.core.os.BundleCompat
+import com.thewizrd.shared_resources.utils.Logger
/**
* A notification listener service to allows us to grab active media sessions from their
@@ -32,4 +39,74 @@ class NotificationListener : NotificationListenerService() {
return ComponentName(context, NotificationListener::class.java)
}
}
+
+ data class CallNotificationData(
+ val key: String,
+ val callType: Int?,
+ val callerName: String?,
+ val callerPhotoIcon: Icon? = null,
+ val notifWhen: Long = 0
+ ) {
+ val callState: Int?
+ get() = when (callType) {
+ Notification.CallStyle.CALL_TYPE_INCOMING -> TelephonyManager.CALL_STATE_RINGING
+ Notification.CallStyle.CALL_TYPE_ONGOING -> TelephonyManager.CALL_STATE_OFFHOOK
+ null -> null
+ else -> TelephonyManager.CALL_STATE_IDLE
+ }
+ }
+
+ override fun onNotificationPosted(sbn: StatusBarNotification?) {
+ if (sbn != null && sbn.isOngoing && sbn.notification.category == Notification.CATEGORY_CALL) {
+ val callType = sbn.notification.extras.getInt(Notification.EXTRA_CALL_TYPE, -1)
+ val person = BundleCompat.getParcelable(
+ sbn.notification.extras,
+ Notification.EXTRA_CALL_PERSON,
+ Person::class.java
+ )
+ val title = sbn.notification.extras.getString(Notification.EXTRA_TITLE)
+
+ Logger.debug("NotificationListener", "call notification rec'vd")
+ Logger.debug("NotificationListener", "key = ${sbn.key}")
+ Logger.debug("NotificationListener", "callType = $callType")
+ Logger.debug("NotificationListener", "person = ${person?.name}")
+ Logger.debug("NotificationListener", "title = $title")
+
+ val data = CallNotificationData(
+ key = sbn.key,
+ callType = if (callType >= 0) callType else null,
+ callerName = person?.name?.toString() ?: title,
+ callerPhotoIcon = person?.icon ?: sbn.notification.getLargeIcon(),
+ notifWhen = sbn.notification.`when`
+ )
+
+ val existingIndex = OngoingCall.callNotifications.indexOfLast { it.key == sbn.key }
+ if (existingIndex < 0) {
+ OngoingCall.callNotifications.add(data)
+ } else {
+ OngoingCall.callNotifications[existingIndex] = data
+ }
+ }
+ }
+
+ override fun onNotificationRemoved(sbn: StatusBarNotification?) {
+ if (sbn != null && sbn.isOngoing && sbn.notification.category == Notification.CATEGORY_CALL) {
+ val callType = sbn.notification.extras.getInt(Notification.EXTRA_CALL_TYPE, -1)
+ val person = BundleCompat.getParcelable(
+ sbn.notification.extras,
+ Notification.EXTRA_CALL_PERSON,
+ Person::class.java
+ )
+ val title = sbn.notification.extras.getString(Notification.EXTRA_TITLE)
+
+ Logger.debug("NotificationListener", "call notification removed")
+ Logger.debug("NotificationListener", "removed key = ${sbn.key}")
+ Logger.debug("NotificationListener", "removed callType = $callType")
+ Logger.debug("NotificationListener", "removed person = ${person?.name}")
+ Logger.debug("NotificationListener", "removed title = $title")
+
+
+ OngoingCall.callNotifications.removeIf { it.key == sbn.key }
+ }
+ }
}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt b/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt
index 39f64933..105488a1 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt
@@ -214,9 +214,9 @@ class WearableDataListenerService : WearableListenerService() {
/* InCall Actions */
else if (messageEvent.path == InCallUIHelper.ConnectPath) {
if (PhoneStatusHelper.callStatePermissionEnabled(ctx) &&
- (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
- PhoneStatusHelper.companionDeviceAssociated(ctx))
+ (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || PhoneStatusHelper.companionDeviceAssociated(
+ ctx
+ ))
) {
CallControllerService.enqueueWork(
ctx, Intent(ctx, CallControllerService::class.java)
diff --git a/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableManager.kt b/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableManager.kt
index cb4bd802..cc556f62 100644
--- a/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableManager.kt
+++ b/mobile/src/main/java/com/thewizrd/simplewear/wearable/WearableManager.kt
@@ -9,7 +9,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
-import android.content.pm.ResolveInfo
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
@@ -267,29 +266,32 @@ class WearableManager(private val mContext: Context) : OnCapabilityChangedListen
}
suspend fun sendSupportedMusicPlayers(nodeID: String) {
- val mediaBrowserInfos = mContext.packageManager.queryIntentServices(
+ val appInfos = mutableListOf()
+
+ mContext.packageManager.queryIntentServices(
Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE),
PackageManager.GET_RESOLVED_FILTER
- )
+ ).mapTo(appInfos) { it.serviceInfo.applicationInfo }
val activeSessions = MediaAppControllerUtils.getActiveMediaSessions(
mContext,
NotificationListener.getComponentName(mContext)
)
- val activeMediaInfos = MediaAppControllerUtils.getMediaAppsFromControllers(
+ MediaAppControllerUtils.getMediaAppsFromControllers(
mContext,
activeSessions
- )
+ ).run { appInfos.addAll(this) }
+
val activeController =
activeSessions.firstOrNull { it.playbackState?.isPlaybackStateActive() == true }
// Sort result
Collections.sort(
- mediaBrowserInfos,
- ResolveInfo.DisplayNameComparator(mContext.packageManager)
+ appInfos,
+ ApplicationInfo.DisplayNameComparator(mContext.packageManager)
)
- val supportedPlayers = ArrayList(mediaBrowserInfos.size)
+ val supportedPlayers = ArrayList(appInfos.size)
val musicPlayers = mutableSetOf()
var activePlayerKey: String? = null
@@ -333,12 +335,7 @@ class WearableManager(private val mContext: Context) : OnCapabilityChangedListen
}
}
- for (info in mediaBrowserInfos) {
- val appInfo = info.serviceInfo.applicationInfo
- addPlayerInfo(appInfo)
- }
-
- for (info in activeMediaInfos) {
+ for (info in appInfos) {
addPlayerInfo(info)
}
@@ -686,6 +683,24 @@ class WearableManager(private val mContext: Context) : OnCapabilityChangedListen
)
}
+ Actions.NFC -> {
+ action = ToggleAction(act, PhoneStatusHelper.isNfcEnabled(mContext))
+ sendMessage(
+ nodeID,
+ WearableHelper.ActionsPath,
+ JSONParser.serializer(action, Action::class.java).stringToBytes()
+ )
+ }
+
+ Actions.BATTERYSAVER -> {
+ action = ToggleAction(act, PhoneStatusHelper.isBatterySaverEnabled(mContext))
+ sendMessage(
+ nodeID,
+ WearableHelper.ActionsPath,
+ JSONParser.serializer(action, Action::class.java).stringToBytes()
+ )
+ }
+
else -> {}
}
}
@@ -868,11 +883,59 @@ class WearableManager(private val mContext: Context) : OnCapabilityChangedListen
Actions.DONOTDISTURB -> {
if (action is MultiChoiceAction) {
mA = action
- mA.setActionSuccessful(PhoneStatusHelper.setDNDState(mContext, DNDChoice.valueOf(mA.choice)))
+ /**
+ * Starting with Vanilla, calls to change DND state could be ignored if we have no companion associations
+ * If so call companion settings app or else continue as usual
+ */
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && !PhoneStatusHelper.companionDeviceAssociated(
+ mContext
+ ) && WearSettingsHelper.isWearSettingsInstalled()
+ ) {
+ val status = performRemoteAction(action)
+ if (status == ActionStatus.REMOTE_FAILURE ||
+ status == ActionStatus.REMOTE_PERMISSION_DENIED
+ ) {
+ mA.setActionSuccessful(
+ PhoneStatusHelper.setDNDState(
+ mContext,
+ DNDChoice.valueOf(mA.choice)
+ )
+ )
+ }
+ } else {
+ mA.setActionSuccessful(
+ PhoneStatusHelper.setDNDState(
+ mContext,
+ DNDChoice.valueOf(mA.choice)
+ )
+ )
+ }
sendMessage(nodeID, WearableHelper.ActionsPath, JSONParser.serializer(mA, Action::class.java).stringToBytes())
} else if (action is ToggleAction) {
tA = action
- tA.setActionSuccessful(PhoneStatusHelper.setDNDState(mContext, tA.isEnabled))
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && !PhoneStatusHelper.companionDeviceAssociated(
+ mContext
+ ) && WearSettingsHelper.isWearSettingsInstalled()
+ ) {
+ val status = performRemoteAction(action)
+ if (status == ActionStatus.REMOTE_FAILURE ||
+ status == ActionStatus.REMOTE_PERMISSION_DENIED
+ ) {
+ tA.setActionSuccessful(
+ PhoneStatusHelper.setDNDState(
+ mContext,
+ tA.isEnabled
+ )
+ )
+ }
+ } else {
+ tA.setActionSuccessful(
+ PhoneStatusHelper.setDNDState(
+ mContext,
+ tA.isEnabled
+ )
+ )
+ }
sendMessage(
nodeID,
WearableHelper.ActionsPath,
@@ -909,25 +972,35 @@ class WearableManager(private val mContext: Context) : OnCapabilityChangedListen
Actions.HOTSPOT -> {
tA = action as ToggleAction
- if (WearSettingsHelper.isWearSettingsInstalled()) {
- val status = performRemoteAction(action)
- if (status == ActionStatus.REMOTE_FAILURE ||
- status == ActionStatus.REMOTE_PERMISSION_DENIED
- ) {
- tA.setActionSuccessful(
- PhoneStatusHelper.setWifiApEnabled(
- mContext,
- tA.isEnabled
- )
- )
+ /* As of Android 15 (SDK 35) QPR2 Hotspot toggle doesn't work w/o root */
+ if (Build.VERSION.SDK_INT_FULL >= (Build.VERSION_CODES_FULL.VANILLA_ICE_CREAM + 2)) {
+ if (WearSettingsHelper.isWearSettingsInstalled()) {
+ val status = performRemoteAction(action)
+ if (status == ActionStatus.REMOTE_FAILURE ||
+ status == ActionStatus.REMOTE_PERMISSION_DENIED
+ ) {
+ tA.setActionSuccessful(status)
+ WearSettingsHelper.launchWearSettings()
+ }
+ } else {
+ tA.setActionSuccessful(PhoneStatusHelper.openWirelessSettings(mContext))
+ tA.isEnabled = PhoneStatusHelper.isWifiApEnabled(mContext)
}
} else {
- tA.setActionSuccessful(
- PhoneStatusHelper.setWifiApEnabled(
- mContext,
- tA.isEnabled
+ if (WearSettingsHelper.isWearSettingsInstalled()) {
+ val status = performRemoteAction(action)
+ if (status == ActionStatus.REMOTE_FAILURE ||
+ status == ActionStatus.REMOTE_PERMISSION_DENIED
+ ) {
+ tA.setActionSuccessful(
+ PhoneStatusHelper.setWifiApEnabled(mContext, tA.isEnabled)
+ )
+ }
+ } else {
+ tA.setActionSuccessful(
+ PhoneStatusHelper.setWifiApEnabled(mContext, tA.isEnabled)
)
- )
+ }
}
sendMessage(
nodeID,
@@ -936,6 +1009,16 @@ class WearableManager(private val mContext: Context) : OnCapabilityChangedListen
)
}
+ Actions.SLEEPTIMER -> {
+ nA = action as NormalAction
+ nA.setActionSuccessful(PhoneStatusHelper.sendPauseMusicCommand(mContext))
+ sendMessage(
+ nodeID,
+ WearableHelper.ActionsPath,
+ JSONParser.serializer(nA, Action::class.java).stringToBytes()
+ )
+ }
+
Actions.TIMEDACTION -> {
val timedAction = action as TimedAction
@@ -957,6 +1040,53 @@ class WearableManager(private val mContext: Context) : OnCapabilityChangedListen
)
}
+ Actions.NFC -> {
+ tA = action as ToggleAction
+ if (WearSettingsHelper.isWearSettingsInstalled()) {
+ val status = performRemoteAction(action)
+
+ if (status == ActionStatus.REMOTE_FAILURE ||
+ status == ActionStatus.REMOTE_PERMISSION_DENIED
+ ) {
+ tA.setActionSuccessful(status)
+ WearSettingsHelper.launchWearSettings()
+ }
+ } else {
+ tA.setActionSuccessful(PhoneStatusHelper.setNfcEnabled(mContext, tA.isEnabled))
+ }
+ sendMessage(
+ nodeID,
+ WearableHelper.ActionsPath,
+ JSONParser.serializer(tA, Action::class.java).stringToBytes()
+ )
+ }
+
+ Actions.BATTERYSAVER -> {
+ tA = action as ToggleAction
+ if (WearSettingsHelper.isWearSettingsInstalled()) {
+ val status = performRemoteAction(action)
+
+ if (status == ActionStatus.REMOTE_FAILURE ||
+ status == ActionStatus.REMOTE_PERMISSION_DENIED
+ ) {
+ tA.setActionSuccessful(status)
+ WearSettingsHelper.launchWearSettings()
+ }
+ } else {
+ tA.setActionSuccessful(
+ PhoneStatusHelper.setBatterySaverEnabled(
+ mContext,
+ tA.isEnabled
+ )
+ )
+ }
+ sendMessage(
+ nodeID,
+ WearableHelper.ActionsPath,
+ JSONParser.serializer(tA, Action::class.java).stringToBytes()
+ )
+ }
+
else -> {
Logger.writeLine(
Log.WARN,
diff --git a/mobile/src/main/res/drawable/ic_access_time.xml b/mobile/src/main/res/drawable/ic_access_time.xml
index 5ebbe0f5..48584475 100644
--- a/mobile/src/main/res/drawable/ic_access_time.xml
+++ b/mobile/src/main/res/drawable/ic_access_time.xml
@@ -1,16 +1,12 @@
+ android:viewportWidth="960"
+ android:viewportHeight="960">
-
-
+ android:pathData="M520,464L520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320L440,479Q440,487 443,494.5Q446,502 452,508L584,640Q595,651 612,651Q629,651 640,640Q651,629 651,612Q651,595 640,584L520,464ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM480,800Q613,800 706.5,706.5Q800,613 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,613 253.5,706.5Q347,800 480,800Z" />
diff --git a/mobile/src/main/res/drawable/ic_baseline_music_note_24.xml b/mobile/src/main/res/drawable/ic_baseline_music_note_24.xml
deleted file mode 100644
index db31fb77..00000000
--- a/mobile/src/main/res/drawable/ic_baseline_music_note_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/mobile/src/main/res/drawable/ic_baseline_play_circle_filled_24.xml b/mobile/src/main/res/drawable/ic_baseline_play_circle_filled_24.xml
deleted file mode 100644
index f0853519..00000000
--- a/mobile/src/main/res/drawable/ic_baseline_play_circle_filled_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/mobile/src/main/res/drawable/ic_select_all.xml b/mobile/src/main/res/drawable/ic_select_all.xml
index bdcfaf49..4c8c8447 100644
--- a/mobile/src/main/res/drawable/ic_select_all.xml
+++ b/mobile/src/main/res/drawable/ic_select_all.xml
@@ -1,12 +1,12 @@
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M160,200Q143,200 131.5,188.5Q120,177 120,160Q120,143 131.5,131.5Q143,120 160,120Q177,120 188.5,131.5Q200,143 200,160Q200,177 188.5,188.5Q177,200 160,200ZM320,200Q303,200 291.5,188.5Q280,177 280,160Q280,143 291.5,131.5Q303,120 320,120Q337,120 348.5,131.5Q360,143 360,160Q360,177 348.5,188.5Q337,200 320,200ZM480,200Q463,200 451.5,188.5Q440,177 440,160Q440,143 451.5,131.5Q463,120 480,120Q497,120 508.5,131.5Q520,143 520,160Q520,177 508.5,188.5Q497,200 480,200ZM640,200Q623,200 611.5,188.5Q600,177 600,160Q600,143 611.5,131.5Q623,120 640,120Q657,120 668.5,131.5Q680,143 680,160Q680,177 668.5,188.5Q657,200 640,200ZM800,200Q783,200 771.5,188.5Q760,177 760,160Q760,143 771.5,131.5Q783,120 800,120Q817,120 828.5,131.5Q840,143 840,160Q840,177 828.5,188.5Q817,200 800,200ZM160,360Q143,360 131.5,348.5Q120,337 120,320Q120,303 131.5,291.5Q143,280 160,280Q177,280 188.5,291.5Q200,303 200,320Q200,337 188.5,348.5Q177,360 160,360ZM800,360Q783,360 771.5,348.5Q760,337 760,320Q760,303 771.5,291.5Q783,280 800,280Q817,280 828.5,291.5Q840,303 840,320Q840,337 828.5,348.5Q817,360 800,360ZM160,520Q143,520 131.5,508.5Q120,497 120,480Q120,463 131.5,451.5Q143,440 160,440Q177,440 188.5,451.5Q200,463 200,480Q200,497 188.5,508.5Q177,520 160,520ZM800,520Q783,520 771.5,508.5Q760,497 760,480Q760,463 771.5,451.5Q783,440 800,440Q817,440 828.5,451.5Q840,463 840,480Q840,497 828.5,508.5Q817,520 800,520ZM160,680Q143,680 131.5,668.5Q120,657 120,640Q120,623 131.5,611.5Q143,600 160,600Q177,600 188.5,611.5Q200,623 200,640Q200,657 188.5,668.5Q177,680 160,680ZM800,680Q783,680 771.5,668.5Q760,657 760,640Q760,623 771.5,611.5Q783,600 800,600Q817,600 828.5,611.5Q840,623 840,640Q840,657 828.5,668.5Q817,680 800,680ZM160,840Q143,840 131.5,828.5Q120,817 120,800Q120,783 131.5,771.5Q143,760 160,760Q177,760 188.5,771.5Q200,783 200,800Q200,817 188.5,828.5Q177,840 160,840ZM320,840Q303,840 291.5,828.5Q280,817 280,800Q280,783 291.5,771.5Q303,760 320,760Q337,760 348.5,771.5Q360,783 360,800Q360,817 348.5,828.5Q337,840 320,840ZM480,840Q463,840 451.5,828.5Q440,817 440,800Q440,783 451.5,771.5Q463,760 480,760Q497,760 508.5,771.5Q520,783 520,800Q520,817 508.5,828.5Q497,840 480,840ZM640,840Q623,840 611.5,828.5Q600,817 600,800Q600,783 611.5,771.5Q623,760 640,760Q657,760 668.5,771.5Q680,783 680,800Q680,817 668.5,828.5Q657,840 640,840ZM800,840Q783,840 771.5,828.5Q760,817 760,800Q760,783 771.5,771.5Q783,760 800,760Q817,760 828.5,771.5Q840,783 840,800Q840,817 828.5,828.5Q817,840 800,840ZM360,680Q327,680 303.5,656.5Q280,633 280,600L280,360Q280,327 303.5,303.5Q327,280 360,280L600,280Q633,280 656.5,303.5Q680,327 680,360L680,600Q680,633 656.5,656.5Q633,680 600,680L360,680ZM360,600L600,600Q600,600 600,600Q600,600 600,600L600,360Q600,360 600,360Q600,360 600,360L360,360Q360,360 360,360Q360,360 360,360L360,600Q360,600 360,600Q360,600 360,600Z" />
diff --git a/mobile/src/main/res/drawable/ic_settings_phone_24dp.xml b/mobile/src/main/res/drawable/ic_settings_phone_24dp.xml
index cabe7a64..d16cb309 100644
--- a/mobile/src/main/res/drawable/ic_settings_phone_24dp.xml
+++ b/mobile/src/main/res/drawable/ic_settings_phone_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M480,440Q463,440 451.5,428.5Q440,417 440,400Q440,383 451.5,371.5Q463,360 480,360Q497,360 508.5,371.5Q520,383 520,400Q520,417 508.5,428.5Q497,440 480,440ZM640,440Q623,440 611.5,428.5Q600,417 600,400Q600,383 611.5,371.5Q623,360 640,360Q657,360 668.5,371.5Q680,383 680,400Q680,417 668.5,428.5Q657,440 640,440ZM800,440Q783,440 771.5,428.5Q760,417 760,400Q760,383 771.5,371.5Q783,360 800,360Q817,360 828.5,371.5Q840,383 840,400Q840,417 828.5,428.5Q817,440 800,440ZM798,840Q673,840 551,785.5Q429,731 329,631Q229,531 174.5,409Q120,287 120,162Q120,144 132,132Q144,120 162,120L324,120Q338,120 349,129.5Q360,139 362,152L388,292Q390,308 387,319Q384,330 376,338L279,436Q299,473 326.5,507.5Q354,542 387,574Q418,605 452,631.5Q486,658 524,680L618,586Q627,577 641.5,572.5Q656,568 670,570L808,598Q822,602 831,612.5Q840,623 840,636L840,798Q840,816 828,828Q816,840 798,840ZM242,360L308,294Q308,294 308,294Q308,294 308,294L290,200Q290,200 290,200Q290,200 290,200L202,200Q202,200 202,200Q202,200 202,200Q207,241 216,281Q225,321 242,360ZM600,716.2Q639,733 679.17,743.87Q719.34,754.74 760,758Q760,758 760,758Q760,758 760,758L760,670Q760,670 760,670Q760,670 760,670L666,650Q666,650 666,650Q666,650 666,650L600,716.2ZM242,360Q225,321 216,281Q207,241 202,200Q202,200 202,200Q202,200 202,200L290,200Q290,200 290,200Q290,200 290,200L308,294Q308,294 308,294Q308,294 308,294L242,360ZM600,716L666,650Q666,650 666,650Q666,650 666,650L760,670Q760,670 760,670Q760,670 760,670L760,758Q760,758 760,758Q760,758 760,758Q719,755 679,744Q639,733 600,716Z" />
diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml
index da0838da..77ca5cd2 100644
--- a/mobile/src/main/res/layout/activity_main.xml
+++ b/mobile/src/main/res/layout/activity_main.xml
@@ -1,34 +1,13 @@
-
-
-
-
-
-
-
+ tools:context=".MainActivity">
+ android:layout_height="match_parent" />
-
+
diff --git a/mobile/src/main/res/layout/fragment_permcheck.xml b/mobile/src/main/res/layout/fragment_permcheck.xml
index dd384148..874126f0 100644
--- a/mobile/src/main/res/layout/fragment_permcheck.xml
+++ b/mobile/src/main/res/layout/fragment_permcheck.xml
@@ -1,587 +1,510 @@
-
+ tools:theme="@style/Theme.Material3Expressive.DayNight.NoActionBar">
-
+ android:background="?colorSurfaceContainer"
+ android:fitsSystemWindows="true"
+ app:expanded="false"
+ app:liftOnScroll="false"
+ app:liftOnScrollTargetViewId="@id/scrollView"
+ tools:expanded="true"
+ tools:stateListAnimator="@null">
+
+
+
+
+
+
+
+
+
+
+ android:orientation="vertical">
-
+ android:orientation="vertical"
+ style="@style/Permissions.Preference.Category.Expressive">
-
+
-
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
-
+
-
+
-
-
-
+
+
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
-
+
-
+
-
-
-
+
+
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
-
+
-
+
-
-
-
+
+
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
-
+
-
+
-
-
-
+
-
-
-
-
-
-
-
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
-
+
-
-
-
-
-
+
-
+
-
-
-
-
-
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
-
+
-
-
-
-
-
+
-
+
-
-
-
-
-
-
-
+ tools:visibility="visible">
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
-
+
-
+
+
+
-
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
-
+
-
-
-
-
-
+
-
+
-
-
-
-
-
-
-
+ android:orientation="vertical"
+ style="@style/Permissions.Preference.Category.Expressive">
-
+
-
+
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="settings">
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/mobile/src/main/res/layout/fragment_timed_actions.xml b/mobile/src/main/res/layout/fragment_timed_actions.xml
index abebd27b..fdff3278 100644
--- a/mobile/src/main/res/layout/fragment_timed_actions.xml
+++ b/mobile/src/main/res/layout/fragment_timed_actions.xml
@@ -1,16 +1,58 @@
-
+ android:background="?colorSurfaceContainer"
+ tools:context=".TimedActionsFragment"
+ tools:theme="@style/Theme.Material3Expressive.DayNight.NoActionBar">
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/layout/layout_action_item.xml b/mobile/src/main/res/layout/layout_action_item.xml
index 763ff0a5..93ed4462 100644
--- a/mobile/src/main/res/layout/layout_action_item.xml
+++ b/mobile/src/main/res/layout/layout_action_item.xml
@@ -10,7 +10,7 @@
app:contentPaddingTop="8dp"
app:contentPaddingBottom="8dp"
app:cardCornerRadius="0dp"
- tools:theme="@style/Theme.Material3.DayNight">
+ tools:theme="@style/Theme.Material3Expressive.DayNight">
-
\ No newline at end of file
diff --git a/mobile/src/main/res/values-de/strings.xml b/mobile/src/main/res/values-de/strings.xml
new file mode 100644
index 00000000..5ec7b968
--- /dev/null
+++ b/mobile/src/main/res/values-de/strings.xml
@@ -0,0 +1,61 @@
+
+
+ "Kameraberechtigung erteilt."
+ "Kameraberechtigung deaktiviert. Bitte klicken Sie hier, um die Steuerung der Taschenlampe vom WearOS-Gerät aus zu aktivieren."
+ "Ermöglicht es Ihnen, Ihren Bildschirm auszuschalten und Ihr Gerät von Ihrem WearOS-Gerät aus zu sperren."
+ "Geräteadministrator aktiviert.
+<b>Hinweis: Um die App zu deinstallieren, verwenden Sie bitte die Option \"Deaktivieren und deinstallieren\"</b>"
+ "Bildschirmsperre aktiviert"
+ "Bildschirmsperre deaktiviert. Bitte klicken Sie hier, um die Bildschirmsperrung von Ihrem WearOS-Gerät aus zu aktivieren."
+ "Bitte beachten Sie, dass Sie den Geräteadministratorzugriff deaktivieren müssen, bevor Sie die App deinstallieren. Sie können die Option \"Deaktivieren und deinstallieren\" in der App verwenden, um dies zu tun."
+ "Bildschirmsperre-Bedienungshilfe"
+ "Bildschirmsperre-Geräteadministratordienst"
+ "Wählen Sie einen Sperrdienst"
+ "Mit WearOS-Gerät koppeln"
+ "App ist mit WearOS-Gerät gekoppelt"
+ "WearOS-Gerät nicht mit der App gekoppelt. Bitte klicken Sie hier, um das Gerät zu koppeln. Dies ist erforderlich, um Anrufe zu verwalten."
+ "Bitte aktivieren Sie die Bluetooth-Erkennung auf Ihrem WearOS-Gerät."
+ "Bitte stellen Sie sicher, dass Bluetooth auf Ihrem WearOS-Gerät aktiviert ist."
+ "Keine Geräte gefunden"
+ "Media Player"
+ "Benachrichtigungszugriff aktiviert"
+ "Benachrichtigungszugriff nicht aktiviert. Bitte klicken Sie hier, um den Media Player-Zugriff zu aktivieren."
+ "Mediensteuerung aktiv"
+ @string/not_torch_turnoff_summary
+ "Trennen"
+ "Ausschalten"
+ "Deinstallieren"
+ "Deaktivieren und deinstallieren"
+ "Um die App zu deinstallieren, klicken Sie hier, um den Geräteadministratorzugriff zu deaktivieren oder deaktivieren Sie ihn unter Einstellungen -> Sicherheit -> Geräteadministrator (Apps)"
+ "Anrufsteuerung aktiv"
+ "Berechtigung verweigert"
+ "Telefonstatuszugriff aktiviert"
+ "Telefonstatuszugriff deaktiviert"
+ "Stumm"
+ "Stummschaltung aufheben"
+ "Lautsprecher aus"
+ "Lautsprecher an"
+ "Medienbenachrichtigungen weiterleiten"
+ "Medienbenachrichtigung auf der Uhr anzeigen, wenn eine aktive Mediensitzung vorhanden ist"
+ "Anrufbenachrichtigungen weiterleiten"
+ "Anrufbenachrichtigung auf der Uhr anzeigen, wenn ein aktiver Anruf vorhanden ist"
+ "NFC, Standort und Mobile Daten"
+ "SimpleWear-Helfer-App erforderlich, um Systemeinstellungen zu ändern"
+ "SimpleWear-Helfer-App installiert"
+ "SimpleWear-Helfer-App nicht aktuell"
+ "Systemeinstellungen"
+ "Berechtigung zum Ändern der Systemeinstellungen erteilt."
+ "Berechtigung zum Ändern der Systemeinstellungen deaktiviert. Bitte klicken Sie hier, um die Steuerung von Helligkeit und WLAN-Hotspot vom WearOS-Gerät aus zu aktivieren."
+ "Benachrichtigungen"
+ "Benachrichtigungsberechtigung aktiviert"
+ "Benachrichtigungsberechtigung deaktiviert"
+ "Ermöglicht es Ihnen, Gesten auszuführen, Ihren Bildschirm auszuschalten und Ihr Gerät von Ihrem WearOS-Gerät aus zu sperren."
+ "Bitte aktivieren Sie den Bedienungshilfendienst, um diese Funktion zu nutzen. Dieser Dienst wird nur verwendet, um Aktionen wie Bildschirmsperre und Gesten auszuführen; es werden keine Benutzerdaten gesammelt. Bitte beachten Sie, dass der Bedienungshilfendienst erneut aktiviert werden muss, wenn die App zwangsweise geschlossen wird."
+ "Bedienungshilfendienst aktiviert"
+ "Bedienungshilfendienst deaktiviert"
+ "Berechtigung zum Planen von Aktionen aktiviert"
+ "Berechtigung zum Planen von Aktionen deaktiviert"
+ "Keine Aktionen geplant"
+ "WLAN, NFC, Standort und mobile Daten"
+ "WLAN, NFC, Bluetooth, Standort und mobile Daten"
+
\ No newline at end of file
diff --git a/mobile/src/main/res/values-es/strings.xml b/mobile/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..c3b31677
--- /dev/null
+++ b/mobile/src/main/res/values-es/strings.xml
@@ -0,0 +1,61 @@
+
+
+ "Permiso de la cámara concedido."
+ "Permiso de la cámara desactivado. Por favor, haga clic para habilitar el control de la linterna desde el dispositivo WearOS"
+ "Le permite apagar la pantalla y bloquear su dispositivo desde su dispositivo WearOS"
+ "Administrador del dispositivo habilitado.
+<b>Nota: Para desinstalar la aplicación, utilice la opción \"Desactivar y desinstalar\"</b>"
+ "Acceso a la pantalla de bloqueo habilitado"
+ "Acceso a la pantalla de bloqueo desactivado. Por favor, haga clic para habilitar el bloqueo de la pantalla desde su dispositivo WearOS"
+ "Tenga en cuenta que si habilita el acceso de administrador del dispositivo, deberá desactivar el acceso antes de desinstalarlo. Puede utilizar la opción \"Desactivar y desinstalar\" en la aplicación para hacerlo."
+ "Servicio de accesibilidad de la pantalla de bloqueo"
+ "Servicio de administrador de dispositivos de la pantalla de bloqueo"
+ "Elija un servicio de bloqueo"
+ "Emparejar con dispositivo WearOS"
+ "La aplicación está emparejada con el dispositivo WearOS"
+ "El dispositivo WearOS no está emparejado con la aplicación. Por favor, haga clic para emparejar el dispositivo. Esto es necesario para administrar las llamadas"
+ "Por favor, habilite la detección de Bluetooth en su dispositivo WearOS"
+ "Por favor, asegúrese de que Bluetooth esté habilitado en su dispositivo WearOS"
+ "No se encontraron dispositivos"
+ "Reproductor multimedia"
+ "Acceso a notificaciones habilitado"
+ "Acceso a notificaciones no habilitado. Por favor, haga clic para habilitar el acceso al reproductor multimedia"
+ "Controlador de medios activo"
+ @string/not_torch_turnoff_summary
+ "Desconectar"
+ "Apagar"
+ "Desinstalar"
+ "Desactivar y desinstalar"
+ "Para desinstalar la aplicación, haga clic aquí para desactivar el acceso de administrador del dispositivo o desactive desde Ajustes -> Seguridad -> Administrador del dispositivo (aplicaciones)"
+ "Controlador de llamadas activo"
+ "Permiso denegado"
+ "Acceso al estado del teléfono habilitado"
+ "Acceso al estado del teléfono deshabilitado"
+ "Silenciar"
+ "Activar sonido"
+ "Altavoz apagado"
+ "Altavoz encendido"
+ "Notificaciones de medios puente"
+ "Mostrar notificación de medios en el reloj cuando hay una sesión de medios activa"
+ "Notificaciones de llamadas puente"
+ "Mostrar notificación de llamada en el reloj cuando hay una llamada activa"
+ "NFC, ubicación y datos móviles"
+ "Se requiere la aplicación auxiliar SimpleWear para alternar la configuración del sistema"
+ "Aplicación auxiliar SimpleWear instalada"
+ "La aplicación auxiliar SimpleWear no está actualizada"
+ "Configuración del sistema"
+ "Permiso para modificar la configuración del sistema concedido."
+ "Permiso para modificar la configuración del sistema deshabilitado. Por favor, haga clic para habilitar el control del brillo y el punto de acceso WiFi desde el dispositivo WearOS"
+ "Notificaciones"
+ "Permiso de notificación habilitado"
+ "Permiso de notificación deshabilitado"
+ "Le permite realizar gestos, apagar la pantalla y bloquear su dispositivo desde su dispositivo WearOS"
+ "Por favor, active el servicio de accesibilidad para utilizar esta función. Este servicio solo se utiliza para realizar acciones como el bloqueo de pantalla y gestos; no se recopilan datos del usuario. Tenga en cuenta que si la aplicación se cierra a la fuerza, el servicio de accesibilidad deberá volver a habilitarse."
+ "Servicio de accesibilidad habilitado"
+ "Servicio de accesibilidad deshabilitado"
+ "Permiso para programar acciones habilitado"
+ "Permiso para programar acciones deshabilitado"
+ "No hay acciones programadas"
+ "WiFi, NFC, ubicación y datos móviles"
+ "WiFi, NFC, Bluetooth, ubicación y datos móviles"
+
\ No newline at end of file
diff --git a/mobile/src/main/res/values-fr/strings.xml b/mobile/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..89e143e6
--- /dev/null
+++ b/mobile/src/main/res/values-fr/strings.xml
@@ -0,0 +1,61 @@
+
+
+ "Autorisation d'accès à la caméra accordée."
+ "Autorisation d'accès à la caméra désactivée. Veuillez cliquer pour activer le contrôle de la lampe de poche depuis l'appareil WearOS"
+ "Vous permet d'éteindre votre écran et de verrouiller votre appareil depuis votre appareil WearOS"
+ "Administrateur de l'appareil activé.
+<b>Remarque : Pour désinstaller l'application, veuillez utiliser l'option \"Désactiver et désinstaller\"</b>"
+ "Accès à l'écran de verrouillage activé"
+ "Accès à l'écran de verrouillage désactivé. Veuillez cliquer pour activer le verrouillage de l'écran depuis votre appareil WearOS"
+ "Veuillez noter que si vous activez l'accès administrateur de l'appareil, vous devrez désactiver l'accès avant de désinstaller. Vous pouvez utiliser l'option \"Désactiver et désinstaller\" dans l'application pour ce faire."
+ "Service d'accessibilité de l'écran de verrouillage"
+ "Service d'administration de l'appareil de l'écran de verrouillage"
+ "Choisissez un service de verrouillage"
+ "Associer avec l'appareil WearOS"
+ "L'application est associée à l'appareil WearOS"
+ "L'appareil WearOS n'est pas associé à l'application. Veuillez cliquer pour associer l'appareil. Ceci est nécessaire pour gérer les appels"
+ "Veuillez activer la découverte Bluetooth sur votre appareil WearOS"
+ "Veuillez vous assurer que le Bluetooth est activé sur votre appareil WearOS"
+ "Aucun appareil trouvé"
+ "Lecteur multimédia"
+ "Accès aux notifications activé"
+ "Accès aux notifications non activé. Veuillez cliquer pour activer l'accès au lecteur multimédia"
+ "Contrôleur multimédia actif"
+ @string/not_torch_turnoff_summary
+ "Déconnecter"
+ "Éteindre"
+ "Désinstaller"
+ "Désactiver et désinstaller"
+ "Pour désinstaller l'application, cliquez ici pour désactiver l'accès administrateur de l'appareil ou désactivez-le depuis Paramètres -> Sécurité -> Administrateur de l'appareil (applications)"
+ "Contrôleur d'appel actif"
+ "Autorisation refusée"
+ "Accès à l'état du téléphone activé"
+ "Accès à l'état du téléphone désactivé"
+ "Muet"
+ "Annuler le mode muet"
+ "Haut-parleur désactivé"
+ "Haut-parleur activé"
+ "Notifications multimédias de pontage"
+ "Afficher la notification multimédia sur la montre lorsqu'il y a une session multimédia active"
+ "Notifications d'appel de pontage"
+ "Afficher la notification d'appel sur la montre lorsqu'il y a un appel actif"
+ "NFC, localisation et données mobiles"
+ "Application d'assistance SimpleWear requise pour activer/désactiver les paramètres système"
+ "Application d'assistance SimpleWear installée"
+ "L'application d'assistance SimpleWear n'est pas à jour"
+ "Paramètres système"
+ "Autorisation de modifier les paramètres système accordée."
+ "Autorisation de modifier les paramètres système désactivée. Veuillez cliquer pour activer le contrôle de la luminosité et du point d'accès WiFi depuis l'appareil WearOS"
+ "Notifications"
+ "Autorisation de notification activée"
+ "Autorisation de notification désactivée"
+ "Vous permet d'effectuer des gestes, d'éteindre votre écran et de verrouiller votre appareil depuis votre appareil WearOS"
+ "Veuillez activer le service d'accessibilité pour utiliser cette fonctionnalité. Ce service est uniquement utilisé pour effectuer des actions telles que le verrouillage de l'écran et les gestes ; aucune donnée utilisateur n'est collectée. Veuillez noter que si l'application est forcée à la fermeture, le service d'accessibilité devra être réactivé."
+ "Service d'accessibilité activé"
+ "Service d'accessibilité désactivé"
+ "Autorisation d'action planifiée activée"
+ "Autorisation d'action planifiée désactivée"
+ "Aucune action planifiée"
+ "WiFi, NFC, localisation et données mobiles"
+ "WiFi, NFC, Bluetooth, localisation et données mobiles"
+
\ No newline at end of file
diff --git a/mobile/src/main/res/values-night/styles.xml b/mobile/src/main/res/values-night/styles.xml
deleted file mode 100644
index 3eda9482..00000000
--- a/mobile/src/main/res/values-night/styles.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
diff --git a/mobile/src/main/res/values-v29/strings.xml b/mobile/src/main/res/values-v29/strings.xml
index 6323125a..f59d9441 100644
--- a/mobile/src/main/res/values-v29/strings.xml
+++ b/mobile/src/main/res/values-v29/strings.xml
@@ -1,4 +1,4 @@
- WiFi, Location and Mobile Data
+ @string/preference_title_wearsettings_v29
\ No newline at end of file
diff --git a/mobile/src/main/res/values-v33/strings.xml b/mobile/src/main/res/values-v33/strings.xml
index ae177f62..80de3c5a 100644
--- a/mobile/src/main/res/values-v33/strings.xml
+++ b/mobile/src/main/res/values-v33/strings.xml
@@ -1,4 +1,4 @@
- WiFi, Bluetooth, Location and Mobile Data
+ @string/preference_title_wearsettings_v33
\ No newline at end of file
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
index ca12cfcf..48971451 100644
--- a/mobile/src/main/res/values/strings.xml
+++ b/mobile/src/main/res/values/strings.xml
@@ -12,9 +12,6 @@
Lock Screen Device Admin Service
Choose a lock service
- Do not Disturb access enabled
- Do not Disturb access disabled. Please click to enable changing Do not Disturb setting from your WearOS device
-
Pair with WearOS device
App is paired with WearOS device
WearOS device not paired with app. Please click to pair device. This is needed to manage calls
@@ -50,7 +47,9 @@
Bridge call notifications
Show call notification on watch when there is an active call
- Location and Mobile Data
+ NFC, Location and Mobile Data
+ WiFi, NFC, Location and Mobile Data
+ WiFi, NFC, Bluetooth, Location and Mobile Data
SimpleWear helper app required to toggle system settings
SimpleWear helper app installed
SimpleWear helper app not up-to-date
@@ -65,10 +64,6 @@
Notification permission enabled
Notification permission disabled
- Bluetooth
- Bluetooth permission enabled
- Bluetooth permission disabled
-
Allows you to perform gestures, turn off your screen and lock your device from your WearOS device
Please enable the accessibility service to use this feature. This service is only used to perform actions like lock screen and gestures; no user data is collected. Please note that if the app is force-closed, the accessibility service will need to be re-enabled.
Accessibility service enabled
@@ -76,5 +71,6 @@
Schedule action permission enabled
Schedule action permission disabled
+ No actions scheduled
diff --git a/mobile/src/main/res/values/styles.xml b/mobile/src/main/res/values/styles.xml
index 6e2dd2e0..045e125f 100644
--- a/mobile/src/main/res/values/styles.xml
+++ b/mobile/src/main/res/values/styles.xml
@@ -1,55 +1,3 @@
+
-
-
-
-
-
-
-
-
-
-
diff --git a/settings.gradle b/settings.gradle
index ba200ee7..18a25fdf 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,4 @@
include ':mobile', ':wear', ':shared_resources', ':unofficialtileapi'
include ':wearsettings'
include ':hidden-api'
+include ':common'
diff --git a/shared_resources/build.gradle b/shared_resources/build.gradle
index 30966e8a..e2ff600f 100644
--- a/shared_resources/build.gradle
+++ b/shared_resources/build.gradle
@@ -3,11 +3,11 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
- compileSdk rootProject.compileSdkVersion
+ compileSdk = libs.versions.compileSdkVersion.get().toInteger()
defaultConfig {
- minSdkVersion rootProject.minSdkVersion
- targetSdkVersion rootProject.targetSdkVersion
+ minSdkVersion libs.versions.minSdkVersion.get().toInteger()
+ targetSdkVersion libs.versions.targetSdkVersion.get().toInteger()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -44,37 +44,37 @@ android {
}
dependencies {
- coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_version"
+ coreLibraryDesugaring libs.desugar.jdk.libs
// Unit Testing
- androidTestImplementation "androidx.test:core:$test_core_version"
+ androidTestImplementation libs.test.core
// AndroidJUnitRunner and JUnit Rules
- androidTestImplementation "androidx.test:runner:$test_runner_version"
- androidTestImplementation "androidx.test:rules:$test_rules_version"
+ androidTestImplementation libs.test.runner
+ androidTestImplementation libs.test.rules
// Assertions
- androidTestImplementation "androidx.test.ext:junit:$junit_version"
- androidTestImplementation "androidx.test.ext:truth:$androidx_truth_version"
- androidTestImplementation "com.google.truth:truth:$google_truth_version"
+ androidTestImplementation libs.androidx.junit
+ androidTestImplementation libs.androidx.truth
+ androidTestImplementation libs.google.truth
// Kotlin
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_version"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_version"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_version"
- implementation "androidx.core:core-ktx:$core_version"
- implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
- implementation "androidx.recyclerview:recyclerview:$recyclerview_version"
+ implementation libs.kotlinx.coroutines.core
+ implementation libs.kotlinx.coroutines.android
+ implementation libs.kotlinx.coroutines.play.services
+ implementation libs.core.ktx
+ implementation libs.lifecycle.runtime.ktx
+ implementation libs.recyclerview
- implementation "androidx.appcompat:appcompat:$appcompat_version"
+ implementation libs.appcompat
- implementation 'com.google.android.gms:play-services-wearable:19.0.0'
+ implementation libs.play.services.wearable
- implementation platform("com.google.firebase:firebase-bom:$firebase_version")
- implementation 'com.google.firebase:firebase-analytics'
- implementation 'com.google.firebase:firebase-crashlytics'
- implementation 'com.google.firebase:firebase-config'
+ implementation platform(libs.firebase.bom)
+ implementation libs.firebase.analytics
+ implementation libs.firebase.crashlytics
+ implementation libs.firebase.config
- implementation "com.jakewharton.timber:timber:$timber_version"
- implementation "com.google.code.gson:gson:$gson_version"
+ implementation libs.timber
+ implementation libs.gson
}
diff --git a/shared_resources/src/main/AndroidManifest.xml b/shared_resources/src/main/AndroidManifest.xml
index e94dcdf8..b3e712d0 100644
--- a/shared_resources/src/main/AndroidManifest.xml
+++ b/shared_resources/src/main/AndroidManifest.xml
@@ -5,7 +5,7 @@
android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" />
-
+
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/Action.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/Action.kt
index 237b1653..d7c947e7 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/Action.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/Action.kt
@@ -28,7 +28,9 @@ abstract class Action(_action: Actions) {
Actions.BLUETOOTH,
Actions.MOBILEDATA,
Actions.TORCH,
- Actions.HOTSPOT ->
+ Actions.HOTSPOT,
+ Actions.NFC,
+ Actions.BATTERYSAVER ->
ToggleAction(action, true)
Actions.LOCATION ->
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/ActionStatus.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/ActionStatus.kt
index e81aa1de..d31bd6e5 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/ActionStatus.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/ActionStatus.kt
@@ -19,7 +19,7 @@ enum class ActionStatus(val value: Int) {
}
init {
- for (status in values()) {
+ for (status in entries) {
map.put(status.value, status)
}
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/Actions.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/Actions.kt
index 67a469a3..d7731316 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/Actions.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/Actions.kt
@@ -18,6 +18,8 @@ enum class Actions(val value: Int) {
PHONE(12),
BRIGHTNESS(13),
HOTSPOT(14),
+ NFC(17),
+ BATTERYSAVER(18),
GESTURES(15),
TIMEDACTION(16);
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/AudioStreamType.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/AudioStreamType.kt
index 6a3d985a..e667c602 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/AudioStreamType.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/AudioStreamType.kt
@@ -16,7 +16,7 @@ enum class AudioStreamType(val value: Int) {
}
init {
- for (stream in values()) {
+ for (stream in entries) {
map.put(stream.value, stream)
}
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/DNDChoice.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/DNDChoice.kt
index 1ba06aa0..c39c72dc 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/DNDChoice.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/DNDChoice.kt
@@ -16,7 +16,7 @@ enum class DNDChoice(val value: Int) {
}
init {
- for (choice in values()) {
+ for (choice in entries) {
map.put(choice.value, choice)
}
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/LocationState.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/LocationState.kt
index 3327d1b7..b3a19f82 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/LocationState.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/LocationState.kt
@@ -16,7 +16,7 @@ enum class LocationState(val value: Int) {
}
init {
- for (choice in values()) {
+ for (choice in entries) {
map.put(choice.value, choice)
}
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/RingerChoice.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/RingerChoice.kt
index 01bd4da3..24a3696a 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/RingerChoice.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/RingerChoice.kt
@@ -15,7 +15,7 @@ enum class RingerChoice(val value: Int) {
}
init {
- for (choice in values()) {
+ for (choice in entries) {
map.put(choice.value, choice)
}
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/TimedAction.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/TimedAction.kt
index a5cde949..90caa830 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/TimedAction.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/actions/TimedAction.kt
@@ -12,7 +12,10 @@ class TimedAction(var timeInMillis: Long, val action: Action) : Action(Actions.T
Actions.TORCH,
Actions.DONOTDISTURB,
Actions.RINGER,
- Actions.HOTSPOT -> true
+ Actions.HOTSPOT,
+ Actions.SLEEPTIMER,
+ Actions.NFC,
+ Actions.BATTERYSAVER -> true
else -> false
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/controls/ActionButtonViewModel.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/controls/ActionButtonViewModel.kt
index 01215f33..03837eac 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/controls/ActionButtonViewModel.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/controls/ActionButtonViewModel.kt
@@ -173,7 +173,7 @@ class ActionButtonViewModel(val action: Action) {
}
Actions.LOCKSCREEN -> {
- drawableResId = R.drawable.ic_lock_outline_white_24dp
+ drawableResId = R.drawable.ic_lock_white_24dp
actionLabelResId = R.string.action_lockscreen
stateLabelResId = 0
}
@@ -216,7 +216,7 @@ class ActionButtonViewModel(val action: Action) {
DNDChoice.SILENCE -> {
drawableResId =
- R.drawable.ic_notifications_off_white_24dp
+ R.drawable.ic_do_not_disturb_silence_white_24dp
stateLabelResId = R.string.dndstate_silence
}
}
@@ -295,6 +295,23 @@ class ActionButtonViewModel(val action: Action) {
actionLabelResId = R.string.action_timedaction
stateLabelResId = 0
}
+
+ Actions.NFC -> {
+ tA = action as ToggleAction
+ drawableResId =
+ if (tA.isEnabled) R.drawable.ic_nfc_on else R.drawable.ic_nfc_off
+ actionLabelResId = R.string.action_nfc
+ stateLabelResId =
+ if (tA.isEnabled) R.string.state_on else R.string.state_off
+ }
+
+ Actions.BATTERYSAVER -> {
+ tA = action as ToggleAction
+ drawableResId = R.drawable.ic_battery_saver
+ actionLabelResId = R.string.action_batterysaver
+ stateLabelResId =
+ if (tA.isEnabled) R.string.state_on else R.string.state_off
+ }
}
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/data/CallState.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/data/CallState.kt
index 01e7eca4..58e77d36 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/data/CallState.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/data/CallState.kt
@@ -1,9 +1,13 @@
package com.thewizrd.shared_resources.data
+import android.telephony.TelephonyManager
+
data class CallState(
val callerName: String? = null,
val callerBitmap: ByteArray? = null,
val callActive: Boolean = false,
+ val callState: Int = TelephonyManager.CALL_STATE_IDLE,
+ val callStartTime: Long = -1L,
val supportedFeatures: Int = 0,
) {
override fun equals(other: Any?): Boolean {
@@ -16,7 +20,9 @@ data class CallState(
if (!callerBitmap.contentEquals(other.callerBitmap)) return false
} else if (other.callerBitmap != null) return false
if (callActive != other.callActive) return false
+ if (callStartTime != other.callStartTime) return false
if (supportedFeatures != other.supportedFeatures) return false
+ if (callState != other.callState) return false
return true
}
@@ -25,7 +31,9 @@ data class CallState(
var result = callerName?.hashCode() ?: 0
result = 31 * result + (callerBitmap?.contentHashCode() ?: 0)
result = 31 * result + callActive.hashCode()
+ result = 31 * result + callStartTime.hashCode()
result = 31 * result + supportedFeatures
+ result = 31 * result + callState
return result
}
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/InCallUIHelper.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/InCallUIHelper.kt
index 17a91472..bee8dadc 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/InCallUIHelper.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/InCallUIHelper.kt
@@ -8,6 +8,7 @@ object InCallUIHelper {
const val ConnectPath = "/incallui/connect"
const val DisconnectPath = "/incallui/disconnect"
+ const val AnswerCallPath = "/incallui/answer"
const val EndCallPath = "/incallui/hangup"
const val MuteMicPath = "/incallui/mute"
const val MuteMicStatusPath = "/incallui/mute/status"
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearConnectionStatus.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearConnectionStatus.kt
index f3c50fda..f89ca72c 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearConnectionStatus.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearConnectionStatus.kt
@@ -16,7 +16,7 @@ enum class WearConnectionStatus(val value: Int) {
}
init {
- for (connectionStatus in values()) {
+ for (connectionStatus in entries) {
map.put(connectionStatus.value, connectionStatus)
}
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearSettingsHelper.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearSettingsHelper.kt
index 83d436d0..d79c7c7a 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearSettingsHelper.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearSettingsHelper.kt
@@ -11,7 +11,7 @@ import com.thewizrd.shared_resources.utils.Logger
object WearSettingsHelper {
// Link to Play Store listing
const val PACKAGE_NAME = "com.thewizrd.wearsettings"
- private const val SUPPORTED_VERSION_CODE: Long = 1030002
+ private const val SUPPORTED_VERSION_CODE: Long = 1040000
fun getPackageName(): String {
var packageName = PACKAGE_NAME
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearableHelper.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearableHelper.kt
index ec5e2c62..4b1605e7 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearableHelper.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/WearableHelper.kt
@@ -11,6 +11,7 @@ import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.wearable.Node
import com.google.android.gms.wearable.PutDataRequest
+import com.thewizrd.shared_resources.BuildConfig
import com.thewizrd.shared_resources.sharedDeps
import com.thewizrd.shared_resources.utils.Logger
@@ -22,9 +23,10 @@ object WearableHelper {
const val CAPABILITY_WEAR_APP = "com.thewizrd.simplewear_wear_app"
// Link to Play Store listing
- private const val PLAY_STORE_APP_URI = "market://details?id=com.thewizrd.simplewear"
+ const val PACKAGE_NAME = "com.thewizrd.simplewear"
+ private const val PLAY_STORE_APP_URI = "market://details?id=$PACKAGE_NAME"
- private const val SUPPORTED_VERSION_CODE: Long = 341916050
+ private const val SUPPORTED_VERSION_CODE: Long = 361917010
fun getPlayStoreURI(): Uri = Uri.parse(PLAY_STORE_APP_URI)
@@ -159,6 +161,12 @@ object WearableHelper {
fun getBLEServiceUUID(): ParcelUuid =
ParcelUuid.fromString("0000DA28-0000-1000-8000-00805F9B34FB")
+ fun getPackageName(): String {
+ var packageName = PACKAGE_NAME
+ if (BuildConfig.DEBUG) packageName += ".debug"
+ return packageName
+ }
+
fun isAppUpToDate(versionCode: Long): Boolean {
return versionCode >= SUPPORTED_VERSION_CODE
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/media/MediaItem.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/media/MediaItem.kt
index 18e577d6..f51ba937 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/media/MediaItem.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/media/MediaItem.kt
@@ -3,6 +3,7 @@ package com.thewizrd.shared_resources.media
data class MediaItem(
val mediaId: String,
val title: String,
+ val subTitle: String? = null,
val icon: ByteArray? = null
) {
override fun equals(other: Any?): Boolean {
@@ -11,6 +12,7 @@ data class MediaItem(
if (mediaId != other.mediaId) return false
if (title != other.title) return false
+ if (subTitle != other.subTitle) return false
if (icon != null) {
if (other.icon == null) return false
if (!icon.contentEquals(other.icon)) return false
@@ -22,6 +24,7 @@ data class MediaItem(
override fun hashCode(): Int {
var result = mediaId.hashCode()
result = 31 * result + title.hashCode()
+ result = 31 * result + subTitle.hashCode()
result = 31 * result + (icon?.contentHashCode() ?: 0)
return result
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/media/QueueItems.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/media/QueueItems.kt
index d9f1fc8c..1eb5dd70 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/media/QueueItems.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/media/QueueItems.kt
@@ -8,6 +8,7 @@ data class QueueItems(
data class QueueItem(
val queueId: Long,
val title: String,
+ val subTitle: String? = null,
val icon: ByteArray? = null
) {
override fun equals(other: Any?): Boolean {
@@ -16,6 +17,7 @@ data class QueueItem(
if (queueId != other.queueId) return false
if (title != other.title) return false
+ if (subTitle != other.subTitle) return false
if (icon != null) {
if (other.icon == null) return false
if (!icon.contentEquals(other.icon)) return false
@@ -27,6 +29,7 @@ data class QueueItem(
override fun hashCode(): Int {
var result = queueId.hashCode()
result = 31 * result + title.hashCode()
+ result = 31 * result + subTitle.hashCode()
result = 31 * result + (icon?.contentHashCode() ?: 0)
return result
}
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/utils/AnalyticsProps.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/utils/AnalyticsProps.kt
index 19c56c86..0e91b44d 100644
--- a/shared_resources/src/main/java/com/thewizrd/shared_resources/utils/AnalyticsProps.kt
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/utils/AnalyticsProps.kt
@@ -2,4 +2,5 @@ package com.thewizrd.shared_resources.utils
object AnalyticsProps {
const val DEVICE_TYPE = "device_type"
+ const val PLATFORM = "platform"
}
\ No newline at end of file
diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/utils/BundleUtils.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/utils/BundleUtils.kt
new file mode 100644
index 00000000..a541643a
--- /dev/null
+++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/utils/BundleUtils.kt
@@ -0,0 +1,9 @@
+package com.thewizrd.shared_resources.utils
+
+import android.os.Bundle
+import androidx.core.os.BundleCompat
+import java.io.Serializable
+
+fun Bundle.getSerializableCompat(key: String?, clazz: Class): T? where T : Serializable {
+ return BundleCompat.getSerializable(this, key, clazz)
+}
\ No newline at end of file
diff --git a/shared_resources/src/main/res/animator/bar1_animator.xml b/shared_resources/src/main/res/animator/bar1_animator.xml
new file mode 100644
index 00000000..28b08a20
--- /dev/null
+++ b/shared_resources/src/main/res/animator/bar1_animator.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/shared_resources/src/main/res/animator/bar2_animator.xml b/shared_resources/src/main/res/animator/bar2_animator.xml
new file mode 100644
index 00000000..ddcb61d4
--- /dev/null
+++ b/shared_resources/src/main/res/animator/bar2_animator.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/shared_resources/src/main/res/animator/bar3_animator.xml b/shared_resources/src/main/res/animator/bar3_animator.xml
new file mode 100644
index 00000000..10744626
--- /dev/null
+++ b/shared_resources/src/main/res/animator/bar3_animator.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/shared_resources/src/main/res/animator/music_note_bounce_animator.xml b/shared_resources/src/main/res/animator/music_note_bounce_animator.xml
new file mode 100644
index 00000000..0e8e2f68
--- /dev/null
+++ b/shared_resources/src/main/res/animator/music_note_bounce_animator.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/shared_resources/src/main/res/drawable/equalizer_animated.xml b/shared_resources/src/main/res/drawable/equalizer_animated.xml
new file mode 100644
index 00000000..d04c3ff0
--- /dev/null
+++ b/shared_resources/src/main/res/drawable/equalizer_animated.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_add_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_add_white_24dp.xml
deleted file mode 100644
index d270d5e8..00000000
--- a/shared_resources/src/main/res/drawable/ic_add_white_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_alarm_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_alarm_white_24dp.xml
index b305c1d5..f296d2a0 100644
--- a/shared_resources/src/main/res/drawable/ic_alarm_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_alarm_white_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M520,504v-144q0,-17 -11.5,-28.5T480,320q-17,0 -28.5,11.5T440,360v159q0,8 3,15.5t9,13.5l112,112q11,11 28,11t28,-11q11,-11 11,-28t-11,-28L520,504ZM480,880q-75,0 -140.5,-28.5t-114,-77q-48.5,-48.5 -77,-114T120,520q0,-75 28.5,-140.5t77,-114q48.5,-48.5 114,-77T480,160q75,0 140.5,28.5t114,77q48.5,48.5 77,114T840,520q0,75 -28.5,140.5t-77,114q-48.5,48.5 -114,77T480,880ZM82,292q-11,-11 -11,-28t11,-28l114,-114q11,-11 28,-11t28,11q11,11 11,28t-11,28L138,292q-11,11 -28,11t-28,-11ZM878,292q-11,11 -28,11t-28,-11L708,178q-11,-11 -11,-28t11,-28q11,-11 28,-11t28,11l114,114q11,11 11,28t-11,28Z"
+ android:fillColor="#FFFFFF" />
diff --git a/shared_resources/src/main/res/drawable/ic_apps_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_apps_white_24dp.xml
index 2875b6d1..fb8c1c31 100644
--- a/shared_resources/src/main/res/drawable/ic_apps_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_apps_white_24dp.xml
@@ -1,10 +1,12 @@
-
+ android:viewportHeight="960"
+ android:viewportWidth="960"
+ android:width="24dp">
+
+ android:fillColor="@android:color/white"
+ android:pathData="M240,800Q207,800 183.5,776.5Q160,753 160,720Q160,687 183.5,663.5Q207,640 240,640Q273,640 296.5,663.5Q320,687 320,720Q320,753 296.5,776.5Q273,800 240,800ZM480,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800ZM720,800Q687,800 663.5,776.5Q640,753 640,720Q640,687 663.5,663.5Q687,640 720,640Q753,640 776.5,663.5Q800,687 800,720Q800,753 776.5,776.5Q753,800 720,800ZM240,560Q207,560 183.5,536.5Q160,513 160,480Q160,447 183.5,423.5Q207,400 240,400Q273,400 296.5,423.5Q320,447 320,480Q320,513 296.5,536.5Q273,560 240,560ZM480,560Q447,560 423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560ZM720,560Q687,560 663.5,536.5Q640,513 640,480Q640,447 663.5,423.5Q687,400 720,400Q753,400 776.5,423.5Q800,447 800,480Q800,513 776.5,536.5Q753,560 720,560ZM240,320Q207,320 183.5,296.5Q160,273 160,240Q160,207 183.5,183.5Q207,160 240,160Q273,160 296.5,183.5Q320,207 320,240Q320,273 296.5,296.5Q273,320 240,320ZM480,320Q447,320 423.5,296.5Q400,273 400,240Q400,207 423.5,183.5Q447,160 480,160Q513,160 536.5,183.5Q560,207 560,240Q560,273 536.5,296.5Q513,320 480,320ZM720,320Q687,320 663.5,296.5Q640,273 640,240Q640,207 663.5,183.5Q687,160 720,160Q753,160 776.5,183.5Q800,207 800,240Q800,273 776.5,296.5Q753,320 720,320Z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_android_24dp.xml b/shared_resources/src/main/res/drawable/ic_baseline_android_24dp.xml
deleted file mode 100644
index ab936171..00000000
--- a/shared_resources/src/main/res/drawable/ic_baseline_android_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/shared_resources/src/main/res/drawable/ic_baseline_arrow_back_24.xml
deleted file mode 100644
index 8e8ac863..00000000
--- a/shared_resources/src/main/res/drawable/ic_baseline_arrow_back_24.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_call_24dp.xml b/shared_resources/src/main/res/drawable/ic_baseline_call_24dp.xml
deleted file mode 100644
index a8814ba6..00000000
--- a/shared_resources/src/main/res/drawable/ic_baseline_call_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_edit_24.xml b/shared_resources/src/main/res/drawable/ic_baseline_edit_24.xml
deleted file mode 100644
index d4d0d9c7..00000000
--- a/shared_resources/src/main/res/drawable/ic_baseline_edit_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_filter_list_24.xml b/shared_resources/src/main/res/drawable/ic_baseline_filter_list_24.xml
deleted file mode 100644
index e7059d11..00000000
--- a/shared_resources/src/main/res/drawable/ic_baseline_filter_list_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_gps_fixed_24dp.xml b/shared_resources/src/main/res/drawable/ic_baseline_gps_fixed_24dp.xml
index 804ca387..cfc3ca92 100644
--- a/shared_resources/src/main/res/drawable/ic_baseline_gps_fixed_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_baseline_gps_fixed_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M440,878v-40q-125,-14 -214.5,-103.5T122,520L82,520q-17,0 -28.5,-11.5T42,480q0,-17 11.5,-28.5T82,440h40q14,-125 103.5,-214.5T440,122v-40q0,-17 11.5,-28.5T480,42q17,0 28.5,11.5T520,82v40q125,14 214.5,103.5T838,440h40q17,0 28.5,11.5T918,480q0,17 -11.5,28.5T878,520h-40q-14,125 -103.5,214.5T520,838v40q0,17 -11.5,28.5T480,918q-17,0 -28.5,-11.5T440,878ZM480,760q116,0 198,-82t82,-198q0,-116 -82,-198t-198,-82q-116,0 -198,82t-82,198q0,116 82,198t198,82ZM480,640q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47Z"
+ android:fillColor="#FFFFFF" />
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_pause_24.xml b/shared_resources/src/main/res/drawable/ic_baseline_pause_24.xml
index 93913e45..702f3133 100644
--- a/shared_resources/src/main/res/drawable/ic_baseline_pause_24.xml
+++ b/shared_resources/src/main/res/drawable/ic_baseline_pause_24.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M640,760Q607,760 583.5,736.5Q560,713 560,680L560,280Q560,247 583.5,223.5Q607,200 640,200L640,200Q673,200 696.5,223.5Q720,247 720,280L720,680Q720,713 696.5,736.5Q673,760 640,760ZM320,760Q287,760 263.5,736.5Q240,713 240,680L240,280Q240,247 263.5,223.5Q287,200 320,200L320,200Q353,200 376.5,223.5Q400,247 400,280L400,680Q400,713 376.5,736.5Q353,760 320,760Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_refresh_24.xml b/shared_resources/src/main/res/drawable/ic_baseline_refresh_24.xml
deleted file mode 100644
index b7fcc3c9..00000000
--- a/shared_resources/src/main/res/drawable/ic_baseline_refresh_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_restart_alt_24.xml b/shared_resources/src/main/res/drawable/ic_baseline_restart_alt_24.xml
deleted file mode 100644
index 91883461..00000000
--- a/shared_resources/src/main/res/drawable/ic_baseline_restart_alt_24.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_ring_volume_24dp.xml b/shared_resources/src/main/res/drawable/ic_baseline_ring_volume_24dp.xml
index d0cd4f2a..f19b113a 100644
--- a/shared_resources/src/main/res/drawable/ic_baseline_ring_volume_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_baseline_ring_volume_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M480,280Q463,280 451.5,268.5Q440,257 440,240L440,120Q440,103 451.5,91.5Q463,80 480,80Q497,80 508.5,91.5Q520,103 520,120L520,240Q520,257 508.5,268.5Q497,280 480,280ZM676,364Q665,352 664.5,336Q664,320 676,308L761,223Q773,211 789.5,211.5Q806,212 818,224Q829,236 829.5,252Q830,268 818,280L733,365Q721,377 704.5,376.5Q688,376 676,364ZM284,364Q272,376 255.5,376.5Q239,377 227,365L142,280Q130,268 130.5,252Q131,236 142,224Q154,212 170.5,211.5Q187,211 199,223L284,308Q296,320 295.5,336Q295,352 284,364ZM136,816L44,726Q32,714 32,698Q32,682 44,670Q132,575 247,527.5Q362,480 480,480Q598,480 712.5,527.5Q827,575 916,670Q928,682 928,698Q928,714 916,726L824,816Q813,827 798.5,828Q784,829 772,820L656,732Q648,726 644,718Q640,710 640,700L640,586Q602,574 562,567Q522,560 480,560Q438,560 398,567Q358,574 320,586L320,700Q320,710 316,718Q312,726 304,732L188,820Q176,829 161.5,828Q147,827 136,816Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_skip_next_24.xml b/shared_resources/src/main/res/drawable/ic_baseline_skip_next_24.xml
index 33b9a037..639facb2 100644
--- a/shared_resources/src/main/res/drawable/ic_baseline_skip_next_24.xml
+++ b/shared_resources/src/main/res/drawable/ic_baseline_skip_next_24.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M660,680L660,280Q660,263 671.5,251.5Q683,240 700,240Q717,240 728.5,251.5Q740,263 740,280L740,680Q740,697 728.5,708.5Q717,720 700,720Q683,720 671.5,708.5Q660,697 660,680ZM220,645L220,315Q220,297 232,286Q244,275 260,275Q265,275 271,276Q277,277 282,281L530,447Q539,453 543.5,461.5Q548,470 548,480Q548,490 543.5,498.5Q539,507 530,513L282,679Q277,683 271,684Q265,685 260,685Q244,685 232,674Q220,663 220,645Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_skip_previous_24.xml b/shared_resources/src/main/res/drawable/ic_baseline_skip_previous_24.xml
index e72a2cc0..76e23dc1 100644
--- a/shared_resources/src/main/res/drawable/ic_baseline_skip_previous_24.xml
+++ b/shared_resources/src/main/res/drawable/ic_baseline_skip_previous_24.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M220,680L220,280Q220,263 231.5,251.5Q243,240 260,240Q277,240 288.5,251.5Q300,263 300,280L300,680Q300,697 288.5,708.5Q277,720 260,720Q243,720 231.5,708.5Q220,697 220,680ZM678,679L430,513Q421,507 416.5,498.5Q412,490 412,480Q412,470 416.5,461.5Q421,453 430,447L678,281Q683,277 689,276Q695,275 700,275Q716,275 728,286Q740,297 740,315L740,645Q740,663 728,674Q716,685 700,685Q695,685 689,684Q683,683 678,679Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_speaker_phone_24.xml b/shared_resources/src/main/res/drawable/ic_baseline_speaker_phone_24.xml
index 19917435..5513e23c 100644
--- a/shared_resources/src/main/res/drawable/ic_baseline_speaker_phone_24.xml
+++ b/shared_resources/src/main/res/drawable/ic_baseline_speaker_phone_24.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M480,280Q450,280 422,289Q394,298 369,315Q355,325 338,324Q321,323 309,311Q297,299 297,282.5Q297,266 311,256Q348,229 391,214.5Q434,200 480,200Q526,200 569,214.5Q612,229 649,256Q663,266 663,282.5Q663,299 651,311Q639,323 622,324Q605,325 591,315Q566,298 538.5,289Q511,280 480,280ZM480,120Q419,120 361.5,140Q304,160 256,198Q242,209 226,208.5Q210,208 198,196Q186,184 187,167.5Q188,151 201,140Q261,92 332,66Q403,40 480,40Q557,40 628,66Q699,92 759,140Q772,151 773,167.5Q774,184 762,196Q750,208 734,208.5Q718,209 704,198Q656,160 598.5,140Q541,120 480,120ZM400,880Q367,880 343.5,856.5Q320,833 320,800L320,480Q320,447 343.5,423.5Q367,400 400,400L560,400Q593,400 616.5,423.5Q640,447 640,480L640,800Q640,833 616.5,856.5Q593,880 560,880L400,880Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_baseline_volume_down_24.xml b/shared_resources/src/main/res/drawable/ic_baseline_volume_down_24.xml
index e76d1b11..2266e273 100644
--- a/shared_resources/src/main/res/drawable/ic_baseline_volume_down_24.xml
+++ b/shared_resources/src/main/res/drawable/ic_baseline_volume_down_24.xml
@@ -1,10 +1,10 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:autoMirrored="true">
+ android:pathData="M360,600L240,600Q223,600 211.5,588.5Q200,577 200,560L200,400Q200,383 211.5,371.5Q223,360 240,360L360,360L492,228Q511,209 535.5,219.5Q560,230 560,257L560,703Q560,730 535.5,740.5Q511,751 492,732L360,600ZM740,480Q740,522 721,559.5Q702,597 671,621Q661,627 650.5,621.5Q640,616 640,604L640,354Q640,342 650.5,336.5Q661,331 671,337Q702,362 721,400Q740,438 740,480Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_battery_charging_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_battery_charging_white_24dp.xml
new file mode 100644
index 00000000..bade8bff
--- /dev/null
+++ b/shared_resources/src/main/res/drawable/ic_battery_charging_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_battery_saver.xml b/shared_resources/src/main/res/drawable/ic_battery_saver.xml
index c4fe5a13..dc8f4997 100644
--- a/shared_resources/src/main/res/drawable/ic_battery_saver.xml
+++ b/shared_resources/src/main/res/drawable/ic_battery_saver.xml
@@ -1,12 +1,9 @@
-
+ android:viewportWidth="960"
+ android:viewportHeight="960">
-
+ android:pathData="M640,760L560,760Q543,760 531.5,748.5Q520,737 520,720Q520,703 531.5,691.5Q543,680 560,680L640,680L640,600Q640,583 651.5,571.5Q663,560 680,560Q697,560 708.5,571.5Q720,583 720,600L720,680L800,680Q817,680 828.5,691.5Q840,703 840,720Q840,737 828.5,748.5Q817,760 800,760L720,760L720,840Q720,857 708.5,868.5Q697,880 680,880Q663,880 651.5,868.5Q640,857 640,840L640,760ZM320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,120Q400,103 411.5,91.5Q423,80 440,80L520,80Q537,80 548.5,91.5Q560,103 560,120L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,453Q680,464 672,472.5Q664,481 653,482Q611,487 575,504.5Q539,522 511,550Q479,582 459.5,625.5Q440,669 440,720Q440,750 447,778Q454,806 468,832Q476,849 468,864.5Q460,880 443,880L320,880Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_battery_std_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_battery_std_white_24dp.xml
index 7f1064a0..c000940c 100644
--- a/shared_resources/src/main/res/drawable/ic_battery_std_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_battery_std_white_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:fillColor="@android:color/white"
+ android:pathData="M320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,120Q400,103 411.5,91.5Q423,80 440,80L520,80Q537,80 548.5,91.5Q560,103 560,120L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,840Q680,857 668.5,868.5Q657,880 640,880L320,880Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_bluetooth_disabled_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_bluetooth_disabled_white_24dp.xml
index a1ad8ea5..d5f5f171 100644
--- a/shared_resources/src/main/res/drawable/ic_bluetooth_disabled_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_bluetooth_disabled_white_24dp.xml
@@ -1,10 +1,12 @@
-
+ android:viewportHeight="960"
+ android:viewportWidth="960"
+ android:width="24dp">
+
+ android:fillColor="@android:color/white"
+ android:pathData="M440,576L284,732Q273,743 256,743Q239,743 228,732Q217,721 217,704Q217,687 228,676L396,508L84,196Q73,185 73,168Q73,151 84,140Q95,129 112,129Q129,129 140,140L820,820Q831,831 831,848Q831,865 820,876Q809,887 792,887Q775,887 764,876L624,736L508,852Q502,858 495,861Q488,864 480,864Q464,864 452,852.5Q440,841 440,823L440,576ZM520,726L566,680L520,634L520,726ZM564,452L508,396L596,308L520,234L520,408L440,328L440,137Q440,119 452,107.5Q464,96 480,96Q488,96 495,99Q502,102 508,108L680,280Q686,286 688.5,293Q691,300 691,308Q691,316 688.5,323Q686,330 680,336L564,452Z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_bluetooth_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_bluetooth_white_24dp.xml
index 08cf89f8..d5b008de 100644
--- a/shared_resources/src/main/res/drawable/ic_bluetooth_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_bluetooth_white_24dp.xml
@@ -1,10 +1,12 @@
-
+ android:viewportHeight="960"
+ android:viewportWidth="960"
+ android:width="24dp">
+
+ android:fillColor="@android:color/white"
+ android:pathData="M440,823L440,576L284,732Q273,743 256,743Q239,743 228,732Q217,721 217,704Q217,687 228,676L424,480L228,284Q217,273 217,256Q217,239 228,228Q239,217 256,217Q273,217 284,228L440,384L440,137Q440,119 452,107.5Q464,96 480,96Q488,96 495,99Q502,102 508,108L680,280Q686,286 688.5,293Q691,300 691,308Q691,316 688.5,323Q686,330 680,336L536,480L680,624Q686,630 688.5,637Q691,644 691,652Q691,660 688.5,667Q686,674 680,680L508,852Q502,858 495,861Q488,864 480,864Q464,864 452,852.5Q440,841 440,823ZM520,384L596,308L520,234L520,384ZM520,726L596,652L520,576L520,726Z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_brightness_auto.xml b/shared_resources/src/main/res/drawable/ic_brightness_auto.xml
index 1d51ac09..4f6f8ee8 100644
--- a/shared_resources/src/main/res/drawable/ic_brightness_auto.xml
+++ b/shared_resources/src/main/res/drawable/ic_brightness_auto.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M312,640L376,640L392,594Q400,573 423.5,560.5Q447,548 505,548Q527,548 544.5,560.5Q562,573 570,594L579,621Q582,629 589.5,634.5Q597,640 606,640L606,640Q621,640 629.5,627.5Q638,615 633,601L519,299Q516,290 505.5,285Q495,280 469,280Q460,280 452,285Q444,290 441,299L312,640ZM426,496L478,346L482,346L534,496L426,496ZM346,800L240,800Q207,800 183.5,776.5Q160,753 160,720L160,614L83,536Q72,524 66,509.5Q60,495 60,480Q60,465 66,450.5Q72,436 83,424L160,346L160,240Q160,207 183.5,183.5Q207,160 240,160L346,160L424,83Q436,72 450.5,66Q465,60 480,60Q495,60 509.5,66Q524,72 536,83L614,160L720,160Q753,160 776.5,183.5Q800,207 800,240L800,346L877,424Q888,436 894,450.5Q900,465 900,480Q900,495 894,509.5Q888,524 877,536L800,614L800,720Q800,753 776.5,776.5Q753,800 720,800L614,800L536,877Q524,888 509.5,894Q495,900 480,900Q465,900 450.5,894Q436,888 424,877L346,800Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_brightness_medium.xml b/shared_resources/src/main/res/drawable/ic_brightness_medium.xml
index 844fffa1..21043f1b 100644
--- a/shared_resources/src/main/res/drawable/ic_brightness_medium.xml
+++ b/shared_resources/src/main/res/drawable/ic_brightness_medium.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M346,800L240,800Q207,800 183.5,776.5Q160,753 160,720L160,614L83,536Q72,524 66,509.5Q60,495 60,480Q60,465 66,450.5Q72,436 83,424L160,346L160,240Q160,207 183.5,183.5Q207,160 240,160L346,160L424,83Q436,72 450.5,66Q465,60 480,60Q495,60 509.5,66Q524,72 536,83L614,160L720,160Q753,160 776.5,183.5Q800,207 800,240L800,346L877,424Q888,436 894,450.5Q900,465 900,480Q900,495 894,509.5Q888,524 877,536L800,614L800,720Q800,753 776.5,776.5Q753,800 720,800L614,800L536,877Q524,888 509.5,894Q495,900 480,900Q465,900 450.5,894Q436,888 424,877L346,800ZM480,680Q563,680 621.5,621.5Q680,563 680,480Q680,397 621.5,338.5Q563,280 480,280L480,680Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_call_end_24dp.xml b/shared_resources/src/main/res/drawable/ic_call_end_24dp.xml
deleted file mode 100644
index fe817022..00000000
--- a/shared_resources/src/main/res/drawable/ic_call_end_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_charging_station_24dp.xml b/shared_resources/src/main/res/drawable/ic_charging_station_24dp.xml
index f9c5dc9d..fe4fb6f2 100644
--- a/shared_resources/src/main/res/drawable/ic_charging_station_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_charging_station_24dp.xml
@@ -1,12 +1,9 @@
-
+ android:viewportHeight="24">
-
+ android:pathData="M17,1H7C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1 17,1zM17,18H7V6h10V18zM12.5,11V9.12c0,-0.53 -0.71,-0.7 -0.95,-0.22l-1.69,3.38C9.7,12.61 9.94,13 10.31,13h1.19v1.88c0,0.53 0.71,0.7 0.95,0.22l1.69,-3.38C14.3,11.39 14.06,11 13.69,11H12.5z" />
diff --git a/shared_resources/src/main/res/drawable/ic_check_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_check_white_24dp.xml
index 609f5657..d4f359f6 100644
--- a/shared_resources/src/main/res/drawable/ic_check_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_check_white_24dp.xml
@@ -1,10 +1,12 @@
-
+ android:viewportHeight="960"
+ android:viewportWidth="960"
+ android:width="24dp">
+
+ android:fillColor="@android:color/white"
+ android:pathData="M382,606L721,267Q733,255 749,255Q765,255 777,267Q789,279 789,295.5Q789,312 777,324L410,692Q398,704 382,704Q366,704 354,692L182,520Q170,508 170.5,491.5Q171,475 183,463Q195,451 211.5,451Q228,451 240,463L382,606Z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_clear_all_24dp.xml b/shared_resources/src/main/res/drawable/ic_clear_all_24dp.xml
deleted file mode 100644
index 9a48563c..00000000
--- a/shared_resources/src/main/res/drawable/ic_clear_all_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_close_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_close_white_24dp.xml
index 6fde7c88..1323d563 100644
--- a/shared_resources/src/main/res/drawable/ic_close_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_close_white_24dp.xml
@@ -1,10 +1,12 @@
-
+ android:viewportHeight="960"
+ android:viewportWidth="960"
+ android:width="24dp">
+
+ android:fillColor="@android:color/white"
+ android:pathData="M480,536L284,732Q273,743 256,743Q239,743 228,732Q217,721 217,704Q217,687 228,676L424,480L228,284Q217,273 217,256Q217,239 228,228Q239,217 256,217Q273,217 284,228L480,424L676,228Q687,217 704,217Q721,217 732,228Q743,239 743,256Q743,273 732,284L536,480L732,676Q743,687 743,704Q743,721 732,732Q721,743 704,743Q687,743 676,732L480,536Z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_delete_outline.xml b/shared_resources/src/main/res/drawable/ic_delete_outline.xml
index 11dc7ad1..0eaef9b6 100644
--- a/shared_resources/src/main/res/drawable/ic_delete_outline.xml
+++ b/shared_resources/src/main/res/drawable/ic_delete_outline.xml
@@ -1,12 +1,12 @@
+ android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L200,240Q183,240 171.5,228.5Q160,217 160,200Q160,183 171.5,171.5Q183,160 200,160L360,160L360,160Q360,143 371.5,131.5Q383,120 400,120L560,120Q577,120 588.5,131.5Q600,143 600,160L600,160L760,160Q777,160 788.5,171.5Q800,183 800,200Q800,217 788.5,228.5Q777,240 760,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM400,680Q417,680 428.5,668.5Q440,657 440,640L440,360Q440,343 428.5,331.5Q417,320 400,320Q383,320 371.5,331.5Q360,343 360,360L360,640Q360,657 371.5,668.5Q383,680 400,680ZM560,680Q577,680 588.5,668.5Q600,657 600,640L600,360Q600,343 588.5,331.5Q577,320 560,320Q543,320 531.5,331.5Q520,343 520,360L520,640Q520,657 531.5,668.5Q543,680 560,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_dialpad_24dp.xml b/shared_resources/src/main/res/drawable/ic_dialpad_24dp.xml
deleted file mode 100644
index b22ee3b3..00000000
--- a/shared_resources/src/main/res/drawable/ic_dialpad_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_do_not_disturb_off_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_do_not_disturb_off_white_24dp.xml
index 97c5a8ee..7d1c8a1d 100644
--- a/shared_resources/src/main/res/drawable/ic_do_not_disturb_off_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_do_not_disturb_off_white_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-60 17,-115.5T146,260l-63,-63q-12,-12 -12,-28.5T83,140q12,-12 28.5,-12t28.5,12l680,680q12,12 12,28t-12,28q-12,12 -28.5,12T763,876l-63,-62q-49,32 -104.5,49T480,880ZM326,440h-6q-17,0 -28.5,11.5T280,480q0,17 11,28.5t28,11.5h87l-80,-80ZM767,653L634,520h6q17,0 28.5,-11.5T680,480q0,-17 -11,-28.5T641,440h-87L307,193q-18,-18 -14.5,-43t26.5,-36q38,-17 78.5,-25.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,42 -8.5,82.5T846,641q-11,23 -35.5,27T767,653Z"
+ android:fillColor="#FFFFFF" />
diff --git a/shared_resources/src/main/res/drawable/ic_do_not_disturb_silence_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_do_not_disturb_silence_white_24dp.xml
new file mode 100644
index 00000000..aa8e36af
--- /dev/null
+++ b/shared_resources/src/main/res/drawable/ic_do_not_disturb_silence_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_error_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_error_white_24dp.xml
index 0253ade8..edc7a084 100644
--- a/shared_resources/src/main/res/drawable/ic_error_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_error_white_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M480,680q17,0 28.5,-11.5T520,640q0,-17 -11.5,-28.5T480,600q-17,0 -28.5,11.5T440,640q0,17 11.5,28.5T480,680ZM480,520q17,0 28.5,-11.5T520,480v-160q0,-17 -11.5,-28.5T480,280q-17,0 -28.5,11.5T440,320v160q0,17 11.5,28.5T480,520ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880Z"
+ android:fillColor="#FFFFFF" />
diff --git a/shared_resources/src/main/res/drawable/ic_icon.xml b/shared_resources/src/main/res/drawable/ic_icon.xml
index 1ae2e5b3..ba93bc13 100644
--- a/shared_resources/src/main/res/drawable/ic_icon.xml
+++ b/shared_resources/src/main/res/drawable/ic_icon.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:fillColor="@android:color/white"
+ android:pathData="M420,880Q394,880 372.5,864.5Q351,849 343,823L320,746Q314,726 301.5,705.5Q289,685 269,663Q235,626 217.5,579Q200,532 200,480Q200,429 217.5,382Q235,335 269,297Q289,274 301.5,254Q314,234 320,214L343,137Q351,111 372.5,95.5Q394,80 420,80L540,80Q566,80 587.5,95.5Q609,111 617,137L640,214Q646,234 658.5,254.5Q671,275 691,297Q725,334 742.5,381Q760,428 760,480Q760,531 742.5,578Q725,625 691,663Q671,686 658.5,706Q646,726 640,746L617,823Q609,849 587.5,864.5Q566,880 540,880L420,880ZM480,680Q563,680 621.5,621.5Q680,563 680,480Q680,397 621.5,338.5Q563,280 480,280Q397,280 338.5,338.5Q280,397 280,480Q280,563 338.5,621.5Q397,680 480,680Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_lightbulb_outline_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_lightbulb_outline_white_24dp.xml
index b59d5814..713b33b3 100644
--- a/shared_resources/src/main/res/drawable/ic_lightbulb_outline_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_lightbulb_outline_white_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M240,200v-40q0,-33 23.5,-56.5T320,80h320q33,0 56.5,23.5T720,160v40L240,200ZM480,620q25,0 42.5,-17.5T540,560q0,-25 -17.5,-42.5T480,500q-25,0 -42.5,17.5T420,560q0,25 17.5,42.5T480,620ZM320,800v-360l-67,-100q-7,-10 -10,-21t-3,-23v-16h480v16q0,12 -3,23t-10,21l-67,100v360q0,33 -23.5,56.5T560,880L400,880q-33,0 -56.5,-23.5T320,800Z"
+ android:fillColor="#FFFFFF" />
diff --git a/shared_resources/src/main/res/drawable/ic_location_off_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_location_off_white_24dp.xml
index d713bc22..f21a961a 100644
--- a/shared_resources/src/main/res/drawable/ic_location_off_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_location_off_white_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M480,853q-14,0 -28,-5t-25,-15q-65,-60 -115,-117t-83.5,-110.5q-33.5,-53.5 -51,-103T160,408q0,-32 5,-61t14,-55L55,168q-12,-12 -12,-28.5T55,111q12,-12 28.5,-12t28.5,12l736,736q12,12 12,28.5T848,904q-12,12 -28.5,12T791,904L627,740q-25,26 -50,51.5T533,833q-11,10 -25,15t-28,5ZM480,80q127,0 223.5,89T800,408q0,35 -10,72.5T760,558q-11,22 -35.5,24.5T683,568L551,436q5,-8 7,-17t2,-19q0,-17 -6,-31.5T537,343q-11,-11 -25.5,-17t-31.5,-6q-10,0 -19,2t-17,7L315,200q-18,-18 -15.5,-42.5T323,120q35,-20 75,-30t82,-10Z"
+ android:fillColor="#fff" />
diff --git a/shared_resources/src/main/res/drawable/ic_location_on_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_location_on_white_24dp.xml
index e364cc09..ada6a502 100644
--- a/shared_resources/src/main/res/drawable/ic_location_on_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_location_on_white_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M480,853q-14,0 -28,-5t-25,-15q-65,-60 -115,-117t-83.5,-110.5q-33.5,-53.5 -51,-103T160,408q0,-150 96.5,-239T480,80q127,0 223.5,89T800,408q0,45 -17.5,94.5t-51,103Q698,659 648,716T533,833q-11,10 -25,15t-28,5ZM480,480q33,0 56.5,-23.5T560,400q0,-33 -23.5,-56.5T480,320q-33,0 -56.5,23.5T400,400q0,33 23.5,56.5T480,480Z"
+ android:fillColor="#fff" />
diff --git a/shared_resources/src/main/res/drawable/ic_lock_outline_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_lock_outline_white_24dp.xml
deleted file mode 100644
index a4c8109a..00000000
--- a/shared_resources/src/main/res/drawable/ic_lock_outline_white_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_lock_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_lock_white_24dp.xml
new file mode 100644
index 00000000..1f299613
--- /dev/null
+++ b/shared_resources/src/main/res/drawable/ic_lock_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_mic_off_24dp.xml b/shared_resources/src/main/res/drawable/ic_mic_off_24dp.xml
deleted file mode 100644
index 20366a91..00000000
--- a/shared_resources/src/main/res/drawable/ic_mic_off_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_mode_edit.xml b/shared_resources/src/main/res/drawable/ic_mode_edit.xml
new file mode 100644
index 00000000..042d1167
--- /dev/null
+++ b/shared_resources/src/main/res/drawable/ic_mode_edit.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_music_note_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_music_note_white_24dp.xml
index 6779d3c8..37e8ff89 100644
--- a/shared_resources/src/main/res/drawable/ic_music_note_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_music_note_white_24dp.xml
@@ -1,10 +1,18 @@
-
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="#FFFFFF">
+
+
+
+
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_network_cell_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_network_cell_white_24dp.xml
index 71555504..e82c7a57 100644
--- a/shared_resources/src/main/res/drawable/ic_network_cell_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_network_cell_white_24dp.xml
@@ -1,14 +1,12 @@
-
+ android:viewportHeight="960"
+ android:viewportWidth="960"
+ android:width="24dp">
+
-
+ android:fillColor="@android:color/white"
+ android:pathData="M60,740L60,540Q60,515 77.5,497.5Q95,480 120,480Q145,480 162.5,497.5Q180,515 180,540L180,740Q180,765 162.5,782.5Q145,800 120,800Q95,800 77.5,782.5Q60,765 60,740ZM300,740L300,440Q300,415 317.5,397.5Q335,380 360,380Q385,380 402.5,397.5Q420,415 420,440L420,740Q420,765 402.5,782.5Q385,800 360,800Q335,800 317.5,782.5Q300,765 300,740ZM540,740L540,340Q540,315 557.5,297.5Q575,280 600,280Q625,280 642.5,297.5Q660,315 660,340L660,740Q660,765 642.5,782.5Q625,800 600,800Q575,800 557.5,782.5Q540,765 540,740ZM780,740L780,220Q780,195 797.5,177.5Q815,160 840,160Q865,160 882.5,177.5Q900,195 900,220L900,740Q900,765 882.5,782.5Q865,800 840,800Q815,800 797.5,782.5Q780,765 780,740Z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_network_wifi_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_network_wifi_white_24dp.xml
index 971c5f6b..5953cd45 100644
--- a/shared_resources/src/main/res/drawable/ic_network_wifi_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_network_wifi_white_24dp.xml
@@ -1,14 +1,12 @@
-
+ android:viewportHeight="960"
+ android:viewportWidth="960"
+ android:width="24dp">
+
-
+ android:fillColor="@android:color/white"
+ android:pathData="M480,280Q392,280 308.5,307.5Q225,335 155,389Q135,405 109.5,404.5Q84,404 66,386Q49,369 49,344Q49,319 69,304Q157,234 262.5,197Q368,160 480,160Q593,160 698,197Q803,234 891,304Q911,319 911,344Q911,369 894,386Q876,404 850.5,404.5Q825,405 805,389Q735,335 652,307.5Q569,280 480,280ZM480,520Q439,520 400.5,530.5Q362,541 327,562Q305,576 279.5,575Q254,574 236,556Q219,539 219.5,514.5Q220,490 240,476Q293,439 354,419.5Q415,400 480,400Q545,400 606,419.5Q667,439 720,476Q740,490 740.5,514.5Q741,539 724,556Q706,574 680.5,575Q655,576 633,562Q598,541 559.5,530.5Q521,520 480,520ZM480,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800Z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_nfc_off.xml b/shared_resources/src/main/res/drawable/ic_nfc_off.xml
new file mode 100644
index 00000000..7faab673
--- /dev/null
+++ b/shared_resources/src/main/res/drawable/ic_nfc_off.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_nfc_on.xml b/shared_resources/src/main/res/drawable/ic_nfc_on.xml
new file mode 100644
index 00000000..86a98e08
--- /dev/null
+++ b/shared_resources/src/main/res/drawable/ic_nfc_on.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_notifications_active_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_notifications_active_white_24dp.xml
index 529ab6df..790ebe2a 100644
--- a/shared_resources/src/main/res/drawable/ic_notifications_active_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_notifications_active_white_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M200,760q-17,0 -28.5,-11.5T160,720q0,-17 11.5,-28.5T200,680h40v-280q0,-83 50,-147.5T420,168v-28q0,-25 17.5,-42.5T480,80q25,0 42.5,17.5T540,140v28q80,20 130,84.5T720,400v280h40q17,0 28.5,11.5T800,720q0,17 -11.5,28.5T760,760L200,760ZM480,880q-33,0 -56.5,-23.5T400,800h160q0,33 -23.5,56.5T480,880ZM120,400q-17,0 -28.5,-13T82,357q8,-75 42,-139.5T211,105q13,-11 29.5,-10t26.5,15q10,14 8,30t-15,28q-39,37 -64,86t-33,106q-2,17 -14,28.5T120,400ZM840,400q-17,0 -29,-11.5T797,360q-8,-57 -33,-106t-64,-86q-13,-12 -15,-28t8,-30q10,-14 26.5,-15t29.5,10q53,48 87,112.5T878,357q2,17 -9.5,30T840,400Z"
+ android:fillColor="#FFFFFF" />
diff --git a/shared_resources/src/main/res/drawable/ic_notifications_off_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_notifications_off_white_24dp.xml
deleted file mode 100644
index 896ce6cc..00000000
--- a/shared_resources/src/main/res/drawable/ic_notifications_off_white_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_outline_location_on_24dp.xml b/shared_resources/src/main/res/drawable/ic_outline_location_on_24dp.xml
index 1550c347..1b7d9a5d 100644
--- a/shared_resources/src/main/res/drawable/ic_outline_location_on_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_outline_location_on_24dp.xml
@@ -1,13 +1,12 @@
-
+ android:viewportHeight="960"
+ android:viewportWidth="960"
+ android:width="24dp">
+
-
+ android:pathData="M480,774Q602,662 661,570.5Q720,479 720,408Q720,299 650.5,229.5Q581,160 480,160Q379,160 309.5,229.5Q240,299 240,408Q240,479 299,570.5Q358,662 480,774ZM480,853Q466,853 452,848Q438,843 427,833Q362,773 312,716Q262,659 228.5,605.5Q195,552 177.5,502.5Q160,453 160,408Q160,258 256.5,169Q353,80 480,80Q607,80 703.5,169Q800,258 800,408Q800,453 782.5,502.5Q765,552 731.5,605.5Q698,659 648,716Q598,773 533,833Q522,843 508,848Q494,853 480,853ZM480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400Q480,400 480,400ZM480,480Q513,480 536.5,456.5Q560,433 560,400Q560,367 536.5,343.5Q513,320 480,320Q447,320 423.5,343.5Q400,367 400,400Q400,433 423.5,456.5Q447,480 480,480Z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_outline_pause_24.xml b/shared_resources/src/main/res/drawable/ic_outline_pause_24.xml
deleted file mode 100644
index 1eb828ec..00000000
--- a/shared_resources/src/main/res/drawable/ic_outline_pause_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_outline_play_arrow_24.xml b/shared_resources/src/main/res/drawable/ic_outline_play_arrow_24.xml
deleted file mode 100644
index c1f50ebd..00000000
--- a/shared_resources/src/main/res/drawable/ic_outline_play_arrow_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_outline_view_apps.xml b/shared_resources/src/main/res/drawable/ic_outline_view_apps.xml
deleted file mode 100644
index d44bc66e..00000000
--- a/shared_resources/src/main/res/drawable/ic_outline_view_apps.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_phone_24dp.xml b/shared_resources/src/main/res/drawable/ic_phone_24dp.xml
index 0ebb796d..de0eef0e 100644
--- a/shared_resources/src/main/res/drawable/ic_phone_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_phone_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M798,840Q673,840 551,785.5Q429,731 329,631Q229,531 174.5,409Q120,287 120,162Q120,144 132,132Q144,120 162,120L324,120Q338,120 349,129.5Q360,139 362,152L388,292Q390,308 387,319Q384,330 376,338L279,436Q299,473 326.5,507.5Q354,542 387,574Q418,605 452,631.5Q486,658 524,680L618,586Q627,577 641.5,572.5Q656,568 670,570L808,598Q822,602 831,612.5Q840,623 840,636L840,798Q840,816 828,828Q816,840 798,840Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_phonelink_erase_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_phonelink_erase_white_24dp.xml
index 16710bf6..ed14cc81 100644
--- a/shared_resources/src/main/res/drawable/ic_phonelink_erase_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_phonelink_erase_white_24dp.xml
@@ -1,10 +1,12 @@
-
+ android:height="24dp"
+ android:tint="#FFFFFF"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ android:fillColor="@android:color/white"
+ android:pathData="M12.5,7.7c-0.28,-0.28 -0.72,-0.28 -1,0L8,11.2 4.5,7.7c-0.28,-0.28 -0.72,-0.28 -1,0s-0.28,0.72 0,1L7,12.2l-3.5,3.5c-0.28,0.28 -0.28,0.72 0,1s0.72,0.28 1,0L8,13.2l3.5,3.5c0.28,0.28 0.72,0.28 1,0s0.28,-0.72 0,-1L9,12.2l3.5,-3.5c0.28,-0.28 0.28,-0.72 0,-1zM19,1H9c-1.1,0 -2,0.9 -2,2v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1V4h10v16H9v-1c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v2c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-2 -2,-2z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_play_arrow_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_play_arrow_white_24dp.xml
index 8469b3a2..7e16914e 100644
--- a/shared_resources/src/main/res/drawable/ic_play_arrow_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_play_arrow_white_24dp.xml
@@ -1,10 +1,12 @@
-
+ android:viewportHeight="24"
+ android:viewportWidth="24"
+ android:width="24dp">
+
+ android:fillColor="@android:color/white"
+ android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_play_circle_filled_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_play_circle_filled_white_24dp.xml
index 5e9f308d..95daa719 100644
--- a/shared_resources/src/main/res/drawable/ic_play_circle_filled_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_play_circle_filled_white_24dp.xml
@@ -1,10 +1,9 @@
-
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+
diff --git a/shared_resources/src/main/res/drawable/ic_play_circle_simpleblue.xml b/shared_resources/src/main/res/drawable/ic_play_circle_simpleblue.xml
index 1fa60989..e15d948f 100644
--- a/shared_resources/src/main/res/drawable/ic_play_circle_simpleblue.xml
+++ b/shared_resources/src/main/res/drawable/ic_play_circle_simpleblue.xml
@@ -1,13 +1,14 @@
-
-
-
+ android:height="108dp"
+ android:viewportWidth="2160"
+ android:viewportHeight="2160">
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_remove_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_remove_white_24dp.xml
deleted file mode 100644
index f9d32fba..00000000
--- a/shared_resources/src/main/res/drawable/ic_remove_white_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_signal_cellular_off_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_signal_cellular_off_white_24dp.xml
index e72b637c..7dfb0106 100644
--- a/shared_resources/src/main/res/drawable/ic_signal_cellular_off_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_signal_cellular_off_white_24dp.xml
@@ -1,10 +1,12 @@
-
+ android:viewportHeight="960"
+ android:viewportWidth="960"
+ android:width="24dp">
+
+ android:fillColor="@android:color/white"
+ android:pathData="M600,800Q575,800 557.5,782.5Q540,765 540,740L540,644L420,560L420,740Q420,765 402.5,782.5Q385,800 360,800Q335,800 317.5,782.5Q300,765 300,740L300,476L70,315Q53,303 49.5,283Q46,263 58,246Q70,229 90.5,225Q111,221 128,233L832,727Q849,739 853,759Q857,779 845,796Q833,813 812.5,816.5Q792,820 775,808L660,728L660,740Q660,765 642.5,782.5Q625,800 600,800ZM900,652L780,568L780,220Q780,195 797.5,177.5Q815,160 840,160Q865,160 882.5,177.5Q900,195 900,220L900,652ZM60,740L60,540Q60,515 77.5,497.5Q95,480 120,480Q145,480 162.5,497.5Q180,515 180,540L180,740Q180,765 162.5,782.5Q145,800 120,800Q95,800 77.5,782.5Q60,765 60,740ZM660,484L540,400L540,340Q540,315 557.5,297.5Q575,280 600,280Q625,280 642.5,297.5Q660,315 660,340L660,484Z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_signal_wifi_off_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_signal_wifi_off_white_24dp.xml
index 4282a6c7..2a62b5ba 100644
--- a/shared_resources/src/main/res/drawable/ic_signal_wifi_off_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_signal_wifi_off_white_24dp.xml
@@ -1,10 +1,12 @@
-
+ android:viewportHeight="960"
+ android:viewportWidth="960"
+ android:width="24dp">
+
+ android:fillColor="@android:color/white"
+ android:pathData="M480,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800ZM302,440L192,363Q182,369 173,375.5Q164,382 155,389Q135,405 109.5,404.5Q84,404 66,386Q49,369 49,344Q49,319 69,304Q74,300 78.5,297Q83,294 88,290L70,278Q53,266 49.5,246Q46,226 58,209Q70,192 90.5,188Q111,184 128,196L832,690Q849,702 853,722Q857,742 845,759Q833,776 812.5,779.5Q792,783 775,771L424,525Q398,530 373.5,539.5Q349,549 326,563Q304,577 279,575.5Q254,574 236,556Q219,539 219.5,514.5Q220,490 240,476Q255,465 270.5,456.5Q286,448 302,440ZM425,283L290,188Q336,174 383.5,167Q431,160 480,160Q593,160 698,197Q803,234 891,304Q911,319 911,344Q911,369 894,386Q876,404 850.5,404.5Q825,405 805,389Q735,335 652,307.5Q569,280 480,280Q466,280 452.5,280.5Q439,281 425,283Z" />
+
diff --git a/shared_resources/src/main/res/drawable/ic_sleep_timer.xml b/shared_resources/src/main/res/drawable/ic_sleep_timer.xml
index 232848ab..03fdb481 100644
--- a/shared_resources/src/main/res/drawable/ic_sleep_timer.xml
+++ b/shared_resources/src/main/res/drawable/ic_sleep_timer.xml
@@ -3,12 +3,14 @@
android:height="24dp"
android:viewportWidth="6.35"
android:viewportHeight="6.35">
-
-
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_smartphone_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_smartphone_white_24dp.xml
index 842906fa..03a3fa40 100644
--- a/shared_resources/src/main/res/drawable/ic_smartphone_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_smartphone_white_24dp.xml
@@ -1,10 +1,12 @@
-
+ android:height="24dp"
+ android:tint="#FFFFFF"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_sync_24dp.xml b/shared_resources/src/main/res/drawable/ic_sync_24dp.xml
deleted file mode 100644
index 04c848ec..00000000
--- a/shared_resources/src/main/res/drawable/ic_sync_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_timelapse.xml b/shared_resources/src/main/res/drawable/ic_timelapse.xml
index 77fc3e48..37f4cd81 100644
--- a/shared_resources/src/main/res/drawable/ic_timelapse.xml
+++ b/shared_resources/src/main/res/drawable/ic_timelapse.xml
@@ -1,12 +1,12 @@
+ android:pathData="M480,720Q580,720 650,650Q720,580 720,480Q720,393 664.5,327Q609,261 524,244Q506,242 493,254Q480,266 480,284L480,480L342,618Q329,631 330,649Q331,667 345,678Q374,701 409,710.5Q444,720 480,720ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_touch_app.xml b/shared_resources/src/main/res/drawable/ic_touch_app.xml
index c1e57eac..9b685eae 100644
--- a/shared_resources/src/main/res/drawable/ic_touch_app.xml
+++ b/shared_resources/src/main/res/drawable/ic_touch_app.xml
@@ -1,12 +1,9 @@
-
+ android:viewportWidth="960"
+ android:viewportHeight="960">
-
+ android:pathData="M419,880Q391,880 366.5,868Q342,856 325,834L124,579Q116,570 117,557.5Q118,545 126,537L126,537Q146,516 174,512Q202,508 226,523L300,568L300,240Q300,223 311.5,211.5Q323,200 340,200Q357,200 369,211.5Q381,223 381,240L381,440L680,440Q730,440 765,475Q800,510 800,560L800,720Q800,786 753,833Q706,880 640,880L419,880ZM479,360Q462,360 450.5,348.5Q439,337 439,320Q439,318 444,300Q452,286 456,271.5Q460,257 460,240Q460,190 425,155Q390,120 340,120Q290,120 255,155Q220,190 220,240Q220,257 224,271.5Q228,286 236,300Q239,305 240,310Q241,315 241,320Q241,337 230,348.5Q219,360 202,360Q191,360 181.5,354Q172,348 167,339Q154,317 147,292Q140,267 140,240Q140,157 198.5,98.5Q257,40 340,40Q423,40 481.5,98.5Q540,157 540,240Q540,267 533,292Q526,317 513,339Q508,348 499,354Q490,360 479,360Z" />
diff --git a/shared_resources/src/main/res/drawable/ic_vibration_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_vibration_white_24dp.xml
index d4b68084..cb3465b8 100644
--- a/shared_resources/src/main/res/drawable/ic_vibration_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_vibration_white_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M320,840q-33,0 -56.5,-23.5T240,760v-560q0,-33 23.5,-56.5T320,120h320q33,0 56.5,23.5T720,200v560q0,33 -23.5,56.5T640,840L320,840ZM480,320q17,0 28.5,-11.5T520,280q0,-17 -11.5,-28.5T480,240q-17,0 -28.5,11.5T440,280q0,17 11.5,28.5T480,320ZM0,560v-160q0,-17 11.5,-28.5T40,360q17,0 28.5,11.5T80,400v160q0,17 -11.5,28.5T40,600q-17,0 -28.5,-11.5T0,560ZM120,640v-320q0,-17 11.5,-28.5T160,280q17,0 28.5,11.5T200,320v320q0,17 -11.5,28.5T160,680q-17,0 -28.5,-11.5T120,640ZM880,560v-160q0,-17 11.5,-28.5T920,360q17,0 28.5,11.5T960,400v160q0,17 -11.5,28.5T920,600q-17,0 -28.5,-11.5T880,560ZM760,640v-320q0,-17 11.5,-28.5T800,280q17,0 28.5,11.5T840,320v320q0,17 -11.5,28.5T800,680q-17,0 -28.5,-11.5T760,640Z"
+ android:fillColor="#FFFFFF" />
diff --git a/shared_resources/src/main/res/drawable/ic_view_apps_filled.xml b/shared_resources/src/main/res/drawable/ic_view_apps_filled.xml
new file mode 100644
index 00000000..28cac974
--- /dev/null
+++ b/shared_resources/src/main/res/drawable/ic_view_apps_filled.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/shared_resources/src/main/res/drawable/ic_view_list_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_view_list_white_24dp.xml
deleted file mode 100644
index 1bf43894..00000000
--- a/shared_resources/src/main/res/drawable/ic_view_list_white_24dp.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/shared_resources/src/main/res/drawable/ic_volume_off_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_volume_off_white_24dp.xml
index a61e4b2e..10df1923 100644
--- a/shared_resources/src/main/res/drawable/ic_volume_off_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_volume_off_white_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M671,783q-11,7 -22,13t-23,11q-15,7 -30.5,0T574,784q-6,-15 1.5,-29.5T598,733q4,-2 7.5,-4t7.5,-4L480,592v111q0,27 -24.5,37.5T412,732L280,600L160,600q-17,0 -28.5,-11.5T120,560v-160q0,-17 11.5,-28.5T160,360h88L84,196q-11,-11 -11,-28t11,-28q11,-11 28,-11t28,11l680,680q11,11 11,28t-11,28q-11,11 -28,11t-28,-11l-93,-93ZM760,479q0,-83 -44,-151.5T598,225q-15,-7 -22,-21.5t-2,-29.5q6,-16 21.5,-23t31.5,0q97,43 155,131t58,197q0,33 -6,65.5T817,607q-8,22 -24.5,27.5t-30.5,0.5q-14,-5 -22.5,-18t-0.5,-30q11,-26 16,-52.5t5,-55.5ZM591,337q33,21 51,63t18,80v10q0,5 -1,10 -2,13 -14,17t-22,-6l-51,-51q-6,-6 -9,-13.5t-3,-15.5v-77q0,-12 10.5,-17.5t20.5,0.5ZM390,278q-6,-6 -6,-14t6,-14l22,-22q19,-19 43.5,-8.5T480,257v63q0,14 -12,19t-22,-5l-56,-56Z"
+ android:fillColor="#FFFFFF" />
diff --git a/shared_resources/src/main/res/drawable/ic_volume_up_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_volume_up_white_24dp.xml
index 7db33791..d23e05d0 100644
--- a/shared_resources/src/main/res/drawable/ic_volume_up_white_24dp.xml
+++ b/shared_resources/src/main/res/drawable/ic_volume_up_white_24dp.xml
@@ -1,10 +1,9 @@
-
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ android:pathData="M760,479q0,-83 -44,-151.5T598,225q-15,-7 -22,-21.5t-2,-29.5q6,-16 21.5,-23t31.5,0q97,43 155,131.5T840,479q0,108 -58,196.5T627,807q-16,7 -31.5,0T574,784q-5,-15 2,-29.5t22,-21.5q74,-34 118,-102.5T760,479ZM280,600L160,600q-17,0 -28.5,-11.5T120,560v-160q0,-17 11.5,-28.5T160,360h120l132,-132q19,-19 43.5,-8.5T480,257v446q0,27 -24.5,37.5T412,732L280,600ZM660,480q0,42 -19,79.5T591,621q-10,6 -20.5,0.5T560,604v-250q0,-12 10.5,-17.5t20.5,0.5q31,25 50,63t19,80Z"
+ android:fillColor="#FFFFFF" />
diff --git a/shared_resources/src/main/res/drawable/ic_wifi_tethering.xml b/shared_resources/src/main/res/drawable/ic_wifi_tethering.xml
index 086acb45..d34b8020 100644
--- a/shared_resources/src/main/res/drawable/ic_wifi_tethering.xml
+++ b/shared_resources/src/main/res/drawable/ic_wifi_tethering.xml
@@ -1,10 +1,12 @@
-
+ android:viewportHeight="960"
+ android:viewportWidth="960"
+ android:width="24dp">
+
+ android:pathData="M233,781Q221,793 204,793Q187,793 176,780Q131,727 105.5,661Q80,595 80,520Q80,437 111.5,364Q143,291 197,237Q251,183 324,151.5Q397,120 480,120Q563,120 636,151.5Q709,183 763,237Q817,291 848.5,364Q880,437 880,520Q880,595 854.5,661Q829,727 784,780Q773,793 756.5,793.5Q740,794 728,782Q717,771 717,754Q717,737 728,724Q762,682 781,630Q800,578 800,520Q800,386 707,293Q614,200 480,200Q346,200 253,293Q160,386 160,520Q160,578 179,629.5Q198,681 233,723Q244,736 244.5,752.5Q245,769 233,781ZM346,668Q334,680 317,680.5Q300,681 290,667Q267,636 253.5,599Q240,562 240,520Q240,420 310,350Q380,280 480,280Q580,280 650,350Q720,420 720,520Q720,562 706.5,599.5Q693,637 670,667Q660,680 643,680.5Q626,681 614,669Q603,658 602.5,641Q602,624 612,610Q625,590 632.5,567.5Q640,545 640,520Q640,454 593,407Q546,360 480,360Q414,360 367,407Q320,454 320,520Q320,546 327.5,568Q335,590 348,610Q358,624 357.5,640.5Q357,657 346,668ZM480,600Q447,600 423.5,576.5Q400,553 400,520Q400,487 423.5,463.5Q447,440 480,440Q513,440 536.5,463.5Q560,487 560,520Q560,553 536.5,576.5Q513,600 480,600Z" />
+
diff --git a/shared_resources/src/main/res/drawable/icon_v3.xml b/shared_resources/src/main/res/drawable/icon_v3.xml
new file mode 100644
index 00000000..cd9be45f
--- /dev/null
+++ b/shared_resources/src/main/res/drawable/icon_v3.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/shared_resources/src/main/res/drawable/music_note_bounce_animated.xml b/shared_resources/src/main/res/drawable/music_note_bounce_animated.xml
new file mode 100644
index 00000000..46d4695a
--- /dev/null
+++ b/shared_resources/src/main/res/drawable/music_note_bounce_animated.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/shared_resources/src/main/res/drawable/rounded_equalizer_24.xml b/shared_resources/src/main/res/drawable/rounded_equalizer_24.xml
new file mode 100644
index 00000000..b512d9a1
--- /dev/null
+++ b/shared_resources/src/main/res/drawable/rounded_equalizer_24.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/shared_resources/src/main/res/values-de/strings.xml b/shared_resources/src/main/res/values-de/strings.xml
new file mode 100644
index 00000000..271eb119
--- /dev/null
+++ b/shared_resources/src/main/res/values-de/strings.xml
@@ -0,0 +1,54 @@
+
+
+ "Taschenlampen-Benachrichtigung"
+ "Berechtigungen"
+ "WLAN"
+ "Bluetooth"
+ "Mobile Daten"
+ "Standort"
+ "Nur Sensoren"
+ "Akkusparen"
+ "Hohe Genauigkeit"
+ "Taschenlampe"
+ "Zum Ausschalten tippen"
+ "Bildschirm sperren"
+ "Lautstärke"
+ "Bitte nicht stören"
+ "Nur Priorität"
+ "Nur Alarme"
+ "Totale Stille"
+ "Klingelton"
+ "Vibrieren"
+ "Ton"
+ "Lautlos"
+ "Aus"
+ "An"
+ "Stummgeschaltet"
+ "Medien abspielen"
+ "Mediensteuerung"
+ "Sleep-Timer"
+ "Einstellungen"
+ "Apps"
+ "Telefon"
+ "Anrufsteuerung"
+ "Anruf läuft…"
+ "Auflegen"
+ "Helligkeit"
+ "WLAN-Hotspot"
+ "Gesten"
+ "NFC"
+ "Energiesparmodus"
+
+
+ "App-Update auf der Uhr verfügbar. Bitte aktualisieren Sie, um fortzufahren…"
+ "App-Update auf dem Telefon verfügbar. Bitte aktualisieren Sie, um fortzufahren…"
+ "Aktualisieren"
+ "Zeitgesteuerte Aktionen"
+ "Löschen"
+ "Status"
+ "Aktion"
+ "Zeit"
+ "Aktion nicht unterstützt"
+ "Home"
+ "Letzte Apps"
+
\ No newline at end of file
diff --git a/shared_resources/src/main/res/values-es/strings.xml b/shared_resources/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..db4115a2
--- /dev/null
+++ b/shared_resources/src/main/res/values-es/strings.xml
@@ -0,0 +1,52 @@
+
+
+ Notificación de linterna
+ Permisos
+ WiFi
+ Bluetooth
+ Datos móviles
+ Ubicación
+ Solo sensores
+ Ahorro de batería
+ Alta precisión
+ Linterna
+ Toca para apagar
+ Pantalla de bloqueo
+ Volumen
+ No molestar
+ Solo prioridad
+ Solo alarmas
+ Silencio total
+ Timbre
+ Vibrar
+ Sonido
+ Silencioso
+ Apagado
+ "Encendido "
+ Silenciado
+ Reproducir multimedia
+ Controlador multimedia
+ Temporizador de apagado
+ Configuración
+ Aplicaciones
+ Teléfono
+ Controlador de llamadas
+ Llamada en curso...
+ Resaca
+ Brillo
+ Punto de acceso WiFi
+ Gestos
+ NFC
+ Ahorro de batería
+ Actualización de la aplicación disponible en el reloj. Actualiza para continuar...
+ Actualización de la aplicación disponible en el teléfono. Actualice para continuar...
+ Actualización
+ Acciones programadas
+ Eliminar
+ Estado
+ Acción
+ Tiempo
+ Acción no compatible
+ Inicio
+ Recientes
+
\ No newline at end of file
diff --git a/shared_resources/src/main/res/values-fr/strings.xml b/shared_resources/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..86ac6198
--- /dev/null
+++ b/shared_resources/src/main/res/values-fr/strings.xml
@@ -0,0 +1,54 @@
+
+
+ "Notification de la lampe de poche"
+ "Autorisations"
+ "WiFi"
+ "Bluetooth"
+ "Données mobiles"
+ "Localisation"
+ "Capteurs uniquement"
+ "Économie de batterie"
+ "Haute précision"
+ "Lampe de poche"
+ "Appuyez pour désactiver"
+ "Verrouiller l'écran"
+ "Volume"
+ "Ne pas déranger"
+ "Priorité uniquement"
+ "Alarmes uniquement"
+ "Silence total"
+ "Sonnerie"
+ "Vibrer"
+ "Son"
+ "Silencieux"
+ "Désactivé"
+ "Activé"
+ "Muet"
+ "Lire le contenu multimédia"
+ "Contrôleur multimédia"
+ "Minuteur de veille"
+ "Paramètres"
+ "Applications"
+ "Téléphone"
+ "Contrôleur d'appel"
+ "Appel en cours…"
+ "Raccrocher"
+ "Luminosité"
+ "Point d'accès WiFi"
+ "Gestes"
+ "NFC"
+ "Économiseur de batterie"
+
+
+ "Mise à jour de l'application disponible sur la montre. Veuillez mettre à jour pour continuer…"
+ "Mise à jour de l'application disponible sur le téléphone. Veuillez mettre à jour pour continuer…"
+ "Mettre à jour"
+ "Actions planifiées"
+ "Supprimer"
+ "État"
+ "Action"
+ "Heure"
+ "Action non prise en charge"
+ "Accueil"
+ "Récentes"
+
\ No newline at end of file
diff --git a/shared_resources/src/main/res/values/strings.xml b/shared_resources/src/main/res/values/strings.xml
index 410a8cff..0361bd02 100644
--- a/shared_resources/src/main/res/values/strings.xml
+++ b/shared_resources/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
- SimpleWear
+ SimpleWear
Flashlight notification
@@ -49,8 +49,9 @@
Brightness
WiFi Hotspot
-
Gestures
+ NFC
+ Battery Saver
App update available on watch. Please update to continue…
diff --git a/shared_resources/src/main/res/xml/locales_config.xml b/shared_resources/src/main/res/xml/locales_config.xml
new file mode 100644
index 00000000..f672f8c7
--- /dev/null
+++ b/shared_resources/src/main/res/xml/locales_config.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wear/build.gradle b/wear/build.gradle
index d679732c..2d0b8db2 100644
--- a/wear/build.gradle
+++ b/wear/build.gradle
@@ -6,15 +6,15 @@ apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
android {
- compileSdk rootProject.compileSdkVersion
+ compileSdk = libs.versions.compileSdkVersion.get().toInteger()
defaultConfig {
applicationId "com.thewizrd.simplewear"
minSdkVersion 26
- targetSdkVersion rootProject.targetSdkVersion
+ targetSdkVersion libs.versions.targetSdkVersion.get().toInteger()
// NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1)
- versionCode 341916051
- versionName "1.16.1"
+ versionCode 361917021
+ versionName "1.17.0"
vectorDrawables.useSupportLibrary = true
}
@@ -24,13 +24,13 @@ android {
applicationIdSuffix ".debug"
debuggable true
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
shrinkResources true
crunchPngs true
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
@@ -50,7 +50,7 @@ android {
}
composeOptions {
- kotlinCompilerExtensionVersion compose_compiler_version
+ kotlinCompilerExtensionVersion libs.versions.compose.compiler.version.get()
}
kotlin {
@@ -63,78 +63,81 @@ dependencies {
implementation project(":shared_resources")
implementation project(":unofficialtileapi")
- coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_version"
+ coreLibraryDesugaring libs.desugar.jdk.libs
// Kotlin
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_version"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_version"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_version"
-
- implementation "androidx.appcompat:appcompat:$appcompat_version"
- implementation "androidx.core:core-ktx:$core_version"
- implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version"
- implementation "androidx.fragment:fragment-ktx:$fragment_version"
- implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
- implementation "androidx.recyclerview:recyclerview:$recyclerview_version"
- implementation "androidx.preference:preference-ktx:$preference_version"
- implementation "androidx.core:core-splashscreen:$coresplash_version"
- implementation "androidx.navigation:navigation-runtime-ktx:$navigation_version"
- implementation "androidx.datastore:datastore:$datastore_version"
-
- implementation platform("com.google.firebase:firebase-bom:$firebase_version")
- implementation 'com.google.firebase:firebase-analytics'
- implementation 'com.google.firebase:firebase-crashlytics'
- implementation 'com.google.firebase:firebase-config'
-
- implementation "com.google.android.material:material:$material_version"
+ implementation libs.kotlinx.coroutines.core
+ implementation libs.kotlinx.coroutines.android
+ implementation libs.kotlinx.coroutines.play.services
+
+ implementation libs.appcompat
+ implementation libs.core.ktx
+ implementation libs.constraintlayout
+ implementation libs.fragment.ktx
+ implementation libs.lifecycle.runtime.ktx
+ implementation libs.recyclerview
+ implementation libs.preference.ktx
+ implementation libs.core.splashscreen
+ implementation(libs.navigation.runtime.ktx)
+ implementation(libs.datastore)
+ implementation libs.palette.ktx
+
+ implementation platform(libs.firebase.bom)
+ implementation libs.firebase.analytics
+ implementation libs.firebase.crashlytics
+ implementation libs.firebase.config
+
+ implementation libs.material
// WearOS
- implementation 'com.google.android.gms:play-services-wearable:19.0.0'
- compileOnly 'com.google.android.wearable:wearable:2.9.0' // Needed for Ambient Mode
+ implementation libs.play.services.wearable
+ compileOnly libs.wearable // Needed for Ambient Mode
- implementation 'androidx.wear:wear:1.3.0'
- implementation 'androidx.wear:wear-ongoing:1.0.0'
- implementation 'androidx.wear:wear-phone-interactions:1.1.0'
- implementation 'androidx.wear:wear-remote-interactions:1.1.0'
- implementation "androidx.wear.watchface:watchface-complications-data:$wear_watchface_version"
- implementation "androidx.wear.watchface:watchface-complications-data-source-ktx:$wear_watchface_version"
+ implementation libs.wear
+ implementation libs.wear.ongoing
+ implementation libs.wear.phone.interactions
+ implementation libs.wear.remote.interactions
+ implementation libs.androidx.watchface.complications.data
+ implementation libs.wear.watchface.complications.data.source.ktx
// WearOS Tiles
- implementation "androidx.wear.tiles:tiles:$wear_tiles_version"
- debugImplementation "androidx.wear.tiles:tiles-renderer:$wear_tiles_version"
- testImplementation "androidx.wear.tiles:tiles-testing:$wear_tiles_version"
- debugImplementation "androidx.wear.tiles:tiles-tooling:$wear_tiles_version"
- implementation "androidx.wear.tiles:tiles-tooling-preview:$wear_tiles_version"
- implementation 'androidx.wear.protolayout:protolayout-material:1.2.1'
- implementation "com.google.android.horologist:horologist-tiles:$horologist_version"
+ implementation libs.wear.tiles
+ debugImplementation libs.wear.tiles.renderer
+ testImplementation libs.wear.tiles.testing
+ debugImplementation libs.wear.tiles.tooling
+ implementation libs.wear.tiles.tooling.preview
+ implementation libs.wear.protolayout.material3
+ implementation libs.horologist.tiles
// WearOS Compose
- implementation "androidx.activity:activity-compose:$activity_version"
- implementation "androidx.compose.compiler:compiler:$compose_compiler_version"
- implementation platform("androidx.compose:compose-bom:$compose_bom_version")
- implementation "androidx.compose.ui:ui"
- implementation "androidx.compose.ui:ui-tooling-preview"
- implementation "androidx.compose.animation:animation-graphics"
- implementation "androidx.compose.runtime:runtime-livedata"
- implementation "androidx.compose.material:material"
- implementation "androidx.compose.material:material-icons-core"
- implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
- implementation "androidx.wear.compose:compose-foundation:$wear_compose_version"
- implementation "androidx.wear.compose:compose-material:$wear_compose_version"
- implementation "androidx.wear.compose:compose-navigation:$wear_compose_version"
- implementation "androidx.wear:wear-tooling-preview:1.0.0"
-
- implementation "com.google.accompanist:accompanist-drawablepainter:$accompanist_version"
- implementation "com.google.android.horologist:horologist-audio-ui:$horologist_version"
- implementation "com.google.android.horologist:horologist-compose-layout:$horologist_version"
- implementation "com.google.android.horologist:horologist-compose-material:$horologist_version"
- implementation "com.google.android.horologist:horologist-compose-tools:$horologist_version"
- implementation "com.google.android.horologist:horologist-media-ui:$horologist_version"
-
- androidTestImplementation platform("androidx.compose:compose-bom:$compose_bom_version")
- androidTestImplementation "androidx.compose.ui:ui-test-junit4"
- debugImplementation "androidx.compose.ui:ui-tooling"
-
- implementation "com.jakewharton.timber:timber:$timber_version"
- implementation "com.google.code.gson:gson:$gson_version"
+ implementation(libs.activity.compose)
+ implementation libs.androidx.compose.compiler
+ implementation platform(libs.compose.bom)
+ implementation libs.androidx.compose.ui
+ implementation libs.androidx.compose.ui.tooling.preview
+ implementation libs.androidx.compose.animation.graphics
+ implementation libs.androidx.compose.runtime.livedata
+ implementation libs.androidx.compose.material.icons.extended.android
+ implementation libs.androidx.compose.material3
+ implementation libs.lifecycle.viewmodel.compose
+ implementation libs.wear.compose.foundation
+ implementation libs.wear.compose.material3
+ implementation libs.wear.compose.navigation
+ implementation libs.wear.compose.ui.tooling
+ implementation libs.wear.tooling.preview
+
+ implementation libs.accompanist.drawablepainter
+ implementation libs.horologist.audio.ui.material3
+ implementation libs.horologist.compose.layout
+ implementation libs.horologist.compose.tools
+ implementation libs.horologist.media.ui.material3
+
+ implementation libs.reorderable
+
+ androidTestImplementation platform(libs.compose.bom)
+ androidTestImplementation libs.androidx.compose.ui.test.junit4
+ debugImplementation libs.androidx.compose.ui.tooling
+
+ implementation libs.timber
+ implementation libs.gson
}
diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml
index 9db4ae1c..fd369ce2 100644
--- a/wear/src/main/AndroidManifest.xml
+++ b/wear/src/main/AndroidManifest.xml
@@ -36,6 +36,7 @@
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:localeConfig="@xml/locales_config"
android:supportsRtl="true"
android:theme="@style/WearAppTheme.Launcher"
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
@@ -95,20 +96,6 @@
android:theme="@style/WearAppTheme.MediaLauncher"
android:taskAffinity=".mediaPlayer" />
-
-
-
-
+
+
+
+
+
+
+
+
0) {
+ com.thewizrd.simplewear.preferences.Settings.setVersionCode(versionCode)
+ }
+ }
+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {
mActivitiesStarted++
diff --git a/wear/src/main/java/com/thewizrd/simplewear/FirebaseConfigurator.kt b/wear/src/main/java/com/thewizrd/simplewear/FirebaseConfigurator.kt
index 647439fd..4a691315 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/FirebaseConfigurator.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/FirebaseConfigurator.kt
@@ -4,6 +4,10 @@ import android.annotation.SuppressLint
import android.content.Context
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
+import com.google.firebase.remoteconfig.ConfigUpdate
+import com.google.firebase.remoteconfig.ConfigUpdateListener
+import com.google.firebase.remoteconfig.FirebaseRemoteConfig
+import com.google.firebase.remoteconfig.FirebaseRemoteConfigException
import com.thewizrd.shared_resources.utils.AnalyticsProps
import com.thewizrd.shared_resources.utils.CrashlyticsLoggingTree
import com.thewizrd.shared_resources.utils.Logger
@@ -11,7 +15,10 @@ import com.thewizrd.shared_resources.utils.Logger
object FirebaseConfigurator {
@SuppressLint("MissingPermission")
fun initialize(context: Context) {
- FirebaseAnalytics.getInstance(context).setUserProperty(AnalyticsProps.DEVICE_TYPE, "watch")
+ FirebaseAnalytics.getInstance(context).run {
+ setUserProperty(AnalyticsProps.DEVICE_TYPE, "watch")
+ setUserProperty(AnalyticsProps.PLATFORM, "Android")
+ }
FirebaseCrashlytics.getInstance().apply {
isCrashlyticsCollectionEnabled = true
@@ -21,5 +28,23 @@ object FirebaseConfigurator {
if (!BuildConfig.DEBUG) {
Logger.registerLogger(CrashlyticsLoggingTree())
}
+
+ // Add Firebase RemoteConfig real-time listener
+ FirebaseRemoteConfig.getInstance().run {
+ addOnConfigUpdateListener(object : ConfigUpdateListener {
+ override fun onUpdate(configUpdate: ConfigUpdate) {
+ Logger.verbose("FirebaseConfigurator", "Remote update received")
+ this@run.activate()
+ }
+
+ override fun onError(error: FirebaseRemoteConfigException) {
+ Logger.error(
+ "FirebaseConfigurator",
+ message = "Error on real-time update",
+ t = error
+ )
+ }
+ })
+ }
}
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/PhoneSyncActivity.kt b/wear/src/main/java/com/thewizrd/simplewear/PhoneSyncActivity.kt
index b8e2bd0c..b440aa4d 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/PhoneSyncActivity.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/PhoneSyncActivity.kt
@@ -1,17 +1,12 @@
package com.thewizrd.simplewear
import android.os.Bundle
-import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.lifecycle.lifecycleScope
import com.thewizrd.simplewear.ui.simplewear.PhoneSyncUi
-import com.thewizrd.simplewear.utils.ErrorMessage
import com.thewizrd.simplewear.viewmodels.PhoneSyncViewModel
-import com.thewizrd.simplewear.viewmodels.WearableListenerViewModel.Companion.ACTION_OPENONPHONE
-import kotlinx.coroutines.launch
class PhoneSyncActivity : ComponentActivity() {
private val phoneSyncViewModel by viewModels()
@@ -27,37 +22,7 @@ class PhoneSyncActivity : ComponentActivity() {
override fun onStart() {
super.onStart()
-
phoneSyncViewModel.initActivityContext(this)
-
- lifecycleScope.launch {
- phoneSyncViewModel.eventFlow.collect { event ->
- when (event.eventType) {
- ACTION_OPENONPHONE -> {
- Toast.makeText(
- this@PhoneSyncActivity,
- R.string.error_syncing,
- Toast.LENGTH_SHORT
- ).show()
- }
- }
- }
- }
-
- lifecycleScope.launch {
- phoneSyncViewModel.errorMessagesFlow.collect { error ->
- when (error) {
- is ErrorMessage.String -> {
- Toast.makeText(applicationContext, error.message, Toast.LENGTH_SHORT).show()
- }
-
- is ErrorMessage.Resource -> {
- Toast.makeText(applicationContext, error.stringId, Toast.LENGTH_SHORT)
- .show()
- }
- }
- }
- }
}
override fun onResume() {
diff --git a/wear/src/main/java/com/thewizrd/simplewear/activities/AppCompatLiteActivity.java b/wear/src/main/java/com/thewizrd/simplewear/activities/AppCompatLiteActivity.java
deleted file mode 100644
index d4cade9e..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/activities/AppCompatLiteActivity.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.thewizrd.simplewear.activities;
-
-import android.os.Bundle;
-import android.util.Log;
-import android.view.LayoutInflater;
-
-import androidx.annotation.ContentView;
-import androidx.annotation.LayoutRes;
-import androidx.annotation.Nullable;
-import androidx.core.view.LayoutInflaterCompat;
-import androidx.fragment.app.FragmentActivity;
-
-/**
- * Adds custom view inflater to inflate AppCompat views instead of inheriting AppCompatActivity
- */
-public class AppCompatLiteActivity extends FragmentActivity {
- private static final String TAG = "AppCompatLiteActivity";
-
- public AppCompatLiteActivity() {
- super();
- initDelegate();
- }
-
- @ContentView
- public AppCompatLiteActivity(@LayoutRes int contentLayoutId) {
- super(contentLayoutId);
- initDelegate();
- }
-
- private void initDelegate() {
- addOnContextAvailableListener(context -> installViewFactory());
- }
-
- private void installViewFactory() {
- LayoutInflater layoutInflater = LayoutInflater.from(this);
- if (layoutInflater.getFactory() == null) {
- LayoutInflaterCompat.setFactory2(layoutInflater, new AppCompatLiteViewInflater());
- } else {
- if (!(layoutInflater.getFactory2() instanceof AppCompatLiteViewInflater)) {
- Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
- + " so we can not install AppCompat's");
- }
- }
- }
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/activities/AppCompatLiteViewInflater.kt b/wear/src/main/java/com/thewizrd/simplewear/activities/AppCompatLiteViewInflater.kt
deleted file mode 100644
index 00d9d3c3..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/activities/AppCompatLiteViewInflater.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-package com.thewizrd.simplewear.activities
-
-import android.content.Context
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.view.View
-import androidx.appcompat.widget.*
-import com.google.android.material.button.MaterialButton
-import com.google.android.material.checkbox.MaterialCheckBox
-import com.google.android.material.radiobutton.MaterialRadioButton
-import com.google.android.material.textfield.MaterialAutoCompleteTextView
-import com.google.android.material.textview.MaterialTextView
-
-class AppCompatLiteViewInflater : LayoutInflater.Factory2 {
- override fun onCreateView(
- parent: View?,
- name: String,
- context: Context,
- attrs: AttributeSet
- ): View? {
- return createView(parent, name, context, attrs)
- }
-
- override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
- return createView(null, name, context, attrs)
- }
-
- protected fun createView(
- parent: View?,
- name: String,
- context: Context,
- attrs: AttributeSet
- ): View? {
- var view: View? = null
-
- // We need to 'inject' our tint aware Views in place of the standard framework versions
- when (name) {
- "TextView" -> {
- view = MaterialTextView(context, attrs)
- verifyNotNull(view, name)
- }
- "ImageView" -> {
- view = AppCompatImageView(context, attrs)
- verifyNotNull(view, name)
- }
- "Button" -> {
- view = MaterialButton(context, attrs)
- verifyNotNull(view, name)
- }
- "EditText" -> {
- view = AppCompatEditText(context, attrs)
- verifyNotNull(view, name)
- }
- "Spinner" -> {
- view = AppCompatSpinner(context, attrs)
- verifyNotNull(view, name)
- }
- "ImageButton" -> {
- view = AppCompatImageButton(context, attrs)
- verifyNotNull(view, name)
- }
- "CheckBox" -> {
- view = MaterialCheckBox(context, attrs)
- verifyNotNull(view, name)
- }
- "RadioButton" -> {
- view = MaterialRadioButton(context, attrs)
- verifyNotNull(view, name)
- }
- "CheckedTextView" -> {
- view = AppCompatCheckedTextView(context, attrs)
- verifyNotNull(view, name)
- }
- "AutoCompleteTextView" -> {
- view = MaterialAutoCompleteTextView(context, attrs)
- verifyNotNull(view, name)
- }
- "MultiAutoCompleteTextView" -> {
- view = AppCompatMultiAutoCompleteTextView(context, attrs)
- verifyNotNull(view, name)
- }
- "RatingBar" -> {
- view = AppCompatRatingBar(context, attrs)
- verifyNotNull(view, name)
- }
- "SeekBar" -> {
- view = AppCompatSeekBar(context, attrs)
- verifyNotNull(view, name)
- }
- "ToggleButton" -> {
- view = AppCompatToggleButton(context, attrs)
- verifyNotNull(view, name)
- }
- }
-
- return view
- }
-
- private fun verifyNotNull(view: View?, name: String) {
- checkNotNull(view) {
- ("${this.javaClass.name} asked to inflate view for <$name>, but returned null")
- }
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/adapters/AddButtonAdapter.kt b/wear/src/main/java/com/thewizrd/simplewear/adapters/AddButtonAdapter.kt
deleted file mode 100644
index ce31994f..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/adapters/AddButtonAdapter.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.thewizrd.simplewear.adapters
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.thewizrd.shared_resources.utils.ContextUtils.dpToPx
-import com.thewizrd.simplewear.R
-import com.thewizrd.simplewear.databinding.LayoutDashAddButtonBinding
-
-class AddButtonAdapter : RecyclerView.Adapter() {
- companion object {
- const val ITEM_TYPE = R.drawable.ic_add_white_24dp
- }
-
- private var onClickListener: View.OnClickListener? = null
-
- fun setOnClickListener(listener: View.OnClickListener?) {
- onClickListener = listener
- }
-
- inner class ViewHolder(binding: LayoutDashAddButtonBinding) :
- RecyclerView.ViewHolder(binding.root)
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- val recyclerView = parent as RecyclerView
- val viewLayoutMgr = recyclerView.layoutManager
- val viewParams = RecyclerView.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
-
- val binding = LayoutDashAddButtonBinding.inflate(LayoutInflater.from(parent.context))
-
- if (viewLayoutMgr is GridLayoutManager) {
- val buttonSize =
- parent.context.resources.getDimensionPixelSize(R.dimen.tile_action_button_size)
- val horizPadding = runCatching {
- val spanCount = viewLayoutMgr.spanCount
- val viewWidth = parent.getMeasuredWidth() - parent.paddingStart - parent.paddingEnd
- val colWidth = viewWidth / spanCount
- colWidth - buttonSize
- }.getOrNull() ?: 0
- val vertPadding = parent.getContext().dpToPx(6f).toInt()
-
- viewParams.setMargins(horizPadding / 2, vertPadding, horizPadding / 2, vertPadding)
-
- binding.root.layoutParams = viewParams
- }
-
- return ViewHolder(binding)
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- holder.itemView.setOnClickListener {
- onClickListener?.onClick(it)
- }
- }
-
- override fun getItemCount(): Int {
- return 1
- }
-
- override fun getItemViewType(position: Int): Int {
- return ITEM_TYPE
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/adapters/DashBattStatusItemAdapter.kt b/wear/src/main/java/com/thewizrd/simplewear/adapters/DashBattStatusItemAdapter.kt
deleted file mode 100644
index 7ade4632..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/adapters/DashBattStatusItemAdapter.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-package com.thewizrd.simplewear.adapters
-
-import android.view.View
-import android.view.ViewGroup
-import android.widget.TextView
-import androidx.core.content.ContextCompat
-import androidx.recyclerview.widget.RecyclerView
-import com.thewizrd.shared_resources.utils.ContextUtils.getAttrColorStateList
-import com.thewizrd.simplewear.R
-import com.thewizrd.simplewear.controls.WearChipButton
-
-class DashBattStatusItemAdapter : RecyclerView.Adapter() {
- companion object {
- const val ITEM_TYPE = R.drawable.ic_battery_std_white_24dp
- }
-
- private var itemPosition = RecyclerView.NO_POSITION
- private var recyclerView: RecyclerView? = null
-
- var isVisible: Boolean = true
- set(value) {
- val oldValue = field
- field = value
-
- if (oldValue != value) {
- notifyItemChanged(0)
- }
- }
-
- var isChecked: Boolean = false
- set(value) {
- val oldValue = field
- field = value
-
- if (oldValue != value) {
- notifyItemChanged(0)
- }
- }
-
- inner class ViewHolder(private val button: WearChipButton) :
- RecyclerView.ViewHolder(button) {
- fun bind(isChecked: Boolean, isVisible: Boolean) {
- if (!isVisible) {
- button.setIconResource(R.drawable.ic_add_white_24dp)
- button.setIconTint(button.context.getAttrColorStateList(R.attr.colorSurface))
- button.setBackgroundColor(button.context.getAttrColorStateList(R.attr.colorOnSurface))
- button.findViewById(R.id.wear_chip_primary_text)?.let {
- it.setTextColor(button.context.getAttrColorStateList(R.attr.colorSurface))
- it.setText(R.string.action_add_batt_state)
- }
- } else if (isChecked) {
- button.setIconResource(R.drawable.ic_close_white_24dp)
- button.setIconTint(button.context.getAttrColorStateList(R.attr.colorSurface))
- button.setBackgroundColor(button.context.getAttrColorStateList(R.attr.colorOnSurface))
- button.findViewById(R.id.wear_chip_primary_text)?.let {
- it.setTextColor(button.context.getAttrColorStateList(R.attr.colorSurface))
- it.setText(R.string.action_remove_batt_state)
- }
- } else {
- button.setIconTint(button.context.getAttrColorStateList(R.attr.colorOnSurface))
- button.setIconResource(R.drawable.ic_battery_std_white_24dp)
- button.setBackgroundColor(
- ContextCompat.getColor(
- button.context,
- R.color.buttonDisabled
- )
- )
- button.findViewById(R.id.wear_chip_primary_text)?.let {
- it.setTextColor(
- ContextCompat.getColor(
- it.context,
- R.color.wear_chip_primary_text_color
- )
- )
- it.setText(R.string.title_batt_state)
- }
- }
-
- button.requestFocus()
- }
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- return ViewHolder(WearChipButton(parent.context).apply {
- setIconResource(R.drawable.ic_battery_std_white_24dp)
- setText(R.string.title_batt_state)
- isCheckable = false
- })
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- itemPosition = holder.bindingAdapterPosition
- holder.bind(isChecked, isVisible)
-
- holder.itemView.setOnClickListener {
- if (!isVisible) {
- isVisible = true
- isChecked = false
- } else if (isChecked) {
- isVisible = false
- isChecked = false
- } else {
- isChecked = true
- }
- }
- }
-
- override fun getItemCount(): Int {
- return 1
- }
-
- override fun getItemViewType(position: Int): Int {
- return ITEM_TYPE
- }
-
- override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
- this.recyclerView = recyclerView
- }
-
- override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
- this.recyclerView = null
- }
-
- fun getSelection(): View? {
- if (itemPosition >= 0) {
- return recyclerView?.findViewHolderForLayoutPosition(itemPosition)?.itemView
- }
-
- return null
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/adapters/TileActionAdapter.kt b/wear/src/main/java/com/thewizrd/simplewear/adapters/TileActionAdapter.kt
deleted file mode 100644
index fd7e9455..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/adapters/TileActionAdapter.kt
+++ /dev/null
@@ -1,184 +0,0 @@
-package com.thewizrd.simplewear.adapters
-
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.ViewCompat
-import androidx.core.widget.ImageViewCompat
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
-import com.thewizrd.shared_resources.actions.Actions
-import com.thewizrd.shared_resources.controls.ActionButtonViewModel
-import com.thewizrd.shared_resources.utils.ContextUtils.dpToPx
-import com.thewizrd.shared_resources.utils.ContextUtils.getAttrColorStateList
-import com.thewizrd.simplewear.R
-import com.thewizrd.simplewear.databinding.LayoutDashButtonBinding
-
-class TileActionAdapter : ListAdapter(
- ActionButtonViewModel.DIFF_CALLBACK
-) {
- private var checkedPosition = RecyclerView.NO_POSITION
- private var recyclerView: RecyclerView? = null
-
- init {
- setHasStableIds(true)
- }
-
- inner class ViewHolder(private val binding: LayoutDashButtonBinding) :
- RecyclerView.ViewHolder(binding.root) {
- fun bind(model: ActionButtonViewModel, isChecked: Boolean) {
- if (isChecked) {
- binding.button.setImageResource(R.drawable.ic_close_white_24dp)
- ImageViewCompat.setImageTintList(
- binding.button,
- itemView.context.getAttrColorStateList(R.attr.colorSurface)
- )
- ViewCompat.setBackgroundTintList(
- binding.button,
- itemView.context.getAttrColorStateList(R.attr.colorOnSurface)
- )
- itemView.requestFocus()
- } else {
- binding.button.setImageResource(model.drawableResId)
- ImageViewCompat.setImageTintList(
- binding.button,
- itemView.context.getAttrColorStateList(R.attr.colorOnSurface)
- )
- ViewCompat.setBackgroundTintList(binding.button, null)
- itemView.clearFocus()
- }
- }
- }
-
- var onLongClickListener: ((RecyclerView.ViewHolder) -> Unit)? = null
- var onListChanged: ((List) -> Unit)? = null
-
- override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
- this.recyclerView = recyclerView
- }
-
- override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
- this.recyclerView = null
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- val recyclerView = parent as RecyclerView
- val viewLayoutMgr = recyclerView.layoutManager
- val viewParams = RecyclerView.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
-
- val binding = LayoutDashButtonBinding.inflate(LayoutInflater.from(parent.context))
-
- if (viewLayoutMgr is GridLayoutManager) {
- val buttonSize =
- parent.context.resources.getDimensionPixelSize(R.dimen.tile_action_button_size)
- val horizPadding = runCatching {
- val spanCount = viewLayoutMgr.spanCount
- val viewWidth = parent.getMeasuredWidth() - parent.paddingStart - parent.paddingEnd
- val colWidth = viewWidth / spanCount
- colWidth - buttonSize
- }.getOrNull() ?: 0
- val vertPadding = parent.getContext().dpToPx(6f).toInt()
-
- viewParams.setMargins(horizPadding / 2, vertPadding, horizPadding / 2, vertPadding)
-
- binding.root.layoutParams = viewParams
- }
-
- return ViewHolder(binding)
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- val isChecked = position == checkedPosition
- holder.bind(getItem(position), isChecked)
-
- holder.itemView.setOnClickListener {
- Log.d("TileAdapter", "onclick $position")
-
- if (isChecked) {
- checkedPosition = RecyclerView.NO_POSITION
- // remove item
- removeAction(holder.bindingAdapterPosition)
- } else {
- val oldPosition = checkedPosition
- checkedPosition = holder.bindingAdapterPosition
-
- if (oldPosition >= 0) {
- notifyItemChanged(oldPosition)
- }
- notifyItemChanged(checkedPosition)
- }
- }
- holder.itemView.setOnLongClickListener {
- onLongClickListener?.invoke(holder)
- true
- }
- }
-
- private fun removeAction(position: Int) {
- val items = currentList.toMutableList()
- if (position != RecyclerView.NO_POSITION) {
- items.removeAt(position)
- }
- submitList(items)
- }
-
- fun removeAction(action: Actions) {
- val items = currentList.toMutableList()
- items.removeIf { it.actionType == action }
- submitList(items)
- }
-
- fun addAction(action: Actions) {
- val items = currentList.toMutableList()
- items.add(ActionButtonViewModel.getViewModelFromAction(action))
- submitList(items)
- }
-
- fun submitActions(actions: List?) {
- submitList(actions?.map {
- ActionButtonViewModel.getViewModelFromAction(it)
- } ?: emptyList())
- }
-
- fun getActions(): List {
- return currentList.map { it.actionType }
- }
-
- override fun getItemViewType(position: Int): Int {
- return R.layout.layout_dash_button
- }
-
- override fun getItemId(position: Int): Long {
- return getItem(position).actionType.value.toLong()
- }
-
- override fun onCurrentListChanged(
- previousList: MutableList,
- currentList: MutableList
- ) {
- super.onCurrentListChanged(previousList, currentList)
- onListChanged?.invoke(currentList)
- }
-
- fun clearSelection() {
- val oldPosition = checkedPosition
- checkedPosition = RecyclerView.NO_POSITION
-
- if (oldPosition >= 0) {
- notifyItemChanged(oldPosition)
- }
- }
-
- fun getSelection(): View? {
- if (checkedPosition >= 0) {
- return recyclerView?.findViewHolderForLayoutPosition(checkedPosition)?.itemView
- }
-
- return null
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/controls/WearChipButton.kt b/wear/src/main/java/com/thewizrd/simplewear/controls/WearChipButton.kt
deleted file mode 100644
index f032f9ed..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/controls/WearChipButton.kt
+++ /dev/null
@@ -1,420 +0,0 @@
-package com.thewizrd.simplewear.controls
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.res.ColorStateList
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.LayerDrawable
-import android.util.AttributeSet
-import android.view.Gravity
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.*
-import androidx.annotation.ColorInt
-import androidx.annotation.ColorRes
-import androidx.annotation.DrawableRes
-import androidx.annotation.IntDef
-import androidx.annotation.StringRes
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.core.content.ContextCompat
-import androidx.core.graphics.drawable.DrawableCompat
-import androidx.core.view.ViewCompat
-import com.thewizrd.simplewear.R
-
-@SuppressLint("RestrictedApi")
-class WearChipButton @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = R.attr.wearChipButtonStyle,
- defStyleRes: Int = DEF_STYLE_RES
-) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes), Checkable {
- companion object {
- private const val DEF_STYLE_RES = R.style.Widget_Wear_WearChipButton
-
- private val ENABLED_STATE_SET = intArrayOf(android.R.attr.state_enabled)
- private val CHECKABLE_STATE_SET = intArrayOf(android.R.attr.state_checkable)
- private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
-
- const val CONTROLTYPE_NONE = 0
- const val CONTROLTYPE_CHECKBOX = 1
- const val CONTROLTYPE_RADIO = 2
- const val CONTROLTYPE_TOGGLE = 3
- }
-
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(
- CONTROLTYPE_NONE,
- CONTROLTYPE_CHECKBOX,
- CONTROLTYPE_RADIO,
- CONTROLTYPE_TOGGLE
- )
- annotation class ControlType
-
- private val mIconView: ImageView
- private val mPrimaryTextView: TextView
- private val mSecondaryTextView: TextView
- private val mSelectionControlContainer: ViewGroup
-
- private var buttonBackgroundTint: ColorStateList? = null
- private var buttonControlTint: ColorStateList? = null
- private var iconTint: ColorStateList? = null
-
- var isCheckable: Boolean = false
- get() = field
- set(value) {
- field = value
- refreshDrawableState()
- }
-
- private var _isChecked = false
-
- init {
- LayoutInflater.from(context).inflate(R.layout.wear_chip_button_layout, this, true)
- mIconView = findViewById(R.id.wear_chip_icon)
- mPrimaryTextView = findViewById(R.id.wear_chip_primary_text)
- mSecondaryTextView = findViewById(R.id.wear_chip_secondary_text)
- mSelectionControlContainer = findViewById(R.id.wear_chip_selection_control_container)
-
- mPrimaryTextView.maxLines = 2
- mSecondaryTextView.maxLines = 1
-
- val a = context.obtainStyledAttributes(
- attrs,
- R.styleable.WearChipButton,
- defStyleAttr,
- defStyleRes
- )
- ViewCompat.saveAttributeDataForStyleable(
- this,
- context, R.styleable.WearChipButton,
- attrs, a, defStyleAttr, defStyleRes
- )
-
- try {
- if (a.hasValue(R.styleable.WearChipButton_icon)) {
- setIconDrawable(a.getDrawable(R.styleable.WearChipButton_icon))
- }
- if (a.hasValue(R.styleable.WearChipButton_primaryText)) {
- setPrimaryText(a.getString(R.styleable.WearChipButton_primaryText))
- }
- if (a.hasValue(R.styleable.WearChipButton_primaryTextMaxLines)) {
- setPrimaryTextMaxLines(
- a.getInt(
- R.styleable.WearChipButton_primaryTextMaxLines,
- mPrimaryTextView.maxLines
- )
- )
- }
- if (a.hasValue(R.styleable.WearChipButton_secondaryText)) {
- setSecondaryText(a.getString(R.styleable.WearChipButton_secondaryText))
- }
- if (a.hasValue(R.styleable.WearChipButton_secondaryTextMaxLines)) {
- setSecondaryTextMaxLines(
- a.getInt(
- R.styleable.WearChipButton_secondaryTextMaxLines,
- mSecondaryTextView.maxLines
- )
- )
- }
- if (a.hasValue(R.styleable.WearChipButton_backgroundTint)) {
- val colorResId = a.getResourceId(R.styleable.WearChipButton_backgroundTint, 0)
- if (colorResId != 0) {
- val tint = ContextCompat.getColorStateList(context, colorResId)
- if (tint != null) {
- buttonBackgroundTint = tint
- }
- }
-
- if (buttonBackgroundTint == null) {
- buttonBackgroundTint =
- a.getColorStateList(R.styleable.WearChipButton_backgroundTint)
- }
- }
- if (a.hasValue(R.styleable.WearChipButton_buttonTint)) {
- val colorResId = a.getResourceId(R.styleable.WearChipButton_buttonTint, 0)
- if (colorResId != 0) {
- val tint = ContextCompat.getColorStateList(context, colorResId)
- if (tint != null) {
- buttonControlTint = tint
- }
- }
-
- if (buttonControlTint == null) {
- buttonControlTint =
- a.getColorStateList(R.styleable.WearChipButton_buttonTint)
- }
- }
- if (a.hasValue(R.styleable.WearChipButton_iconTint)) {
- val colorResId = a.getResourceId(R.styleable.WearChipButton_iconTint, 0)
- if (colorResId != 0) {
- val tint = ContextCompat.getColorStateList(context, colorResId)
- if (tint != null) {
- iconTint = tint
- }
- }
-
- if (iconTint == null) {
- iconTint =
- a.getColorStateList(R.styleable.WearChipButton_iconTint)
- }
-
- setIconTint(iconTint)
- }
- if (a.hasValue(R.styleable.WearChipButton_android_checkable)) {
- isCheckable = a.getBoolean(R.styleable.WearChipButton_android_checkable, false)
- }
- if (a.hasValue(R.styleable.WearChipButton_android_checked)) {
- isChecked = a.getBoolean(R.styleable.WearChipButton_android_checked, false)
- }
- if (a.hasValue(R.styleable.WearChipButton_controlType)) {
- updateControlType(a.getInt(R.styleable.WearChipButton_controlType, 0))
- }
- if (a.hasValue(R.styleable.WearChipButton_minHeight)) {
- minHeight = a.getDimensionPixelSize(R.styleable.WearChipButton_minHeight, 0)
- }
- } finally {
- a.recycle()
- }
-
- updateBackgroundTint()
- updateButtonControlTint()
- }
-
- fun setIconResource(@DrawableRes resId: Int) {
- mIconView.setImageResource(resId)
- mIconView.visibility = if (resId == 0) View.GONE else View.VISIBLE
- }
-
- fun setIconDrawable(drawable: Drawable?) {
- mIconView.setImageDrawable(drawable)
- mIconView.visibility = if (drawable == null) View.GONE else View.VISIBLE
- }
-
- fun setIconTint(tint: ColorStateList?) {
- mIconView.imageTintList = tint
- }
-
- fun setIconTintResource(@ColorRes iconTintResId: Int) {
- setIconTint(ContextCompat.getColorStateList(context, iconTintResId))
- }
-
- fun setPrimaryText(@StringRes resId: Int) {
- if (resId == 0) {
- setPrimaryText(null)
- } else {
- mPrimaryTextView.setText(resId)
- mPrimaryTextView.visibility = if (resId == 0) View.GONE else View.VISIBLE
- }
- }
-
- fun setPrimaryText(text: CharSequence?) {
- mPrimaryTextView.text = text
- mPrimaryTextView.visibility = if (text == null) View.GONE else View.VISIBLE
- }
-
- fun setPrimaryTextMaxLines(maxLines: Int) {
- mPrimaryTextView.maxLines = maxLines
- }
-
- fun setSecondaryText(@StringRes resId: Int) {
- if (resId == 0) {
- setSecondaryText(null)
- } else {
- mSecondaryTextView.setText(resId)
- mSecondaryTextView.visibility = if (resId == 0) View.GONE else View.VISIBLE
- if (resId == 0) {
- mPrimaryTextView.maxLines = 2
- } else {
- mPrimaryTextView.maxLines = 1
- }
- }
- }
-
- fun setSecondaryText(text: CharSequence?) {
- mSecondaryTextView.text = text
- mSecondaryTextView.visibility = if (text == null) View.GONE else View.VISIBLE
- if (text == null) {
- mPrimaryTextView.maxLines = 2
- } else {
- mPrimaryTextView.maxLines = 1
- }
- }
-
- fun setSecondaryTextMaxLines(maxLines: Int) {
- mSecondaryTextView.maxLines = maxLines
- }
-
- fun setText(@StringRes primaryResId: Int, @StringRes secondaryResId: Int = 0) {
- setPrimaryText(primaryResId)
- setSecondaryText(secondaryResId)
- }
-
- fun setText(primaryText: CharSequence?, secondaryText: CharSequence? = null) {
- setPrimaryText(primaryText)
- setSecondaryText(secondaryText)
- }
-
- fun setControlView(view: View?) {
- mSelectionControlContainer.removeAllViews()
- if (view != null) {
- mSelectionControlContainer.addView(view)
- mSelectionControlContainer.visibility = View.VISIBLE
- } else {
- mSelectionControlContainer.visibility = View.GONE
- }
- }
-
- fun getControlView(): View? {
- return mSelectionControlContainer.getChildAt(0)
- }
-
- fun setControlViewVisibility(visibility: Int) {
- if (getControlView() != null) {
- mSelectionControlContainer.visibility = visibility
- }
- }
-
- fun getBackgroundColor(): ColorStateList? {
- return buttonBackgroundTint
- }
-
- fun setBackgroundColor(tint: ColorStateList?) {
- buttonBackgroundTint = tint
- updateBackgroundTint()
- }
-
- override fun setBackgroundColor(@ColorInt color: Int) {
- buttonBackgroundTint = ColorStateList.valueOf(color)
- updateBackgroundTint()
- }
-
- fun getControlButtonColor(): ColorStateList? {
- return buttonControlTint
- }
-
- fun setControlButtonColor(tint: ColorStateList?) {
- buttonControlTint = tint
- updateButtonControlTint()
- }
-
- fun updateControlType(@ControlType controlType: Int) {
- when (controlType) {
- CONTROLTYPE_CHECKBOX -> {
- setControlView(CheckBox(context).apply {
- layoutParams = FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- ).apply {
- gravity = Gravity.CENTER
- }
- isClickable = false
- isDuplicateParentStateEnabled = true
- buttonDrawable =
- ContextCompat.getDrawable(context, R.drawable.wear_checkbox_icon)
- buttonTintList = buttonControlTint
- })
- setControlViewVisibility(View.VISIBLE)
- }
- CONTROLTYPE_RADIO -> {
- setControlView(RadioButton(context).apply {
- layoutParams = FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- ).apply {
- gravity = Gravity.CENTER
- }
- isClickable = false
- isDuplicateParentStateEnabled = true
- buttonDrawable = ContextCompat.getDrawable(context, R.drawable.wear_radio_icon)
- buttonTintList = buttonControlTint
- })
- setControlViewVisibility(View.VISIBLE)
- }
- CONTROLTYPE_TOGGLE -> {
- setControlView(CheckBox(context).apply {
- layoutParams = FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- ).apply {
- gravity = Gravity.CENTER
- }
- isClickable = false
- isDuplicateParentStateEnabled = true
- buttonDrawable = ContextCompat.getDrawable(context, R.drawable.wear_switch_icon)
- buttonTintList = buttonControlTint
- })
- setControlViewVisibility(View.VISIBLE)
- }
- else -> {
- setControlView(null)
- }
- }
- }
-
- private fun updateBackgroundTint() {
- val backgroundDrawable = background
- if (backgroundDrawable != null) {
- if (backgroundDrawable is LayerDrawable) {
- val layerCount = backgroundDrawable.numberOfLayers
- for (i in 1 until layerCount) {
- val layer = backgroundDrawable.getDrawable(i)
- val id = backgroundDrawable.getId(i)
- if (id == R.id.start_accent) {
- layer.alpha = 0xFF
- backgroundDrawable.setDrawable(i, layer.setButtonBackgroundDrawableTint())
- } else {
- layer.alpha = 0
- }
- }
- return
- }
-
- val tintable = DrawableCompat.wrap(backgroundDrawable).mutate()
- DrawableCompat.setTintList(tintable, buttonBackgroundTint)
- background = tintable
- }
- }
-
- private fun updateButtonControlTint() {
- val control = getControlView() as? CompoundButton
- control?.buttonTintList = buttonControlTint
- }
-
- private fun Drawable.setButtonBackgroundDrawableTint(): Drawable {
- val tintable = DrawableCompat.wrap(this).mutate()
- DrawableCompat.setTintList(tintable, buttonBackgroundTint)
- return tintable
- }
-
- override fun setChecked(checked: Boolean) {
- _isChecked = checked
- refreshDrawableState()
- }
-
- override fun isChecked(): Boolean {
- return _isChecked
- }
-
- override fun toggle() {
- isChecked = !isChecked
- }
-
- override fun onCreateDrawableState(extraSpace: Int): IntArray {
- val drawableState = super.onCreateDrawableState(extraSpace + 3)
-
- if (isCheckable) {
- mergeDrawableStates(drawableState, CHECKABLE_STATE_SET)
- }
-
- if (isChecked) {
- mergeDrawableStates(drawableState, CHECKED_STATE_SET)
- }
-
- if (isEnabled) {
- mergeDrawableStates(drawableState, ENABLED_STATE_SET)
- }
-
- return drawableState
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/helpers/AcceptDenyDialog.java b/wear/src/main/java/com/thewizrd/simplewear/helpers/AcceptDenyDialog.java
deleted file mode 100644
index 49243001..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/helpers/AcceptDenyDialog.java
+++ /dev/null
@@ -1,159 +0,0 @@
-package com.thewizrd.simplewear.helpers;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-import androidx.annotation.StyleRes;
-
-import com.thewizrd.simplewear.databinding.AcceptDenyDialogBinding;
-
-public class AcceptDenyDialog extends Dialog {
- private final AcceptDenyDialogBinding binding;
- protected OnClickListener mPositiveButtonListener;
- protected OnClickListener mNegativeButtonListener;
- private final View.OnClickListener mButtonHandler;
-
- public AcceptDenyDialog(@NonNull Context context) {
- this(context, 0);
- }
-
- public AcceptDenyDialog(@NonNull Context context, @StyleRes int themeResId) {
- super(context, themeResId);
- mButtonHandler = new android.view.View.OnClickListener() {
- public void onClick(View v) {
- if (v == binding.buttonPositive && AcceptDenyDialog.this.mPositiveButtonListener != null) {
- AcceptDenyDialog.this.mPositiveButtonListener.onClick(AcceptDenyDialog.this, DialogInterface.BUTTON_POSITIVE);
- } else if (v == binding.buttonNegative && AcceptDenyDialog.this.mNegativeButtonListener != null) {
- AcceptDenyDialog.this.mNegativeButtonListener.onClick(AcceptDenyDialog.this, DialogInterface.BUTTON_NEGATIVE);
- }
-
- AcceptDenyDialog.this.dismiss();
- }
- };
- binding = AcceptDenyDialogBinding.inflate(LayoutInflater.from(getContext()));
- setContentView(binding.getRoot());
- binding.buttonPositive.setOnClickListener(mButtonHandler);
- binding.buttonNegative.setOnClickListener(mButtonHandler);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- binding.title.getRootView().requestFocus();
- }
-
- @Nullable
- public View getButton(int which) {
- switch (which) {
- case DialogInterface.BUTTON_POSITIVE:
- return binding.buttonPositive;
- case DialogInterface.BUTTON_NEGATIVE:
- return binding.buttonNegative;
- default:
- return null;
- }
- }
-
- public void setIcon(@Nullable Drawable icon) {
- binding.icon.setVisibility(icon == null ? View.GONE : View.VISIBLE);
- binding.icon.setImageDrawable(icon);
- }
-
- public void setIcon(@DrawableRes int resId) {
- binding.icon.setVisibility(resId == 0 ? View.GONE : View.VISIBLE);
- binding.icon.setImageResource(resId);
- }
-
- public void setMessage(@Nullable CharSequence message) {
- binding.message.setText(message);
- binding.message.setVisibility(message == null ? View.GONE : View.VISIBLE);
- }
-
- public void setMessage(@StringRes int resId) {
- binding.message.setText(resId);
- binding.message.setVisibility(resId == 0 ? View.GONE : View.VISIBLE);
- }
-
- public void setTitle(@Nullable CharSequence message) {
- binding.title.setText(message);
- }
-
- public void setTitle(@StringRes int resId) {
- binding.title.setText(resId);
- }
-
- public void setButton(int which, OnClickListener listener) {
- switch (which) {
- case DialogInterface.BUTTON_NEGATIVE:
- this.mNegativeButtonListener = listener;
- break;
- case DialogInterface.BUTTON_POSITIVE:
- this.mPositiveButtonListener = listener;
- break;
- default:
- return;
- }
-
- binding.spacer.setVisibility(this.mPositiveButtonListener != null && this.mNegativeButtonListener != null ? View.INVISIBLE : View.GONE);
- binding.buttonPositive.setVisibility(this.mPositiveButtonListener == null ? View.GONE : View.VISIBLE);
- binding.buttonNegative.setVisibility(this.mNegativeButtonListener == null ? View.GONE : View.VISIBLE);
- binding.buttonPanel.setVisibility(this.mPositiveButtonListener == null && this.mNegativeButtonListener == null ? View.GONE : View.VISIBLE);
- }
-
- public void setPositiveButton(OnClickListener listener) {
- this.setButton(DialogInterface.BUTTON_POSITIVE, listener);
- }
-
- public void setNegativeButton(OnClickListener listener) {
- this.setButton(DialogInterface.BUTTON_NEGATIVE, listener);
- }
-
- public static class Builder {
- private final Context mContext;
- private CharSequence message;
- private final DialogInterface.OnClickListener onClickListener;
-
- public Builder(Context context, DialogInterface.OnClickListener onClickListener) {
- mContext = context;
- this.onClickListener = onClickListener;
- }
-
- private void apply(AcceptDenyDialog dialog) {
- dialog.setMessage(message);
- dialog.setPositiveButton(onClickListener);
- dialog.setNegativeButton(onClickListener);
- }
-
- public Builder setMessage(@StringRes int resId) {
- message = mContext.getString(resId);
- return this;
- }
-
- public Builder setMessage(CharSequence message) {
- this.message = message;
- return this;
- }
-
- public AcceptDenyDialog create() {
- AcceptDenyDialog dialog = new AcceptDenyDialog(mContext);
- dialog.create();
- apply(dialog);
- return dialog;
- }
-
- public AcceptDenyDialog show() {
- AcceptDenyDialog dialog = this.create();
- dialog.show();
- return dialog;
- }
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/helpers/ConfirmationResultReceiver.kt b/wear/src/main/java/com/thewizrd/simplewear/helpers/ConfirmationResultReceiver.kt
deleted file mode 100644
index c8761e33..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/helpers/ConfirmationResultReceiver.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.thewizrd.simplewear.helpers
-
-import android.app.Activity
-import androidx.fragment.app.Fragment
-import androidx.wear.widget.ConfirmationOverlay
-
-fun Activity.showConfirmationOverlay(success: Boolean) {
- val overlay = ConfirmationOverlay()
- if (!success) {
- overlay.setType(ConfirmationOverlay.FAILURE_ANIMATION)
- } else {
- overlay.setType(ConfirmationOverlay.OPEN_ON_PHONE_ANIMATION)
- }
- overlay.showOn(this)
-}
-
-fun Fragment.showConfirmationOverlay(success: Boolean) {
- val overlay = ConfirmationOverlay()
- if (!success) {
- overlay.setType(ConfirmationOverlay.FAILURE_ANIMATION)
- } else {
- overlay.setType(ConfirmationOverlay.OPEN_ON_PHONE_ANIMATION)
- }
- overlay.showAbove(requireView())
-}
-
-fun Activity.showConfirmationOverlay(@ConfirmationOverlay.OverlayType type: Int) {
- val overlay = ConfirmationOverlay()
- overlay.setType(type)
- overlay.showOn(this)
-}
-
-fun Fragment.showConfirmationOverlay(@ConfirmationOverlay.OverlayType type: Int) {
- val overlay = ConfirmationOverlay()
- overlay.setType(type)
- overlay.showAbove(requireView())
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/helpers/SpacerItemDecoration.kt b/wear/src/main/java/com/thewizrd/simplewear/helpers/SpacerItemDecoration.kt
deleted file mode 100644
index 91367236..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/helpers/SpacerItemDecoration.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.thewizrd.simplewear.helpers
-
-import android.graphics.Rect
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-
-class SpacerItemDecoration : RecyclerView.ItemDecoration {
- private val horizontalSpace: Int?
- private val verticalSpace: Int?
-
- constructor(space: Int) : super() {
- horizontalSpace = space
- verticalSpace = space
- }
-
- constructor(horizontalSpace: Int? = null, verticalSpace: Int? = null) : super() {
- this.horizontalSpace = horizontalSpace
- this.verticalSpace = verticalSpace
- }
-
- override fun getItemOffsets(
- outRect: Rect,
- view: View,
- parent: RecyclerView,
- state: RecyclerView.State
- ) {
- super.getItemOffsets(outRect, view, parent, state)
-
- verticalSpace?.let {
- outRect.top = it / 2
- outRect.bottom = it / 2
- }
- horizontalSpace?.let {
- outRect.left = it / 2
- outRect.right = it / 2
- }
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/helpers/TileActionsItemTouchCallback.kt b/wear/src/main/java/com/thewizrd/simplewear/helpers/TileActionsItemTouchCallback.kt
deleted file mode 100644
index 9e3fa4e4..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/helpers/TileActionsItemTouchCallback.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.thewizrd.simplewear.helpers
-
-import androidx.recyclerview.widget.ItemTouchHelper
-import androidx.recyclerview.widget.RecyclerView
-import com.thewizrd.simplewear.adapters.AddButtonAdapter
-import com.thewizrd.simplewear.adapters.TileActionAdapter
-import java.util.*
-
-class TileActionsItemTouchCallback(private val adapter: TileActionAdapter) :
- ItemTouchHelper.SimpleCallback(
- ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.START or ItemTouchHelper.END,
- 0
- ) {
- override fun onMove(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- target: RecyclerView.ViewHolder
- ): Boolean {
- return if (viewHolder is AddButtonAdapter.ViewHolder || target is AddButtonAdapter.ViewHolder) {
- false
- } else {
- val from = viewHolder.bindingAdapterPosition
- val to = target.bindingAdapterPosition
-
- val items = adapter.currentList.toMutableList()
- Collections.swap(items, from, to)
- adapter.submitList(items)
-
- true
- }
- }
-
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
-
- override fun isItemViewSwipeEnabled(): Boolean {
- return false
- }
-
- override fun isLongPressDragEnabled(): Boolean {
- return false
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/media/AudioOutputRepository.kt b/wear/src/main/java/com/thewizrd/simplewear/media/AudioOutputRepository.kt
index 3d00d40d..e3177e86 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/media/AudioOutputRepository.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/media/AudioOutputRepository.kt
@@ -14,5 +14,5 @@ class NoopAudioOutputRepository : AudioOutputRepository {
override fun close() {}
- override fun launchOutputSelection(closeOnConnect: Boolean) {}
+ override fun launchOutputSelection(closeOnConnect: Boolean, clientPackageName: String?) {}
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/media/MediaItemModel.kt b/wear/src/main/java/com/thewizrd/simplewear/media/MediaItemModel.kt
index 95e4907b..509e9a21 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/media/MediaItemModel.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/media/MediaItemModel.kt
@@ -5,6 +5,7 @@ import android.graphics.Bitmap
data class MediaItemModel(val id: String) {
var icon: Bitmap? = null
var title: String? = null
+ var subTitle: String? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -15,6 +16,7 @@ data class MediaItemModel(val id: String) {
if (id != other.id) return false
if (icon != other.icon) return false
if (title != other.title) return false
+ if (subTitle != other.subTitle) return false
return true
}
@@ -23,6 +25,7 @@ data class MediaItemModel(val id: String) {
var result = id.hashCode()
result = 31 * result + (icon?.hashCode() ?: 0)
result = 31 * result + (title?.hashCode() ?: 0)
+ result = 31 * result + (subTitle?.hashCode() ?: 0)
return result
}
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerViewModel.kt b/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerViewModel.kt
index b2f39118..7c520be0 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerViewModel.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerViewModel.kt
@@ -86,6 +86,8 @@ data class PlayerState(
val positionState: PositionState? = null
) {
fun isEmpty(): Boolean = title.isNullOrEmpty() && artist.isNullOrEmpty()
+
+ val key = "${title}|${artist}"
}
data class MediaPagerState(
@@ -441,7 +443,7 @@ class MediaPlayerViewModel(app: Application) : WearableListenerViewModel(app) {
sendMessage(
mPhoneNodeWithApp!!.id,
MediaHelper.MediaPlayerConnectPath,
- if (state.isAutoLaunch) state.isAutoLaunch.booleanToBytes() else state.mediaPlayerDetails.packageName?.stringToBytes()
+ if (state.isAutoLaunch) true.booleanToBytes() else state.mediaPlayerDetails.packageName?.stringToBytes()
)
}
}
@@ -646,6 +648,7 @@ class MediaPlayerViewModel(app: Application) : WearableListenerViewModel(app) {
MediaItemModel(item.queueId.toString()).apply {
this.icon = item.icon?.toBitmap()
this.title = item.title
+ this.subTitle = item.subTitle
}
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/preferences/AddActionDialogBuilder.kt b/wear/src/main/java/com/thewizrd/simplewear/preferences/AddActionDialogBuilder.kt
deleted file mode 100644
index e5e5aefc..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/preferences/AddActionDialogBuilder.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-package com.thewizrd.simplewear.preferences
-
-import android.app.AlertDialog
-import android.content.Context
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.recyclerview.widget.AsyncDifferConfig
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
-import com.thewizrd.shared_resources.actions.Actions
-import com.thewizrd.shared_resources.controls.ActionButtonViewModel
-import com.thewizrd.shared_resources.helpers.ListAdapterOnClickInterface
-import com.thewizrd.shared_resources.utils.ContextUtils.dpToPx
-import com.thewizrd.simplewear.controls.WearChipButton
-import com.thewizrd.simplewear.databinding.DialogAddactionBinding
-import com.thewizrd.simplewear.helpers.SpacerItemDecoration
-
-class AddActionDialogBuilder(private val context: Context, private val actionsList: List) {
- private lateinit var adapter: ActionsListAdapter
- private var onActionSelectedListener: OnActionSelectedListener? = null
- private lateinit var binding: DialogAddactionBinding
-
- interface OnActionSelectedListener {
- fun onActionSelected(action: Actions)
- }
-
- private fun createView(): View {
- binding = DialogAddactionBinding.inflate(LayoutInflater.from(context))
-
- adapter = ActionsListAdapter(ActionButtonViewModel.DIFF_CALLBACK)
-
- binding.recyclerView.layoutManager = LinearLayoutManager(context)
- binding.recyclerView.adapter = adapter
- binding.recyclerView.addItemDecoration(
- SpacerItemDecoration(
- verticalSpace = context.dpToPx(4f).toInt()
- )
- )
-
- adapter.submitList(actionsList.map {
- ActionButtonViewModel.getViewModelFromAction(it)
- })
-
- return binding.root
- }
-
- fun setOnActionSelectedListener(listener: OnActionSelectedListener?): AddActionDialogBuilder {
- this.onActionSelectedListener = listener
- return this
- }
-
- fun show() {
- val dialog = AlertDialog.Builder(context)
- .setCancelable(true)
- .setView(createView())
- .create()
-
- adapter.setOnClickListener(object : ListAdapterOnClickInterface {
- override fun onClick(view: View, item: ActionButtonViewModel) {
- onActionSelectedListener?.onActionSelected(item.actionType)
- dialog.dismiss()
- }
- })
-
- dialog.show()
- }
-
- private class ActionsListAdapter :
- ListAdapter {
- private var onClickListener: ListAdapterOnClickInterface? = null
-
- constructor(diffCallback: DiffUtil.ItemCallback) : super(diffCallback)
- protected constructor(config: AsyncDifferConfig) : super(config)
-
- fun setOnClickListener(onClickListener: ListAdapterOnClickInterface?) {
- this.onClickListener = onClickListener
- }
-
- inner class ViewHolder(private val button: WearChipButton) :
- RecyclerView.ViewHolder(button) {
- fun bind(model: ActionButtonViewModel) {
- button.setPrimaryText(model.actionLabelResId)
- button.setIconResource(model.drawableResId)
- itemView.setOnClickListener {
- onClickListener?.onClick(it, model)
- }
- }
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- // create a new view
- val v = WearChipButton(parent.context).apply {
- layoutParams = RecyclerView.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- }
-
- return ViewHolder(v)
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- holder.bind(getItem(position))
- }
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardConfigActivity.kt b/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardConfigActivity.kt
deleted file mode 100644
index 00a3b1bf..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardConfigActivity.kt
+++ /dev/null
@@ -1,187 +0,0 @@
-package com.thewizrd.simplewear.preferences
-
-import android.content.DialogInterface
-import android.graphics.Rect
-import android.os.Bundle
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import androidx.core.view.InputDeviceCompat
-import androidx.core.view.MotionEventCompat
-import androidx.core.view.ViewConfigurationCompat
-import androidx.recyclerview.widget.ConcatAdapter
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
-import androidx.recyclerview.widget.ItemTouchHelper
-import com.thewizrd.shared_resources.actions.Actions
-import com.thewizrd.shared_resources.utils.AnalyticsLogger
-import com.thewizrd.simplewear.R
-import com.thewizrd.simplewear.activities.AppCompatLiteActivity
-import com.thewizrd.simplewear.adapters.AddButtonAdapter
-import com.thewizrd.simplewear.adapters.DashBattStatusItemAdapter
-import com.thewizrd.simplewear.adapters.TileActionAdapter
-import com.thewizrd.simplewear.databinding.LayoutDashboardConfigBinding
-import com.thewizrd.simplewear.helpers.AcceptDenyDialog
-import com.thewizrd.simplewear.helpers.TileActionsItemTouchCallback
-import com.thewizrd.simplewear.ui.navigation.Screen
-import kotlin.math.roundToInt
-
-class DashboardConfigActivity : AppCompatLiteActivity() {
- companion object {
- private val MAX_BUTTONS = Actions.entries.size
- private val DEFAULT_TILES = Actions.entries
- }
-
- private lateinit var binding: LayoutDashboardConfigBinding
- private lateinit var concatAdapter: ConcatAdapter
- private lateinit var dashBattStatAdapter: DashBattStatusItemAdapter
- private lateinit var actionAdapter: TileActionAdapter
- private lateinit var addButtonAdapter: AddButtonAdapter
- private lateinit var itemTouchHelper: ItemTouchHelper
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- binding = LayoutDashboardConfigBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- binding.tileGridLayout.layoutManager = GridLayoutManager(this, 3).also { layoutMgr ->
- layoutMgr.spanSizeLookup = object : SpanSizeLookup() {
- override fun getSpanSize(position: Int): Int {
- return if (concatAdapter.getItemViewType(position) == DashBattStatusItemAdapter.ITEM_TYPE) {
- 3
- } else {
- 1
- }
- }
- }
- }
-
- dashBattStatAdapter = DashBattStatusItemAdapter().apply {
- isVisible = Settings.isShowBatStatus()
- }
- actionAdapter = TileActionAdapter()
- addButtonAdapter = AddButtonAdapter()
-
- binding.tileGridLayout.adapter = ConcatAdapter(
- ConcatAdapter.Config.Builder()
- .setIsolateViewTypes(false)
- .build(),
- dashBattStatAdapter,
- actionAdapter
- ).also {
- concatAdapter = it
- }
-
- itemTouchHelper = ItemTouchHelper(TileActionsItemTouchCallback(actionAdapter))
- itemTouchHelper.attachToRecyclerView(binding.tileGridLayout)
-
- actionAdapter.onLongClickListener = {
- itemTouchHelper.startDrag(it)
- }
-
- actionAdapter.onListChanged = {
- if (it.size >= MAX_BUTTONS) {
- concatAdapter.removeAdapter(addButtonAdapter)
- } else {
- concatAdapter.addAdapter(addButtonAdapter)
- }
- }
-
- val config = Settings.getDashboardConfig()
-
- config?.let {
- actionAdapter.submitActions(it)
- } ?: run {
- actionAdapter.submitActions(DEFAULT_TILES)
- }
-
- addButtonAdapter.setOnClickListener {
- val allowedActions = Actions.entries.toMutableList()
- // Remove current actions
- allowedActions.removeAll(actionAdapter.getActions())
-
- AddActionDialogBuilder(this, allowedActions)
- .setOnActionSelectedListener(object :
- AddActionDialogBuilder.OnActionSelectedListener {
- override fun onActionSelected(action: Actions) {
- actionAdapter.addAction(action)
- }
- })
- .show()
- }
-
- binding.resetButton.setOnClickListener {
- AcceptDenyDialog.Builder(this) { _, which ->
- when (which) {
- DialogInterface.BUTTON_POSITIVE -> {
- actionAdapter.submitActions(DEFAULT_TILES)
- Settings.setDashboardConfig(null)
-
- dashBattStatAdapter.isVisible = true
- Settings.setShowBatStatus(true)
- }
- }
- }
- .setMessage(R.string.message_reset_to_default)
- .show()
- }
-
- binding.saveButton.setOnClickListener {
- val currentList = actionAdapter.getActions()
- Settings.setDashboardConfig(currentList)
-
- Settings.setShowBatStatus(dashBattStatAdapter.isVisible)
-
- // Close activity
- finish()
- }
- }
-
- override fun onStart() {
- super.onStart()
-
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.DashboardConfig.route)
- })
- }
-
- override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
- if (ev?.action == MotionEvent.ACTION_DOWN) {
- val currSelection = actionAdapter.getSelection()
- if (currSelection != null) {
- val r = Rect().also {
- currSelection.getGlobalVisibleRect(it)
- }
- if (!r.contains(ev.rawX.toInt(), ev.rawY.toInt())) {
- actionAdapter.clearSelection()
- }
- } else {
- dashBattStatAdapter.getSelection()?.let { battView ->
- val r = Rect().also {
- battView.getGlobalVisibleRect(it)
- }
- if (!r.contains(ev.rawX.toInt(), ev.rawY.toInt())) {
- dashBattStatAdapter.isChecked = false
- }
- }
- }
- }
- return super.dispatchTouchEvent(ev)
- }
-
- override fun onGenericMotionEvent(event: MotionEvent): Boolean {
- if (event.action == MotionEvent.ACTION_SCROLL && event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)) {
- // Don't forget the negation here
- val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) *
- ViewConfigurationCompat.getScaledVerticalScrollFactor(
- ViewConfiguration.get(this), this
- )
-
- // Swap these axes if you want to do horizontal scrolling instead
- binding.root.scrollBy(0, delta.roundToInt())
-
- return true
- }
- return super.onGenericMotionEvent(event)
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardTileConfigActivity.kt b/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardTileConfigActivity.kt
deleted file mode 100644
index 8d89e70d..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/preferences/DashboardTileConfigActivity.kt
+++ /dev/null
@@ -1,186 +0,0 @@
-package com.thewizrd.simplewear.preferences
-
-import android.content.DialogInterface
-import android.graphics.Rect
-import android.os.Bundle
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import androidx.core.view.InputDeviceCompat
-import androidx.core.view.MotionEventCompat
-import androidx.core.view.ViewConfigurationCompat
-import androidx.recyclerview.widget.ConcatAdapter
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.ItemTouchHelper
-import com.thewizrd.shared_resources.actions.Actions
-import com.thewizrd.shared_resources.utils.AnalyticsLogger
-import com.thewizrd.simplewear.R
-import com.thewizrd.simplewear.activities.AppCompatLiteActivity
-import com.thewizrd.simplewear.adapters.AddButtonAdapter
-import com.thewizrd.simplewear.adapters.DashBattStatusItemAdapter
-import com.thewizrd.simplewear.adapters.TileActionAdapter
-import com.thewizrd.simplewear.databinding.LayoutTileDashboardConfigBinding
-import com.thewizrd.simplewear.helpers.AcceptDenyDialog
-import com.thewizrd.simplewear.helpers.TileActionsItemTouchCallback
-import com.thewizrd.simplewear.preferences.DashboardTileUtils.DEFAULT_TILES
-import com.thewizrd.simplewear.preferences.DashboardTileUtils.MAX_BUTTONS
-import com.thewizrd.simplewear.preferences.DashboardTileUtils.isActionAllowed
-import com.thewizrd.simplewear.ui.navigation.Screen
-import kotlin.math.roundToInt
-
-class DashboardTileConfigActivity : AppCompatLiteActivity() {
- private lateinit var binding: LayoutTileDashboardConfigBinding
- private lateinit var concatAdapter: ConcatAdapter
- private lateinit var dashBattStatAdapter: DashBattStatusItemAdapter
- private lateinit var actionAdapter: TileActionAdapter
- private lateinit var addButtonAdapter: AddButtonAdapter
- private lateinit var itemTouchHelper: ItemTouchHelper
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- binding = LayoutTileDashboardConfigBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- binding.tileGridLayout.layoutManager = GridLayoutManager(this, 3).also { layoutMgr ->
- layoutMgr.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
- override fun getSpanSize(position: Int): Int {
- return if (concatAdapter.getItemViewType(position) == DashBattStatusItemAdapter.ITEM_TYPE) {
- 3
- } else {
- 1
- }
- }
- }
- }
-
- dashBattStatAdapter = DashBattStatusItemAdapter().apply {
- isVisible = Settings.isShowTileBatStatus()
- }
- actionAdapter = TileActionAdapter()
- addButtonAdapter = AddButtonAdapter()
-
- binding.tileGridLayout.adapter = ConcatAdapter(
- ConcatAdapter.Config.Builder()
- .setIsolateViewTypes(false)
- .build(),
- dashBattStatAdapter,
- actionAdapter
- ).also {
- concatAdapter = it
- }
-
- itemTouchHelper = ItemTouchHelper(TileActionsItemTouchCallback(actionAdapter))
- itemTouchHelper.attachToRecyclerView(binding.tileGridLayout)
-
- actionAdapter.onLongClickListener = {
- itemTouchHelper.startDrag(it)
- }
-
- actionAdapter.onListChanged = {
- if (it.size >= MAX_BUTTONS) {
- concatAdapter.removeAdapter(addButtonAdapter)
- } else {
- concatAdapter.addAdapter(addButtonAdapter)
- }
- }
-
- val config = Settings.getDashboardTileConfig()
-
- config?.let {
- actionAdapter.submitActions(it)
- } ?: run {
- actionAdapter.submitActions(DEFAULT_TILES)
- }
-
- addButtonAdapter.setOnClickListener {
- val allowedActions = Actions.entries.toMutableList()
- // Remove current actions
- allowedActions.removeAll(actionAdapter.getActions())
- // Remove other actions which need an activity
- allowedActions.removeIf { !isActionAllowed(it) }
-
- AddActionDialogBuilder(this, allowedActions)
- .setOnActionSelectedListener(object :
- AddActionDialogBuilder.OnActionSelectedListener {
- override fun onActionSelected(action: Actions) {
- actionAdapter.addAction(action)
- }
- })
- .show()
- }
-
- binding.resetButton.setOnClickListener {
- AcceptDenyDialog.Builder(this) { _, which ->
- when (which) {
- DialogInterface.BUTTON_POSITIVE -> {
- actionAdapter.submitActions(DEFAULT_TILES)
- Settings.setDashboardTileConfig(null)
-
- dashBattStatAdapter.isVisible = true
- Settings.setShowTileBatStatus(true)
- }
- }
- }
- .setMessage(R.string.message_reset_to_default)
- .show()
- }
-
- binding.saveButton.setOnClickListener {
- val currentList = actionAdapter.getActions()
- Settings.setDashboardTileConfig(currentList)
-
- Settings.setShowTileBatStatus(dashBattStatAdapter.isVisible)
-
- // Close activity
- finish()
- }
- }
-
- override fun onStart() {
- super.onStart()
-
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.DashboardTileConfig.route)
- })
- }
-
- override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
- if (ev?.action == MotionEvent.ACTION_DOWN) {
- val currSelection = actionAdapter.getSelection()
- if (currSelection != null) {
- val r = Rect().also {
- currSelection.getGlobalVisibleRect(it)
- }
- if (!r.contains(ev.rawX.toInt(), ev.rawY.toInt())) {
- actionAdapter.clearSelection()
- }
- } else {
- dashBattStatAdapter.getSelection()?.let { battView ->
- val r = Rect().also {
- battView.getGlobalVisibleRect(it)
- }
- if (!r.contains(ev.rawX.toInt(), ev.rawY.toInt())) {
- dashBattStatAdapter.isChecked = false
- }
- }
- }
- }
- return super.dispatchTouchEvent(ev)
- }
-
- override fun onGenericMotionEvent(event: MotionEvent): Boolean {
- if (event.action == MotionEvent.ACTION_SCROLL && event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)) {
- // Don't forget the negation here
- val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) *
- ViewConfigurationCompat.getScaledVerticalScrollFactor(
- ViewConfiguration.get(this), this
- )
-
- // Swap these axes if you want to do horizontal scrolling instead
- binding.root.scrollBy(0, delta.roundToInt())
-
- return true
- }
- return super.onGenericMotionEvent(event)
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/preferences/Settings.kt b/wear/src/main/java/com/thewizrd/simplewear/preferences/Settings.kt
index f0e1d156..1899e47c 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/preferences/Settings.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/preferences/Settings.kt
@@ -1,10 +1,15 @@
package com.thewizrd.simplewear.preferences
+import android.content.SharedPreferences
import androidx.core.content.edit
import com.google.gson.reflect.TypeToken
import com.thewizrd.shared_resources.actions.Actions
import com.thewizrd.shared_resources.appLib
import com.thewizrd.shared_resources.utils.JSONParser
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.callbackFlow
import java.time.Instant
object Settings {
@@ -18,6 +23,7 @@ object Settings {
const val KEY_SHOWBATSTATUS = "key_showbatstatus"
const val KEY_SHOWTILEBATSTATUS = "key_showtilebatstatus"
private const val KEY_LASTUPDATECHECK = "key_lastupdatecheck"
+ private const val KEY_VERSIONCODE = "key_versioncode"
fun useGridLayout(): Boolean {
return appLib.preferences.getBoolean(KEY_LAYOUTMODE, true)
@@ -68,6 +74,18 @@ object Settings {
}
}
+ fun getDashboardTileConfigFlow() = callbackFlow {
+ val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ when (key) {
+ KEY_DASHTILECONFIG -> trySend(getDashboardTileConfig())
+ }
+ }
+
+ appLib.preferences.registerOnSharedPreferenceChangeListener(listener)
+
+ awaitClose { appLib.preferences.unregisterOnSharedPreferenceChangeListener(listener) }
+ }.buffer(Channel.UNLIMITED)
+
fun setDashboardTileConfig(actions: List?) {
appLib.preferences.edit {
putString(KEY_DASHTILECONFIG, actions?.let {
@@ -85,6 +103,18 @@ object Settings {
}
}
+ fun getDashboardConfigFlow() = callbackFlow {
+ val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ when (key) {
+ KEY_DASHCONFIG -> trySend(getDashboardConfig())
+ }
+ }
+
+ appLib.preferences.registerOnSharedPreferenceChangeListener(listener)
+
+ awaitClose { appLib.preferences.unregisterOnSharedPreferenceChangeListener(listener) }
+ }.buffer(Channel.UNLIMITED)
+
fun setDashboardConfig(actions: List?) {
appLib.preferences.edit {
putString(KEY_DASHCONFIG, actions?.let {
@@ -98,6 +128,18 @@ object Settings {
return appLib.preferences.getBoolean(KEY_SHOWBATSTATUS, true)
}
+ fun isShowBatStatusFlow() = callbackFlow {
+ val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ when (key) {
+ KEY_SHOWBATSTATUS -> trySend(isShowBatStatus())
+ }
+ }
+
+ appLib.preferences.registerOnSharedPreferenceChangeListener(listener)
+
+ awaitClose { appLib.preferences.unregisterOnSharedPreferenceChangeListener(listener) }
+ }.buffer(Channel.UNLIMITED)
+
fun setShowBatStatus(value: Boolean) {
appLib.preferences.edit {
putBoolean(KEY_SHOWBATSTATUS, value)
@@ -125,4 +167,14 @@ object Settings {
putLong(KEY_LASTUPDATECHECK, value.epochSecond)
}
}
+
+ fun getVersionCode(): Long {
+ return appLib.preferences.getLong(KEY_VERSIONCODE, 0)
+ }
+
+ fun setVersionCode(value: Long) {
+ appLib.preferences.edit {
+ putLong(KEY_VERSIONCODE, value)
+ }
+ }
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/components/CircularWavyProgressIndicator.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/components/CircularWavyProgressIndicator.kt
new file mode 100644
index 00000000..638c521a
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/components/CircularWavyProgressIndicator.kt
@@ -0,0 +1,23 @@
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
+package com.thewizrd.simplewear.ui.components
+
+import androidx.compose.material3.CircularWavyProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.wear.compose.material3.MaterialTheme
+
+@Composable
+fun CircularWavyProgressIndicator(
+ modifier: Modifier = Modifier,
+ color: Color = MaterialTheme.colorScheme.primary,
+ trackColor: Color = MaterialTheme.colorScheme.secondaryContainer
+) {
+ CircularWavyProgressIndicator(
+ modifier = modifier,
+ color = color,
+ trackColor = trackColor
+ )
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/components/ConfirmationOverlay.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/components/ConfirmationOverlay.kt
index b4a4d007..7a526f83 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/components/ConfirmationOverlay.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/components/ConfirmationOverlay.kt
@@ -1,102 +1,176 @@
package com.thewizrd.simplewear.ui.components
-import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalAccessibilityManager
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.dialog.Dialog
-import androidx.wear.compose.material.dialog.DialogDefaults
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
-import com.google.android.horologist.compose.layout.rememberColumnState
-import com.google.android.horologist.compose.material.ConfirmationContent
+import androidx.compose.ui.res.stringResource
+import androidx.wear.compose.material3.ConfirmationDialog
+import androidx.wear.compose.material3.ConfirmationDialogDefaults
+import androidx.wear.compose.material3.FailureConfirmationDialog
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.SuccessConfirmationDialog
+import androidx.wear.compose.material3.Text
+import com.thewizrd.simplewear.R
import com.thewizrd.simplewear.viewmodels.ConfirmationData
-import kotlinx.coroutines.delay
+import com.thewizrd.simplewear.viewmodels.ConfirmationType
-@OptIn(ExperimentalHorologistApi::class, ExperimentalAnimationGraphicsApi::class)
@Composable
fun ConfirmationOverlay(
confirmationData: ConfirmationData?,
onTimeout: () -> Unit,
showDialog: Boolean = confirmationData != null
) {
- val currentOnDismissed by rememberUpdatedState(onTimeout)
- val durationMillis = remember(confirmationData) {
- confirmationData?.durationMs ?: DialogDefaults.ShortDurationMillis
- }
-
- val a11yDurationMillis = LocalAccessibilityManager.current?.calculateRecommendedTimeoutMillis(
- originalTimeoutMillis = durationMillis,
- containsIcons = confirmationData?.iconResId != null,
- containsText = confirmationData?.title != null,
- containsControls = false,
- ) ?: durationMillis
-
- val columnState = rememberColumnState(
- ScalingLazyColumnDefaults.responsive(
- verticalArrangement = Arrangement.spacedBy(
- space = 4.dp,
- alignment = Alignment.CenterVertically
- ),
- additionalPaddingAtBottom = 0.dp,
- ),
- )
+ when (confirmationData?.confirmationType) {
+ ConfirmationType.Success -> {
+ if (confirmationData.message != null) {
+ ConfirmationDialog(
+ visible = showDialog,
+ onDismissRequest = onTimeout,
+ colors = ConfirmationDialogDefaults.successColors(),
+ text = {
+ Text(text = confirmationData.message)
+ },
+ content = {
+ ConfirmationDialogDefaults.SuccessIcon(
+ modifier = Modifier.size(ConfirmationDialogDefaults.SmallIconSize)
+ )
+ }
+ )
+ } else {
+ SuccessConfirmationDialog(
+ visible = showDialog,
+ onDismissRequest = onTimeout,
+ curvedText = null
+ )
+ }
+ }
- LaunchedEffect(a11yDurationMillis, confirmationData) {
- if (showDialog) {
- delay(a11yDurationMillis)
- currentOnDismissed()
+ ConfirmationType.Failure -> {
+ if (confirmationData.message != null) {
+ ConfirmationDialog(
+ visible = showDialog,
+ onDismissRequest = onTimeout,
+ colors = ConfirmationDialogDefaults.failureColors(),
+ text = {
+ Text(text = confirmationData.message)
+ },
+ content = {
+ ConfirmationDialogDefaults.FailureIcon(
+ modifier = Modifier.size(ConfirmationDialogDefaults.SmallIconSize)
+ )
+ }
+ )
+ } else {
+ FailureConfirmationDialog(
+ visible = showDialog,
+ onDismissRequest = onTimeout,
+ curvedText = null
+ )
+ }
}
- }
- Dialog(
- showDialog = showDialog,
- onDismissRequest = currentOnDismissed,
- scrollState = columnState.state,
- ) {
- ConfirmationContent(
- icon = confirmationData?.animatedVectorResId?.let { iconResId ->
- {
- val image = AnimatedImageVector.animatedVectorResource(iconResId)
+ ConfirmationType.OpenOnPhone -> {
+ ConfirmationDialog(
+ visible = showDialog,
+ onDismissRequest = onTimeout,
+ colors = ConfirmationDialogDefaults.colors(),
+ text = confirmationData.message?.let {
+ {
+ Text(text = it)
+ }
+ },
+ content = {
+ val image =
+ AnimatedImageVector.animatedVectorResource(R.drawable.open_on_phone_animation)
var atEnd by remember { mutableStateOf(false) }
Icon(
- modifier = Modifier.size(48.dp),
+ modifier = Modifier.size(
+ if (confirmationData.message != null) {
+ ConfirmationDialogDefaults.SmallIconSize
+ } else {
+ ConfirmationDialogDefaults.IconSize
+ }
+ ),
painter = rememberAnimatedVectorPainter(image, atEnd),
- contentDescription = null
+ contentDescription = stringResource(R.string.common_open_on_phone)
)
- LaunchedEffect(iconResId) {
+ LaunchedEffect(Unit) {
atEnd = !atEnd
}
}
- } ?: confirmationData?.iconResId?.let { iconResId ->
- {
+ )
+ }
+
+ else -> {
+ ConfirmationDialog(
+ visible = showDialog,
+ onDismissRequest = onTimeout,
+ colors = ConfirmationDialogDefaults.colors(),
+ text = confirmationData?.message?.let {
+ {
+ Text(text = it)
+ }
+ },
+ content = confirmationData?.animatedVectorResId?.let { iconResId ->
+ {
+ val image = AnimatedImageVector.animatedVectorResource(iconResId)
+ var atEnd by remember { mutableStateOf(false) }
+
+ Icon(
+ modifier = Modifier.size(
+ if (confirmationData.message != null) {
+ ConfirmationDialogDefaults.SmallIconSize
+ } else {
+ ConfirmationDialogDefaults.IconSize
+ }
+ ),
+ painter = rememberAnimatedVectorPainter(image, atEnd),
+ contentDescription = null
+ )
+
+ LaunchedEffect(iconResId) {
+ atEnd = !atEnd
+ }
+ }
+ } ?: confirmationData?.iconResId?.let { iconResId ->
+ {
+ Icon(
+ modifier = Modifier.size(
+ if (confirmationData.message != null) {
+ ConfirmationDialogDefaults.SmallIconSize
+ } else {
+ ConfirmationDialogDefaults.IconSize
+ }
+ ),
+ painter = painterResource(iconResId),
+ contentDescription = null
+ )
+ }
+ } ?: {
Icon(
- modifier = Modifier.size(48.dp),
- painter = painterResource(iconResId),
+ modifier = Modifier.size(
+ if (confirmationData?.message != null) {
+ ConfirmationDialogDefaults.SmallIconSize
+ } else {
+ ConfirmationDialogDefaults.IconSize
+ }
+ ),
+ painter = painterResource(R.drawable.ic_check_white_24dp),
contentDescription = null
)
}
- },
- title = confirmationData?.title,
- columnState = columnState,
- showPositionIndicator = false,
- )
+ )
+ }
}
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/components/HorizontalPagerScreen.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/components/HorizontalPagerScreen.kt
new file mode 100644
index 00000000..760af286
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/components/HorizontalPagerScreen.kt
@@ -0,0 +1,49 @@
+package com.thewizrd.simplewear.ui.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.isUnspecified
+import androidx.wear.compose.foundation.pager.HorizontalPager
+import androidx.wear.compose.foundation.pager.PagerScope
+import androidx.wear.compose.foundation.pager.PagerState
+import androidx.wear.compose.material3.HorizontalPageIndicator
+import androidx.wear.compose.material3.HorizontalPagerScaffold
+import androidx.wear.compose.material3.PageIndicatorDefaults
+
+@Composable
+fun HorizontalPagerScreen(
+ modifier: Modifier = Modifier,
+ pagerState: PagerState,
+ hidePagerIndicator: Boolean = false,
+ pagerKey: ((index: Int) -> Any)? = null,
+ userScrollEnabled: Boolean = true,
+ pagerIndicatorBackgroundColor: Color = Color.Unspecified,
+ content: @Composable PagerScope.(page: Int) -> Unit,
+) {
+ HorizontalPagerScaffold(
+ modifier = modifier,
+ pagerState = pagerState,
+ pageIndicator = if (hidePagerIndicator) {
+ null
+ } else {
+ {
+ HorizontalPageIndicator(
+ pagerState = pagerState,
+ backgroundColor = if (pagerIndicatorBackgroundColor.isUnspecified) {
+ PageIndicatorDefaults.backgroundColor
+ } else {
+ pagerIndicatorBackgroundColor
+ }
+ )
+ }
+ }
+ ) {
+ HorizontalPager(
+ state = pagerState,
+ key = pagerKey,
+ userScrollEnabled = userScrollEnabled,
+ content = content
+ )
+ }
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/components/LoadingContent.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/components/LoadingContent.kt
index 993170d8..6b54c6b6 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/components/LoadingContent.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/components/LoadingContent.kt
@@ -5,7 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.wear.compose.material.CircularProgressIndicator
+import androidx.wear.compose.material3.CircularProgressIndicator
@Composable
fun LoadingContent(
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/components/PullRefresh.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/components/PullRefresh.kt
deleted file mode 100644
index dec59eaf..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/components/PullRefresh.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.thewizrd.simplewear.ui.components
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.pullrefresh.PullRefreshState
-import androidx.compose.material.pullrefresh.pullRefresh
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-
-@OptIn(ExperimentalMaterialApi::class)
-@Composable
-fun PullRefresh(
- modifier: Modifier = Modifier,
- state: PullRefreshState,
- indicator: @Composable BoxScope.() -> Unit,
- content: @Composable () -> Unit
-) {
- Box(
- modifier = modifier.pullRefresh(state)
- ) {
- content()
-
- indicator()
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/components/ScalingLazyColumn.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/components/ScalingLazyColumn.kt
deleted file mode 100644
index 5981462f..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/components/ScalingLazyColumn.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.thewizrd.simplewear.ui.components
-
-import androidx.compose.foundation.gestures.ScrollableDefaults
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
-import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
-import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
-import androidx.wear.compose.foundation.rotary.rotaryScrollable
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.compose.layout.ScalingLazyColumnState
-import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
-
-@ExperimentalWearFoundationApi
-@ExperimentalHorologistApi
-@Composable
-fun ScalingLazyColumn(
- modifier: Modifier = Modifier,
- scrollState: ScalingLazyColumnState = rememberResponsiveColumnState(),
- focusRequester: FocusRequester = rememberActiveFocusRequester(),
- content: ScalingLazyListScope.() -> Unit
-) {
- androidx.wear.compose.foundation.lazy.ScalingLazyColumn(
- modifier = modifier
- .fillMaxSize()
- .rotaryScrollable(
- behavior = RotaryScrollableDefaults.behavior(scrollState),
- focusRequester = focusRequester,
- reverseDirection = scrollState.reverseLayout
- ),
- state = scrollState.state,
- contentPadding = scrollState.contentPadding,
- reverseLayout = scrollState.reverseLayout,
- verticalArrangement = scrollState.verticalArrangement,
- horizontalAlignment = scrollState.horizontalAlignment,
- flingBehavior = ScrollableDefaults.flingBehavior(),
- rotaryScrollableBehavior = null,
- userScrollEnabled = scrollState.userScrollEnabled,
- scalingParams = scrollState.scalingParams,
- anchorType = scrollState.anchorType,
- autoCentering = scrollState.autoCentering,
- content = content
- )
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/components/SwipeToDismissPagerScreen.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/components/SwipeToDismissPagerScreen.kt
deleted file mode 100644
index c1621e0f..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/components/SwipeToDismissPagerScreen.kt
+++ /dev/null
@@ -1,236 +0,0 @@
-package com.thewizrd.simplewear.ui.components
-
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.PagerDefaults
-import androidx.compose.foundation.pager.PagerState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalViewConfiguration
-import androidx.compose.ui.platform.ViewConfiguration
-import androidx.compose.ui.semantics.ScrollAxisRange
-import androidx.compose.ui.semantics.horizontalScrollAxisRange
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
-import androidx.wear.compose.foundation.HierarchicalFocusCoordinator
-import androidx.wear.compose.foundation.SwipeToDismissBoxState
-import androidx.wear.compose.foundation.edgeSwipeToDismiss
-import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
-import com.google.android.horologist.compose.layout.PagerScaffold
-import com.google.android.horologist.compose.pager.HorizontalPagerDefaults
-import kotlinx.coroutines.coroutineScope
-
-// https://slack-chats.kotlinlang.org/t/16230979/problem-changing-basicswipetodismiss-background-color-gt
-@OptIn(ExperimentalFoundationApi::class, ExperimentalWearFoundationApi::class)
-@Composable
-fun SwipeToDismissPagerScreen(
- modifier: Modifier = Modifier,
- state: PagerState,
- hidePagerIndicator: Boolean = false,
- timeText: (@Composable () -> Unit)? = null,
- pagerKey: ((index: Int) -> Any)? = null,
- content: @Composable (Int) -> Unit
-) {
- val screenWidth = with(LocalDensity.current) {
- LocalConfiguration.current.screenWidthDp.dp.toPx()
- }
- var allowPaging by remember { mutableStateOf(true) }
-
- val originalTouchSlop = LocalViewConfiguration.current.touchSlop
-
- CustomTouchSlopProvider(newTouchSlop = originalTouchSlop * 2) {
- PagerScaffold(
- modifier = Modifier.fillMaxSize(),
- timeText = timeText,
- pagerState = if (hidePagerIndicator) null else state
- ) {
- HorizontalPager(
- modifier = modifier
- .pointerInput(screenWidth) {
- coroutineScope {
- awaitEachGesture {
- allowPaging = true
- val firstDown =
- awaitFirstDown(false, PointerEventPass.Initial)
- val xPosition = firstDown.position.x
- // Define edge zone of 15%
- allowPaging = xPosition > screenWidth * 0.15f
- }
- }
- }
- .semantics {
- horizontalScrollAxisRange = if (allowPaging) {
- ScrollAxisRange(value = { state.currentPage.toFloat() },
- maxValue = { 3f })
- } else {
- // signals system swipe to dismiss that they can take over
- ScrollAxisRange(value = { 0f },
- maxValue = { 0f })
- }
- },
- state = state,
- flingBehavior = PagerDefaults.flingBehavior(
- state,
- snapAnimationSpec = tween(150, 0),
- ),
- userScrollEnabled = allowPaging,
- key = pagerKey
- ) { page ->
- HierarchicalFocusCoordinator(requiresFocus = { page == state.currentPage }) {
- content(page)
- }
- }
- }
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun SwipeToDismissPagerScreen(
- modifier: Modifier = Modifier,
- isRoot: Boolean = true,
- swipeToDismissBoxState: SwipeToDismissBoxState = rememberSwipeToDismissBoxState(),
- state: PagerState,
- hidePagerIndicator: Boolean = false,
- timeText: (@Composable () -> Unit)? = null,
- pagerKey: ((index: Int) -> Any)? = null,
- content: @Composable (Int) -> Unit
-) {
- if (isRoot) {
- SwipeToDismissPagerScreen(
- modifier,
- state,
- hidePagerIndicator,
- timeText,
- pagerKey,
- content
- )
- } else {
- SwipeToDismissPagerScreen(
- modifier,
- swipeToDismissBoxState,
- state,
- hidePagerIndicator,
- timeText,
- pagerKey,
- content
- )
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class, ExperimentalWearFoundationApi::class)
-@Composable
-private fun SwipeToDismissPagerScreen(
- modifier: Modifier = Modifier,
- swipeToDismissBoxState: SwipeToDismissBoxState = rememberSwipeToDismissBoxState(),
- state: PagerState,
- hidePagerIndicator: Boolean = false,
- timeText: (@Composable () -> Unit)? = null,
- pagerKey: ((index: Int) -> Any)? = null,
- content: @Composable (Int) -> Unit
-) {
- PagerScaffold(
- modifier = Modifier
- .fillMaxSize()
- .edgeSwipeToDismiss(swipeToDismissBoxState),
- timeText = timeText,
- pagerState = if (hidePagerIndicator) null else state
- ) {
- HorizontalPager(
- modifier = modifier,
- state = state,
- flingBehavior = HorizontalPagerDefaults.flingParams(state),
- key = pagerKey
- ) { page ->
- HierarchicalFocusCoordinator(requiresFocus = { page == state.currentPage }) {
- content(page)
- }
- }
- }
-}
-
-//MARK: - Horologist code
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun ClippedBox(pagerState: PagerState, content: @Composable () -> Unit) {
- val shape = rememberClipWhenScrolling(pagerState)
- Box(
- modifier = Modifier
- .fillMaxSize()
- .optionalClip(shape),
- ) {
- content()
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun rememberClipWhenScrolling(state: PagerState): State {
- val shape = if (LocalConfiguration.current.isScreenRound) CircleShape else null
- return remember(state) {
- derivedStateOf {
- if (shape != null && state.currentPageOffsetFraction != 0f) {
- shape
- } else {
- null
- }
- }
- }
-}
-
-private fun Modifier.optionalClip(shapeState: State): Modifier {
- val shape = shapeState.value
-
- return if (shape != null) {
- clip(shape)
- } else {
- this
- }
-}
-
-
-// MARK: - Steve Bower code
-
-@Composable
-private fun CustomTouchSlopProvider(
- newTouchSlop: Float,
- content: @Composable () -> Unit
-) {
- CompositionLocalProvider(
- LocalViewConfiguration provides CustomTouchSlop(
- newTouchSlop,
- LocalViewConfiguration.current
- )
- ) {
- content()
- }
-}
-
-private class CustomTouchSlop(
- private val customTouchSlop: Float,
- currentConfiguration: ViewConfiguration
-) : ViewConfiguration by currentConfiguration {
- override val touchSlop: Float
- get() = customTouchSlop
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/components/TimerSource.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/components/TimerSource.kt
new file mode 100644
index 00000000..2121c5c3
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/components/TimerSource.kt
@@ -0,0 +1,69 @@
+package com.thewizrd.simplewear.ui.components
+
+import android.text.format.DateUtils
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.wear.compose.material3.TimeSource
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import java.util.concurrent.TimeUnit
+import kotlin.math.abs
+
+/**
+ * Based on [androidx.wear.compose.material3.DefaultTimeSource]
+ */
+class ElapsedTimeSource(private val startTimeMillis: Long) : TimeSource {
+ @Composable
+ override fun currentTime(): String =
+ elapsedTimeDuration({ System.currentTimeMillis() }, startTimeMillis).value
+}
+
+@Composable
+private fun elapsedTimeDuration(time: () -> Long, startTimeMillis: Long): State {
+ val composableScope = rememberCoroutineScope()
+ var currentTime by remember { mutableLongStateOf(time()) }
+
+ val timeText = remember {
+ derivedStateOf { formatDuration(currentTime - startTimeMillis) }
+ }
+
+ val context = LocalContext.current
+ val updatedTimeLambda by rememberUpdatedState(time)
+
+ DisposableEffect(context, updatedTimeLambda) {
+ currentTime = updatedTimeLambda()
+
+ val timerJob = composableScope.launch {
+ while (isActive) {
+ currentTime = updatedTimeLambda()
+
+ val nowMillis = currentTime
+ var delayMillis = 1000 - (abs(nowMillis - startTimeMillis) % 1000)
+
+ delayMillis++
+ delay(delayMillis)
+ }
+ }
+
+ onDispose {
+ timerJob.cancel()
+ }
+ }
+ return timeText
+}
+
+private fun formatDuration(elapsedMillis: Long): String {
+ return DateUtils.formatElapsedTime(
+ TimeUnit.MILLISECONDS.toSeconds(elapsedMillis)
+ )
+}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/compose/LazyGridScrollIndicator.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/compose/LazyGridScrollIndicator.kt
new file mode 100644
index 00000000..9fba2b90
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/compose/LazyGridScrollIndicator.kt
@@ -0,0 +1,23 @@
+package com.thewizrd.simplewear.ui.compose
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.material3.ScrollIndicatorColors
+import androidx.wear.compose.material3.ScrollIndicatorDefaults
+
+@Composable
+fun LazyGridScrollIndicator(
+ lazyGridState: LazyGridState,
+ modifier: Modifier = Modifier,
+ colors: ScrollIndicatorColors = ScrollIndicatorDefaults.colors(),
+ reverseDirection: Boolean = false,
+ positionAnimationSpec: AnimationSpec = ScrollIndicatorDefaults.PositionAnimationSpec,
+) = androidx.wear.compose.material3.ScrollIndicator(
+ state = rememberLazyGridScrollState(lazyGridState),
+ modifier = modifier,
+ colors = colors,
+ reverseDirection = reverseDirection,
+ positionAnimationSpec = positionAnimationSpec
+)
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/compose/LazyGridStateAdapter.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/compose/LazyGridStateAdapter.kt
new file mode 100644
index 00000000..5bb4b2ce
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/compose/LazyGridStateAdapter.kt
@@ -0,0 +1,176 @@
+package com.thewizrd.simplewear.ui.compose
+
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.wear.compose.foundation.ScrollInfoProvider
+import com.thewizrd.shared_resources.utils.Logger
+
+internal class LazyGridScrollInfoProvider(val state: LazyGridState) : ScrollInfoProvider {
+ override val isScrollAwayValid: Boolean
+ get() = state.layoutInfo.totalItemsCount > 0
+ override val isScrollable: Boolean
+ get() = state.layoutInfo.totalItemsCount > 0
+ override val isScrollInProgress: Boolean
+ get() = state.isScrollInProgress
+ override val anchorItemOffset: Float
+ get() =
+ state.layoutInfo.visibleItemsInfo.firstOrNull()?.let {
+ if (it.index != 0) {
+ return@let Float.NaN
+ }
+ -it.offset.toOffset(state.layoutInfo.orientation).toFloat()
+ } ?: Float.NaN
+ override val lastItemOffset: Float
+ get() {
+ val layoutInfo = state.layoutInfo
+ val lazyColumnHeightPx = layoutInfo.viewportSize.height
+ val reverseLayout = state.layoutInfo.reverseLayout
+ return if (reverseLayout) {
+ layoutInfo.visibleItemsInfo.firstOrNull()?.let {
+ if (it.index != 0) {
+ return@let 0f
+ }
+ val bottomEdge =
+ -it.offset.toOffset(state.layoutInfo.orientation) + lazyColumnHeightPx + layoutInfo.viewportStartOffset
+ (lazyColumnHeightPx - bottomEdge).toFloat().coerceAtLeast(0f)
+ } ?: 0f
+ } else {
+ layoutInfo.visibleItemsInfo.lastOrNull()?.let {
+ if (it.index != layoutInfo.totalItemsCount - 1) {
+ return@let 0f
+ }
+ val bottomEdge =
+ it.offset.toOffset(state.layoutInfo.orientation) + it.size.toSize(state.layoutInfo.orientation) - layoutInfo.viewportStartOffset
+ (lazyColumnHeightPx - bottomEdge).toFloat().coerceAtLeast(0f)
+ } ?: 0f
+ }
+ }
+}
+
+@Composable
+fun rememberLazyGridScrollState(lazyGridState: LazyGridState): ScrollState {
+ val scrollState = rememberScrollState()
+
+ LaunchedEffect(lazyGridState) {
+ snapshotFlow { lazyGridState.layoutInfo }
+ .collect { layoutInfo ->
+ val positionFraction = lazyGridState.positionFraction
+
+ val viewportSize = if (layoutInfo.orientation == Orientation.Vertical) {
+ layoutInfo.viewportSize.height
+ } else {
+ layoutInfo.viewportSize.width
+ }
+
+ val sizeFraction = lazyGridState.sizeFraction(viewportSize.toFloat())
+
+ @Suppress("UNCHECKED_CAST")
+ runCatching {
+ ScrollState::class.java.getDeclaredField("_maxValueState").run {
+ isAccessible = true
+ (get(scrollState) as MutableState).run {
+ value = (100 / sizeFraction).toInt()
+ }
+ }
+
+ ScrollState::class.java.getDeclaredField("value\$delegate").run {
+ isAccessible = true
+ (get(scrollState) as MutableState).run {
+ value = (positionFraction * 100f / sizeFraction).toInt()
+ }
+ }
+
+ ScrollState::class.java.getDeclaredField("viewportSize\$delegate").run {
+ isAccessible = true
+ (get(scrollState) as MutableState).run {
+ value = (viewportSize * sizeFraction).toInt()
+ }
+ }
+ }.onFailure {
+ Logger.debug("LazyGridScrollState", it)
+ }
+
+ scrollState.scrollTo(scrollState.value)
+ }
+ }
+
+ return scrollState
+}
+
+internal fun IntOffset.toOffset(orientation: Orientation): Int {
+ return if (orientation == Orientation.Vertical) {
+ this.y
+ } else {
+ this.x
+ }
+}
+
+internal fun IntSize.toSize(orientation: Orientation): Int {
+ return if (orientation == Orientation.Vertical) {
+ this.height
+ } else {
+ this.width
+ }
+}
+
+internal val LazyGridState.positionFraction: Float
+ get() {
+ return if (layoutInfo.visibleItemsInfo.isEmpty()) {
+ 0.0f
+ } else {
+ val decimalFirstItemIndex = decimalFirstItemIndex()
+ val decimalLastItemIndex = decimalLastItemIndex()
+
+ val decimalLastItemIndexDistanceFromEnd = layoutInfo.totalItemsCount -
+ decimalLastItemIndex
+
+ if (decimalFirstItemIndex + decimalLastItemIndexDistanceFromEnd == 0.0f) {
+ 0.0f
+ } else {
+ decimalFirstItemIndex /
+ (decimalFirstItemIndex + decimalLastItemIndexDistanceFromEnd)
+ }
+ }
+ }
+
+internal fun LazyGridState.decimalLastItemIndex(): Float {
+ if (layoutInfo.visibleItemsInfo.isEmpty()) return 0f
+ val lastItem = layoutInfo.visibleItemsInfo.last()
+ // Coerce item sizes to at least 1 to avoid divide by zero for zero height items
+ val lastItemVisibleSize =
+ (layoutInfo.viewportEndOffset - lastItem.offset.toOffset(layoutInfo.orientation))
+ .coerceAtMost(lastItem.size.toSize(layoutInfo.orientation)).coerceAtLeast(1)
+ return lastItem.index.toFloat() +
+ lastItemVisibleSize.toFloat() / lastItem.size.toSize(layoutInfo.orientation)
+ .coerceAtLeast(1).toFloat()
+}
+
+internal fun LazyGridState.decimalFirstItemIndex(): Float {
+ if (layoutInfo.visibleItemsInfo.isEmpty()) return 0f
+ val firstItem = layoutInfo.visibleItemsInfo.first()
+ val firstItemOffset =
+ firstItem.offset.toOffset(layoutInfo.orientation) - layoutInfo.viewportStartOffset
+ // Coerce item size to at least 1 to avoid divide by zero for zero height items
+ return firstItem.index.toFloat() -
+ firstItemOffset.coerceAtMost(0).toFloat() /
+ firstItem.size.toSize(layoutInfo.orientation).coerceAtLeast(1).toFloat()
+}
+
+internal fun LazyGridState.sizeFraction(scrollableContainerSizePx: Float) =
+ if (layoutInfo.totalItemsCount == 0) {
+ 1.0f
+ } else {
+ val decimalFirstItemIndex = decimalFirstItemIndex()
+ val decimalLastItemIndex = decimalLastItemIndex()
+
+ (decimalLastItemIndex - decimalFirstItemIndex) /
+ layoutInfo.totalItemsCount.toFloat()
+ }
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/tools/WearPreviewDevices.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/compose/tools/WearPreviewDevices.kt
similarity index 66%
rename from wear/src/main/java/com/thewizrd/simplewear/ui/tools/WearPreviewDevices.kt
rename to wear/src/main/java/com/thewizrd/simplewear/ui/compose/tools/WearPreviewDevices.kt
index aa776d77..5d45949e 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/tools/WearPreviewDevices.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/compose/tools/WearPreviewDevices.kt
@@ -1,20 +1,4 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.thewizrd.simplewear.ui.tools
+package com.thewizrd.simplewear.ui.compose.tools
import androidx.compose.ui.tooling.preview.Preview
import androidx.wear.tooling.preview.devices.WearDevices
@@ -40,6 +24,13 @@ import androidx.wear.tooling.preview.devices.WearDevices
showBackground = true,
group = "Devices - Square"
)
+@Preview(
+ device = WearDevices.RECT,
+ showSystemUi = true,
+ backgroundColor = 0xff000000,
+ showBackground = true,
+ group = "Devices - Rect"
+)
public annotation class WearPreviewDevices
@Preview(
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/AppLauncherScreen.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/AppLauncherScreen.kt
index 5a0ca150..c440b036 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/AppLauncherScreen.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/AppLauncherScreen.kt
@@ -8,7 +8,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -19,46 +20,46 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastAll
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavController
-import androidx.wear.compose.foundation.SwipeToDismissBoxState
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumnState
import androidx.wear.compose.foundation.lazy.items
-import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
-import androidx.wear.compose.material.Chip
-import androidx.wear.compose.material.ChipDefaults
-import androidx.wear.compose.material.CompactChip
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.PositionIndicator
-import androidx.wear.compose.material.Switch
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.TimeText
-import androidx.wear.compose.material.ToggleChip
-import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.compose.layout.ScalingLazyColumn
-import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
-import com.google.android.horologist.compose.layout.ScalingLazyColumnState
-import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
-import com.google.android.horologist.compose.layout.scrollAway
-import com.google.android.horologist.compose.material.ListHeaderDefaults.firstItemPadding
-import com.google.android.horologist.compose.material.ResponsiveListHeader
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.foundation.pager.rememberPagerState
+import androidx.wear.compose.material3.AnimatedPage
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.CompactButton
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.SwitchButton
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+import com.google.android.horologist.compose.layout.ColumnItemType
+import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding
import com.thewizrd.shared_resources.actions.ActionStatus
import com.thewizrd.shared_resources.helpers.WearConnectionStatus
import com.thewizrd.shared_resources.helpers.WearableHelper
+import com.thewizrd.shared_resources.utils.JSONParser
+import com.thewizrd.shared_resources.utils.getSerializableCompat
import com.thewizrd.simplewear.PhoneSyncActivity
import com.thewizrd.simplewear.R
import com.thewizrd.simplewear.controls.AppItemViewModel
import com.thewizrd.simplewear.ui.components.ConfirmationOverlay
+import com.thewizrd.simplewear.ui.components.HorizontalPagerScreen
import com.thewizrd.simplewear.ui.components.LoadingContent
-import com.thewizrd.simplewear.ui.components.SwipeToDismissPagerScreen
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
import com.thewizrd.simplewear.ui.theme.findActivity
import com.thewizrd.simplewear.viewmodels.AppLauncherUiState
import com.thewizrd.simplewear.viewmodels.AppLauncherViewModel
@@ -67,14 +68,9 @@ import com.thewizrd.simplewear.viewmodels.ConfirmationViewModel
import com.thewizrd.simplewear.viewmodels.WearableListenerViewModel
import kotlinx.coroutines.launch
-@OptIn(
- ExperimentalHorologistApi::class
-)
@Composable
fun AppLauncherScreen(
- modifier: Modifier = Modifier,
- navController: NavController,
- swipeToDismissBoxState: SwipeToDismissBoxState = rememberSwipeToDismissBoxState()
+ modifier: Modifier = Modifier
) {
val context = LocalContext.current
val activity = context.findActivity()
@@ -86,41 +82,26 @@ fun AppLauncherScreen(
val confirmationViewModel = viewModel()
val confirmationData by confirmationViewModel.confirmationEventsFlow.collectAsState()
- val scrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Unspecified,
- last = ScalingLazyColumnDefaults.ItemType.Chip,
- )
- )
-
- val isRoot = navController.previousBackStackEntry == null
-
val pagerState = rememberPagerState(
initialPage = 0,
pageCount = { 2 }
)
- SwipeToDismissPagerScreen(
+ HorizontalPagerScreen(
modifier = modifier,
- isRoot = isRoot,
- swipeToDismissBoxState = swipeToDismissBoxState,
- state = pagerState,
+ pagerState = pagerState,
hidePagerIndicator = uiState.isLoading,
- timeText = {
- if (pagerState.currentPage == 0) {
- TimeText(modifier = Modifier.scrollAway { scrollState })
- }
- }
) { pageIdx ->
- if (pageIdx == 0) {
- AppLauncherScreen(
- appLauncherViewModel = appLauncherViewModel,
- scrollState = scrollState
- )
- } else {
- AppLauncherSettings(
- appLauncherViewModel = appLauncherViewModel
- )
+ AnimatedPage(pageIdx, pagerState) {
+ if (pageIdx == 0) {
+ AppLauncherScreen(
+ appLauncherViewModel = appLauncherViewModel
+ )
+ } else {
+ AppLauncherSettings(
+ appLauncherViewModel = appLauncherViewModel
+ )
+ }
}
}
@@ -159,7 +140,7 @@ fun AppLauncherScreen(
WearConnectionStatus.APPNOTINSTALLED -> {
// Open store on remote device
- appLauncherViewModel.openPlayStore(activity)
+ appLauncherViewModel.openPlayStore()
// Navigate
activity.startActivity(
@@ -178,7 +159,10 @@ fun AppLauncherScreen(
WearableHelper.LaunchAppPath -> {
val status =
- event.data.getSerializable(WearableListenerViewModel.EXTRA_STATUS) as ActionStatus
+ event.data.getSerializableCompat(
+ WearableListenerViewModel.EXTRA_STATUS,
+ ActionStatus::class.java
+ )
when (status) {
ActionStatus.SUCCESS -> {
@@ -186,13 +170,9 @@ fun AppLauncherScreen(
}
ActionStatus.PERMISSION_DENIED -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_permissiondenied)
- )
- )
+ confirmationViewModel.showFailure(message = context.getString(R.string.error_permissiondenied_wear))
- appLauncherViewModel.openAppOnPhone(activity, false)
+ appLauncherViewModel.openAppOnPhone(false)
}
ActionStatus.FAILURE -> {
@@ -204,6 +184,15 @@ fun AppLauncherScreen(
else -> {}
}
}
+
+ WearableListenerViewModel.ACTION_SHOWCONFIRMATION -> {
+ val jsonData =
+ event.data.getString(WearableListenerViewModel.EXTRA_ACTIONDATA)
+
+ JSONParser.deserializer(jsonData, ConfirmationData::class.java)?.let {
+ confirmationViewModel.showConfirmation(it)
+ }
+ }
}
}
}
@@ -215,117 +204,134 @@ fun AppLauncherScreen(
}
}
-@OptIn(ExperimentalHorologistApi::class)
@Composable
private fun AppLauncherScreen(
- appLauncherViewModel: AppLauncherViewModel,
- scrollState: ScalingLazyColumnState
+ appLauncherViewModel: AppLauncherViewModel
) {
- val context = LocalContext.current
- val activity = context.findActivity()
-
val uiState by appLauncherViewModel.uiState.collectAsState()
AppLauncherScreen(
uiState = uiState,
- scrollState = scrollState,
onItemClicked = {
- appLauncherViewModel.openRemoteApp(activity, it)
+ appLauncherViewModel.openRemoteApp(it)
},
onRefresh = {
appLauncherViewModel.refreshApps()
}
)
+
+ LaunchedEffect(uiState.loadAppIcons) {
+ if (!uiState.isLoading && uiState.loadAppIcons && uiState.appsList.isNotEmpty() && uiState.appsList.fastAll { it.bitmapIcon == null }) {
+ appLauncherViewModel.refreshApps()
+ }
+ }
}
-@OptIn(ExperimentalHorologistApi::class)
@Composable
private fun AppLauncherScreen(
uiState: AppLauncherUiState,
- scrollState: ScalingLazyColumnState = rememberResponsiveColumnState(),
+ scrollState: TransformingLazyColumnState = rememberTransformingLazyColumnState(),
onItemClicked: (AppItemViewModel) -> Unit = {},
onRefresh: () -> Unit = {}
) {
- Box(
- modifier = Modifier.fillMaxSize()
- ) {
- LoadingContent(
- empty = uiState.appsList.isEmpty(),
- emptyContent = {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .wrapContentHeight(),
- contentAlignment = Alignment.Center
- ) {
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button
+ )
+
+ val transformationSpec = rememberTransformationSpec()
+
+ ScreenScaffold(
+ scrollState = scrollState,
+ contentPadding = contentPadding
+ ) { contentPadding ->
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ LoadingContent(
+ empty = uiState.appsList.isEmpty(),
+ emptyContent = {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentHeight(),
+ contentAlignment = Alignment.Center
) {
- Text(
- modifier = Modifier.padding(horizontal = 14.dp),
- text = stringResource(id = R.string.error_noapps),
- textAlign = TextAlign.Center
- )
- CompactChip(
- label = {
- Text(text = stringResource(id = R.string.action_refresh))
- },
- icon = {
- Icon(
- painter = painterResource(id = R.drawable.ic_baseline_refresh_24),
- contentDescription = stringResource(id = R.string.action_refresh)
- )
- },
- onClick = onRefresh
- )
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 14.dp),
+ text = stringResource(id = R.string.error_noapps),
+ textAlign = TextAlign.Center
+ )
+ CompactButton(
+ label = {
+ Text(text = stringResource(id = R.string.action_refresh))
+ },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.Refresh,
+ contentDescription = stringResource(id = R.string.action_refresh)
+ )
+ },
+ onClick = onRefresh
+ )
+ }
}
- }
- },
- loading = uiState.isLoading
- ) {
- ScalingLazyColumn(
- modifier = Modifier.fillMaxSize(),
- columnState = scrollState,
+ },
+ loading = uiState.isLoading
) {
- item {
- ResponsiveListHeader(contentPadding = firstItemPadding()) {
- Text(text = stringResource(id = R.string.action_apps))
+ TransformingLazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = scrollState,
+ contentPadding = contentPadding
+ ) {
+ item {
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec)
+ ) {
+ Text(text = stringResource(id = R.string.action_apps))
+ }
}
- }
- items(
- items = uiState.appsList,
- key = { Pair(it.activityName, it.packageName) }
- ) { appItem ->
- Chip(
- modifier = Modifier.fillMaxWidth(),
- label = {
- Text(text = appItem.appLabel ?: "")
- },
- icon = if (uiState.loadAppIcons) {
- appItem.bitmapIcon?.let {
- {
- Icon(
- modifier = Modifier.requiredSize(ChipDefaults.IconSize),
- bitmap = it.asImageBitmap(),
- contentDescription = appItem.appLabel,
- tint = Color.Unspecified
- )
+ items(
+ items = uiState.appsList,
+ key = { Pair(it.activityName, it.packageName) }
+ ) { appItem ->
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ label = {
+ Text(text = appItem.appLabel ?: "")
+ },
+ icon = if (uiState.loadAppIcons) {
+ appItem.bitmapIcon?.let {
+ {
+ Icon(
+ modifier = Modifier.requiredSize(ButtonDefaults.IconSize),
+ bitmap = it.asImageBitmap(),
+ contentDescription = appItem.appLabel,
+ tint = Color.Unspecified
+ )
+ }
}
+ } else {
+ null
+ },
+ onClick = {
+ onItemClicked(appItem)
}
- } else {
- null
- },
- colors = ChipDefaults.secondaryChipColors(),
- onClick = {
- onItemClicked(appItem)
- }
- )
+ )
+ }
}
}
-
- PositionIndicator(scalingLazyListState = scrollState.state)
}
}
}
@@ -344,47 +350,51 @@ private fun AppLauncherSettings(
)
}
-@OptIn(ExperimentalHorologistApi::class)
@Composable
private fun AppLauncherSettings(
uiState: AppLauncherUiState,
onCheckChanged: (Boolean) -> Unit = {}
) {
- val scrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Unspecified,
- last = ScalingLazyColumnDefaults.ItemType.Chip,
- )
- )
+ val columnState = rememberTransformingLazyColumnState()
- ScalingLazyColumn(
- columnState = scrollState
- ) {
- item {
- ResponsiveListHeader(
- modifier = Modifier.fillMaxWidth(),
- contentPadding = firstItemPadding()
- ) {
- Text(text = stringResource(id = R.string.title_settings))
- }
- }
- item {
- ToggleChip(
- modifier = Modifier.fillMaxWidth(),
- label = {
- Text(text = stringResource(id = R.string.pref_loadicons_title))
- },
- checked = uiState.loadAppIcons,
- onCheckedChange = onCheckChanged,
- toggleControl = {
- Switch(checked = uiState.loadAppIcons)
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
+ )
+ val transformationSpec = rememberTransformationSpec()
+
+ ScreenScaffold(
+ scrollState = columnState,
+ contentPadding = contentPadding
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ state = columnState,
+ contentPadding = contentPadding
+ ) {
+ item {
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec)
+ ) {
+ Text(text = stringResource(id = R.string.title_settings))
}
- )
+ }
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth(),
+ label = {
+ Text(text = stringResource(id = R.string.pref_loadicons_title))
+ },
+ checked = uiState.loadAppIcons,
+ onCheckedChange = onCheckChanged
+ )
+ }
}
}
}
-@OptIn(ExperimentalHorologistApi::class)
@WearPreviewDevices
@Composable
private fun PreviewAppLauncherScreen() {
@@ -408,7 +418,6 @@ private fun PreviewAppLauncherScreen() {
AppLauncherScreen(uiState = uiState)
}
-@OptIn(ExperimentalHorologistApi::class)
@WearPreviewDevices
@Composable
private fun PreviewLoadingAppLauncherScreen() {
@@ -426,7 +435,6 @@ private fun PreviewLoadingAppLauncherScreen() {
AppLauncherScreen(uiState = uiState)
}
-@OptIn(ExperimentalHorologistApi::class)
@WearPreviewDevices
@Composable
private fun PreviewNoContentAppLauncherScreen() {
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/CallManagerUi.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/CallManagerUi.kt
index f615a52b..9026a724 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/CallManagerUi.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/CallManagerUi.kt
@@ -1,32 +1,34 @@
package com.thewizrd.simplewear.ui.simplewear
import android.content.Intent
-import android.graphics.Bitmap
-import androidx.annotation.DrawableRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
-import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
-import androidx.compose.foundation.layout.FlowRowOverflow
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.requiredSizeIn
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.VolumeUp
+import androidx.compose.material.icons.rounded.Call
+import androidx.compose.material.icons.rounded.CallEnd
+import androidx.compose.material.icons.rounded.Dialpad
+import androidx.compose.material.icons.rounded.MicOff
+import androidx.compose.material.icons.rounded.MoreHoriz
+import androidx.compose.material.icons.rounded.SpeakerPhone
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -38,56 +40,83 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
-import androidx.wear.compose.material.Button
-import androidx.wear.compose.material.ButtonDefaults
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.Scaffold
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.TimeText
-import androidx.wear.compose.material.Vignette
-import androidx.wear.compose.material.VignettePosition
-import androidx.wear.compose.material.dialog.Dialog
-import androidx.wear.compose.material.ripple
-import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.foundation.requestFocusOnHierarchyActive
+import androidx.wear.compose.foundation.rotary.rotaryScrollable
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.Dialog
+import androidx.wear.compose.material3.FilledIconButton
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.IconButtonDefaults
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ListHeaderDefaults
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TimeText
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+import androidx.wear.compose.material3.ripple
+import androidx.wear.compose.material3.touchTargetAwareSize
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
+import com.google.android.horologist.audio.ui.material3.VolumeLevelIndicator
+import com.google.android.horologist.audio.ui.material3.volumeRotaryBehavior
+import com.google.android.horologist.compose.layout.ColumnItemType
+import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding
import com.thewizrd.shared_resources.actions.ActionStatus
import com.thewizrd.shared_resources.actions.Actions
import com.thewizrd.shared_resources.actions.AudioStreamType
import com.thewizrd.shared_resources.helpers.InCallUIHelper
import com.thewizrd.shared_resources.helpers.WearConnectionStatus
+import com.thewizrd.shared_resources.helpers.WearableHelper
+import com.thewizrd.shared_resources.utils.JSONParser
+import com.thewizrd.shared_resources.utils.getSerializableCompat
import com.thewizrd.simplewear.PhoneSyncActivity
import com.thewizrd.simplewear.R
import com.thewizrd.simplewear.ui.components.ConfirmationOverlay
+import com.thewizrd.simplewear.ui.components.ElapsedTimeSource
import com.thewizrd.simplewear.ui.components.LoadingContent
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
import com.thewizrd.simplewear.ui.navigation.Screen
-import com.thewizrd.simplewear.ui.theme.activityViewModel
import com.thewizrd.simplewear.ui.theme.findActivity
+import com.thewizrd.simplewear.ui.utils.rememberFocusRequester
import com.thewizrd.simplewear.viewmodels.CallManagerUiState
import com.thewizrd.simplewear.viewmodels.CallManagerViewModel
+import com.thewizrd.simplewear.viewmodels.CallUiState
import com.thewizrd.simplewear.viewmodels.ConfirmationData
import com.thewizrd.simplewear.viewmodels.ConfirmationViewModel
+import com.thewizrd.simplewear.viewmodels.ValueActionViewModel
+import com.thewizrd.simplewear.viewmodels.ValueActionVolumeViewModel
import com.thewizrd.simplewear.viewmodels.WearableListenerViewModel
import kotlinx.coroutines.launch
+import java.util.concurrent.TimeUnit
+import kotlin.math.sqrt
+import kotlin.random.Random
@Composable
fun CallManagerUi(
@@ -98,28 +127,40 @@ fun CallManagerUi(
val activity = context.findActivity()
val lifecycleOwner = LocalLifecycleOwner.current
- val callManagerViewModel = activityViewModel()
+ val callManagerViewModel = viewModel()
val uiState by callManagerViewModel.uiState.collectAsState()
+ val valueActionViewModel = viewModel()
+ val volumeViewModel = remember(context, valueActionViewModel) {
+ ValueActionVolumeViewModel(context, valueActionViewModel)
+ }
+ val volumeUiState by volumeViewModel.volumeUiState.collectAsState()
+
val confirmationViewModel = viewModel()
val confirmationData by confirmationViewModel.confirmationEventsFlow.collectAsState()
- Scaffold(
- modifier = modifier.background(MaterialTheme.colors.background),
- vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
- timeText = {
- if (!uiState.isLoading) TimeText()
- },
- ) {
+ ScreenScaffold(
+ modifier = modifier,
+ scrollIndicator = {
+ VolumeLevelIndicator(
+ volumeUiState = { volumeUiState },
+ displayIndicatorEvents = volumeViewModel.displayIndicatorEvents
+ )
+ }
+ ) { contentPadding ->
LoadingContent(
empty = !uiState.isCallActive,
emptyContent = {
- NoCallActiveScreen()
+ NoCallActiveScreen(
+ modifier = Modifier.padding(contentPadding)
+ )
},
loading = uiState.isLoading
) {
CallManagerUi(
+ modifier = Modifier.padding(contentPadding),
callManagerViewModel = callManagerViewModel,
+ volumeViewModel = volumeViewModel,
navController = navController
)
}
@@ -156,7 +197,7 @@ fun CallManagerUi(
WearConnectionStatus.APPNOTINSTALLED -> {
// Open store on remote device
- callManagerViewModel.openPlayStore(activity)
+ callManagerViewModel.openPlayStore()
// Navigate
activity.startActivity(
@@ -174,16 +215,50 @@ fun CallManagerUi(
InCallUIHelper.ConnectPath -> {
val status =
- event.data.getSerializable(WearableListenerViewModel.EXTRA_STATUS) as ActionStatus
+ event.data.getSerializableCompat(
+ WearableListenerViewModel.EXTRA_STATUS,
+ ActionStatus::class.java
+ )
if (status == ActionStatus.PERMISSION_DENIED) {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_permissiondenied)
+ confirmationViewModel.showOpenOnPhoneForFailure(
+ message = context.getString(
+ R.string.error_permissiondenied_wear
)
)
- callManagerViewModel.openAppOnPhone(activity, false)
+ callManagerViewModel.openAppOnPhone(false)
+ }
+ }
+
+ WearableHelper.AudioVolumePath, WearableHelper.ValueStatusSetPath -> {
+ val status =
+ event.data.getSerializableCompat(
+ WearableListenerViewModel.EXTRA_STATUS,
+ ActionStatus::class.java
+ )
+
+ when (status) {
+ ActionStatus.UNKNOWN, ActionStatus.FAILURE -> {
+ confirmationViewModel.showFailure(message = context.getString(R.string.error_actionfailed))
+ }
+
+ ActionStatus.PERMISSION_DENIED -> {
+ confirmationViewModel.showFailure(message = context.getString(R.string.error_permissiondenied_wear))
+
+ valueActionViewModel.openAppOnPhone(false)
+ }
+
+ else -> {}
+ }
+ }
+
+ WearableListenerViewModel.ACTION_SHOWCONFIRMATION -> {
+ val jsonData =
+ event.data.getString(WearableListenerViewModel.EXTRA_ACTIONDATA)
+
+ JSONParser.deserializer(jsonData, ConfirmationData::class.java)?.let {
+ confirmationViewModel.showConfirmation(it)
}
}
}
@@ -191,61 +266,92 @@ fun CallManagerUi(
}
}
- LaunchedEffect(Unit) {
+ LaunchedEffect(lifecycleOwner) {
// Update statuses
+ valueActionViewModel.onActionUpdated(Actions.VOLUME, AudioStreamType.VOICE_CALL)
+
callManagerViewModel.refreshCallState()
+ valueActionViewModel.refreshState()
}
}
@Composable
fun CallManagerUi(
+ modifier: Modifier = Modifier,
callManagerViewModel: CallManagerViewModel,
+ volumeViewModel: ValueActionVolumeViewModel,
navController: NavController
) {
- val context = LocalContext.current
- val activity = context.findActivity()
-
val uiState by callManagerViewModel.uiState.collectAsState()
+ val volumeUiState by volumeViewModel.volumeUiState.collectAsState()
var showKeyPadUi by remember { mutableStateOf(false) }
- CallManagerUi(
- uiState = uiState,
- onShowKeypadUi = {
- showKeyPadUi = true
- },
- onMute = {
- callManagerViewModel.setMuteEnabled(!uiState.isMuted)
- },
- onSpeakerPhone = {
- callManagerViewModel.enableSpeakerphone(!uiState.isSpeakerPhoneOn)
- },
- onVolume = {
- navController.navigate(
- Screen.ValueAction.getRoute(Actions.VOLUME, AudioStreamType.VOICE_CALL)
- )
- },
- onEndCall = {
- callManagerViewModel.endCall()
- }
- )
-
- Dialog(
- modifier = Modifier.fillMaxSize(),
- showDialog = showKeyPadUi,
- onDismissRequest = { showKeyPadUi = false }
- ) {
- KeypadScreen(
- onKeyPressed = { digit ->
- callManagerViewModel.requestSendDTMFTone(digit)
+ if (uiState.callUiState == CallUiState.INCOMING) {
+ IncomingCallUi(
+ modifier = modifier,
+ uiState = uiState,
+ onVolume = {
+ navController.navigate(
+ Screen.ValueAction.getRoute(Actions.VOLUME, AudioStreamType.VOICE_CALL)
+ )
+ },
+ onAnswerCall = {
+ callManagerViewModel.answerCall()
+ },
+ onEndCall = {
+ callManagerViewModel.endCall()
}
)
+ } else {
+ CallManagerUi(
+ modifier = modifier
+ .requestFocusOnHierarchyActive()
+ .rotaryScrollable(
+ focusRequester = rememberFocusRequester(),
+ behavior = volumeRotaryBehavior(
+ volumeUiStateProvider = { volumeUiState },
+ onRotaryVolumeInput = { newValue -> volumeViewModel.setVolume(newValue) }
+ )
+ ),
+ uiState = uiState,
+ onShowKeypadUi = {
+ showKeyPadUi = true
+ },
+ onMute = {
+ callManagerViewModel.setMuteEnabled(!uiState.isMuted)
+ },
+ onSpeakerPhone = {
+ callManagerViewModel.enableSpeakerphone(!uiState.isSpeakerPhoneOn)
+ },
+ onVolume = {
+ navController.navigate(
+ Screen.ValueAction.getRoute(Actions.VOLUME, AudioStreamType.VOICE_CALL)
+ )
+ },
+ onEndCall = {
+ callManagerViewModel.endCall()
+ }
+ )
+
+ Dialog(
+ modifier = Modifier.fillMaxSize(),
+ visible = showKeyPadUi,
+ onDismissRequest = { showKeyPadUi = false }
+ ) {
+ KeypadScreen(
+ onKeyPressed = { digit ->
+ callManagerViewModel.requestSendDTMFTone(digit)
+ }
+ )
+ }
}
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable
private fun CallManagerUi(
+ modifier: Modifier = Modifier,
uiState: CallManagerUiState,
onMute: () -> Unit = {},
onShowKeypadUi: () -> Unit = {},
@@ -256,55 +362,104 @@ private fun CallManagerUi(
val isPreview = LocalInspectionMode.current
val isRound = LocalConfiguration.current.isScreenRound
- Box(
- modifier = Modifier.fillMaxSize()
- ) {
- if (isPreview) {
- TimeText()
- }
+ val isLargeHeight = LocalConfiguration.current.screenHeightDp >= 225
+ val isLargeWidth = LocalConfiguration.current.screenWidthDp >= 225
+
+ val buttonSize = if (isLargeWidth || isLargeHeight) {
+ IconButtonDefaults.SmallButtonSize
+ } else {
+ 40.dp
+ }
+
+ val buttonRowPadding = if (isRound) 16.dp else 8.dp
+ var showMenuDialog by remember { mutableStateOf(false) }
+
+ Box(modifier = modifier.fillMaxSize()) {
if (uiState.callerBitmap != null) {
+ val colorScheme = MaterialTheme.colorScheme
Image(
- modifier = Modifier.fillMaxSize(),
bitmap = uiState.callerBitmap.asImageBitmap(),
- contentDescription = stringResource(R.string.desc_contact_photo)
+ contentDescription = stringResource(R.string.desc_contact_photo),
+ contentScale = ContentScale.Crop,
+ alpha = 0.6f,
+ modifier =
+ modifier
+ .fillMaxSize(0.65f)
+ .align(Alignment.Center)
+ .drawWithCache {
+ val gradientBrush =
+ Brush.radialGradient(
+ 0.95f to Color.Transparent,
+ 1f to colorScheme.background,
+ )
+ onDrawWithContent {
+ drawRect(colorScheme.background)
+ drawContent()
+ drawRect(color = colorScheme.primaryContainer, alpha = 0.05f)
+ drawRect(color = colorScheme.onPrimary, alpha = 0.35f)
+ drawRect(gradientBrush)
+ }
+ },
)
}
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(5.dp)
- ) {
- Spacer(modifier = Modifier.height(16.dp))
+ if (isPreview) {
+ TimeText()
+ }
+ Column(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.weight(1f, fill = true)
.fillMaxWidth()
- .padding(horizontal = if (isRound) 32.dp else 8.dp),
- contentAlignment = Alignment.Center
+ .padding(ListHeaderDefaults.ContentPadding),
+ contentAlignment = Alignment.BottomCenter
) {
Text(
modifier = Modifier
.wrapContentHeight()
.basicMarquee(iterations = Int.MAX_VALUE),
text = uiState.callerName ?: stringResource(id = R.string.message_callactive),
+ style = if (isLargeWidth || isLargeHeight) {
+ MaterialTheme.typography.labelLarge
+ } else {
+ MaterialTheme.typography.labelMedium
+ },
maxLines = 1,
overflow = TextOverflow.Visible,
textAlign = TextAlign.Center
)
}
- FlowRow(
+ if (uiState.callStartTime > -1L) {
+ val timerSource = remember(uiState.callStartTime) {
+ ElapsedTimeSource(uiState.callStartTime)
+ }
+
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = timerSource.currentTime(),
+ style = if (isLargeWidth || isLargeHeight) {
+ MaterialTheme.typography.bodyMedium
+ } else {
+ MaterialTheme.typography.bodySmall
+ },
+ textAlign = TextAlign.Center
+ )
+ }
+
+ Row(
modifier = Modifier
- .fillMaxWidth(),
- maxItemsInEachRow = 2,
- horizontalArrangement = Arrangement.SpaceEvenly,
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ .fillMaxWidth()
+ .padding(start = buttonRowPadding, end = buttonRowPadding, top = 8.dp)
+ .weight(1f, fill = true),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.Top
) {
CallUiButton(
- iconResourceId = R.drawable.ic_mic_off_24dp,
+ imageVector = Icons.Rounded.MicOff,
+ buttonSize = buttonSize,
isChecked = uiState.isMuted,
onClick = onMute,
contentDescription = if (uiState.isMuted) {
@@ -313,49 +468,307 @@ private fun CallManagerUi(
stringResource(R.string.label_mute)
}
)
- if (uiState.canSendDTMFKeys) {
+
+ if (uiState.canSendDTMFKeys || uiState.supportsSpeaker) {
+ CallUiButton(
+ imageVector = Icons.Rounded.MoreHoriz,
+ buttonSize = buttonSize,
+ onClick = { showMenuDialog = true },
+ contentDescription = stringResource(R.string.action_volume)
+ )
+ } else {
CallUiButton(
- iconResourceId = R.drawable.ic_dialpad_24dp,
- onClick = onShowKeypadUi,
- contentDescription = stringResource(R.string.label_keypad)
+ imageVector = Icons.AutoMirrored.Rounded.VolumeUp,
+ buttonSize = buttonSize,
+ onClick = onVolume,
+ contentDescription = stringResource(R.string.action_volume)
)
}
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = if (buttonSize > 40.dp) 4.dp else 0.dp),
+ ) {
+ FilledIconButton(
+ modifier = Modifier.touchTargetAwareSize(buttonSize),
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer
+ ),
+ onClick = onEndCall
+ ) {
+ Icon(
+ modifier = Modifier.size(IconButtonDefaults.iconSizeFor(buttonSize)),
+ imageVector = Icons.Rounded.CallEnd,
+ contentDescription = stringResource(id = R.string.action_hangup)
+ )
+ }
+ }
+ }
+
+ Dialog(
+ visible = showMenuDialog,
+ onDismissRequest = { showMenuDialog = false }
+ ) {
+ val columnState = rememberTransformingLazyColumnState()
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
+ )
+ val transformationSpec = rememberTransformationSpec()
+
+ ScreenScaffold(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = contentPadding
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = columnState,
+ contentPadding = contentPadding
+ ) {
+ item {
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec)
+ ) {
+ Text(text = stringResource(R.string.title_callcontroller))
+ }
+ }
+
+ if (uiState.canSendDTMFKeys) {
+ item {
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ label = {
+ Text(text = stringResource(R.string.label_keypad))
+ },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.Dialpad,
+ contentDescription = stringResource(R.string.label_keypad)
+ )
+ },
+ onClick = {
+ onShowKeypadUi()
+ showMenuDialog = false
+ }
+ )
+ }
+ }
+
if (uiState.supportsSpeaker) {
- CallUiButton(
- iconResourceId = R.drawable.ic_baseline_speaker_phone_24,
- isChecked = uiState.isSpeakerPhoneOn,
- onClick = onSpeakerPhone,
- contentDescription = if (uiState.isSpeakerPhoneOn) {
- stringResource(R.string.desc_speakerphone_on)
- } else {
- stringResource(R.string.desc_speakerphone_off)
+ item {
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ label = {
+ Text(
+ text = if (uiState.isSpeakerPhoneOn) {
+ stringResource(R.string.desc_speakerphone_on)
+ } else {
+ stringResource(R.string.desc_speakerphone_off)
+ }
+ )
+ },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.SpeakerPhone,
+ contentDescription = if (uiState.isSpeakerPhoneOn) {
+ stringResource(R.string.desc_speakerphone_on)
+ } else {
+ stringResource(R.string.desc_speakerphone_off)
+ }
+ )
+ },
+ colors = if (uiState.isSpeakerPhoneOn) {
+ ButtonDefaults.buttonColors()
+ } else {
+ ButtonDefaults.filledTonalButtonColors()
+ },
+ onClick = onSpeakerPhone
+ )
+ }
+ }
+
+ item {
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ label = {
+ Text(text = stringResource(R.string.action_volume))
+ },
+ icon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.VolumeUp,
+ contentDescription = stringResource(R.string.action_volume)
+ )
+ },
+ onClick = {
+ onVolume()
+ showMenuDialog = false
}
)
}
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
+@Composable
+private fun IncomingCallUi(
+ modifier: Modifier = Modifier,
+ uiState: CallManagerUiState,
+ onVolume: () -> Unit = {},
+ onAnswerCall: () -> Unit = {},
+ onEndCall: () -> Unit = {}
+) {
+ val isPreview = LocalInspectionMode.current
+ val isRound = LocalConfiguration.current.isScreenRound
+
+ val isLargeHeight = LocalConfiguration.current.screenHeightDp >= 225
+ val isLargeWidth = LocalConfiguration.current.screenWidthDp >= 225
+
+ val buttonSize = if (isLargeWidth || isLargeHeight) {
+ IconButtonDefaults.SmallButtonSize
+ } else {
+ 40.dp
+ }
+
+ val buttonRowPadding = if (isRound) 16.dp else 8.dp
+
+ Box(modifier = modifier.fillMaxSize()) {
+ if (uiState.callerBitmap != null) {
+ val colorScheme = MaterialTheme.colorScheme
+ Image(
+ bitmap = uiState.callerBitmap.asImageBitmap(),
+ contentDescription = stringResource(R.string.desc_contact_photo),
+ contentScale = ContentScale.Crop,
+ alpha = 0.6f,
+ modifier =
+ modifier
+ .fillMaxSize(0.65f)
+ .align(Alignment.Center)
+ .drawWithCache {
+ val gradientBrush =
+ Brush.radialGradient(
+ 0.95f to Color.Transparent,
+ 1f to colorScheme.background,
+ )
+ onDrawWithContent {
+ drawRect(colorScheme.background)
+ drawContent()
+ drawRect(color = colorScheme.primaryContainer, alpha = 0.05f)
+ drawRect(color = colorScheme.onPrimary, alpha = 0.35f)
+ drawRect(gradientBrush)
+ }
+ },
+ )
+ }
+
+ if (isPreview) {
+ TimeText()
+ }
+
+ Column(modifier = Modifier.fillMaxSize()) {
+ Box(
+ modifier = Modifier
+ .weight(1f, fill = true)
+ .fillMaxWidth()
+ .padding(ListHeaderDefaults.ContentPadding),
+ contentAlignment = Alignment.BottomCenter
+ ) {
+ Text(
+ modifier = Modifier
+ .wrapContentHeight()
+ .basicMarquee(iterations = Int.MAX_VALUE),
+ text = uiState.callerName ?: stringResource(id = R.string.message_callactive),
+ style = if (isLargeWidth || isLargeHeight) {
+ MaterialTheme.typography.labelLarge
+ } else {
+ MaterialTheme.typography.labelMedium
+ },
+ maxLines = 1,
+ overflow = TextOverflow.Visible,
+ textAlign = TextAlign.Center
+ )
+ }
+
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.call_notification_incoming_text),
+ style = if (isLargeWidth || isLargeHeight) {
+ MaterialTheme.typography.bodySmall
+ } else {
+ MaterialTheme.typography.bodyExtraSmall
+ },
+ textAlign = TextAlign.Center
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = buttonRowPadding, end = buttonRowPadding, top = 8.dp)
+ .weight(1f, fill = true),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.Top
+ ) {
+ FilledIconButton(
+ modifier = Modifier.touchTargetAwareSize(buttonSize),
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer
+ ),
+ onClick = onEndCall
+ ) {
+ Icon(
+ modifier = Modifier.size(IconButtonDefaults.iconSizeFor(buttonSize)),
+ imageVector = Icons.Rounded.CallEnd,
+ contentDescription = stringResource(id = R.string.action_hangup)
+ )
+ }
+
CallUiButton(
- iconResourceId = R.drawable.ic_volume_up_white_24dp,
+ isEnabled = false,
+ imageVector = Icons.AutoMirrored.Rounded.VolumeUp,
+ buttonSize = buttonSize,
onClick = onVolume,
contentDescription = stringResource(R.string.action_volume)
)
}
+ }
- Button(
- modifier = Modifier
- .requiredSize(40.dp)
- .align(Alignment.CenterHorizontally),
- colors = ButtonDefaults.primaryButtonColors(
- backgroundColor = colorResource(id = android.R.color.holo_red_dark),
+ Box(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = if (buttonSize > 40.dp) 4.dp else 0.dp),
+ ) {
+ FilledIconButton(
+ modifier = Modifier.touchTargetAwareSize(buttonSize),
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = Color(0xFF1E8D41),
contentColor = Color.White
),
- onClick = onEndCall
+ onClick = onAnswerCall
) {
Icon(
- painter = painterResource(id = R.drawable.ic_call_end_24dp),
- contentDescription = stringResource(id = R.string.action_hangup)
+ modifier = Modifier.size(IconButtonDefaults.iconSizeFor(buttonSize)),
+ imageVector = Icons.Rounded.Call,
+ contentDescription = stringResource(id = R.string.call_notification_answer_action)
)
}
-
- Spacer(modifier = Modifier.height(4.dp))
}
}
}
@@ -363,49 +776,38 @@ private fun CallManagerUi(
@Composable
private fun CallUiButton(
modifier: Modifier = Modifier,
+ buttonSize: Dp = IconButtonDefaults.DefaultButtonSize,
+ isEnabled: Boolean = true,
isChecked: Boolean = false,
- @DrawableRes iconResourceId: Int,
+ imageVector: ImageVector,
contentDescription: String?,
onClick: () -> Unit = {}
) {
- Box(
- modifier = modifier
- .requiredSizeIn(40.dp, 40.dp)
- .clickable(
- onClick = onClick,
- role = Role.Button,
- interactionSource = remember { MutableInteractionSource() },
- indication = ripple(
- color = MaterialTheme.colors.onSurface,
- radius = 20.dp
- )
- )
- .border(
- width = 1.dp,
- brush = SolidColor(if (isChecked) Color.White else Color.Transparent),
- shape = MaterialTheme.shapes.small
- ),
- contentAlignment = Alignment.Center,
- ) {
- Box(
- modifier = Modifier.padding(
- horizontal = 12.dp
- )
- ) {
- Icon(
- modifier = Modifier.requiredSize(24.dp),
- painter = painterResource(id = iconResourceId),
- contentDescription = contentDescription
- )
+ FilledIconButton(
+ modifier = modifier.touchTargetAwareSize(buttonSize),
+ onClick = onClick,
+ enabled = isEnabled,
+ colors = if (isChecked) {
+ IconButtonDefaults.filledIconButtonColors()
+ } else {
+ IconButtonDefaults.filledTonalIconButtonColors()
}
+ ) {
+ Icon(
+ modifier = Modifier.requiredSize(IconButtonDefaults.iconSizeFor(buttonSize)),
+ imageVector = imageVector,
+ contentDescription = contentDescription
+ )
}
}
@WearPreviewDevices
@Composable
-private fun NoCallActiveScreen() {
+private fun NoCallActiveScreen(
+ modifier: Modifier = Modifier
+) {
Box(
- modifier = Modifier.fillMaxSize(),
+ modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
@@ -423,75 +825,108 @@ private fun NoCallActiveScreen() {
private fun KeypadScreen(
onKeyPressed: (Char) -> Unit = {}
) {
+ val config = LocalConfiguration.current
+
val isPreview = LocalInspectionMode.current
- val context = LocalContext.current
- val isRound = LocalConfiguration.current.isScreenRound
- val screenHeightDp = LocalConfiguration.current.screenHeightDp
+ val isRound = config.isScreenRound
+ val isLargeWidth = config.screenWidthDp >= 225
+
+ val headerPadding: PaddingValues = remember(config) {
+ if (isRound) {
+ val screenHeightDp = config.screenHeightDp
+ val screenWidthDp = config.smallestScreenWidthDp
+ val maxSquareEdge = (sqrt(((screenHeightDp * screenWidthDp) / 2).toDouble()))
+ val inset = Dp(((screenHeightDp - maxSquareEdge) / 2).toFloat())
+ PaddingValues(
+ start = inset, top = inset, end = inset,
+ bottom = ListHeaderDefaults.ContentPadding.calculateBottomPadding()
+ )
+ } else {
+ ListHeaderDefaults.ContentPadding
+ }
+ }
var keypadText by remember { mutableStateOf("") }
val digits by remember {
- derivedStateOf { listOf('1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#') }
+ derivedStateOf {
+ listOf(
+ '1', '2', '3',
+ '4', '5', '6',
+ '7', '8', '9',
+ '*', '0', '#'
+ )
+ }
}
Column(
- modifier = Modifier.fillMaxSize()
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- Box(
+ Row(
modifier = Modifier
.fillMaxWidth()
- .fillMaxHeight(0.2f)
- .background(Color(0xFF444444))
- .padding(
- start = if (isRound) 48.dp else 8.dp,
- end = if (isRound) 48.dp else 8.dp,
- bottom = 4.dp
- )
- .clipToBounds(),
- contentAlignment = Alignment.BottomCenter
+ .padding(headerPadding)
) {
- Text(
- modifier = Modifier.wrapContentWidth(
- align = Alignment.End,
- unbounded = true
- ),
- text = if (isPreview) "01234567891110" else keypadText,
- fontWeight = FontWeight.Light,
- fontSize = 18.sp,
- maxLines = 1,
- textAlign = TextAlign.Center,
- overflow = TextOverflow.Visible
- )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clipToBounds(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ modifier = Modifier.wrapContentWidth(align = Alignment.End, unbounded = true),
+ text = if (isPreview) "01234567891110123" else keypadText,
+ style = MaterialTheme.typography.bodyMedium,
+ letterSpacing = 1.5.sp,
+ fontSize = if (isLargeWidth) 14.sp else 12.sp,
+ maxLines = 1,
+ textAlign = TextAlign.Center,
+ overflow = TextOverflow.Visible,
+ softWrap = true
+ )
+ }
}
- BoxWithConstraints {
+ Row(
+ modifier = Modifier.fillMaxWidth(0.65f)
+ ) {
FlowRow(
modifier = Modifier
.fillMaxSize()
.padding(
- start = if (isRound) 32.dp else 8.dp,
- end = if (isRound) 32.dp else 8.dp,
- bottom = if (isRound) 32.dp else 8.dp
+ bottom = 8.dp
),
maxItemsInEachRow = 3,
- horizontalArrangement = Arrangement.Center,
- verticalArrangement = Arrangement.Center,
- overflow = FlowRowOverflow.Visible
+ maxLines = 4,
+ verticalArrangement = Arrangement.SpaceBetween,
+ horizontalArrangement = Arrangement.SpaceBetween
) {
digits.forEach {
Box(
modifier = Modifier
+ .requiredHeightIn(max = 32.dp)
.weight(1f, fill = true)
- .height((this@BoxWithConstraints.maxHeight - if (isRound) 32.dp else 8.dp) / 4)
- .clickable {
+ .clickable(
+ role = Role.Button,
+ interactionSource = remember { MutableInteractionSource() },
+ indication = ripple(
+ color = MaterialTheme.colorScheme.onSurface,
+ radius = 20.dp,
+ bounded = false
+ )
+ ) {
keypadText += it
onKeyPressed.invoke(it)
},
contentAlignment = Alignment.Center
) {
Text(
- text = it + "",
+ modifier = Modifier
+ .fillMaxSize()
+ .align(Alignment.Center),
+ text = "$it",
maxLines = 1,
textAlign = TextAlign.Center,
- fontSize = 16.sp
+ style = MaterialTheme.typography.labelLarge
)
}
}
@@ -504,16 +939,25 @@ private fun KeypadScreen(
@WearPreviewFontScales
@Composable
private fun PreviewCallManagerUi() {
- val bmp = remember {
- Bitmap.createBitmap(intArrayOf(0x50400080), 1, 1, Bitmap.Config.ARGB_8888)
+ val context = LocalContext.current
+
+ val background = remember(context) {
+ ContextCompat.getDrawable(context, R.drawable.sample_image)?.toBitmap()
}
val uiState = remember {
CallManagerUiState(
connectionStatus = WearConnectionStatus.CONNECTED,
- callerBitmap = bmp,
+ callerName = if (Random.nextInt(0, 2) == 1) {
+ "(123) 456-7890"
+ } else {
+ null
+ },
+ callerBitmap = background,
+ callStartTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60),
isSpeakerPhoneOn = true,
isCallActive = true,
+ callUiState = CallUiState.ONGOING,
isMuted = true,
supportsSpeaker = true,
canSendDTMFKeys = true
@@ -521,4 +965,36 @@ private fun PreviewCallManagerUi() {
}
CallManagerUi(uiState = uiState)
-}
\ No newline at end of file
+}
+
+@WearPreviewDevices
+@WearPreviewFontScales
+@Composable
+private fun PreviewIncomingCallUi() {
+ val context = LocalContext.current
+
+ val background = remember(context) {
+ ContextCompat.getDrawable(context, R.drawable.sample_image)?.toBitmap()
+ }
+
+ val uiState = remember {
+ CallManagerUiState(
+ connectionStatus = WearConnectionStatus.CONNECTED,
+ callerName = if (Random.nextInt(0, 2) == 1) {
+ "(123) 456-7890"
+ } else {
+ null
+ },
+ callerBitmap = background,
+ callStartTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60),
+ isSpeakerPhoneOn = true,
+ isCallActive = true,
+ callUiState = CallUiState.INCOMING,
+ isMuted = true,
+ supportsSpeaker = true,
+ canSendDTMFKeys = true
+ )
+ }
+
+ IncomingCallUi(uiState = uiState)
+}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/Dashboard.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/Dashboard.kt
index 3d6fdc17..b305899c 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/Dashboard.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/Dashboard.kt
@@ -12,14 +12,12 @@ import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.rounded.Info
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -41,22 +39,14 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.withStarted
import androidx.navigation.NavController
import androidx.preference.PreferenceManager
-import androidx.wear.compose.material.ChipDefaults
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.PositionIndicator
-import androidx.wear.compose.material.Scaffold
-import androidx.wear.compose.material.TimeText
-import androidx.wear.compose.material.Vignette
-import androidx.wear.compose.material.VignettePosition
-import androidx.wear.compose.material.dialog.Dialog
+import androidx.wear.compose.material3.AlertDialog
+import androidx.wear.compose.material3.AlertDialogDefaults
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.Text
import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
-import com.google.android.horologist.compose.layout.rememberColumnState
-import com.google.android.horologist.compose.layout.scrollAway
-import com.google.android.horologist.compose.material.AlertContent
-import com.google.android.horologist.compose.material.AlertDialog
-import com.google.android.horologist.compose.material.Chip
import com.thewizrd.shared_resources.actions.Action
import com.thewizrd.shared_resources.actions.ActionStatus
import com.thewizrd.shared_resources.actions.Actions
@@ -107,12 +97,10 @@ fun Dashboard(
var showUpdateDialog by remember { mutableStateOf(false) }
var showAppUpdateConfirmation by remember { mutableStateOf(false) }
- Scaffold(
- modifier = modifier.background(MaterialTheme.colors.background),
- timeText = { TimeText(modifier = Modifier.scrollAway { scrollState }) },
- vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
- positionIndicator = { PositionIndicator(scrollState = scrollState) }
- ) {
+ ScreenScaffold(
+ modifier = modifier,
+ scrollState = scrollState
+ ) { contentPadding ->
DashboardScreen(
dashboardViewModel = dashboardViewModel,
scrollState = scrollState,
@@ -121,26 +109,30 @@ fun Dashboard(
}
AlertDialog(
- showDialog = showUpdateDialog,
- onDismiss = {
+ visible = showUpdateDialog,
+ onDismissRequest = {
Settings.setLastUpdateCheckTime(Instant.now())
showUpdateDialog = false
},
icon = {
Icon(
- painter = rememberVectorPainter(image = Icons.Default.Info),
+ painter = rememberVectorPainter(image = Icons.Rounded.Info),
contentDescription = null
)
},
- message = stringResource(id = R.string.message_wearappupdate_available)
+ title = {},
+ text = {
+ Text(text = stringResource(id = R.string.message_wearappupdate_available))
+ }
) {
item {
Spacer(modifier = Modifier.height(12.dp))
}
item {
- Chip(
- modifier = Modifier.padding(horizontal = 16.dp),
- label = stringResource(id = R.string.action_update),
+ Button(
+ label = {
+ Text(text = stringResource(id = R.string.action_update))
+ },
onClick = {
runCatching {
// Open store on device
@@ -156,57 +148,55 @@ fun Dashboard(
}
if (inAppUpdateMgr.updatePriority <= 3) {
item {
- Chip(
- label = stringResource(id = android.R.string.cancel),
+ FilledTonalButton(
+ label = {
+ Text(text = stringResource(id = android.R.string.cancel))
+ },
onClick = {
Settings.setLastUpdateCheckTime(Instant.now())
showUpdateDialog = false
- },
- colors = ChipDefaults.secondaryChipColors()
+ }
)
}
}
}
- if (showAppUpdateConfirmation) {
- var startAnim by remember { mutableStateOf(false) }
- val dialogScrollState = rememberColumnState(
- ScalingLazyColumnDefaults.responsive(),
- )
+ AlertDialog(
+ visible = showAppUpdateConfirmation,
+ onDismissRequest = {
+ Settings.setLastUpdateCheckTime(Instant.now())
+ showAppUpdateConfirmation = false
+ },
+ icon = {
+ var startAnim by remember { mutableStateOf(false) }
- Dialog(
- showDialog = showAppUpdateConfirmation,
- onDismissRequest = {
- Settings.setLastUpdateCheckTime(Instant.now())
- showAppUpdateConfirmation = false
- },
- scrollState = dialogScrollState.state
- ) {
- AlertContent(
- icon = {
- Icon(
- modifier = Modifier.size(36.dp),
- painter = rememberAnimatedVectorPainter(
- animatedImageVector = AnimatedImageVector.animatedVectorResource(id = R.drawable.open_on_phone_animation),
- atEnd = startAnim
- ),
- contentDescription = null
- )
- },
- message = stringResource(id = R.string.message_phoneappupdate_available),
- onOk = {
+ Icon(
+ modifier = Modifier.size(36.dp),
+ painter = rememberAnimatedVectorPainter(
+ animatedImageVector = AnimatedImageVector.animatedVectorResource(id = R.drawable.open_on_phone_animation),
+ atEnd = startAnim
+ ),
+ contentDescription = null
+ )
+
+ LaunchedEffect(showAppUpdateConfirmation) {
+ delay(250)
+ startAnim = true
+ }
+ },
+ title = {},
+ text = {
+ Text(text = stringResource(id = R.string.message_phoneappupdate_available))
+ },
+ edgeButton = {
+ AlertDialogDefaults.EdgeButton(
+ onClick = {
Settings.setLastUpdateCheckTime(Instant.now())
showAppUpdateConfirmation = false
- },
- state = dialogScrollState
+ }
)
}
-
- LaunchedEffect(showAppUpdateConfirmation) {
- delay(250)
- startAnim = true
- }
- }
+ )
ConfirmationOverlay(
confirmationData = confirmationData,
@@ -228,26 +218,6 @@ fun Dashboard(
}
}
}
-
- Settings.KEY_DASHCONFIG -> {
- lifecycleOwner.lifecycleScope.launch {
- runCatching {
- lifecycleOwner.withStarted {
- dashboardViewModel.resetDashboard()
- }
- }
- }
- }
-
- Settings.KEY_SHOWBATSTATUS -> {
- lifecycleOwner.lifecycleScope.launch {
- runCatching {
- lifecycleOwner.withStarted {
- dashboardViewModel.showBatteryState(Settings.isShowBatStatus())
- }
- }
- }
- }
}
}
}
@@ -321,7 +291,7 @@ fun Dashboard(
WearConnectionStatus.CONNECTING -> {}
WearConnectionStatus.APPNOTINSTALLED -> {
// Open store on remote device
- dashboardViewModel.openPlayStore(activity)
+ dashboardViewModel.openPlayStore()
// Navigate
activity.startActivity(
@@ -357,9 +327,9 @@ fun Dashboard(
ActionStatus.PERMISSION_DENIED -> {
if (action.actionType == Actions.TORCH) {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_torch_action)
+ confirmationViewModel.showFailure(
+ message = context.getString(
+ R.string.error_torch_action
)
)
} else if (action.actionType == Actions.SLEEPTIMER) {
@@ -370,51 +340,35 @@ fun Dashboard(
if (intentAndroid.resolveActivity(activity.packageManager) != null) {
activity.startActivity(intentAndroid)
- Toast.makeText(
- activity,
- R.string.error_sleeptimer_notinstalled,
- Toast.LENGTH_LONG
- ).show()
} else {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_sleeptimer_notinstalled)
+ confirmationViewModel.showFailure(
+ message = context.getString(
+ R.string.error_sleeptimer_notinstalled
)
)
}
} else {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_permissiondenied)
+ confirmationViewModel.showFailure(
+ message = context.getString(
+ R.string.error_permissiondenied_wear
)
)
}
- dashboardViewModel.openAppOnPhone(activity, false)
+ dashboardViewModel.openAppOnPhone(false)
}
ActionStatus.TIMEOUT -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_sendmessage)
- )
- )
+ confirmationViewModel.showFailure(message = context.getString(R.string.error_sendmessage))
}
ActionStatus.REMOTE_FAILURE -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_remoteactionfailed)
- )
- )
+ confirmationViewModel.showFailure(message = context.getString(R.string.error_remoteactionfailed))
}
ActionStatus.REMOTE_PERMISSION_DENIED -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_permissiondenied)
- )
- )
+ confirmationViewModel.showFailure(message = context.getString(R.string.error_permissiondenied_wear))
+ dashboardViewModel.openAppOnPhone(false)
}
ActionStatus.SUCCESS -> {
@@ -467,7 +421,7 @@ fun Dashboard(
phoneVersionCode?.let {
showAppUpdateConfirmation = !WearableHelper.isAppUpToDate(it)
if (showAppUpdateConfirmation) {
- dashboardViewModel.openPlayStore(activity, false)
+ dashboardViewModel.openPlayStore(false)
}
}
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/DashboardConfigUi.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/DashboardConfigUi.kt
new file mode 100644
index 00000000..cfc1db6d
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/DashboardConfigUi.kt
@@ -0,0 +1,497 @@
+package com.thewizrd.simplewear.ui.simplewear
+
+import android.view.MotionEvent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Add
+import androidx.compose.material.icons.rounded.RestartAlt
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.motionEventSpy
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.NavController
+import androidx.wear.compose.foundation.lazy.items
+import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
+import androidx.wear.compose.foundation.rotary.rotaryScrollable
+import androidx.wear.compose.material3.AlertDialog
+import androidx.wear.compose.material3.AlertDialogDefaults
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.EdgeButton
+import androidx.wear.compose.material3.FilledIconButton
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.IconButtonDefaults
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
+import com.google.android.horologist.compose.layout.ColumnItemType
+import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding
+import com.thewizrd.shared_resources.actions.Actions
+import com.thewizrd.shared_resources.controls.ActionButtonViewModel
+import com.thewizrd.simplewear.R
+import com.thewizrd.simplewear.preferences.Settings
+import com.thewizrd.simplewear.ui.compose.LazyGridScrollIndicator
+import com.thewizrd.simplewear.ui.compose.LazyGridScrollInfoProvider
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
+import com.thewizrd.simplewear.ui.utils.ReorderHapticFeedbackType
+import com.thewizrd.simplewear.ui.utils.rememberFocusRequester
+import com.thewizrd.simplewear.ui.utils.rememberReorderHapticFeedback
+import sh.calvin.reorderable.ReorderableItem
+import sh.calvin.reorderable.rememberReorderableLazyGridState
+import java.util.Collections
+
+private val MAX_BUTTONS = Actions.entries.size
+private val DEFAULT_TILES = Actions.entries
+
+@Composable
+fun DashboardConfigUi(
+ modifier: Modifier = Modifier,
+ navController: NavController
+) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ var tileConfig by remember {
+ mutableStateOf(Settings.getDashboardConfig() ?: DEFAULT_TILES)
+ }
+
+ DashboardConfigUi(
+ modifier = modifier,
+ tileConfig = tileConfig,
+ onSaveItems = { items, isBatteryVisible ->
+ if (items.isEmpty()) {
+ Settings.setDashboardConfig(null)
+ } else {
+ Settings.setDashboardConfig(items)
+ }
+
+ Settings.setShowBatStatus(isBatteryVisible)
+
+ navController.popBackStack()
+ }
+ )
+
+ LaunchedEffect(lifecycleOwner) {
+ Settings.getDashboardConfigFlow().collect {
+ tileConfig = it ?: DEFAULT_TILES
+ }
+ }
+}
+
+@Composable
+private fun DashboardConfigUi(
+ modifier: Modifier = Modifier,
+ lazyGridState: LazyGridState = rememberLazyGridState(),
+ focusRequester: FocusRequester = rememberFocusRequester(),
+ tileConfig: List = DEFAULT_TILES,
+ initialBatteryStatusShown: Boolean = Settings.isShowBatStatus(),
+ onSaveItems: (actions: List, showBatteryStatus: Boolean) -> Unit = { _, _ -> }
+) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val haptic = rememberReorderHapticFeedback()
+
+ var showConfirmation by remember { mutableStateOf(false) }
+ var showAddTileDialog by remember { mutableStateOf(false) }
+
+ val userTileConfigList: MutableList =
+ remember(tileConfig) { tileConfig.toMutableStateList() }
+ val selectionList =
+ remember { MutableList(MAX_BUTTONS) { false }.toMutableStateList() }
+
+ var isBatteryVisible by remember { mutableStateOf(initialBatteryStatusShown) }
+ var batterySelected by remember { mutableStateOf(false) }
+
+ val reorderableGridState = rememberReorderableLazyGridState(
+ lazyGridState = lazyGridState
+ ) { from, to ->
+ // Offset index by 2 to account for header & battery state
+ Collections.swap(userTileConfigList, from.index - 2, to.index - 2)
+ haptic.performHapticFeedback(ReorderHapticFeedbackType.MOVE)
+ }
+
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button
+ )
+
+ ScreenScaffold(
+ scrollInfoProvider = LazyGridScrollInfoProvider(lazyGridState),
+ scrollIndicator = {
+ LazyGridScrollIndicator(lazyGridState = lazyGridState)
+ },
+ contentPadding = contentPadding
+ ) { contentPadding ->
+ LazyVerticalGrid(
+ modifier = modifier
+ .fillMaxSize()
+ .rotaryScrollable(RotaryScrollableDefaults.behavior(lazyGridState), focusRequester)
+ .motionEventSpy { event ->
+ if (event.action == MotionEvent.ACTION_DOWN) {
+ selectionList.replaceAll { false }
+ batterySelected = false
+ }
+ },
+ columns = GridCells.Fixed(3),
+ state = lazyGridState,
+ contentPadding = contentPadding,
+ horizontalArrangement = Arrangement.Center,
+ verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
+ userScrollEnabled = true
+ ) {
+ item(span = { GridItemSpan(3) }) {
+ ListHeader(modifier = Modifier.fillMaxWidth()) {
+ Text(text = stringResource(id = R.string.title_dash_config))
+ }
+ }
+
+ // Battery State
+ item(span = { GridItemSpan(3) }) {
+ AnimatedVisibility(
+ visible = batterySelected,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ label = {
+ Text(text = stringResource(id = R.string.action_remove_batt_state))
+ },
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_close_white_24dp),
+ contentDescription = stringResource(id = R.string.action_remove_batt_state)
+ )
+ },
+ onClick = {
+ isBatteryVisible = false
+ batterySelected = false
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.White,
+ contentColor = MaterialTheme.colorScheme.primaryContainer,
+ iconColor = MaterialTheme.colorScheme.primaryContainer,
+ )
+ )
+ }
+
+ AnimatedVisibility(
+ visible = !batterySelected && isBatteryVisible,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ FilledTonalButton(
+ modifier = Modifier.fillMaxWidth(),
+ label = {
+ Text(text = stringResource(id = R.string.title_batt_state))
+ },
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_battery_std_white_24dp),
+ contentDescription = stringResource(id = R.string.title_batt_state)
+ )
+ },
+ onClick = {
+ batterySelected = true
+ }
+ )
+ }
+
+ AnimatedVisibility(
+ visible = !batterySelected && !isBatteryVisible,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ label = {
+ Text(text = stringResource(id = R.string.action_add_batt_state))
+ },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.Add,
+ contentDescription = stringResource(id = R.string.action_add_batt_state)
+ )
+ },
+ onClick = {
+ isBatteryVisible = true
+ batterySelected = false
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.White,
+ contentColor = MaterialTheme.colorScheme.primaryContainer,
+ iconColor = MaterialTheme.colorScheme.primaryContainer,
+ )
+ )
+ }
+ }
+
+ itemsIndexed(
+ items = userTileConfigList,
+ key = { index, item -> (item as? Actions) ?: index },
+ span = { index, _ ->
+ GridItemSpan(1)
+ }
+ ) { index, item ->
+ if (item is Actions) {
+ val model = remember(item) {
+ ActionButtonViewModel.getViewModelFromAction(item)
+ }
+
+ ReorderableItem(
+ modifier = Modifier.wrapContentSize(),
+ state = reorderableGridState,
+ key = item
+ ) { _ ->
+ Box(
+ modifier = Modifier
+ .padding(4.dp)
+ .draggableHandle(
+ onDragStarted = {
+ haptic.performHapticFeedback(ReorderHapticFeedbackType.START)
+ },
+ onDragStopped = {
+ haptic.performHapticFeedback(ReorderHapticFeedbackType.END)
+ },
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ AnimatedVisibility(
+ visible = selectionList[index],
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ FilledIconButton(
+ onClick = {
+ userTileConfigList.removeAt(index)
+ if (!userTileConfigList.contains("")) {
+ userTileConfigList.add("")
+ }
+ },
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = Color.White,
+ contentColor = MaterialTheme.colorScheme.primaryContainer
+ )
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_close_white_24dp),
+ contentDescription = stringResource(id = android.R.string.cancel),
+ )
+ }
+ }
+
+ AnimatedVisibility(
+ visible = !selectionList[index],
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ FilledTonalButton(
+ onClick = { selectionList[index] = true }
+ ) {
+ Icon(
+ painter = painterResource(id = model.drawableResId),
+ contentDescription = stringResource(id = model.actionLabelResId)
+ )
+ }
+ }
+ }
+ }
+ } else {
+ Box(
+ modifier = Modifier
+ .padding(4.dp)
+ .wrapContentSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ FilledIconButton(
+ onClick = {
+ showAddTileDialog = true
+ },
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = Color.White,
+ contentColor = MaterialTheme.colorScheme.primaryContainer
+ )
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Add,
+ contentDescription = stringResource(id = R.string.action_add_batt_state)
+ )
+ }
+ }
+ }
+ }
+
+ item(span = { GridItemSpan(3) }) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(
+ 12.dp,
+ Alignment.CenterHorizontally
+ )
+ ) {
+ AlertDialogDefaults.DismissButton(
+ modifier = Modifier.align(Alignment.CenterVertically),
+ onClick = {
+ showConfirmation = true
+ },
+ content = {
+ Icon(
+ modifier = Modifier.size(28.dp),
+ imageVector = Icons.Rounded.RestartAlt,
+ contentDescription = stringResource(R.string.message_reset_to_default),
+ )
+ }
+ )
+ AlertDialogDefaults.ConfirmButton(
+ modifier = Modifier.align(Alignment.CenterVertically),
+ onClick = {
+ onSaveItems(
+ userTileConfigList.filterIsInstance(),
+ isBatteryVisible
+ )
+ }
+ )
+ }
+ }
+ }
+
+ LaunchedEffect(lifecycleOwner) {
+ lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.RESUMED) {
+ focusRequester.requestFocus()
+ }
+ }
+ }
+
+ AlertDialog(
+ visible = showConfirmation,
+ onDismissRequest = {
+ showConfirmation = false
+ },
+ confirmButton = {
+ AlertDialogDefaults.ConfirmButton(
+ onClick = {
+ // Reset state
+ userTileConfigList.clear()
+ userTileConfigList.addAll(DEFAULT_TILES)
+ batterySelected = false
+ isBatteryVisible = true
+
+ // Reset settings
+ Settings.setDashboardConfig(null)
+ Settings.setShowBatStatus(true)
+
+ showConfirmation = false
+ }
+ )
+ },
+ dismissButton = {
+ AlertDialogDefaults.DismissButton(
+ onClick = {
+ showConfirmation = false
+ }
+ )
+ },
+ title = {
+ Text(text = stringResource(id = R.string.message_reset_to_default))
+ }
+ )
+
+ if (showAddTileDialog) {
+ val allowedActions = Actions.entries.toMutableList()
+ // Remove current actions
+ allowedActions.removeAll(userTileConfigList.filterIsInstance())
+
+ AlertDialog(
+ modifier = Modifier.fillMaxSize(),
+ visible = showAddTileDialog,
+ onDismissRequest = { showAddTileDialog = false },
+ title = { Text(text = stringResource(id = R.string.title_actions)) },
+ edgeButton = {
+ EdgeButton(
+ onClick = { showAddTileDialog = false }
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_close_white_24dp),
+ contentDescription = stringResource(android.R.string.cancel)
+ )
+ }
+ }
+ ) {
+ items(allowedActions) { action ->
+ val model = remember(action) {
+ ActionButtonViewModel.getViewModelFromAction(action)
+ }
+
+ FilledTonalButton(
+ modifier = Modifier.fillMaxWidth(),
+ label = {
+ Text(text = stringResource(id = model.actionLabelResId))
+ },
+ icon = {
+ Icon(
+ painter = painterResource(id = model.drawableResId),
+ contentDescription = stringResource(id = model.actionLabelResId)
+ )
+ },
+ onClick = {
+ val index = userTileConfigList.indexOfFirst { it !is Actions }
+ if (index >= 0) {
+ userTileConfigList[index] = action
+ }
+ addAddButtonIfNeeded(userTileConfigList)
+ showAddTileDialog = false
+ }
+ )
+ }
+ }
+ }
+
+ LaunchedEffect(tileConfig) {
+ addAddButtonIfNeeded(userTileConfigList)
+ }
+}
+
+private fun addAddButtonIfNeeded(userTileConfigList: MutableList) {
+ if (userTileConfigList.size < MAX_BUTTONS && !userTileConfigList.contains("")) {
+ userTileConfigList.add("")
+ }
+}
+
+@WearPreviewDevices
+@WearPreviewFontScales
+@Composable
+private fun PreviewDashboardConfigUi() {
+ DashboardConfigUi(
+ initialBatteryStatusShown = true
+ )
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/DashboardScreen.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/DashboardScreen.kt
index c58e2131..26605cfe 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/DashboardScreen.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/DashboardScreen.kt
@@ -1,5 +1,3 @@
-@file:OptIn(ExperimentalWearFoundationApi::class, ExperimentalMaterialApi::class)
-
package com.thewizrd.simplewear.ui.simplewear
import android.content.ComponentName
@@ -13,7 +11,6 @@ import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
@@ -26,13 +23,14 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.pullrefresh.PullRefreshDefaults
-import androidx.compose.material.pullrefresh.PullRefreshIndicator
-import androidx.compose.material.pullrefresh.rememberPullRefreshState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ViewList
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.pulltorefresh.PullToRefreshBox
+import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
+import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -42,15 +40,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalViewConfiguration
-import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -62,24 +57,22 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavOptions
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
+import androidx.wear.compose.foundation.requestFocusOnHierarchyActive
import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
import androidx.wear.compose.foundation.rotary.rotaryScrollable
-import androidx.wear.compose.material.Button
-import androidx.wear.compose.material.ButtonDefaults
-import androidx.wear.compose.material.Chip
-import androidx.wear.compose.material.ChipColors
-import androidx.wear.compose.material.ChipDefaults
-import androidx.wear.compose.material.CircularProgressIndicator
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.OutlinedChip
-import androidx.wear.compose.material.Switch
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.TimeText
-import androidx.wear.compose.material.ToggleChip
-import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonColors
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.CircularProgressIndicator
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.OutlinedButton
+import androidx.wear.compose.material3.SwitchButton
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TimeText
+import androidx.wear.compose.material3.contentColorFor
+import com.google.android.horologist.images.base.paintable.ImageVectorPaintable.Companion.asPaintable
import com.thewizrd.shared_resources.actions.Actions
import com.thewizrd.shared_resources.actions.BatteryStatus
import com.thewizrd.shared_resources.controls.ActionButtonViewModel
@@ -90,19 +83,21 @@ import com.thewizrd.shared_resources.utils.ContextUtils.isSmallestWidth
import com.thewizrd.simplewear.R
import com.thewizrd.simplewear.controls.onClick
import com.thewizrd.simplewear.preferences.Settings
-import com.thewizrd.simplewear.ui.components.PullRefresh
import com.thewizrd.simplewear.ui.components.WearDivider
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
import com.thewizrd.simplewear.ui.navigation.Screen
+import com.thewizrd.simplewear.ui.utils.fillDashboard
+import com.thewizrd.simplewear.ui.utils.rememberFocusRequester
import com.thewizrd.simplewear.viewmodels.DashboardState
import com.thewizrd.simplewear.viewmodels.DashboardViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
-import kotlin.math.sqrt
@Composable
fun DashboardScreen(
+ modifier: Modifier = Modifier,
dashboardViewModel: DashboardViewModel,
scrollState: ScrollState = rememberScrollState(),
navController: NavController
@@ -114,6 +109,7 @@ fun DashboardScreen(
val uiState by dashboardViewModel.uiState.collectAsState()
DashboardScreen(
+ modifier = modifier,
isRefreshing = refreshing,
onRefresh = {
refreshing = true
@@ -159,6 +155,7 @@ fun DashboardScreen(
)
}
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun DashboardScreen(
modifier: Modifier = Modifier,
@@ -171,30 +168,21 @@ fun DashboardScreen(
onDashTileSettingsClick: () -> Unit = {},
) {
val isPreview = LocalInspectionMode.current
- val configuration = LocalConfiguration.current
- val refreshThreshold = remember(configuration) {
- (configuration.screenHeightDp / 3f).dp
- }
-
- val pullRefreshState = rememberPullRefreshState(
- refreshing = isRefreshing,
- onRefresh = onRefresh,
- refreshThreshold = refreshThreshold,
- refreshingOffset = PullRefreshDefaults.RefreshingOffset + 4.dp
- )
+ val pullRefreshState = rememberPullToRefreshState()
- PullRefresh(
+ PullToRefreshBox(
modifier = modifier.fillMaxSize(),
+ isRefreshing = isRefreshing,
+ onRefresh = onRefresh,
state = pullRefreshState,
indicator = {
- PullRefreshIndicator(
+ PullToRefreshDefaults.LoadingIndicator(
modifier = Modifier.align(Alignment.TopCenter),
- refreshing = isRefreshing,
state = pullRefreshState,
- backgroundColor = MaterialTheme.colors.surface,
- contentColor = MaterialTheme.colors.primary,
- scale = true
+ isRefreshing = isRefreshing,
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
) {
@@ -202,8 +190,9 @@ fun DashboardScreen(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
+ .requestFocusOnHierarchyActive()
.rotaryScrollable(
- focusRequester = rememberActiveFocusRequester(),
+ focusRequester = rememberFocusRequester(),
behavior = RotaryScrollableDefaults.behavior(scrollState)
),
) {
@@ -251,7 +240,7 @@ private fun DeviceStateChip(
isStatusLoading: Boolean = false,
connectionStatus: WearConnectionStatus? = null
) {
- OutlinedChip(
+ OutlinedButton(
modifier = Modifier.fillMaxWidth(),
icon = {
Icon(
@@ -292,11 +281,8 @@ private fun DeviceStateChip(
},
onClick = {},
enabled = false,
- colors = transparentChipColors(),
- border = ChipDefaults.outlinedChipBorder(
- borderColor = Color.Transparent,
- disabledBorderColor = Color.Transparent
- )
+ colors = transparentButtonColors(),
+ border = null
)
}
@@ -309,11 +295,17 @@ private fun BatteryStatusChip(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
- OutlinedChip(
+ OutlinedButton(
modifier = Modifier.weight(1f, fill = true),
icon = {
Icon(
- painter = painterResource(id = R.drawable.ic_battery_std_white_24dp),
+ painter = painterResource(
+ id = if (batteryStatus?.isCharging == true) {
+ R.drawable.ic_battery_charging_white_24dp
+ } else {
+ R.drawable.ic_battery_std_white_24dp
+ }
+ ),
contentDescription = stringResource(R.string.title_batt_state)
)
},
@@ -340,11 +332,8 @@ private fun BatteryStatusChip(
},
onClick = {},
enabled = false,
- colors = transparentChipColors(),
- border = ChipDefaults.outlinedChipBorder(
- borderColor = Color.Transparent,
- disabledBorderColor = Color.Transparent
- )
+ colors = transparentButtonColors(),
+ border = null
)
if (isStatusLoading) {
Box(
@@ -361,19 +350,6 @@ private fun BatteryStatusChip(
}
}
-@Composable
-private fun transparentChipColors(): ChipColors = ChipDefaults.chipColors(
- backgroundColor = Color.Transparent,
- disabledBackgroundColor = Color.Transparent,
- contentColor = Color.White,
- disabledContentColor = Color.White,
- iconColor = Color.White,
- disabledIconColor = Color.White,
- secondaryContentColor = MaterialTheme.colors.onSurfaceVariant,
- disabledSecondaryContentColor = MaterialTheme.colors.onSurfaceVariant,
-)
-
-@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun ActionsList(
actions: List,
@@ -392,7 +368,7 @@ private fun ActionsList(
.heightIn(min = 48.dp)
.wrapContentHeight(align = Alignment.CenterVertically),
text = stringResource(id = R.string.title_actions),
- style = MaterialTheme.typography.button,
+ style = MaterialTheme.typography.labelMedium,
textAlign = TextAlign.Center,
maxLines = 1
)
@@ -475,19 +451,10 @@ private fun ActionGridButton(
interactionSource = interactionSource,
enabled = model.buttonState != null,
colors = model.buttonState?.let { state ->
- ButtonDefaults.buttonColors(
- backgroundColor = if (state) {
- colorResource(id = R.color.colorPrimary)
- } else {
- MaterialTheme.colors.surface
- }
- )
+ actionButtonColors(state)
} ?: run {
// Indeterminate state
- ButtonDefaults.buttonColors(
- backgroundColor = colorResource(id = R.color.colorPrimaryDark),
- disabledBackgroundColor = colorResource(id = R.color.colorPrimaryDark)
- )
+ indeterminateActionButtonColors()
},
onClick = {
if (isClickable && model.getItemViewType() != ActionItemType.READONLY_ACTION) {
@@ -531,23 +498,14 @@ private fun ActionListButton(
) {
val context = LocalContext.current
- Chip(
+ Button(
modifier = Modifier.fillMaxWidth(),
enabled = model.buttonState != null,
colors = model.buttonState?.let { state ->
- ChipDefaults.chipColors(
- backgroundColor = if (state) {
- colorResource(id = R.color.colorPrimary)
- } else {
- MaterialTheme.colors.surface
- }
- )
+ actionButtonColors(state)
} ?: run {
// Indeterminate state
- ChipDefaults.chipColors(
- backgroundColor = colorResource(id = R.color.colorPrimaryDark),
- disabledBackgroundColor = colorResource(id = R.color.colorPrimaryDark)
- )
+ indeterminateActionButtonColors()
},
label = {
Text(
@@ -606,7 +564,7 @@ private fun LayoutPreferenceButton(
) {
val lifecycleOwner = LocalLifecycleOwner.current
- Chip(
+ FilledTonalButton(
modifier = Modifier.fillMaxWidth(),
label = {
Text(text = stringResource(id = R.string.pref_layout))
@@ -625,7 +583,7 @@ private fun LayoutPreferenceButton(
painter = if (isGridLayout) {
painterResource(id = R.drawable.ic_apps_white_24dp)
} else {
- painterResource(id = R.drawable.ic_view_list_white_24dp)
+ Icons.AutoMirrored.Rounded.ViewList.asPaintable().rememberPainter()
},
contentDescription = if (isGridLayout) {
stringResource(id = R.string.option_grid)
@@ -634,9 +592,6 @@ private fun LayoutPreferenceButton(
}
)
},
- colors = ChipDefaults.secondaryChipColors(
- secondaryContentColor = MaterialTheme.colors.onSurfaceVariant
- ),
onClick = {
AnalyticsLogger.logEvent("dash_layout_btn_clicked", Bundle().apply {
putBoolean("isGridLayout", isGridLayout)
@@ -655,18 +610,17 @@ private fun LayoutPreferenceButton(
private fun DashboardConfigButton(
onClick: () -> Unit = {}
) {
- Chip(
+ FilledTonalButton(
modifier = Modifier.fillMaxWidth(),
label = {
Text(text = stringResource(id = R.string.pref_title_dasheditor))
},
icon = {
Icon(
- painter = painterResource(id = R.drawable.ic_baseline_edit_24),
+ painter = painterResource(id = R.drawable.ic_mode_edit),
contentDescription = stringResource(id = R.string.pref_title_dasheditor)
)
},
- colors = ChipDefaults.secondaryChipColors(),
onClick = onClick
)
}
@@ -675,18 +629,17 @@ private fun DashboardConfigButton(
private fun TileDashboardConfigButton(
onClick: () -> Unit = {}
) {
- Chip(
+ FilledTonalButton(
modifier = Modifier.fillMaxWidth(),
label = {
Text(text = stringResource(id = R.string.pref_title_tiledasheditor))
},
icon = {
Icon(
- painter = painterResource(id = R.drawable.ic_baseline_edit_24),
+ painter = painterResource(id = R.drawable.ic_mode_edit),
contentDescription = stringResource(id = R.string.pref_title_tiledasheditor)
)
},
- colors = ChipDefaults.secondaryChipColors(),
onClick = onClick
)
}
@@ -701,7 +654,7 @@ private fun MediaControllerSwitch() {
var isChecked by remember { mutableStateOf(false) }
- ToggleChip(
+ SwitchButton(
modifier = Modifier.fillMaxWidth(),
checked = isChecked,
onCheckedChange = {
@@ -718,11 +671,6 @@ private fun MediaControllerSwitch() {
text = stringResource(id = R.string.pref_title_mediacontroller_launcher),
maxLines = 10
)
- },
- toggleControl = {
- Switch(
- checked = isChecked
- )
}
)
@@ -732,24 +680,51 @@ private fun MediaControllerSwitch() {
}
}
-@Stable
-private fun Modifier.fillDashboard(): Modifier = composed {
- val isRound = LocalConfiguration.current.isScreenRound
- val screenHeightDp = LocalConfiguration.current.screenHeightDp
-
- var bottomInset = Dp(screenHeightDp - (screenHeightDp * 0.8733032f))
+@Composable
+private fun transparentButtonColors(): ButtonColors = ButtonDefaults.outlinedButtonColors(
+ contentColor = MaterialTheme.colorScheme.onSurface,
+ secondaryContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ iconColor = MaterialTheme.colorScheme.onSurface,
+ disabledContentColor = MaterialTheme.colorScheme.onSurface,
+ disabledSecondaryContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ disabledIconColor = MaterialTheme.colorScheme.onSurface,
+)
- if (isRound) {
- val screenWidthDp = LocalConfiguration.current.smallestScreenWidthDp
- val maxSquareEdge = (sqrt(((screenHeightDp * screenWidthDp) / 2).toFloat()))
- bottomInset = Dp((screenHeightDp - (maxSquareEdge * 0.8733032f)) / 2)
- }
+@Composable
+private fun actionButtonColors(state: Boolean): ButtonColors {
+ return ButtonDefaults.buttonColors(
+ containerColor = if (state) {
+ MaterialTheme.colorScheme.primary
+ } else {
+ MaterialTheme.colorScheme.surfaceContainer
+ },
+ contentColor = if (state) {
+ MaterialTheme.colorScheme.onPrimary
+ } else {
+ MaterialTheme.colorScheme.onSurface
+ },
+ secondaryContentColor = if (state) {
+ MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
+ } else {
+ MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
+ },
+ iconColor = if (state) {
+ MaterialTheme.colorScheme.onPrimary
+ } else {
+ MaterialTheme.colorScheme.onSurface
+ }
+ )
+}
- fillMaxSize().padding(
- start = if (isRound) 14.dp else 8.dp,
- end = if (isRound) 14.dp else 8.dp,
- top = if (isRound) 36.dp else 8.dp,
- bottom = bottomInset
+@Composable
+private fun indeterminateActionButtonColors(): ButtonColors {
+ val color = MaterialTheme.colorScheme.primaryContainer
+
+ return ButtonDefaults.buttonColors(
+ disabledContainerColor = color,
+ disabledContentColor = contentColorFor(color),
+ disabledSecondaryContentColor = contentColorFor(color).copy(alpha = 0.8f),
+ disabledIconColor = contentColorFor(color)
)
}
@@ -780,9 +755,65 @@ private fun PreviewDashboardScreen() {
}
}
+@WearPreviewDevices
+@Composable
+private fun PreviewDashboardScreen_List() {
+ val dashboardState = remember {
+ DashboardState(
+ connectionStatus = WearConnectionStatus.CONNECTED,
+ isStatusLoading = true,
+ batteryStatus = BatteryStatus(100, false),
+ isGridLayout = false,
+ showBatteryState = true,
+ isActionsClickable = true,
+ actions = Actions.entries.map {
+ ActionButtonViewModel.getViewModelFromAction(it)
+ }
+ )
+ }
+
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ DashboardScreen(
+ dashboardState = dashboardState
+ )
+ }
+}
+
+@WearPreviewDevices
+@Composable
+private fun PreviewDashboardScreen_Indeterminate() {
+ val dashboardState = remember {
+ DashboardState(
+ connectionStatus = WearConnectionStatus.CONNECTED,
+ isStatusLoading = true,
+ batteryStatus = BatteryStatus(100, false),
+ isGridLayout = true,
+ showBatteryState = true,
+ isActionsClickable = true,
+ actions = Actions.entries.map {
+ ActionButtonViewModel.getViewModelFromAction(it).apply {
+ buttonState = null
+ }
+ }
+ )
+ }
+
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ DashboardScreen(
+ dashboardState = dashboardState
+ )
+ }
+}
+
private fun ActionButtonViewModel.getItemViewType(): Int {
return when (this.actionType) {
- Actions.WIFI, Actions.BLUETOOTH, Actions.MOBILEDATA, Actions.TORCH, Actions.HOTSPOT -> {
+ Actions.WIFI, Actions.BLUETOOTH, Actions.MOBILEDATA, Actions.TORCH, Actions.HOTSPOT, Actions.NFC, Actions.BATTERYSAVER -> {
ActionItemType.TOGGLE_ACTION
}
@@ -802,7 +833,6 @@ private fun ActionButtonViewModel.getItemViewType(): Int {
}
Actions.RINGER -> ActionItemType.MULTICHOICE_ACTION
- else -> ActionItemType.TOGGLE_ACTION
}
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/DashboardTileConfigUi.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/DashboardTileConfigUi.kt
new file mode 100644
index 00000000..84627da7
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/DashboardTileConfigUi.kt
@@ -0,0 +1,515 @@
+package com.thewizrd.simplewear.ui.simplewear
+
+import android.view.MotionEvent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Add
+import androidx.compose.material.icons.rounded.RestartAlt
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.motionEventSpy
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.NavController
+import androidx.wear.compose.foundation.lazy.items
+import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
+import androidx.wear.compose.foundation.rotary.rotaryScrollable
+import androidx.wear.compose.material3.AlertDialog
+import androidx.wear.compose.material3.AlertDialogDefaults
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.EdgeButton
+import androidx.wear.compose.material3.FilledIconButton
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.IconButtonDefaults
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
+import com.google.android.horologist.compose.layout.ColumnItemType
+import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding
+import com.thewizrd.shared_resources.actions.Actions
+import com.thewizrd.shared_resources.controls.ActionButtonViewModel
+import com.thewizrd.simplewear.R
+import com.thewizrd.simplewear.preferences.DashboardTileUtils
+import com.thewizrd.simplewear.preferences.DashboardTileUtils.DEFAULT_TILES
+import com.thewizrd.simplewear.preferences.DashboardTileUtils.isActionAllowed
+import com.thewizrd.simplewear.preferences.Settings
+import com.thewizrd.simplewear.ui.compose.LazyGridScrollIndicator
+import com.thewizrd.simplewear.ui.compose.LazyGridScrollInfoProvider
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
+import com.thewizrd.simplewear.ui.utils.ReorderHapticFeedbackType
+import com.thewizrd.simplewear.ui.utils.rememberFocusRequester
+import com.thewizrd.simplewear.ui.utils.rememberReorderHapticFeedback
+import com.thewizrd.simplewear.wearable.tiles.DashboardTileProviderService
+import sh.calvin.reorderable.ReorderableItem
+import sh.calvin.reorderable.rememberReorderableLazyGridState
+import java.util.Collections
+
+@Composable
+fun DashboardTileConfigUi(
+ modifier: Modifier = Modifier,
+ navController: NavController
+) {
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ var isConfigChanged by remember { mutableStateOf(false) }
+
+ var tileConfig by remember {
+ mutableStateOf(Settings.getDashboardTileConfig() ?: DEFAULT_TILES)
+ }
+
+ DashboardTileConfigUi(
+ modifier = modifier,
+ tileConfig = tileConfig,
+ onSaveItems = { items, isBatteryVisible ->
+ if (items.isEmpty()) {
+ Settings.setDashboardTileConfig(null)
+ } else {
+ Settings.setDashboardTileConfig(items)
+ }
+
+ Settings.setShowTileBatStatus(isBatteryVisible)
+
+ navController.popBackStack()
+ }
+ )
+
+ LaunchedEffect(lifecycleOwner) {
+ Settings.getDashboardTileConfigFlow().collect {
+ tileConfig = it ?: DEFAULT_TILES
+ isConfigChanged = true
+ }
+ }
+
+ DisposableEffect(Unit) {
+ onDispose {
+ if (isConfigChanged) {
+ // Trigger tile update
+ DashboardTileProviderService.requestTileUpdate(context)
+ }
+ }
+ }
+}
+
+@Composable
+private fun DashboardTileConfigUi(
+ modifier: Modifier = Modifier,
+ lazyGridState: LazyGridState = rememberLazyGridState(),
+ focusRequester: FocusRequester = rememberFocusRequester(),
+ tileConfig: List = DEFAULT_TILES,
+ initialTileBatteryStatusShown: Boolean = Settings.isShowTileBatStatus(),
+ onSaveItems: (actions: List, showBatteryStatus: Boolean) -> Unit = { _, _ -> }
+) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val haptic = rememberReorderHapticFeedback()
+
+ var showConfirmation by remember { mutableStateOf(false) }
+ var showAddTileDialog by remember { mutableStateOf(false) }
+
+ val userTileConfigList: MutableList =
+ remember(tileConfig) { tileConfig.toMutableStateList() }
+ val selectionList =
+ remember { MutableList(DashboardTileUtils.MAX_BUTTONS) { false }.toMutableStateList() }
+
+ var isBatteryVisible by remember { mutableStateOf(initialTileBatteryStatusShown) }
+ var batterySelected by remember { mutableStateOf(false) }
+
+ val reorderableGridState = rememberReorderableLazyGridState(
+ lazyGridState = lazyGridState
+ ) { from, to ->
+ // Offset index by 2 to account for header & battery state
+ Collections.swap(userTileConfigList, from.index - 2, to.index - 2)
+ haptic.performHapticFeedback(ReorderHapticFeedbackType.MOVE)
+ }
+
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button
+ )
+
+ ScreenScaffold(
+ scrollInfoProvider = LazyGridScrollInfoProvider(lazyGridState),
+ scrollIndicator = {
+ LazyGridScrollIndicator(lazyGridState = lazyGridState)
+ },
+ contentPadding = contentPadding
+ ) { contentPadding ->
+ LazyVerticalGrid(
+ modifier = modifier
+ .fillMaxSize()
+ .rotaryScrollable(RotaryScrollableDefaults.behavior(lazyGridState), focusRequester)
+ .motionEventSpy { event ->
+ if (event.action == MotionEvent.ACTION_DOWN) {
+ selectionList.replaceAll { false }
+ batterySelected = false
+ }
+ },
+ columns = GridCells.Fixed(3),
+ state = lazyGridState,
+ contentPadding = contentPadding,
+ horizontalArrangement = Arrangement.Center,
+ verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
+ userScrollEnabled = true
+ ) {
+ item(span = { GridItemSpan(3) }) {
+ ListHeader(modifier = Modifier.fillMaxWidth()) {
+ Text(text = stringResource(id = R.string.title_dashtile_config))
+ }
+ }
+
+ // Battery State
+ item(span = { GridItemSpan(3) }) {
+ AnimatedVisibility(
+ visible = batterySelected,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ label = {
+ Text(text = stringResource(id = R.string.action_remove_batt_state))
+ },
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_close_white_24dp),
+ contentDescription = stringResource(id = R.string.action_remove_batt_state)
+ )
+ },
+ onClick = {
+ isBatteryVisible = false
+ batterySelected = false
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.White,
+ contentColor = MaterialTheme.colorScheme.primaryContainer,
+ iconColor = MaterialTheme.colorScheme.primaryContainer,
+ )
+ )
+ }
+
+ AnimatedVisibility(
+ visible = !batterySelected && isBatteryVisible,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ FilledTonalButton(
+ modifier = Modifier.fillMaxWidth(),
+ label = {
+ Text(text = stringResource(id = R.string.title_batt_state))
+ },
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_battery_std_white_24dp),
+ contentDescription = stringResource(id = R.string.title_batt_state)
+ )
+ },
+ onClick = {
+ batterySelected = true
+ }
+ )
+ }
+
+ AnimatedVisibility(
+ visible = !batterySelected && !isBatteryVisible,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ label = {
+ Text(text = stringResource(id = R.string.action_add_batt_state))
+ },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.Add,
+ contentDescription = stringResource(id = R.string.action_add_batt_state)
+ )
+ },
+ onClick = {
+ isBatteryVisible = true
+ batterySelected = false
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.White,
+ contentColor = MaterialTheme.colorScheme.primaryContainer,
+ iconColor = MaterialTheme.colorScheme.primaryContainer,
+ )
+ )
+ }
+ }
+
+ itemsIndexed(
+ items = userTileConfigList,
+ key = { index, item -> (item as? Actions) ?: index },
+ span = { index, _ ->
+ GridItemSpan(1)
+ }
+ ) { index, item ->
+ if (item is Actions) {
+ val model = remember(item) {
+ ActionButtonViewModel.getViewModelFromAction(item)
+ }
+
+ ReorderableItem(
+ modifier = Modifier.wrapContentSize(),
+ state = reorderableGridState,
+ key = item
+ ) { _ ->
+ Box(
+ modifier = Modifier
+ .padding(4.dp)
+ .draggableHandle(
+ onDragStarted = {
+ haptic.performHapticFeedback(ReorderHapticFeedbackType.START)
+ },
+ onDragStopped = {
+ haptic.performHapticFeedback(ReorderHapticFeedbackType.END)
+ },
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ AnimatedVisibility(
+ visible = selectionList[index],
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ FilledIconButton(
+ onClick = {
+ userTileConfigList.removeAt(index)
+ if (!userTileConfigList.contains("")) {
+ userTileConfigList.add("")
+ }
+ },
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = Color.White,
+ contentColor = MaterialTheme.colorScheme.primaryContainer
+ )
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_close_white_24dp),
+ contentDescription = stringResource(id = android.R.string.cancel),
+ )
+ }
+ }
+
+ AnimatedVisibility(
+ visible = !selectionList[index],
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ FilledTonalButton(
+ onClick = { selectionList[index] = true }
+ ) {
+ Icon(
+ painter = painterResource(id = model.drawableResId),
+ contentDescription = stringResource(id = model.actionLabelResId)
+ )
+ }
+ }
+ }
+ }
+ } else {
+ Box(
+ modifier = Modifier
+ .padding(4.dp)
+ .wrapContentSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ FilledIconButton(
+ onClick = {
+ showAddTileDialog = true
+ },
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = Color.White,
+ contentColor = MaterialTheme.colorScheme.primaryContainer
+ )
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Add,
+ contentDescription = stringResource(id = R.string.action_add_batt_state)
+ )
+ }
+ }
+ }
+ }
+
+ item(span = { GridItemSpan(3) }) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(
+ 12.dp,
+ Alignment.CenterHorizontally
+ )
+ ) {
+ AlertDialogDefaults.DismissButton(
+ modifier = Modifier.align(Alignment.CenterVertically),
+ onClick = {
+ showConfirmation = true
+ },
+ content = {
+ Icon(
+ modifier = Modifier.size(28.dp),
+ imageVector = Icons.Rounded.RestartAlt,
+ contentDescription = stringResource(R.string.message_reset_to_default),
+ )
+ }
+ )
+ AlertDialogDefaults.ConfirmButton(
+ modifier = Modifier.align(Alignment.CenterVertically),
+ onClick = {
+ onSaveItems(
+ userTileConfigList.filterIsInstance(),
+ isBatteryVisible
+ )
+ }
+ )
+ }
+ }
+ }
+
+ LaunchedEffect(lifecycleOwner) {
+ lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.RESUMED) {
+ focusRequester.requestFocus()
+ }
+ }
+ }
+
+ AlertDialog(
+ visible = showConfirmation,
+ onDismissRequest = {
+ showConfirmation = false
+ },
+ confirmButton = {
+ AlertDialogDefaults.ConfirmButton(
+ onClick = {
+ // Reset state
+ userTileConfigList.clear()
+ userTileConfigList.addAll(DEFAULT_TILES)
+ batterySelected = false
+ isBatteryVisible = true
+
+ // Reset settings
+ Settings.setDashboardTileConfig(null)
+ Settings.setShowTileBatStatus(true)
+
+ showConfirmation = false
+ }
+ )
+ },
+ dismissButton = {
+ AlertDialogDefaults.DismissButton(
+ onClick = {
+ showConfirmation = false
+ }
+ )
+ },
+ title = {
+ Text(text = stringResource(id = R.string.message_reset_to_default))
+ }
+ )
+
+ if (showAddTileDialog) {
+ val allowedActions = Actions.entries.toMutableList()
+ // Remove current actions
+ allowedActions.removeAll(userTileConfigList.filterIsInstance())
+ // Remove other actions which need an activity
+ allowedActions.removeIf { !isActionAllowed(it) }
+
+ AlertDialog(
+ modifier = Modifier.fillMaxSize(),
+ visible = showAddTileDialog,
+ onDismissRequest = { showAddTileDialog = false },
+ title = { Text(text = stringResource(id = R.string.title_actions)) },
+ edgeButton = {
+ EdgeButton(
+ onClick = { showAddTileDialog = false }
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_close_white_24dp),
+ contentDescription = stringResource(android.R.string.cancel)
+ )
+ }
+ }
+ ) {
+ items(allowedActions) { action ->
+ val model = remember(action) {
+ ActionButtonViewModel.getViewModelFromAction(action)
+ }
+
+ FilledTonalButton(
+ modifier = Modifier.fillMaxWidth(),
+ label = {
+ Text(text = stringResource(id = model.actionLabelResId))
+ },
+ icon = {
+ Icon(
+ painter = painterResource(id = model.drawableResId),
+ contentDescription = stringResource(id = model.actionLabelResId)
+ )
+ },
+ onClick = {
+ val index = userTileConfigList.indexOfFirst { it !is Actions }
+ if (index >= 0) {
+ userTileConfigList[index] = action
+ }
+ addAddButtonIfNeeded(userTileConfigList)
+ showAddTileDialog = false
+ }
+ )
+ }
+ }
+ }
+
+ LaunchedEffect(tileConfig) {
+ addAddButtonIfNeeded(userTileConfigList)
+ }
+}
+
+private fun addAddButtonIfNeeded(userTileConfigList: MutableList) {
+ if (userTileConfigList.size < DashboardTileUtils.MAX_BUTTONS && !userTileConfigList.contains("")) {
+ userTileConfigList.add("")
+ }
+}
+
+@WearPreviewDevices
+@WearPreviewFontScales
+@Composable
+private fun PreviewDashboardTileConfigUi() {
+ DashboardTileConfigUi(
+ initialTileBatteryStatusShown = true
+ )
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/GesturesUi.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/GesturesUi.kt
index 474a7092..0b4c9723 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/GesturesUi.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/GesturesUi.kt
@@ -1,4 +1,4 @@
-@file:OptIn(ExperimentalLayoutApi::class, ExperimentalHorologistApi::class)
+@file:OptIn(ExperimentalLayoutApi::class)
package com.thewizrd.simplewear.ui.simplewear
@@ -22,15 +22,15 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
-import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
-import androidx.compose.material.icons.automirrored.outlined.ArrowBack
-import androidx.compose.material.icons.filled.KeyboardArrowDown
-import androidx.compose.material.icons.filled.KeyboardArrowUp
-import androidx.compose.material.icons.outlined.Home
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowLeft
+import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
+import androidx.compose.material.icons.rounded.Home
+import androidx.compose.material.icons.rounded.KeyboardArrowDown
+import androidx.compose.material.icons.rounded.KeyboardArrowUp
+import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -40,9 +40,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.rotary.onRotaryScrollEvent
import androidx.compose.ui.platform.LocalConfiguration
@@ -56,28 +54,30 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
-import androidx.wear.compose.foundation.SwipeToDismissBoxState
-import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
-import androidx.wear.compose.material.CompactChip
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.TimeText
-import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
+import androidx.wear.compose.foundation.pager.rememberPagerState
+import androidx.wear.compose.material3.AnimatedPage
+import androidx.wear.compose.material3.CompactButton
+import androidx.wear.compose.material3.FilledIconButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.Text
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.compose.material.Button
import com.thewizrd.shared_resources.actions.ActionStatus
import com.thewizrd.shared_resources.actions.GestureActionState
import com.thewizrd.shared_resources.helpers.GestureUIHelper
import com.thewizrd.shared_resources.helpers.WearConnectionStatus
+import com.thewizrd.shared_resources.utils.JSONParser
+import com.thewizrd.shared_resources.utils.getSerializableCompat
import com.thewizrd.simplewear.PhoneSyncActivity
import com.thewizrd.simplewear.R
import com.thewizrd.simplewear.ui.components.ConfirmationOverlay
+import com.thewizrd.simplewear.ui.components.HorizontalPagerScreen
import com.thewizrd.simplewear.ui.components.LoadingContent
-import com.thewizrd.simplewear.ui.components.SwipeToDismissPagerScreen
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
import com.thewizrd.simplewear.ui.theme.activityViewModel
import com.thewizrd.simplewear.ui.theme.findActivity
+import com.thewizrd.simplewear.ui.utils.rememberFocusRequester
import com.thewizrd.simplewear.viewmodels.ConfirmationData
import com.thewizrd.simplewear.viewmodels.ConfirmationViewModel
import com.thewizrd.simplewear.viewmodels.GestureUiState
@@ -95,8 +95,7 @@ import kotlin.math.sqrt
@Composable
fun GesturesUi(
modifier: Modifier = Modifier,
- navController: NavController,
- swipeToDismissBoxState: SwipeToDismissBoxState = rememberSwipeToDismissBoxState()
+ navController: NavController
) {
val context = LocalContext.current
val activity = context.findActivity()
@@ -112,59 +111,73 @@ fun GesturesUi(
if (uiState.actionState.accessibilityEnabled && uiState.actionState.keyEventSupported) 2 else 1
}
- val isRoot = navController.previousBackStackEntry == null
-
- SwipeToDismissPagerScreen(
- modifier = modifier.background(MaterialTheme.colors.background),
- isRoot = isRoot,
- swipeToDismissBoxState = swipeToDismissBoxState,
- state = pagerState,
- timeText = {
- if (!uiState.isLoading) TimeText()
- },
- hidePagerIndicator = uiState.isLoading
+ HorizontalPagerScreen(
+ modifier = modifier,
+ pagerState = pagerState,
+ hidePagerIndicator = uiState.isLoading,
) { pageIdx ->
- LoadingContent(
- empty = !uiState.actionState.accessibilityEnabled,
- emptyContent = {
- NoAccessibilityScreen(
- onRefresh = {
- gestureUiViewModel.refreshState()
- }
- )
- },
- loading = uiState.isLoading
- ) {
- when (pageIdx) {
- // Gestures
- 0 -> {
- GestureScreen(
- modifier = modifier,
- uiState = uiState,
- onDPadDirection = { direction ->
- when (direction) {
- KeyEvent.KEYCODE_DPAD_UP -> gestureUiViewModel.requestDPad(top = 1)
- KeyEvent.KEYCODE_DPAD_DOWN -> gestureUiViewModel.requestDPad(bottom = 1)
- KeyEvent.KEYCODE_DPAD_LEFT -> gestureUiViewModel.requestDPad(left = 1)
- KeyEvent.KEYCODE_DPAD_RIGHT -> gestureUiViewModel.requestDPad(right = 1)
+ AnimatedPage(pageIdx, pagerState) {
+ ScreenScaffold { contentPadding ->
+ LoadingContent(
+ empty = !uiState.actionState.accessibilityEnabled,
+ emptyContent = {
+ NoAccessibilityScreen(
+ modifier = Modifier.padding(contentPadding),
+ onRefresh = {
+ gestureUiViewModel.refreshState()
}
- },
- onDPadClicked = {
- gestureUiViewModel.requestDPadClick()
- },
- onScroll = { dX, dY, screenWidth, screenHeight ->
- gestureUiViewModel.requestScroll(dX, dY, screenWidth, screenHeight)
+ )
+ },
+ loading = uiState.isLoading
+ ) {
+ when (pageIdx) {
+ // Gestures
+ 0 -> {
+ GestureScreen(
+ modifier = modifier.padding(contentPadding),
+ uiState = uiState,
+ onDPadDirection = { direction ->
+ when (direction) {
+ KeyEvent.KEYCODE_DPAD_UP -> gestureUiViewModel.requestDPad(
+ top = 1
+ )
+
+ KeyEvent.KEYCODE_DPAD_DOWN -> gestureUiViewModel.requestDPad(
+ bottom = 1
+ )
+
+ KeyEvent.KEYCODE_DPAD_LEFT -> gestureUiViewModel.requestDPad(
+ left = 1
+ )
+
+ KeyEvent.KEYCODE_DPAD_RIGHT -> gestureUiViewModel.requestDPad(
+ right = 1
+ )
+ }
+ },
+ onDPadClicked = {
+ gestureUiViewModel.requestDPadClick()
+ },
+ onScroll = { dX, dY, screenWidth, screenHeight ->
+ gestureUiViewModel.requestScroll(
+ dX,
+ dY,
+ screenWidth,
+ screenHeight
+ )
+ }
+ )
}
- )
- }
- // Buttons
- 1 -> {
- ButtonScreen(
- modifier = modifier,
- onKeyPressed = { keyEvent ->
- gestureUiViewModel.requestKeyEvent(keyEvent)
+ // Buttons
+ 1 -> {
+ ButtonScreen(
+ modifier = modifier.padding(contentPadding),
+ onKeyPressed = { keyEvent ->
+ gestureUiViewModel.requestKeyEvent(keyEvent)
+ }
+ )
}
- )
+ }
}
}
}
@@ -201,7 +214,7 @@ fun GesturesUi(
WearConnectionStatus.APPNOTINSTALLED -> {
// Open store on remote device
- gestureUiViewModel.openPlayStore(activity)
+ gestureUiViewModel.openPlayStore()
// Navigate
activity.startActivity(
@@ -219,16 +232,28 @@ fun GesturesUi(
GestureUIHelper.GestureStatusPath -> {
val status =
- event.data.getSerializable(WearableListenerViewModel.EXTRA_STATUS) as ActionStatus
+ event.data.getSerializableCompat(
+ WearableListenerViewModel.EXTRA_STATUS,
+ ActionStatus::class.java
+ )
if (status == ActionStatus.PERMISSION_DENIED) {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_permissiondenied)
+ confirmationViewModel.showOpenOnPhoneForFailure(
+ message = context.getString(
+ R.string.error_permissiondenied_wear
)
)
- gestureUiViewModel.openAppOnPhone(activity, false)
+ gestureUiViewModel.openAppOnPhone(false)
+ }
+ }
+
+ WearableListenerViewModel.ACTION_SHOWCONFIRMATION -> {
+ val jsonData =
+ event.data.getString(WearableListenerViewModel.EXTRA_ACTIONDATA)
+
+ JSONParser.deserializer(jsonData, ConfirmationData::class.java)?.let {
+ confirmationViewModel.showConfirmation(it)
}
}
}
@@ -280,7 +305,7 @@ private fun GestureScreen(
context.resources.displayMetrics.widthPixels
}
- val focusRequester = remember { FocusRequester() }
+ val focusRequester = rememberFocusRequester()
Box(
modifier = modifier
@@ -370,8 +395,8 @@ private fun GestureScreen(
.clickable(uiState.actionState.dpadSupported) {
onDPadDirection(KeyEvent.KEYCODE_DPAD_UP)
},
- imageVector = Icons.Filled.KeyboardArrowUp,
- tint = Color.White,
+ imageVector = Icons.Rounded.KeyboardArrowUp,
+ tint = MaterialTheme.colorScheme.primary,
contentDescription = stringResource(R.string.label_arrow_up)
)
Icon(
@@ -382,8 +407,8 @@ private fun GestureScreen(
.clickable(uiState.actionState.dpadSupported) {
onDPadDirection(KeyEvent.KEYCODE_DPAD_DOWN)
},
- imageVector = Icons.Filled.KeyboardArrowDown,
- tint = Color.White,
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ tint = MaterialTheme.colorScheme.primary,
contentDescription = stringResource(R.string.label_arrow_down)
)
Icon(
@@ -394,8 +419,8 @@ private fun GestureScreen(
.clickable(uiState.actionState.dpadSupported) {
onDPadDirection(KeyEvent.KEYCODE_DPAD_LEFT)
},
- imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft,
- tint = Color.White,
+ imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowLeft,
+ tint = MaterialTheme.colorScheme.primary,
contentDescription = stringResource(R.string.label_arrow_left)
)
Icon(
@@ -406,8 +431,8 @@ private fun GestureScreen(
.clickable(uiState.actionState.dpadSupported) {
onDPadDirection(KeyEvent.KEYCODE_DPAD_RIGHT)
},
- imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
- tint = Color.White,
+ imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
+ tint = MaterialTheme.colorScheme.primary,
contentDescription = stringResource(R.string.label_arrow_right)
)
if (uiState.actionState.dpadSupported) {
@@ -420,7 +445,7 @@ private fun GestureScreen(
) {
onDPadClicked()
}
- .background(Color.White, shape = RoundedCornerShape(50))
+ .background(MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(50))
)
}
@@ -449,23 +474,35 @@ private fun ButtonScreen(
verticalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterVertically),
overflow = FlowRowOverflow.Visible,
) {
- Button(
- imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
- contentDescription = stringResource(id = R.string.label_back),
+ FilledIconButton(
+ content = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = stringResource(id = R.string.label_back)
+ )
+ },
onClick = {
onKeyPressed(KeyEvent.KEYCODE_BACK)
}
)
- Button(
- imageVector = Icons.Outlined.Home,
- contentDescription = stringResource(id = R.string.label_home),
+ FilledIconButton(
+ content = {
+ Icon(
+ imageVector = Icons.Rounded.Home,
+ contentDescription = stringResource(id = R.string.label_home),
+ )
+ },
onClick = {
onKeyPressed(KeyEvent.KEYCODE_HOME)
}
)
- Button(
- id = R.drawable.ic_outline_view_apps,
- contentDescription = stringResource(id = R.string.label_recents),
+ FilledIconButton(
+ content = {
+ Icon(
+ painter = painterResource(R.drawable.ic_view_apps_filled),
+ contentDescription = stringResource(id = R.string.label_recents),
+ )
+ },
onClick = {
onKeyPressed(KeyEvent.KEYCODE_APP_SWITCH)
}
@@ -478,10 +515,11 @@ private fun ButtonScreen(
@WearPreviewFontScales
@Composable
private fun NoAccessibilityScreen(
+ modifier: Modifier = Modifier,
onRefresh: () -> Unit = {}
) {
Box(
- modifier = Modifier
+ modifier = modifier
.fillMaxSize()
.wrapContentHeight(),
contentAlignment = Alignment.Center
@@ -495,13 +533,13 @@ private fun NoAccessibilityScreen(
text = stringResource(R.string.message_accessibility_svc_disabled),
textAlign = TextAlign.Center
)
- CompactChip(
+ CompactButton(
label = {
Text(text = stringResource(id = R.string.action_refresh))
},
icon = {
Icon(
- painter = painterResource(id = R.drawable.ic_baseline_refresh_24),
+ imageVector = Icons.Rounded.Refresh,
contentDescription = stringResource(id = R.string.action_refresh)
)
},
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayer.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayer.kt
index 13a11dd9..ad53f538 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayer.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayer.kt
@@ -1,130 +1,122 @@
package com.thewizrd.simplewear.ui.simplewear
-import android.os.Build
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavType
import androidx.navigation.navArgument
-import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.TimeText
+import androidx.wear.compose.material3.TimeTextDefaults
import androidx.wear.compose.navigation.SwipeDismissableNavHost
import androidx.wear.compose.navigation.composable
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
-import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState
import com.thewizrd.shared_resources.actions.Actions
import com.thewizrd.shared_resources.actions.AudioStreamType
import com.thewizrd.shared_resources.utils.AnalyticsLogger
import com.thewizrd.shared_resources.utils.JSONParser
+import com.thewizrd.shared_resources.utils.getSerializableCompat
import com.thewizrd.simplewear.controls.AppItemViewModel
import com.thewizrd.simplewear.ui.navigation.Screen
import com.thewizrd.simplewear.ui.theme.WearAppTheme
-import com.thewizrd.simplewear.ui.theme.findActivity
@Composable
fun MediaPlayer(
startDestination: String = Screen.MediaPlayer.autoLaunch()
) {
WearAppTheme {
- val context = LocalContext.current
- val activity = context.findActivity()
-
val navController = rememberSwipeDismissableNavController()
- val swipeToDismissBoxState = rememberSwipeToDismissBoxState()
- val swipeDismissNavState = rememberSwipeDismissableNavHostState(
- swipeToDismissBoxState = swipeToDismissBoxState
- )
- SwipeDismissableNavHost(
- navController = navController,
- startDestination = startDestination,
- state = swipeDismissNavState
+ AppScaffold(
+ timeText = {
+ TimeText(backgroundColor = TimeTextDefaults.backgroundColor().copy(alpha = 0.5f))
+ }
) {
- composable(route = Screen.MediaPlayerList.route) {
- MediaPlayerListUi(navController = navController)
+ SwipeDismissableNavHost(
+ navController = navController,
+ startDestination = startDestination
+ ) {
+ composable(route = Screen.MediaPlayerList.route) {
+ MediaPlayerListUi(navController = navController)
- LaunchedEffect(navController) {
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.MediaPlayerList.route)
- })
+ LaunchedEffect(navController) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.MediaPlayerList.route)
+ })
+ }
}
- }
- composable(
- route = Screen.MediaPlayer.route + "?autoLaunch={autoLaunch}&app={app}",
- arguments = listOf(
- navArgument("autoLaunch") {
- type = NavType.BoolType
- defaultValue = false
- },
- navArgument("app") {
- type = NavType.StringType
- nullable = true
- }
- )
- ) { backstackEntry ->
- val autoLaunch = backstackEntry.arguments?.getBoolean("autoLaunch")
- val app = remember(backstackEntry) {
- JSONParser.deserializer(
- backstackEntry.arguments?.getString("app"),
- AppItemViewModel::class.java
+ composable(
+ route = Screen.MediaPlayer.route + "?autoLaunch={autoLaunch}&app={app}",
+ arguments = listOf(
+ navArgument("autoLaunch") {
+ type = NavType.BoolType
+ defaultValue = false
+ },
+ navArgument("app") {
+ type = NavType.StringType
+ nullable = true
+ }
)
- }
+ ) { backstackEntry ->
+ val autoLaunch = backstackEntry.arguments?.getBoolean("autoLaunch")
+ val app = remember(backstackEntry) {
+ JSONParser.deserializer(
+ backstackEntry.arguments?.getString("app"),
+ AppItemViewModel::class.java
+ )
+ }
- MediaPlayerUi(
- navController = navController,
- swipeToDismissBoxState = swipeToDismissBoxState,
- autoLaunch = autoLaunch ?: (app == null),
- app = app
- )
+ MediaPlayerUi(
+ navController = navController,
+ autoLaunch = autoLaunch ?: (app == null),
+ app = app
+ )
- LaunchedEffect(navController) {
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.MediaPlayer.route)
- app?.let {
- putString("app", it.packageName)
- }
- })
+ LaunchedEffect(navController) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.MediaPlayer.route)
+ app?.let {
+ putString("app", it.packageName)
+ }
+ })
+ }
}
- }
- composable(
- route = Screen.ValueAction.route + "/{actionId}?streamType={streamType}",
- arguments = listOf(
- navArgument("actionId") {
- type = NavType.IntType
- },
- navArgument("streamType") {
- type = NavType.EnumType(AudioStreamType::class.java)
- defaultValue = AudioStreamType.MUSIC
+ composable(
+ route = Screen.ValueAction.route + "/{actionId}?streamType={streamType}",
+ arguments = listOf(
+ navArgument("actionId") {
+ type = NavType.IntType
+ },
+ navArgument("streamType") {
+ type = NavType.EnumType(AudioStreamType::class.java)
+ defaultValue = AudioStreamType.MUSIC
+ }
+ )
+ ) { backstackEntry ->
+ val actionType = backstackEntry.arguments?.getInt("actionId")?.let {
+ Actions.valueOf(it)
}
- )
- ) { backstackEntry ->
- val actionType = backstackEntry.arguments?.getInt("actionId")?.let {
- Actions.valueOf(it)
- }
- val streamType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- backstackEntry.arguments?.getSerializable(
+ val streamType = backstackEntry.arguments?.getSerializableCompat(
"streamType",
AudioStreamType::class.java
)
- } else {
- backstackEntry.arguments?.getSerializable("streamType") as AudioStreamType
- }
- ValueActionScreen(
- actionType = actionType ?: Actions.VOLUME,
- audioStreamType = streamType
- )
+ ValueActionScreen(
+ actionType = actionType ?: Actions.VOLUME,
+ audioStreamType = streamType
+ )
- LaunchedEffect(navController, actionType) {
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.ValueAction.route)
- actionType?.let {
- putString("actionType", it.name)
- }
- })
+ LaunchedEffect(navController, actionType) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.ValueAction.route)
+ actionType?.let {
+ putString("actionType", it.name)
+ }
+ })
+ }
}
}
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerListUi.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerListUi.kt
index 667d5660..dde8c27f 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerListUi.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerListUi.kt
@@ -1,9 +1,6 @@
-@file:OptIn(ExperimentalWearFoundationApi::class, ExperimentalHorologistApi::class)
-
package com.thewizrd.simplewear.ui.simplewear
import android.content.Intent
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -14,7 +11,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ClearAll
+import androidx.compose.material.icons.rounded.FilterList
+import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -38,43 +38,42 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.NavOptions
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.items
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
-import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
-import androidx.wear.compose.foundation.rotary.rotaryScrollable
-import androidx.wear.compose.material.Checkbox
-import androidx.wear.compose.material.Chip
-import androidx.wear.compose.material.ChipDefaults
-import androidx.wear.compose.material.CompactChip
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.PositionIndicator
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.TimeText
-import androidx.wear.compose.material.ToggleChip
-import androidx.wear.compose.material.dialog.Dialog
-import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.foundation.pager.rememberPagerState
+import androidx.wear.compose.material3.AlertDialogContent
+import androidx.wear.compose.material3.AnimatedPage
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.CheckboxButton
+import androidx.wear.compose.material3.CompactButton
+import androidx.wear.compose.material3.Dialog
+import androidx.wear.compose.material3.EdgeButton
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.compose.layout.ScalingLazyColumn
-import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
-import com.google.android.horologist.compose.layout.ScalingLazyColumnState
-import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
-import com.google.android.horologist.compose.layout.scrollAway
-import com.google.android.horologist.compose.material.ListHeaderDefaults
-import com.google.android.horologist.compose.material.ResponsiveListHeader
+import com.google.android.horologist.compose.layout.ColumnItemType
+import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding
import com.thewizrd.shared_resources.actions.ActionStatus
import com.thewizrd.shared_resources.helpers.MediaHelper
import com.thewizrd.shared_resources.helpers.WearConnectionStatus
+import com.thewizrd.shared_resources.utils.JSONParser
+import com.thewizrd.shared_resources.utils.getSerializableCompat
import com.thewizrd.simplewear.PhoneSyncActivity
import com.thewizrd.simplewear.R
import com.thewizrd.simplewear.controls.AppItemViewModel
-import com.thewizrd.simplewear.helpers.showConfirmationOverlay
-import com.thewizrd.simplewear.preferences.Settings
import com.thewizrd.simplewear.ui.components.ConfirmationOverlay
+import com.thewizrd.simplewear.ui.components.HorizontalPagerScreen
import com.thewizrd.simplewear.ui.components.LoadingContent
-import com.thewizrd.simplewear.ui.components.SwipeToDismissPagerScreen
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
import com.thewizrd.simplewear.ui.navigation.Screen
import com.thewizrd.simplewear.ui.theme.findActivity
import com.thewizrd.simplewear.viewmodels.ConfirmationData
@@ -99,37 +98,28 @@ fun MediaPlayerListUi(
val confirmationViewModel = viewModel()
val confirmationData by confirmationViewModel.confirmationEventsFlow.collectAsState()
- val scrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Unspecified,
- last = ScalingLazyColumnDefaults.ItemType.Chip,
- )
- )
-
val pagerState = rememberPagerState(
initialPage = 0,
pageCount = { 2 }
)
- SwipeToDismissPagerScreen(
- state = pagerState,
+ HorizontalPagerScreen(
+ modifier = modifier,
+ pagerState = pagerState,
hidePagerIndicator = uiState.isLoading,
- timeText = {
- if (pagerState.currentPage == 0) {
- TimeText(modifier = Modifier.scrollAway { scrollState })
- }
- }
) { pageIdx ->
- if (pageIdx == 0) {
- MediaPlayerListScreen(
- mediaPlayerListViewModel = mediaPlayerListViewModel,
- navController = navController,
- scrollState = scrollState
- )
- } else {
- MediaPlayerListSettings(
- mediaPlayerListViewModel = mediaPlayerListViewModel
- )
+ AnimatedPage(pageIdx, pagerState) {
+ if (pageIdx == 0) {
+ MediaPlayerListScreen(
+ mediaPlayerListViewModel = mediaPlayerListViewModel,
+ confirmationViewModel = confirmationViewModel,
+ navController = navController
+ )
+ } else {
+ MediaPlayerListSettings(
+ mediaPlayerListViewModel = mediaPlayerListViewModel
+ )
+ }
}
}
@@ -168,7 +158,7 @@ fun MediaPlayerListUi(
WearConnectionStatus.APPNOTINSTALLED -> {
// Open store on remote device
- mediaPlayerListViewModel.openPlayStore(activity)
+ mediaPlayerListViewModel.openPlayStore()
// Navigate
activity.startActivity(
@@ -186,25 +176,28 @@ fun MediaPlayerListUi(
MediaHelper.MusicPlayersPath -> {
val status =
- event.data.getSerializable(WearableListenerViewModel.EXTRA_STATUS) as ActionStatus
+ event.data.getSerializableCompat(
+ WearableListenerViewModel.EXTRA_STATUS,
+ ActionStatus::class.java
+ )
if (status == ActionStatus.PERMISSION_DENIED) {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_permissiondenied)
+ confirmationViewModel.showOpenOnPhoneForFailure(
+ message = context.getString(
+ R.string.error_permissiondenied_wear
)
)
- mediaPlayerListViewModel.openAppOnPhone(
- activity,
- false
- )
+ mediaPlayerListViewModel.openAppOnPhone(false)
}
}
MediaHelper.MediaPlayerAutoLaunchPath -> {
val status =
- event.data.getSerializable(WearableListenerViewModel.EXTRA_STATUS) as ActionStatus
+ event.data.getSerializableCompat(
+ WearableListenerViewModel.EXTRA_STATUS,
+ ActionStatus::class.java
+ )
if (status == ActionStatus.SUCCESS) {
navController.navigate(
@@ -216,6 +209,15 @@ fun MediaPlayerListUi(
)
}
}
+
+ WearableListenerViewModel.ACTION_SHOWCONFIRMATION -> {
+ val jsonData =
+ event.data.getString(WearableListenerViewModel.EXTRA_ACTIONDATA)
+
+ JSONParser.deserializer(jsonData, ConfirmationData::class.java)?.let {
+ confirmationViewModel.showConfirmation(it)
+ }
+ }
}
}
}
@@ -231,18 +233,14 @@ fun MediaPlayerListUi(
@Composable
private fun MediaPlayerListScreen(
mediaPlayerListViewModel: MediaPlayerListViewModel,
- navController: NavController,
- scrollState: ScalingLazyColumnState
+ confirmationViewModel: ConfirmationViewModel,
+ navController: NavController
) {
- val context = LocalContext.current
- val activity = context.findActivity()
-
val lifecycleOwner = LocalLifecycleOwner.current
val uiState by mediaPlayerListViewModel.uiState.collectAsState()
MediaPlayerListScreen(
uiState = uiState,
- scrollState = scrollState,
onItemClicked = {
lifecycleOwner.lifecycleScope.launch {
val success = mediaPlayerListViewModel.startMediaApp(it)
@@ -256,7 +254,7 @@ private fun MediaPlayerListScreen(
.build()
)
} else {
- activity.showConfirmationOverlay(false)
+ confirmationViewModel.showFailure()
}
}
},
@@ -270,13 +268,21 @@ private fun MediaPlayerListScreen(
@Composable
private fun MediaPlayerListScreen(
uiState: MediaPlayerListUiState,
- scrollState: ScalingLazyColumnState = rememberResponsiveColumnState(),
onItemClicked: (AppItemViewModel) -> Unit = {},
onRefresh: () -> Unit = {}
) {
- Box(
- modifier = Modifier.fillMaxSize()
- ) {
+ val columnState = rememberTransformingLazyColumnState()
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
+ )
+ val transformationSpec = rememberTransformationSpec()
+
+ ScreenScaffold(
+ modifier = Modifier.fillMaxSize(),
+ scrollState = columnState,
+ contentPadding = contentPadding
+ ) { contentPadding ->
LoadingContent(
empty = uiState.mediaAppsSet.isEmpty(),
emptyContent = {
@@ -296,13 +302,13 @@ private fun MediaPlayerListScreen(
text = stringResource(id = R.string.error_nomusicplayers),
textAlign = TextAlign.Center
)
- CompactChip(
+ CompactButton(
label = {
Text(text = stringResource(id = R.string.action_refresh))
},
icon = {
Icon(
- painter = painterResource(id = R.drawable.ic_baseline_refresh_24),
+ imageVector = Icons.Rounded.Refresh,
contentDescription = stringResource(id = R.string.action_refresh)
)
},
@@ -313,12 +319,18 @@ private fun MediaPlayerListScreen(
},
loading = uiState.isLoading
) {
- ScalingLazyColumn(
+ TransformingLazyColumn(
modifier = Modifier.fillMaxSize(),
- columnState = scrollState,
+ state = columnState,
+ contentPadding = contentPadding
) {
item {
- ResponsiveListHeader(contentPadding = ListHeaderDefaults.firstItemPadding()) {
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec)
+ ) {
Text(text = stringResource(id = R.string.action_apps))
}
}
@@ -327,15 +339,18 @@ private fun MediaPlayerListScreen(
items = uiState.mediaAppsSet.toList(),
key = { Pair(it.activityName, it.packageName) }
) { mediaItem ->
- Chip(
- modifier = Modifier.fillMaxWidth(),
+ Button(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
label = {
Text(text = mediaItem.appLabel ?: "")
},
icon = mediaItem.bitmapIcon?.let {
{
Icon(
- modifier = Modifier.requiredSize(ChipDefaults.IconSize),
+ modifier = Modifier.requiredSize(ButtonDefaults.IconSize),
bitmap = it.asImageBitmap(),
contentDescription = mediaItem.appLabel,
tint = Color.Unspecified
@@ -343,9 +358,9 @@ private fun MediaPlayerListScreen(
}
},
colors = if (mediaItem.key == uiState.activePlayerKey) {
- ChipDefaults.gradientBackgroundChipColors()
+ ButtonDefaults.filledVariantButtonColors()
} else {
- ChipDefaults.secondaryChipColors()
+ ButtonDefaults.filledTonalButtonColors()
},
onClick = {
onItemClicked(mediaItem)
@@ -353,8 +368,6 @@ private fun MediaPlayerListScreen(
)
}
}
-
- PositionIndicator(scalingLazyListState = scrollState.state)
}
}
}
@@ -367,9 +380,6 @@ private fun MediaPlayerListSettings(
MediaPlayerListSettings(
uiState = uiState,
- onCheckChanged = {
- Settings.setAutoLaunchMediaCtrls(it)
- },
onCommitSelectedItems = {
mediaPlayerListViewModel.updateFilteredApps(it)
}
@@ -379,55 +389,55 @@ private fun MediaPlayerListSettings(
@Composable
private fun MediaPlayerListSettings(
uiState: MediaPlayerListUiState,
- onCheckChanged: (Boolean) -> Unit = {},
onCommitSelectedItems: (Set) -> Unit = {}
) {
- val scrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Unspecified,
- last = ScalingLazyColumnDefaults.ItemType.Chip,
- )
+ val columnState = rememberTransformingLazyColumnState()
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
)
+ val transformationSpec = rememberTransformationSpec()
var showFilterDialog by remember { mutableStateOf(false) }
- ScalingLazyColumn(
- columnState = scrollState
- ) {
- item {
- ResponsiveListHeader(
- modifier = Modifier.fillMaxWidth(),
- contentPadding = ListHeaderDefaults.firstItemPadding()
- ) {
- Text(text = stringResource(id = R.string.title_settings))
- }
- }
- item {
- Chip(
- modifier = Modifier.fillMaxWidth(),
- label = {
- Text(text = stringResource(id = R.string.title_filter_apps))
- },
- onClick = {
- showFilterDialog = true
- },
- colors = ChipDefaults.secondaryChipColors(),
- icon = {
- Icon(
- painter = painterResource(id = R.drawable.ic_baseline_filter_list_24),
- contentDescription = stringResource(id = R.string.title_filter_apps)
- )
+ ScreenScaffold(
+ scrollState = columnState,
+ contentPadding = contentPadding
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ state = columnState,
+ contentPadding = contentPadding
+ ) {
+ item {
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec)
+ ) {
+ Text(text = stringResource(id = R.string.title_settings))
}
- )
+ }
+ item {
+ FilledTonalButton(
+ modifier = Modifier.fillMaxWidth(),
+ label = {
+ Text(text = stringResource(id = R.string.title_filter_apps))
+ },
+ onClick = {
+ showFilterDialog = true
+ },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.FilterList,
+ contentDescription = stringResource(id = R.string.title_filter_apps)
+ )
+ }
+ )
+ }
}
- }
- val dialogScrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Unspecified,
- last = ScalingLazyColumnDefaults.ItemType.Chip,
- )
- )
+ }
var selectedItems by remember(uiState.filteredAppsList) {
mutableStateOf(uiState.filteredAppsList)
@@ -435,18 +445,15 @@ private fun MediaPlayerListSettings(
Dialog(
modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colors.background),
- showDialog = showFilterDialog,
+ .fillMaxSize(),
+ visible = showFilterDialog,
onDismissRequest = {
onCommitSelectedItems.invoke(selectedItems)
showFilterDialog = false
- },
- scrollState = dialogScrollState.state
+ }
) {
MediaPlayerFilterScreen(
uiState = uiState,
- dialogScrollState = dialogScrollState,
selectedItems = selectedItems,
onSelectedItemsChanged = {
selectedItems = it
@@ -456,43 +463,39 @@ private fun MediaPlayerListSettings(
showFilterDialog = false
}
)
-
- LaunchedEffect(showFilterDialog) {
- dialogScrollState.state.scrollToItem(0)
- }
}
}
@Composable
private fun MediaPlayerFilterScreen(
uiState: MediaPlayerListUiState,
- dialogScrollState: ScalingLazyColumnState = rememberResponsiveColumnState(),
selectedItems: Set = emptySet(),
onSelectedItemsChanged: (Set) -> Unit = {},
onDismissRequest: () -> Unit = {}
) {
- ScalingLazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .rotaryScrollable(
- focusRequester = rememberActiveFocusRequester(),
- behavior = RotaryScrollableDefaults.behavior(dialogScrollState)
- ),
- columnState = dialogScrollState,
- ) {
- item {
- ResponsiveListHeader(
- modifier = Modifier.fillMaxWidth(),
- contentPadding = ListHeaderDefaults.firstItemPadding()
- ) {
- Text(text = stringResource(id = R.string.title_filter_apps))
- }
+ AlertDialogContent(
+ modifier = Modifier.fillMaxSize(),
+ title = {
+ Text(text = stringResource(id = R.string.title_filter_apps))
+ },
+ edgeButton = {
+ EdgeButton(
+ content = {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_check_white_24dp),
+ contentDescription = stringResource(id = android.R.string.ok)
+ )
+ },
+ onClick = {
+ onDismissRequest.invoke()
+ }
+ )
}
-
+ ) {
items(uiState.allMediaAppsSet.toList()) {
val isChecked = selectedItems.contains(it.packageName!!)
- ToggleChip(
+ CheckboxButton(
modifier = Modifier.fillMaxWidth(),
checked = isChecked,
onCheckedChange = { checked ->
@@ -508,11 +511,6 @@ private fun MediaPlayerFilterScreen(
Text(
text = it.appLabel!!
)
- },
- toggleControl = {
- Checkbox(
- checked = isChecked
- )
}
)
}
@@ -522,43 +520,20 @@ private fun MediaPlayerFilterScreen(
}
item {
- Chip(
+ FilledTonalButton(
modifier = Modifier.fillMaxWidth(),
label = {
Text(text = stringResource(id = R.string.clear_all))
},
icon = {
Icon(
- painter = painterResource(id = R.drawable.ic_clear_all_24dp),
+ imageVector = Icons.Rounded.ClearAll,
contentDescription = stringResource(id = R.string.clear_all)
)
},
onClick = {
onSelectedItemsChanged.invoke(emptySet())
- },
- colors = ChipDefaults.secondaryChipColors(
- backgroundColor = MaterialTheme.colors.onBackground,
- contentColor = MaterialTheme.colors.background
- )
- )
- }
-
- item {
- Chip(
- modifier = Modifier.fillMaxWidth(),
- label = {
- Text(text = stringResource(id = android.R.string.ok))
- },
- icon = {
- Icon(
- painter = painterResource(id = R.drawable.ic_check_white_24dp),
- contentDescription = stringResource(id = android.R.string.ok)
- )
- },
- onClick = {
- onDismissRequest.invoke()
- },
- colors = ChipDefaults.primaryChipColors()
+ }
)
}
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerUi.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerUi.kt
index 78b353c7..387ae4f5 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerUi.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerUi.kt
@@ -1,49 +1,64 @@
-@file:OptIn(
- ExperimentalHorologistApi::class, ExperimentalFoundationApi::class,
- ExperimentalWearFoundationApi::class, ExperimentalWearMaterialApi::class
-)
+@file:OptIn(ExperimentalHorologistApi::class)
package com.thewizrd.simplewear.ui.simplewear
import android.content.Intent
-import androidx.compose.foundation.ExperimentalFoundationApi
+import android.graphics.Bitmap
+import android.widget.Toast
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
@@ -52,50 +67,67 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.NavOptions
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
-import androidx.wear.compose.foundation.SwipeToDismissBoxState
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.items
-import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.foundation.pager.rememberPagerState
import androidx.wear.compose.foundation.rotary.rotaryScrollable
-import androidx.wear.compose.material.ButtonDefaults
-import androidx.wear.compose.material.ChipDefaults
-import androidx.wear.compose.material.CompactChip
-import androidx.wear.compose.material.ExperimentalWearMaterialApi
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.TimeText
-import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.ButtonGroup
+import androidx.wear.compose.material3.ButtonGroupDefaults
+import androidx.wear.compose.material3.CompactButton
+import androidx.wear.compose.material3.FilledIconButton
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.IconButtonDefaults
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ListHeaderDefaults
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.PageIndicatorDefaults
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+import androidx.wear.compose.material3.touchTargetAwareSize
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.audio.ui.VolumePositionIndicator
import com.google.android.horologist.audio.ui.VolumeUiState
import com.google.android.horologist.audio.ui.VolumeViewModel
-import com.google.android.horologist.audio.ui.components.actions.SettingsButton
-import com.google.android.horologist.audio.ui.volumeRotaryBehavior
+import com.google.android.horologist.audio.ui.material3.VolumeLevelIndicator
+import com.google.android.horologist.audio.ui.material3.components.actions.SettingsButton
+import com.google.android.horologist.audio.ui.material3.components.actions.SettingsButtonDefaults
+import com.google.android.horologist.audio.ui.material3.volumeRotaryBehavior
import com.google.android.horologist.compose.ambient.AmbientAware
import com.google.android.horologist.compose.ambient.AmbientState
-import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
-import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
-import com.google.android.horologist.compose.layout.scrollAway
-import com.google.android.horologist.compose.material.Chip
+import com.google.android.horologist.compose.layout.ColumnItemType
+import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding
+import com.google.android.horologist.images.base.paintable.BitmapPaintable.Companion.asPaintable
import com.google.android.horologist.media.model.PlaybackStateEvent
import com.google.android.horologist.media.model.TimestampProvider
-import com.google.android.horologist.media.ui.components.ControlButtonLayout
-import com.google.android.horologist.media.ui.components.animated.AnimatedMediaControlButtons
-import com.google.android.horologist.media.ui.components.animated.MarqueeTextMediaDisplay
-import com.google.android.horologist.media.ui.components.controls.MediaButton
-import com.google.android.horologist.media.ui.components.display.LoadingMediaDisplay
-import com.google.android.horologist.media.ui.components.display.NothingPlayingDisplay
-import com.google.android.horologist.media.ui.screens.player.PlayerScreen
+import com.google.android.horologist.media.ui.material3.components.ButtonGroupLayoutDefaults
+import com.google.android.horologist.media.ui.material3.components.ambient.AmbientMediaControlButtons
+import com.google.android.horologist.media.ui.material3.components.ambient.AmbientMediaInfoDisplay
+import com.google.android.horologist.media.ui.material3.components.ambient.AmbientSeekToNextButton
+import com.google.android.horologist.media.ui.material3.components.ambient.AmbientSeekToPreviousButton
+import com.google.android.horologist.media.ui.material3.components.animated.AnimatedMediaControlButtons
+import com.google.android.horologist.media.ui.material3.components.animated.AnimatedMediaInfoDisplay
+import com.google.android.horologist.media.ui.material3.components.background.ArtworkImageBackground
+import com.google.android.horologist.media.ui.material3.components.display.TextMediaDisplay
+import com.google.android.horologist.media.ui.material3.screens.player.PlayerScreen
import com.google.android.horologist.media.ui.state.LocalTimestampProvider
import com.google.android.horologist.media.ui.state.mapper.TrackPositionUiModelMapper
+import com.google.android.horologist.media.ui.state.model.MediaUiModel
import com.thewizrd.shared_resources.actions.ActionStatus
+import com.thewizrd.shared_resources.actions.Actions
import com.thewizrd.shared_resources.actions.AudioStreamState
import com.thewizrd.shared_resources.actions.AudioStreamType
import com.thewizrd.shared_resources.helpers.MediaHelper
import com.thewizrd.shared_resources.helpers.WearConnectionStatus
import com.thewizrd.shared_resources.media.PlaybackState
+import com.thewizrd.shared_resources.utils.JSONParser
+import com.thewizrd.shared_resources.utils.getSerializableCompat
import com.thewizrd.simplewear.PhoneSyncActivity
import com.thewizrd.simplewear.R
import com.thewizrd.simplewear.controls.AppItemViewModel
@@ -111,11 +143,15 @@ import com.thewizrd.simplewear.media.PlayerUiController
import com.thewizrd.simplewear.media.toPlaybackStateEvent
import com.thewizrd.simplewear.ui.ambient.ambientMode
import com.thewizrd.simplewear.ui.components.ConfirmationOverlay
+import com.thewizrd.simplewear.ui.components.HorizontalPagerScreen
import com.thewizrd.simplewear.ui.components.LoadingContent
-import com.thewizrd.simplewear.ui.components.ScalingLazyColumn
-import com.thewizrd.simplewear.ui.components.SwipeToDismissPagerScreen
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
import com.thewizrd.simplewear.ui.navigation.Screen
import com.thewizrd.simplewear.ui.theme.findActivity
+import com.thewizrd.simplewear.ui.utils.DynamicThemePrimaryColorsFromImage
+import com.thewizrd.simplewear.ui.utils.MinContrastOfPrimaryVsBackground
+import com.thewizrd.simplewear.ui.utils.contrastAgainst
+import com.thewizrd.simplewear.ui.utils.rememberDominantColorState
import com.thewizrd.simplewear.ui.utils.rememberFocusRequester
import com.thewizrd.simplewear.viewmodels.ConfirmationData
import com.thewizrd.simplewear.viewmodels.ConfirmationViewModel
@@ -123,13 +159,13 @@ import com.thewizrd.simplewear.viewmodels.WearableListenerViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@Composable
fun MediaPlayerUi(
modifier: Modifier = Modifier,
navController: NavController,
- swipeToDismissBoxState: SwipeToDismissBoxState = rememberSwipeToDismissBoxState(),
app: AppItemViewModel? = null,
autoLaunch: Boolean = (app == null),
) {
@@ -150,8 +186,6 @@ fun MediaPlayerUi(
val confirmationViewModel = viewModel()
val confirmationData by confirmationViewModel.confirmationEventsFlow.collectAsState()
- val isRoot = navController.previousBackStackEntry == null
-
val pagerState = rememberPagerState(
initialPage = 0,
pageCount = { mediaPagerState.pageCount }
@@ -190,54 +224,52 @@ fun MediaPlayerUi(
}
}
- SwipeToDismissPagerScreen(
- modifier = modifier,
- isRoot = isRoot,
- swipeToDismissBoxState = swipeToDismissBoxState,
- state = pagerState,
- hidePagerIndicator = ambientState.isAmbient || uiState.isLoading || !uiState.isPlayerAvailable,
- timeText = {
- if (pagerState.currentPage == 0) {
- TimeText()
- }
- },
- pagerKey = keyFunc
- ) { pageIdx ->
- val key = keyFunc(pageIdx)
-
- when (key) {
- MediaPageType.Player -> {
- MediaPlayerControlsPage(
- mediaPlayerViewModel = mediaPlayerViewModel,
- volumeViewModel = volumeViewModel,
- navController = navController,
- ambientState = ambientState
- )
- }
+ PlayerDynamicTheme(
+ uiState = uiState
+ ) {
+ HorizontalPagerScreen(
+ modifier = modifier,
+ pagerState = pagerState,
+ hidePagerIndicator = ambientState.isAmbient || uiState.isLoading || !uiState.isPlayerAvailable,
+ pagerKey = keyFunc,
+ pagerIndicatorBackgroundColor = PageIndicatorDefaults.backgroundColor.copy(alpha = 0.5f)
+ ) { pageIdx ->
+ val key = keyFunc(pageIdx)
+
+ when (key) {
+ MediaPageType.Player -> {
+ MediaPlayerControlsPage(
+ mediaPlayerViewModel = mediaPlayerViewModel,
+ volumeViewModel = volumeViewModel,
+ navController = navController,
+ ambientState = ambientState
+ )
+ }
- MediaPageType.CustomControls -> {
- MediaCustomControlsPage(
- mediaPlayerViewModel = mediaPlayerViewModel
- )
- }
+ MediaPageType.CustomControls -> {
+ MediaCustomControlsPage(
+ mediaPlayerViewModel = mediaPlayerViewModel
+ )
+ }
- MediaPageType.Browser -> {
- MediaBrowserPage(
- mediaPlayerViewModel = mediaPlayerViewModel
- )
- }
+ MediaPageType.Browser -> {
+ MediaBrowserPage(
+ mediaPlayerViewModel = mediaPlayerViewModel
+ )
+ }
- MediaPageType.Queue -> {
- MediaQueuePage(
- mediaPlayerViewModel = mediaPlayerViewModel
- )
+ MediaPageType.Queue -> {
+ MediaQueuePage(
+ mediaPlayerViewModel = mediaPlayerViewModel
+ )
+ }
}
- }
- LaunchedEffect(pagerState, pagerState.targetPage, pagerState.currentPage) {
- val targetPageKey = keyFunc(pagerState.targetPage)
- if (mediaPagerState.currentPageKey != targetPageKey) {
- mediaPlayerViewModel.updateCurrentPage(targetPageKey)
+ LaunchedEffect(pagerState, pagerState.targetPage, pagerState.currentPage) {
+ val targetPageKey = keyFunc(pagerState.targetPage)
+ if (mediaPagerState.currentPageKey != targetPageKey) {
+ mediaPlayerViewModel.updateCurrentPage(targetPageKey)
+ }
}
}
}
@@ -290,7 +322,7 @@ fun MediaPlayerUi(
WearConnectionStatus.APPNOTINSTALLED -> {
// Open store on remote device
- mediaPlayerViewModel.openPlayStore(activity)
+ mediaPlayerViewModel.openPlayStore()
// Navigate
activity.startActivity(
@@ -309,29 +341,40 @@ fun MediaPlayerUi(
MediaHelper.MediaPlayerConnectPath,
MediaHelper.MediaPlayerAutoLaunchPath -> {
val actionStatus =
- event.data.getSerializable(WearableListenerViewModel.EXTRA_STATUS) as ActionStatus
+ event.data.getSerializableCompat(
+ WearableListenerViewModel.EXTRA_STATUS,
+ ActionStatus::class.java
+ )
if (actionStatus == ActionStatus.PERMISSION_DENIED) {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_permissiondenied)
+ confirmationViewModel.showOpenOnPhoneForFailure(
+ message = context.getString(
+ R.string.error_permissiondenied_wear
)
)
- mediaPlayerViewModel.openAppOnPhone(activity, false)
+ mediaPlayerViewModel.openAppOnPhone(false)
}
}
MediaHelper.MediaPlayPath -> {
val actionStatus =
- event.data.getSerializable(WearableListenerViewModel.EXTRA_STATUS) as ActionStatus
+ event.data.getSerializableCompat(
+ WearableListenerViewModel.EXTRA_STATUS,
+ ActionStatus::class.java
+ )
if (actionStatus == ActionStatus.TIMEOUT) {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_playback_failed)
- )
- )
+ confirmationViewModel.showFailure(message = context.getString(R.string.error_playback_failed))
+ }
+ }
+
+ WearableListenerViewModel.ACTION_SHOWCONFIRMATION -> {
+ val jsonData =
+ event.data.getString(WearableListenerViewModel.EXTRA_ACTIONDATA)
+
+ JSONParser.deserializer(jsonData, ConfirmationData::class.java)?.let {
+ confirmationViewModel.showConfirmation(it)
}
}
}
@@ -396,6 +439,11 @@ private fun MediaPlayerControlsPage(
.build()
)
},
+ onOpenVolume = {
+ navController.navigate(
+ Screen.ValueAction.getRoute(Actions.VOLUME, AudioStreamType.MUSIC)
+ )
+ },
onVolumeUp = {
volumeViewModel.increaseVolume()
},
@@ -422,6 +470,7 @@ private fun MediaPlayerControlsPage(
ambientState: AmbientState = AmbientState.Interactive,
onRefresh: () -> Unit = {},
onOpenPlayerList: () -> Unit = {},
+ onOpenVolume: () -> Unit = {},
onVolumeUp: () -> Unit = {},
onVolumeDown: () -> Unit = {},
playerUiController: PlayerUiController = NoopPlayerUiController(),
@@ -433,92 +482,114 @@ private fun MediaPlayerControlsPage(
// Progress
val timestampProvider = remember { TimestampProvider { System.currentTimeMillis() } }
- LoadingContent(
- empty = !uiState.isPlayerAvailable && !isAmbient,
- emptyContent = {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .wrapContentHeight(),
- contentAlignment = Alignment.Center
- ) {
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
+ ScreenScaffold(
+ modifier = Modifier.fillMaxSize(),
+ scrollIndicator = {
+ VolumeLevelIndicator(
+ volumeUiState = { volumeUiState },
+ displayIndicatorEvents = displayVolumeIndicatorEvents
+ )
+ }
+ ) { contentPadding ->
+ LoadingContent(
+ empty = !uiState.isPlayerAvailable && !isAmbient,
+ emptyContent = {
+ Box(
+ modifier = Modifier
+ .padding(contentPadding)
+ .fillMaxSize()
+ .wrapContentHeight(),
+ contentAlignment = Alignment.Center
) {
- Text(
- modifier = Modifier.padding(horizontal = 14.dp),
- text = stringResource(id = R.string.error_nomusicplayers),
- textAlign = TextAlign.Center
- )
- CompactChip(
- label = {
- Text(text = stringResource(id = R.string.action_retry))
- },
- icon = {
- Icon(
- painter = painterResource(id = R.drawable.ic_baseline_refresh_24),
- contentDescription = stringResource(id = R.string.action_retry)
- )
- },
- onClick = onRefresh
- )
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 14.dp),
+ text = stringResource(id = R.string.error_nomusicplayers),
+ textAlign = TextAlign.Center
+ )
+ CompactButton(
+ label = {
+ Text(text = stringResource(id = R.string.action_retry))
+ },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.Refresh,
+ contentDescription = stringResource(id = R.string.action_retry)
+ )
+ },
+ onClick = onRefresh
+ )
+ }
}
- }
- },
- loading = uiState.isLoading && !isAmbient
- ) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center,
+ },
+ loading = uiState.isLoading && !isAmbient
) {
- PlayerScreen(
- modifier = modifier,
- mediaDisplay = {
- if (uiState.isPlaybackLoading && !isAmbient) {
- LoadingMediaDisplay()
- } else if (!playerState.isEmpty()) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ PlayerScreen(
+ modifier = modifier,
+ mediaDisplay = {
+ val mediaUiModel = remember(playerState, uiState.isPlaybackLoading) {
+ if (!playerState.isEmpty() && !uiState.isPlaybackLoading) {
+ MediaUiModel.Ready(
+ id = "",
+ title = playerState.title ?: "",
+ subtitle = playerState.artist ?: ""
+ )
+ } else {
+ MediaUiModel.Loading
+ }
+ }
+
if (!isAmbient) {
- MarqueeTextMediaDisplay(
- title = playerState.title,
- artist = playerState.artist
+ AnimatedMediaInfoDisplay(
+ media = mediaUiModel,
+ loading = uiState.isPlaybackLoading
)
} else {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = playerState.title.orEmpty(),
- modifier = Modifier
- .fillMaxWidth(0.7f)
- .padding(top = 2.dp, bottom = .8.dp),
- color = MaterialTheme.colors.onBackground,
- textAlign = TextAlign.Center,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1,
- style = MaterialTheme.typography.button,
- )
- Text(
- text = playerState.artist.orEmpty(),
- modifier = Modifier
- .fillMaxWidth(0.8f)
- .padding(top = 2.dp, bottom = .6.dp),
- color = MaterialTheme.colors.onBackground,
- textAlign = TextAlign.Center,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1,
- style = MaterialTheme.typography.body2,
+ AmbientMediaInfoDisplay(
+ media = mediaUiModel,
+ loading = uiState.isPlaybackLoading
+ )
+ }
+ },
+ controlButtons = {
+ if (!isAmbient) {
+ CompositionLocalProvider(LocalTimestampProvider provides timestampProvider) {
+ AnimatedMediaControlButtons(
+ onPlayButtonClick = {
+ playerUiController.play()
+ },
+ onPauseButtonClick = {
+ playerUiController.pause()
+ },
+ playPauseButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING,
+ playing = playerState.playbackState == PlaybackState.PLAYING,
+ onSeekToPreviousButtonClick = {
+ playerUiController.skipToPreviousMedia()
+ },
+ seekToPreviousButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING,
+ onSeekToNextButtonClick = {
+ playerUiController.skipToNextMedia()
+ },
+ seekToNextButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING,
+ trackPositionUiModel = TrackPositionUiModelMapper.map(
+ playbackStateEvent
+ )
)
}
- }
- } else {
- NothingPlayingDisplay()
- }
- },
- controlButtons = {
- if (!isAmbient) {
- CompositionLocalProvider(LocalTimestampProvider provides timestampProvider) {
- AnimatedMediaControlButtons(
+ } else {
+ val leftButtonPadding =
+ ButtonGroupLayoutDefaults.getSideButtonsPadding(isLeftButton = true)
+ val rightButtonPadding =
+ ButtonGroupLayoutDefaults.getSideButtonsPadding(isLeftButton = false)
+
+ AmbientMediaControlButtons(
onPlayButtonClick = {
playerUiController.play()
},
@@ -527,123 +598,296 @@ private fun MediaPlayerControlsPage(
},
playPauseButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING,
playing = playerState.playbackState == PlaybackState.PLAYING,
- onSeekToPreviousButtonClick = {
- playerUiController.skipToPreviousMedia()
- },
- seekToPreviousButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING,
- onSeekToNextButtonClick = {
- playerUiController.skipToNextMedia()
- },
- seekToNextButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING,
- trackPositionUiModel = TrackPositionUiModelMapper.map(
- playbackStateEvent
- )
- )
- }
- } else {
- ControlButtonLayout(
- leftButton = {},
- middleButton = {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .clip(CircleShape),
- contentAlignment = Alignment.Center,
- ) {
- if (playerState.playbackState == PlaybackState.PLAYING) {
- MediaButton(
- onClick = {},
- icon = ImageVector.vectorResource(id = R.drawable.ic_outline_pause_24),
- contentDescription = stringResource(id = R.string.horologist_pause_button_content_description)
- )
- } else {
- MediaButton(
- onClick = {},
- icon = ImageVector.vectorResource(id = R.drawable.ic_outline_play_arrow_24),
- contentDescription = stringResource(id = R.string.horologist_play_button_content_description)
- )
- }
- }
- },
- rightButton = {}
- )
- }
- },
- buttons = {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- if (!isAmbient) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- SettingsButton(
- onClick = onVolumeDown,
- imageVector = ImageVector.vectorResource(id = R.drawable.ic_baseline_volume_down_24),
- contentDescription = stringResource(R.string.horologist_volume_screen_volume_down_content_description),
- tapTargetSize = ButtonDefaults.ExtraSmallButtonSize
- )
- Spacer(modifier = Modifier.width(10.dp))
- uiState.mediaPlayerDetails.bitmapIcon?.let {
- Image(
- modifier = Modifier
- .size(32.dp)
- .clickable(onClick = onOpenPlayerList),
- bitmap = it.asImageBitmap(),
- contentDescription = stringResource(R.string.desc_open_player_list)
+ leftButton = {
+ AmbientSeekToPreviousButton(
+ onClick = {
+ playerUiController.skipToPreviousMedia()
+ },
+ buttonPadding = leftButtonPadding,
+ enabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING,
)
- } ?: run {
- Image(
- modifier = Modifier
- .size(32.dp)
- .clickable(onClick = onOpenPlayerList),
- painter = painterResource(R.drawable.ic_play_circle_filled_white_24dp),
- contentDescription = stringResource(R.string.desc_open_player_list)
+ },
+ rightButton = {
+ AmbientSeekToNextButton(
+ onClick = {
+ playerUiController.skipToNextMedia()
+ },
+ buttonPadding = rightButtonPadding,
+ enabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING,
)
}
- Spacer(modifier = Modifier.width(10.dp))
- SettingsButton(
- onClick = onVolumeUp,
- imageVector = ImageVector.vectorResource(id = R.drawable.ic_volume_up_white_24dp),
- contentDescription = stringResource(R.string.horologist_volume_screen_volume_up_content_description),
- tapTargetSize = ButtonDefaults.ExtraSmallButtonSize
- )
- }
+ )
}
- }
- },
- background = {
- playerState.artworkBitmap?.takeUnless { isAmbient }?.let {
- Image(
+ },
+ buttons = {
+ SettingsButtonsLayout(
modifier = Modifier.fillMaxSize(),
- bitmap = it.asImageBitmap(),
- colorFilter = ColorFilter.tint(
- Color.Black.copy(alpha = 0.66f),
- BlendMode.SrcAtop
+ isAmbient = isAmbient,
+ leftButton = SettingsButtonData(
+ onClick = onVolumeDown,
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_baseline_volume_down_24),
+ contentDescription = stringResource(R.string.horologist_volume_screen_volume_down_content_description)
+ ),
+ brandImage = BrandImageData(
+ bitmap = uiState.mediaPlayerDetails.bitmapIcon,
+ onClick = onOpenPlayerList
),
- contentDescription = stringResource(R.string.desc_artwork)
+ rightButton = SettingsButtonData(
+ onClick = onVolumeUp,
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_volume_up_white_24dp),
+ contentDescription = stringResource(R.string.horologist_volume_screen_volume_up_content_description)
+ ),
+ onOpenVolume = onOpenVolume
+ )
+ },
+ background = {
+ ArtworkImageBackground(
+ artwork = playerState.artworkBitmap?.takeUnless { isAmbient }
+ ?.asPaintable()
)
}
+ )
+
+ LaunchedEffect(uiState, uiState.pagerState) {
+ if (uiState.pagerState.currentPageKey == MediaPageType.Player) {
+ delay(500)
+ focusRequester.requestFocus()
+ }
}
- )
+ }
+ }
+ }
+}
- VolumePositionIndicator(
- volumeUiState = { volumeUiState },
- displayIndicatorEvents = displayVolumeIndicatorEvents
+private data class SettingsButtonData(
+ val imageVector: ImageVector,
+ val contentDescription: String,
+ val onClick: () -> Unit
+)
+
+private data class BrandImageData(
+ val imageVector: ImageVector? = null,
+ val painter: Painter? = null,
+ val bitmap: Bitmap? = null,
+ val onClick: () -> Unit
+)
+
+@Composable
+private fun SettingsButtonsLayout(
+ modifier: Modifier = Modifier,
+ isAmbient: Boolean,
+ leftButton: SettingsButtonData,
+ brandImage: BrandImageData,
+ rightButton: SettingsButtonData,
+ onOpenVolume: () -> Unit = {},
+) {
+ val isRound = LocalConfiguration.current.isScreenRound
+
+ if (isRound) {
+ RoundSettingsButtonsLayout(
+ modifier = modifier,
+ isAmbient = isAmbient,
+ leftButton = leftButton,
+ brandImage = brandImage,
+ rightButton = rightButton,
+ onOpenVolume = onOpenVolume
+ )
+ } else {
+ SimpleSettingsButtonsLayout(
+ modifier = modifier,
+ isAmbient = isAmbient,
+ leftButton = leftButton,
+ brandImage = brandImage,
+ rightButton = rightButton
+ )
+ }
+}
+
+@Composable
+private fun SimpleSettingsButtonsLayout(
+ modifier: Modifier = Modifier,
+ isAmbient: Boolean,
+ leftButton: SettingsButtonData,
+ brandImage: BrandImageData,
+ rightButton: SettingsButtonData,
+) {
+ Box(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Spacer(modifier = Modifier.fillMaxWidth(0.11f))
+ SettingsButton(
+ modifier = Modifier.weight(1f),
+ onClick = leftButton.onClick,
+ imageVector = leftButton.imageVector,
+ contentDescription = leftButton.contentDescription,
+ iconSize = ButtonDefaults.ExtraSmallIconSize,
+ buttonColors = if (!isAmbient) {
+ SettingsButtonDefaults.buttonColors()
+ } else {
+ SettingsButtonDefaults.ambientButtonColors()
+ },
+ border = if (!isAmbient) {
+ null
+ } else {
+ SettingsButtonDefaults.ambientButtonBorder(true)
+ }
)
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxSize()
+ .clickable(onClick = brandImage.onClick),
+ contentAlignment = Alignment.Center
+ ) {
+ BrandImage(brandImage)
+ }
+ SettingsButton(
+ modifier = Modifier.weight(1f),
+ onClick = rightButton.onClick,
+ imageVector = rightButton.imageVector,
+ contentDescription = rightButton.contentDescription,
+ iconSize = ButtonDefaults.ExtraSmallIconSize,
+ buttonColors = if (!isAmbient) {
+ SettingsButtonDefaults.buttonColors()
+ } else {
+ SettingsButtonDefaults.ambientButtonColors()
+ },
+ border = if (!isAmbient) {
+ null
+ } else {
+ SettingsButtonDefaults.ambientButtonBorder(true)
+ }
+ )
+ Spacer(modifier = Modifier.fillMaxWidth(0.11f))
+ }
+ }
+}
- LaunchedEffect(uiState, uiState.pagerState) {
- if (uiState.pagerState.currentPageKey == MediaPageType.Player) {
- delay(500)
- focusRequester.requestFocus()
+@Composable
+private fun RoundSettingsButtonsLayout(
+ modifier: Modifier = Modifier,
+ isAmbient: Boolean,
+ leftButton: SettingsButtonData,
+ brandImage: BrandImageData,
+ rightButton: SettingsButtonData,
+ onOpenVolume: () -> Unit = {},
+) {
+ val screenHeightDp = LocalConfiguration.current.screenHeightDp
+ val isLargeWidth = LocalConfiguration.current.screenWidthDp >= 225
+ val horizontalSpacerFraction = if (isLargeWidth) 0.11f else 0.145f
+
+ BoxWithConstraints(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Row(
+ modifier = Modifier.padding(bottom = (maxHeight * 0.012f)),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Spacer(modifier = Modifier.fillMaxWidth(horizontalSpacerFraction))
+ if (isLargeWidth) {
+ SettingsButton(
+ modifier = Modifier.weight(1f),
+ alignment = Alignment.TopCenter,
+ onClick = leftButton.onClick,
+ imageVector = leftButton.imageVector,
+ contentDescription = leftButton.contentDescription,
+ iconSize = ButtonDefaults.ExtraSmallIconSize,
+ buttonColors = if (!isAmbient) {
+ SettingsButtonDefaults.buttonColors()
+ } else {
+ SettingsButtonDefaults.ambientButtonColors()
+ },
+ border = if (!isAmbient) {
+ null
+ } else {
+ SettingsButtonDefaults.ambientButtonBorder(true)
+ }
+ )
+ }
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxSize()
+ .padding(
+ bottom = if (isLargeWidth) {
+ (screenHeightDp * 0.03f).dp
+ } else {
+ 0.dp
+ }
+ )
+ .clickable(
+ onClick = brandImage.onClick,
+ indication = null,
+ interactionSource = null,
+ role = Role.Button
+ ),
+ contentAlignment = if (isLargeWidth) {
+ Alignment.BottomCenter
+ } else {
+ Alignment.TopCenter
}
+ ) {
+ BrandImage(brandImage)
}
+ SettingsButton(
+ modifier = Modifier.weight(1f),
+ alignment = Alignment.TopCenter,
+ onClick = if (isLargeWidth) rightButton.onClick else onOpenVolume,
+ imageVector = rightButton.imageVector,
+ contentDescription = rightButton.contentDescription,
+ iconSize = ButtonDefaults.ExtraSmallIconSize,
+ buttonColors = if (!isAmbient) {
+ SettingsButtonDefaults.buttonColors()
+ } else {
+ SettingsButtonDefaults.ambientButtonColors()
+ },
+ border = if (!isAmbient) {
+ null
+ } else {
+ SettingsButtonDefaults.ambientButtonBorder(true)
+ }
+ )
+ Spacer(modifier = Modifier.fillMaxWidth(horizontalSpacerFraction))
}
}
}
+@Composable
+private fun BrandImage(data: BrandImageData) {
+ if (data.imageVector != null) {
+ Image(
+ modifier = Modifier.touchTargetAwareSize(IconButtonDefaults.LargeIconSize),
+ imageVector = data.imageVector,
+ contentDescription = stringResource(R.string.desc_open_player_list)
+ )
+ } else if (data.painter != null) {
+ Image(
+ modifier = Modifier.touchTargetAwareSize(IconButtonDefaults.LargeIconSize),
+ painter = data.painter,
+ contentDescription = stringResource(R.string.desc_open_player_list)
+ )
+ } else if (data.bitmap != null) {
+ Image(
+ modifier = Modifier.touchTargetAwareSize(IconButtonDefaults.LargeIconSize),
+ bitmap = data.bitmap.asImageBitmap(),
+ contentDescription = stringResource(R.string.desc_open_player_list)
+ )
+ } else {
+ Image(
+ modifier = Modifier.touchTargetAwareSize(IconButtonDefaults.LargeIconSize),
+ painter = painterResource(R.drawable.ic_play_circle_filled_white_24dp),
+ contentDescription = stringResource(R.string.desc_open_player_list)
+ )
+ }
+}
+
@Composable
private fun MediaCustomControlsPage(
mediaPlayerViewModel: MediaPlayerViewModel
@@ -670,34 +914,129 @@ private fun MediaCustomControlsPage(
focusRequester: FocusRequester = rememberFocusRequester(),
onItemClick: (MediaItemModel) -> Unit = {}
) {
+ val context = LocalContext.current
+ val compositionScope = rememberCoroutineScope()
+
LoadingContent(
empty = false,
emptyContent = {},
loading = uiState.isLoading
) {
- val scrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Chip,
- last = ScalingLazyColumnDefaults.ItemType.Chip
- )
+ val columnState = rememberTransformingLazyColumnState()
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
)
+ val transformationSpec = rememberTransformationSpec()
+
+ val buttonGroupItems = remember(uiState.mediaCustomItems) {
+ if (uiState.mediaCustomItems.size > 3) {
+ uiState.mediaCustomItems.filter { it.id != MediaHelper.ACTIONITEM_PLAY && it.icon != null }
+ .take(3)
+ } else {
+ emptyList()
+ }
+ }
+ val listItems by remember(uiState.mediaCustomItems) {
+ derivedStateOf { uiState.mediaCustomItems.filterNot { buttonGroupItems.contains(it) } }
+ }
- Box(
- modifier = Modifier.fillMaxSize()
- ) {
- TimeText(Modifier.scrollAway { scrollState })
-
- ScalingLazyColumn(
- scrollState = scrollState,
- focusRequester = focusRequester
+ ScreenScaffold(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = contentPadding,
+ scrollState = columnState
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .focusable()
+ .focusRequester(focusRequester),
+ state = columnState,
+ contentPadding = contentPadding
) {
- items(uiState.mediaCustomItems) { item ->
- Chip(
- label = item.title ?: "",
+ item {
+ ListHeader(
+ contentPadding = PaddingValues(
+ start = ListHeaderDefaults.ContentPadding.calculateStartPadding(
+ LocalLayoutDirection.current
+ ),
+ top = 0.dp,
+ end = ListHeaderDefaults.ContentPadding.calculateEndPadding(
+ LocalLayoutDirection.current
+ ),
+ bottom = ListHeaderDefaults.ContentPadding.calculateBottomPadding()
+ )
+ ) {
+ TextMediaDisplay(
+ title = uiState.playerState.title ?: "",
+ subtitle = uiState.playerState.artist ?: "",
+ titleIcon = uiState.mediaPlayerDetails.bitmapIcon?.asPaintable()
+ )
+ }
+ }
+
+ if (buttonGroupItems.isNotEmpty()) {
+ item {
+ Column {
+ ButtonGroup(
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = ButtonGroupDefaults.fullWidthPaddings()
+ ) {
+ buttonGroupItems.forEach {
+ val interactionSource = remember { MutableInteractionSource() }
+
+ FilledIconButton(
+ modifier = Modifier.animateWidth(interactionSource),
+ onClick = {
+ onItemClick(it)
+ },
+ interactionSource = interactionSource,
+ content = {
+ it.icon?.let { bmp ->
+ Icon(
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ bitmap = bmp.asImageBitmap(),
+ contentDescription = it.title
+ )
+ }
+ },
+ onLongClickLabel = it.title,
+ onLongClick = {
+ compositionScope.launch {
+ if (isActive && !it.title.isNullOrEmpty()) {
+ Toast.makeText(
+ context,
+ it.title,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(4.dp))
+ }
+ }
+ }
+
+ items(listItems) { item ->
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ label = {
+ Text(text = item.title ?: "")
+ },
+ secondaryLabel = item.subTitle?.let {
+ { Text(text = it) }
+ },
icon = {
item.icon?.let { bmp ->
Icon(
- modifier = Modifier.size(ChipDefaults.IconSize),
+ modifier = Modifier.size(ButtonDefaults.IconSize),
bitmap = bmp.asImageBitmap(),
tint = Color.White,
contentDescription = item.title
@@ -706,14 +1045,13 @@ private fun MediaCustomControlsPage(
},
onClick = {
onItemClick(item)
- },
- colors = ChipDefaults.secondaryChipColors()
+ }
)
}
}
LaunchedEffect(Unit) {
- scrollState.state.scrollToItem(0)
+ columnState.scrollToItem(0)
}
LaunchedEffect(uiState, uiState.pagerState) {
@@ -751,48 +1089,79 @@ private fun MediaBrowserPage(
focusRequester: FocusRequester = rememberFocusRequester(),
onItemClick: (MediaItemModel) -> Unit = {}
) {
- LoadingContent(
- empty = false,
- emptyContent = {},
- loading = uiState.isLoading
- ) {
- val scrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Chip,
- last = ScalingLazyColumnDefaults.ItemType.Chip
- )
- )
-
- Box(
- modifier = Modifier.fillMaxSize()
+ val columnState = rememberTransformingLazyColumnState()
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
+ )
+ val transformationSpec = rememberTransformationSpec()
+
+ ScreenScaffold(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = contentPadding,
+ scrollState = columnState
+ ) { contentPadding ->
+ LoadingContent(
+ empty = false,
+ emptyContent = {},
+ loading = uiState.isLoading
) {
- TimeText(Modifier.scrollAway { scrollState })
-
- ScalingLazyColumn(
- scrollState = scrollState,
- focusRequester = focusRequester
+ TransformingLazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = columnState,
+ contentPadding = contentPadding
) {
+ item {
+ ListHeader(
+ contentPadding = PaddingValues(
+ start = ListHeaderDefaults.ContentPadding.calculateStartPadding(
+ LocalLayoutDirection.current
+ ),
+ top = 0.dp,
+ end = ListHeaderDefaults.ContentPadding.calculateEndPadding(
+ LocalLayoutDirection.current
+ ),
+ bottom = ListHeaderDefaults.ContentPadding.calculateBottomPadding()
+ )
+ ) {
+ TextMediaDisplay(
+ title = uiState.playerState.title ?: "",
+ subtitle = uiState.playerState.artist ?: "",
+ titleIcon = uiState.mediaPlayerDetails.bitmapIcon?.asPaintable()
+ )
+ }
+ }
+
items(uiState.mediaBrowserItems) { item ->
- Chip(
- label = if (item.id == MediaHelper.ACTIONITEM_BACK) {
- stringResource(id = R.string.label_back)
- } else {
- item.title ?: ""
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ label = {
+ Text(
+ text = if (item.id == MediaHelper.ACTIONITEM_BACK) {
+ stringResource(id = R.string.label_back)
+ } else {
+ item.title ?: ""
+ }
+ )
+ },
+ secondaryLabel = item.takeIf { it.id != MediaHelper.ACTIONITEM_BACK }?.subTitle?.let {
+ { Text(text = it) }
},
icon = {
if (item.id == MediaHelper.ACTIONITEM_BACK) {
Icon(
- modifier = Modifier.size(ChipDefaults.IconSize),
- painter = painterResource(id = R.drawable.ic_baseline_arrow_back_24),
- tint = Color.White,
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = stringResource(id = R.string.label_back)
)
} else {
item.icon?.let { bmp ->
Icon(
- modifier = Modifier.size(ChipDefaults.IconSize),
+ modifier = Modifier.size(ButtonDefaults.IconSize),
bitmap = bmp.asImageBitmap(),
- tint = Color.White,
contentDescription = item.title
)
}
@@ -800,14 +1169,13 @@ private fun MediaBrowserPage(
},
onClick = {
onItemClick(item)
- },
- colors = ChipDefaults.secondaryChipColors()
+ }
)
}
}
LaunchedEffect(Unit) {
- scrollState.state.scrollToItem(0)
+ columnState.scrollToItem(0)
}
LaunchedEffect(uiState, uiState.pagerState) {
@@ -845,65 +1213,118 @@ private fun MediaQueuePage(
focusRequester: FocusRequester = rememberFocusRequester(),
onItemClick: (MediaItemModel) -> Unit = {}
) {
- val lifecycleOwner = LocalLifecycleOwner.current
-
- LoadingContent(
- empty = false,
- emptyContent = {},
- loading = uiState.isLoading
- ) {
- val scrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Chip,
- last = ScalingLazyColumnDefaults.ItemType.Chip
- )
- )
+ val activeQueueItemIndex = remember(uiState.activeQueueItemId, uiState.mediaQueueItems) {
+ (uiState.activeQueueItemId.takeIf { it > -1L }?.let { activeId ->
+ uiState.mediaQueueItems.indexOfFirst { it.id.toLong() == activeId }.takeIf { it > 0 }
+ } ?: 0) + 1
+ }
- Box(
- modifier = Modifier.fillMaxSize()
+ val columnState = rememberTransformingLazyColumnState(
+ initialAnchorItemIndex = 1
+ )
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
+ )
+ val transformationSpec = rememberTransformationSpec()
+
+ ScreenScaffold(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = contentPadding,
+ scrollState = columnState
+ ) { contentPadding ->
+ LoadingContent(
+ empty = false,
+ emptyContent = {},
+ loading = uiState.isLoading
) {
- TimeText(Modifier.scrollAway { scrollState })
-
- ScalingLazyColumn(
- scrollState = scrollState,
- focusRequester = focusRequester
+ TransformingLazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = columnState,
+ contentPadding = contentPadding
) {
+ item {
+ ListHeader(
+ contentPadding = PaddingValues(
+ start = ListHeaderDefaults.ContentPadding.calculateStartPadding(
+ LocalLayoutDirection.current
+ ),
+ top = 0.dp,
+ end = ListHeaderDefaults.ContentPadding.calculateEndPadding(
+ LocalLayoutDirection.current
+ ),
+ bottom = ListHeaderDefaults.ContentPadding.calculateBottomPadding()
+ )
+ ) {
+ TextMediaDisplay(
+ title = uiState.playerState.title ?: "",
+ subtitle = uiState.playerState.artist ?: "",
+ titleIcon = uiState.mediaPlayerDetails.bitmapIcon?.asPaintable()
+ )
+ }
+ }
+
items(uiState.mediaQueueItems) { item ->
- Chip(
- label = item.title ?: "",
- icon = {
- item.icon?.let { bmp ->
- Icon(
- modifier = Modifier.size(ChipDefaults.IconSize),
+ Button(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ label = {
+ Text(text = item.title ?: "")
+ },
+ secondaryLabel = item.subTitle?.let {
+ { Text(text = it) }
+ },
+ icon = item.icon?.let { bmp ->
+ {
+ Image(
+ modifier = Modifier
+ .size(ButtonDefaults.IconSize)
+ .clip(RoundedCornerShape(4.dp)),
bitmap = bmp.asImageBitmap(),
- contentDescription = item.title,
- tint = Color.Unspecified
+ contentDescription = item.title
)
}
+ } ?: run {
+ if (item.id.toLong() == uiState.activeQueueItemId) {
+ {
+ val image =
+ AnimatedImageVector.animatedVectorResource(R.drawable.equalizer_animated)
+ var atEnd by remember { mutableStateOf(false) }
+
+ Icon(
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ painter = rememberAnimatedVectorPainter(image, atEnd),
+ contentDescription = item.title
+ )
+
+ LaunchedEffect(uiState.playerState) {
+ atEnd =
+ uiState.playerState.playbackState == PlaybackState.PLAYING
+ }
+ }
+ } else {
+ null
+ }
},
onClick = {
onItemClick(item)
- lifecycleOwner.lifecycleScope.launch {
- delay(1000)
- scrollState.state.scrollToItem(0)
- }
},
colors = if (item.id.toLong() == uiState.activeQueueItemId) {
- ChipDefaults.gradientBackgroundChipColors()
+ ButtonDefaults.filledVariantButtonColors()
} else {
- ChipDefaults.secondaryChipColors()
+ ButtonDefaults.filledTonalButtonColors()
}
)
}
}
- LaunchedEffect(Unit) {
- if (uiState.activeQueueItemId != -1L) {
- uiState.mediaQueueItems.indexOfFirst {
- it.id.toLong() == uiState.activeQueueItemId
- }.takeIf { it > 0 }?.run {
- scrollState.state.scrollToItem(this)
- }
+ LaunchedEffect(uiState.activeQueueItemId, uiState.mediaQueueItems) {
+ delay(500)
+
+ if (isActive && !columnState.isScrollInProgress) {
+ columnState.animateScrollToItem(activeQueueItemIndex)
}
}
@@ -917,6 +1338,42 @@ private fun MediaQueuePage(
}
}
+/**
+ * Theme that updates the colors dynamically depending on the image
+ * Source: Jetcaster (https://github.com/android/compose-samples/blob/main/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt)
+ */
+@Composable
+private fun PlayerDynamicTheme(
+ uiState: MediaPlayerUiState,
+ content: @Composable () -> Unit
+) {
+ val surfaceColor = MaterialTheme.colorScheme.background
+ val dominantColorState = rememberDominantColorState(
+ defaultColor = MaterialTheme.colorScheme.background,
+ defaultOnColor = MaterialTheme.colorScheme.onBackground
+ ) { color ->
+ // We want a color which has sufficient contrast against the surface color
+ color.contrastAgainst(surfaceColor) >= MinContrastOfPrimaryVsBackground
+ }
+ DynamicThemePrimaryColorsFromImage(dominantColorState) {
+ // Update the dominantColorState with colors coming from the image
+ LaunchedEffect(uiState.playerState.artworkBitmap) {
+ val key = uiState.playerState.key
+
+ if (uiState.playerState.artworkBitmap != null) {
+ dominantColorState.updateColorsFromImage(
+ key,
+ uiState.playerState.artworkBitmap,
+ false
+ )
+ } else {
+ dominantColorState.reset()
+ }
+ }
+ content()
+ }
+}
+
@WearPreviewDevices
@WearPreviewFontScales
@Composable
@@ -998,16 +1455,65 @@ private fun PreviewCustomControls() {
val uiState = remember {
MediaPlayerUiState(
isPlayerAvailable = true,
+ playerState = PlayerState(
+ playbackState = PlaybackState.PLAYING,
+ title = "Title",
+ artist = "Artist",
+ artworkBitmap = ContextCompat.getDrawable(context, R.drawable.sample_image)!!
+ .toBitmap()
+ ),
+ mediaPlayerDetails = AppItemViewModel().apply {
+ activityName = "Media Player"
+ bitmapIcon =
+ ContextCompat.getDrawable(context, R.mipmap.ic_launcher_round)!!.toBitmap()
+ },
mediaCustomItems = List(5) {
MediaItemModel(it.toString()).apply {
title = "Item ${it + 1}"
icon = ContextCompat.getDrawable(context, R.drawable.ic_icon)!!.toBitmap()
}
- }
+ },
+ activeQueueItemId = 0
)
}
MediaCustomControlsPage(
uiState = uiState
)
+}
+
+@WearPreviewDevices
+@WearPreviewFontScales
+@Composable
+private fun PreviewMediaQueue() {
+ val context = LocalContext.current
+
+ val uiState = remember {
+ MediaPlayerUiState(
+ isPlayerAvailable = true,
+ playerState = PlayerState(
+ playbackState = PlaybackState.PLAYING,
+ title = "Title",
+ artist = "Artist",
+ artworkBitmap = ContextCompat.getDrawable(context, R.drawable.sample_image)!!
+ .toBitmap()
+ ),
+ mediaPlayerDetails = AppItemViewModel().apply {
+ activityName = "Media Player"
+ bitmapIcon =
+ ContextCompat.getDrawable(context, R.mipmap.ic_launcher_round)!!.toBitmap()
+ },
+ mediaQueueItems = List(5) {
+ MediaItemModel(it.toString()).apply {
+ title = "Item ${it + 1}"
+ icon = ContextCompat.getDrawable(context, R.drawable.sample_image)!!.toBitmap()
+ }
+ },
+ activeQueueItemId = 0
+ )
+ }
+
+ MediaQueuePage(
+ uiState = uiState
+ )
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/PhoneSyncUi.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/PhoneSyncUi.kt
index 84da6104..b9de976d 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/PhoneSyncUi.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/PhoneSyncUi.kt
@@ -5,9 +5,17 @@ import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.content.Intent
import android.os.Build
+import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.background
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -17,46 +25,59 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Sync
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+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.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
-import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
-import androidx.wear.compose.material.Button
-import androidx.wear.compose.material.ButtonDefaults
-import androidx.wear.compose.material.CircularProgressIndicator
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.ListHeader
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.Scaffold
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.TimeText
-import androidx.wear.compose.material.Vignette
-import androidx.wear.compose.material.VignettePosition
-import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.ArcProgressIndicator
+import androidx.wear.compose.material3.ArcProgressIndicatorDefaults
+import androidx.wear.compose.material3.CircularProgressIndicator
+import androidx.wear.compose.material3.FilledIconButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.IconButtonDefaults
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.ProgressIndicatorDefaults
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.Text
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
-import com.google.accompanist.drawablepainter.rememberDrawablePainter
+import com.google.android.horologist.images.base.paintable.ImageVectorPaintable.Companion.asPaintable
import com.thewizrd.shared_resources.helpers.WearConnectionStatus
+import com.thewizrd.shared_resources.utils.JSONParser
import com.thewizrd.simplewear.R
+import com.thewizrd.simplewear.ui.components.ConfirmationOverlay
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
import com.thewizrd.simplewear.ui.theme.WearAppTheme
import com.thewizrd.simplewear.ui.theme.activityViewModel
import com.thewizrd.simplewear.ui.theme.findActivity
+import com.thewizrd.simplewear.utils.ErrorMessage
+import com.thewizrd.simplewear.viewmodels.ConfirmationData
+import com.thewizrd.simplewear.viewmodels.ConfirmationViewModel
import com.thewizrd.simplewear.viewmodels.PhoneSyncUiState
import com.thewizrd.simplewear.viewmodels.PhoneSyncViewModel
+import com.thewizrd.simplewear.viewmodels.WearableListenerViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
@@ -69,10 +90,8 @@ fun PhoneSyncUi(
val phoneSyncViewModel = activityViewModel()
WearAppTheme {
- Scaffold(
- modifier = modifier.background(MaterialTheme.colors.background),
- vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
- timeText = { TimeText() },
+ AppScaffold(
+ modifier = modifier
) {
PhoneSyncUi(phoneSyncViewModel)
}
@@ -90,6 +109,9 @@ private fun PhoneSyncUi(
val uiState by phoneSyncViewModel.uiState.collectAsState()
+ val confirmationViewModel = viewModel()
+ val confirmationData by confirmationViewModel.confirmationEventsFlow.collectAsState()
+
val bluetoothRequestLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
@@ -143,7 +165,7 @@ private fun PhoneSyncUi(
WearConnectionStatus.APPNOTINSTALLED -> {
lifecycleOwner.lifecycleScope.launch {
- phoneSyncViewModel.openPlayStore(activity)
+ phoneSyncViewModel.openPlayStore()
}
}
@@ -151,6 +173,42 @@ private fun PhoneSyncUi(
}
}
)
+
+ ConfirmationOverlay(
+ confirmationData = confirmationData,
+ onTimeout = { confirmationViewModel.clearFlow() },
+ )
+
+ LaunchedEffect(lifecycleOwner) {
+ lifecycleOwner.lifecycleScope.launch {
+ phoneSyncViewModel.eventFlow.collect { event ->
+ when (event.eventType) {
+ WearableListenerViewModel.ACTION_SHOWCONFIRMATION -> {
+ val jsonData =
+ event.data.getString(WearableListenerViewModel.EXTRA_ACTIONDATA)
+
+ JSONParser.deserializer(jsonData, ConfirmationData::class.java)?.let {
+ confirmationViewModel.showConfirmation(it)
+ }
+ }
+ }
+ }
+ }
+
+ lifecycleOwner.lifecycleScope.launch {
+ phoneSyncViewModel.errorMessagesFlow.collect { error ->
+ when (error) {
+ is ErrorMessage.String -> {
+ Toast.makeText(context, error.message, Toast.LENGTH_SHORT).show()
+ }
+
+ is ErrorMessage.Resource -> {
+ Toast.makeText(context, error.stringId, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+ }
}
@Composable
@@ -163,11 +221,13 @@ private fun PhoneSyncUi(
val context = LocalContext.current
val isRound = LocalConfiguration.current.isScreenRound
- Box(
+ ScreenScaffold(
modifier = Modifier.fillMaxSize()
- ) {
+ ) { contentPadding ->
Column(
- modifier = Modifier.fillMaxSize()
+ modifier = Modifier
+ .padding(contentPadding)
+ .fillMaxSize()
) {
ListHeader(
modifier = Modifier
@@ -202,7 +262,6 @@ private fun PhoneSyncUi(
}
},
overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.button,
textAlign = TextAlign.Center
)
}
@@ -215,14 +274,12 @@ private fun PhoneSyncUi(
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
) {
if (uiState.showWifiButton) {
- Button(
- modifier = Modifier.requiredSize(36.dp),
- onClick = onWifiButtonClicked,
- colors = ButtonDefaults.primaryButtonColors(
- backgroundColor = colorResource(id = R.color.colorPrimary)
- )
+ FilledIconButton(
+ modifier = Modifier.requiredSize(IconButtonDefaults.ExtraSmallButtonSize),
+ onClick = onWifiButtonClicked
) {
Icon(
+ modifier = Modifier.requiredSize(IconButtonDefaults.SmallIconSize - 4.dp),
painter = painterResource(id = R.drawable.ic_network_wifi_white_24dp),
contentDescription = stringResource(id = R.string.action_wifi)
)
@@ -232,45 +289,69 @@ private fun PhoneSyncUi(
Box(
contentAlignment = Alignment.Center
) {
- if (uiState.isLoading) {
- CircularProgressIndicator(
- modifier = Modifier.requiredSize(44.dp),
- trackColor = Color.Transparent,
- strokeWidth = 4.dp
- )
+ if (!isRound) {
+ Box {
+ var isVisible by remember { mutableStateOf(true) }
+
+ androidx.compose.animation.AnimatedVisibility(
+ visible = isVisible,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier.requiredSize(IconButtonDefaults.ExtraSmallButtonSize + 12.dp),
+ strokeWidth = 4.dp,
+ colors = ProgressIndicatorDefaults.colors(
+ trackColor = Color.Transparent,
+ indicatorColor = MaterialTheme.colorScheme.onSurface
+ )
+ )
+ }
+
+ LaunchedEffect(uiState.isLoading) {
+ if (!uiState.isLoading) {
+ delay(500)
+ }
+ if (isActive) {
+ isVisible = uiState.isLoading
+ }
+ }
+ }
}
- Button(
- modifier = Modifier.requiredSize(36.dp),
- onClick = onSyncButtonClicked,
- colors = ButtonDefaults.primaryButtonColors(
- backgroundColor = colorResource(id = R.color.colorPrimary)
- )
+ FilledIconButton(
+ modifier = Modifier.requiredSize(IconButtonDefaults.ExtraSmallButtonSize),
+ onClick = onSyncButtonClicked
) {
+ // Allow resume on rotation
+ var currentRotation by remember { mutableFloatStateOf(0f) }
+ val rotation = remember { Animatable(currentRotation) }
+
Icon(
- modifier = Modifier.size(24.dp),
+ modifier = Modifier
+ .requiredSize(IconButtonDefaults.SmallIconSize - 4.dp)
+ .rotate(
+ when (uiState.connectionStatus) {
+ WearConnectionStatus.DISCONNECTED,
+ WearConnectionStatus.APPNOTINSTALLED -> 0f
+
+ else -> rotation.value
+ }
+ ),
painter = when (uiState.connectionStatus) {
WearConnectionStatus.DISCONNECTED -> {
painterResource(id = R.drawable.ic_phonelink_erase_white_24dp)
}
WearConnectionStatus.CONNECTING, WearConnectionStatus.CONNECTED -> {
- val drawable = remember(context) {
- ContextCompat.getDrawable(
- context,
- android.R.drawable.ic_popup_sync
- )
- }
- rememberDrawablePainter(
- drawable = drawable
- )
+ Icons.Rounded.Sync.asPaintable().rememberPainter()
}
WearConnectionStatus.APPNOTINSTALLED -> {
painterResource(id = R.drawable.common_full_open_on_phone)
}
- null -> painterResource(id = R.drawable.ic_sync_24dp)
+ null -> Icons.Rounded.Sync.asPaintable().rememberPainter()
},
contentDescription = when (uiState.connectionStatus) {
WearConnectionStatus.DISCONNECTED -> stringResource(R.string.status_disconnected)
@@ -280,18 +361,28 @@ private fun PhoneSyncUi(
null -> null
}
)
+
+ LaunchedEffect(Unit) {
+ rotation.animateTo(
+ targetValue = currentRotation + 360f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1200, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart
+ )
+ ) {
+ currentRotation = value
+ }
+ }
}
}
if (uiState.showBTButton) {
- Button(
- modifier = Modifier.requiredSize(36.dp),
- onClick = onBTButtonClicked,
- colors = ButtonDefaults.primaryButtonColors(
- backgroundColor = colorResource(id = R.color.colorPrimary)
- )
+ FilledIconButton(
+ modifier = Modifier.requiredSize(IconButtonDefaults.ExtraSmallButtonSize),
+ onClick = onBTButtonClicked
) {
Icon(
+ modifier = Modifier.requiredSize(IconButtonDefaults.SmallIconSize - 4.dp),
painter = painterResource(id = R.drawable.ic_bluetooth_white_24dp),
contentDescription = stringResource(id = R.string.action_bt)
)
@@ -299,6 +390,36 @@ private fun PhoneSyncUi(
}
}
}
+
+ if (isRound) {
+ Box(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = contentPadding.calculateBottomPadding() / 2),
+ ) {
+ var isVisible by remember { mutableStateOf(true) }
+
+ AnimatedVisibility(
+ visible = isVisible,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ ArcProgressIndicator(
+ modifier =
+ Modifier.size(ArcProgressIndicatorDefaults.recommendedIndeterminateDiameter)
+ )
+ }
+
+ LaunchedEffect(uiState.isLoading) {
+ if (!uiState.isLoading) {
+ delay(500)
+ }
+ if (isActive) {
+ isVisible = uiState.isLoading
+ }
+ }
+ }
+ }
}
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/SimpleWearApp.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/SimpleWearApp.kt
index 75622b28..9c108a4a 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/SimpleWearApp.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/SimpleWearApp.kt
@@ -1,6 +1,5 @@
package com.thewizrd.simplewear.ui.simplewear
-import android.os.Build
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -10,6 +9,7 @@ import androidx.navigation.NavType
import androidx.navigation.activity
import androidx.navigation.navArgument
import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
+import androidx.wear.compose.material3.AppScaffold
import androidx.wear.compose.navigation.SwipeDismissableNavHost
import androidx.wear.compose.navigation.composable
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
@@ -19,9 +19,8 @@ import com.thewizrd.shared_resources.actions.AudioStreamType
import com.thewizrd.shared_resources.actions.TimedAction
import com.thewizrd.shared_resources.utils.AnalyticsLogger
import com.thewizrd.shared_resources.utils.JSONParser
+import com.thewizrd.shared_resources.utils.getSerializableCompat
import com.thewizrd.simplewear.media.MediaPlayerActivity
-import com.thewizrd.simplewear.preferences.DashboardConfigActivity
-import com.thewizrd.simplewear.preferences.DashboardTileConfigActivity
import com.thewizrd.simplewear.ui.navigation.Screen
import com.thewizrd.simplewear.ui.theme.WearAppTheme
@@ -38,158 +37,160 @@ fun SimpleWearApp(
swipeToDismissBoxState = swipeToDismissBoxState
)
- SwipeDismissableNavHost(
- navController = navController,
- startDestination = startDestination,
- state = swipeDismissNavState
- ) {
- composable(Screen.Dashboard.route) {
- Dashboard(navController = navController)
-
- LaunchedEffect(navController) {
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.Dashboard.route)
- })
+ AppScaffold {
+ SwipeDismissableNavHost(
+ navController = navController,
+ startDestination = startDestination,
+ state = swipeDismissNavState
+ ) {
+ composable(Screen.Dashboard.route) {
+ Dashboard(navController = navController)
+
+ LaunchedEffect(navController) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.Dashboard.route)
+ })
+ }
}
- }
- composable(
- route = Screen.ValueAction.route + "/{actionId}?streamType={streamType}",
- arguments = listOf(
- navArgument("actionId") {
- type = NavType.IntType
- },
- navArgument("streamType") {
- type = NavType.EnumType(AudioStreamType::class.java)
- defaultValue = AudioStreamType.MUSIC
+ composable(
+ route = Screen.ValueAction.route + "/{actionId}?streamType={streamType}",
+ arguments = listOf(
+ navArgument("actionId") {
+ type = NavType.IntType
+ },
+ navArgument("streamType") {
+ type = NavType.EnumType(AudioStreamType::class.java)
+ defaultValue = AudioStreamType.MUSIC
+ }
+ )
+ ) { backstackEntry ->
+ val actionType = backstackEntry.arguments?.getInt("actionId")?.let {
+ Actions.valueOf(it)
}
- )
- ) { backstackEntry ->
- val actionType = backstackEntry.arguments?.getInt("actionId")?.let {
- Actions.valueOf(it)
- }
- val streamType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- backstackEntry.arguments?.getSerializable(
+ val streamType = backstackEntry.arguments?.getSerializableCompat(
"streamType",
AudioStreamType::class.java
)
- } else {
- backstackEntry.arguments?.getSerializable("streamType") as AudioStreamType
- }
- ValueActionScreen(
- actionType = actionType ?: Actions.VOLUME,
- audioStreamType = streamType
- )
+ ValueActionScreen(
+ actionType = actionType ?: Actions.VOLUME,
+ audioStreamType = streamType
+ )
- LaunchedEffect(navController, actionType) {
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.ValueAction.route)
- actionType?.let {
- putString("actionType", it.name)
- }
- })
+ LaunchedEffect(navController, actionType) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.ValueAction.route)
+ actionType?.let {
+ putString("actionType", it.name)
+ }
+ })
+ }
}
- }
- activity(route = Screen.MediaPlayerList.route) {
- targetPackage = context.packageName
- activityClass = MediaPlayerActivity::class
- }
+ activity(route = Screen.MediaPlayerList.route) {
+ targetPackage = context.packageName
+ activityClass = MediaPlayerActivity::class
+ }
- composable(Screen.AppLauncher.route) {
- AppLauncherScreen(
- navController = navController,
- swipeToDismissBoxState = swipeToDismissBoxState
- )
+ composable(Screen.AppLauncher.route) {
+ AppLauncherScreen()
- LaunchedEffect(navController) {
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.AppLauncher.route)
- })
+ LaunchedEffect(navController) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.AppLauncher.route)
+ })
+ }
}
- }
- composable(Screen.CallManager.route) {
- CallManagerUi(navController = navController)
+ composable(Screen.CallManager.route) {
+ CallManagerUi(navController = navController)
- LaunchedEffect(navController) {
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.CallManager.route)
- })
+ LaunchedEffect(navController) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.CallManager.route)
+ })
+ }
}
- }
- composable(Screen.GesturesAction.route) {
- GesturesUi(
- navController = navController,
- swipeToDismissBoxState = swipeToDismissBoxState
- )
+ composable(Screen.GesturesAction.route) {
+ GesturesUi(navController = navController)
- LaunchedEffect(navController) {
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.GesturesAction.route)
- })
+ LaunchedEffect(navController) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.GesturesAction.route)
+ })
+ }
}
- }
- composable(Screen.TimedActions.route) {
- TimedActionUi(navController = navController)
+ composable(Screen.TimedActions.route) {
+ TimedActionUi(navController = navController)
- LaunchedEffect(navController) {
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.TimedActions.route)
- })
+ LaunchedEffect(navController) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.TimedActions.route)
+ })
+ }
}
- }
- composable(
- route = Screen.TimedActionDetail.route + "?action={action}",
- arguments = listOf(
- navArgument("action") {
- type = NavType.StringType
- nullable = false
+ composable(
+ route = Screen.TimedActionDetail.route + "?action={action}",
+ arguments = listOf(
+ navArgument("action") {
+ type = NavType.StringType
+ nullable = false
+ }
+ )
+ ) { backstackEntry ->
+ val action = remember(backstackEntry) {
+ JSONParser.deserializer(
+ backstackEntry.arguments?.getString("action"),
+ TimedAction::class.java
+ )!!
+ }
+
+ TimedActionDetailUi(
+ action = action,
+ navController = navController
+ )
+
+ LaunchedEffect(navController) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.TimedActionDetail.route)
+ putString("actionType", action.action.actionType.name)
+ })
}
- )
- ) { backstackEntry ->
- val action = remember(backstackEntry) {
- JSONParser.deserializer(
- backstackEntry.arguments?.getString("action"),
- TimedAction::class.java
- )!!
}
- TimedActionDetailUi(
- action = action,
- navController = navController
- )
+ composable(Screen.TimedActionSetup.route) {
+ TimedActionSetupUi(navController = navController)
- LaunchedEffect(navController) {
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.TimedActionDetail.route)
- putString("actionType", action.action.actionType.name)
- })
+ LaunchedEffect(navController) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.TimedActionSetup.route)
+ })
+ }
}
- }
- composable(Screen.TimedActionSetup.route) {
- TimedActionSetupUi(navController = navController)
+ composable(Screen.DashboardConfig.route) {
+ DashboardConfigUi(navController = navController)
- LaunchedEffect(navController) {
- AnalyticsLogger.logEvent("nav_route", Bundle().apply {
- putString("screen", Screen.TimedActionSetup.route)
- })
+ LaunchedEffect(navController) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.DashboardConfig.route)
+ })
+ }
}
- }
- activity(route = Screen.DashboardConfig.route) {
- targetPackage = context.packageName
- activityClass = DashboardConfigActivity::class
- }
+ composable(Screen.DashboardTileConfig.route) {
+ DashboardTileConfigUi(navController = navController)
- activity(route = Screen.DashboardTileConfig.route) {
- targetPackage = context.packageName
- activityClass = DashboardTileConfigActivity::class
+ LaunchedEffect(navController) {
+ AnalyticsLogger.logEvent("nav_route", Bundle().apply {
+ putString("screen", Screen.DashboardTileConfig.route)
+ })
+ }
+ }
}
}
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/TimedActionSetupUi.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/TimedActionSetupUi.kt
index 778bb9d3..944ed728 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/TimedActionSetupUi.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/TimedActionSetupUi.kt
@@ -1,21 +1,8 @@
-@file:OptIn(
- ExperimentalFoundationApi::class, ExperimentalHorologistApi::class,
- ExperimentalWearFoundationApi::class
-)
-
package com.thewizrd.simplewear.ui.simplewear
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.PagerState
-import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -25,10 +12,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -36,27 +22,33 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.items
-import androidx.wear.compose.material.ButtonDefaults
-import androidx.wear.compose.material.ChipDefaults
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.ListHeader
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.TimeText
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.foundation.pager.PagerState
+import androidx.wear.compose.foundation.pager.rememberPagerState
+import androidx.wear.compose.material3.AnimatedPage
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonColors
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.FilledIconButton
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.FilledTonalIconButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.RadioButton
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.SwitchButton
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TimePicker
+import androidx.wear.compose.material3.TimePickerType
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.composables.TimePicker
-import com.google.android.horologist.compose.layout.PagerScaffold
-import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
-import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
-import com.google.android.horologist.compose.layout.scrollAway
-import com.google.android.horologist.compose.material.Button
-import com.google.android.horologist.compose.material.Chip
-import com.google.android.horologist.compose.material.ResponsiveListHeader
-import com.google.android.horologist.compose.material.ToggleChip
-import com.google.android.horologist.compose.material.ToggleChipToggleControl
+import com.google.android.horologist.compose.layout.ColumnItemType
+import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding
import com.thewizrd.shared_resources.actions.Action
import com.thewizrd.shared_resources.actions.ActionStatus
import com.thewizrd.shared_resources.actions.Actions
@@ -65,16 +57,19 @@ import com.thewizrd.shared_resources.actions.TimedAction
import com.thewizrd.shared_resources.actions.ToggleAction
import com.thewizrd.shared_resources.controls.ActionButtonViewModel
import com.thewizrd.shared_resources.helpers.WearableHelper
+import com.thewizrd.shared_resources.utils.JSONParser
+import com.thewizrd.shared_resources.utils.getSerializableCompat
import com.thewizrd.simplewear.R
-import com.thewizrd.simplewear.helpers.showConfirmationOverlay
import com.thewizrd.simplewear.ui.components.ConfirmationOverlay
-import com.thewizrd.simplewear.ui.components.ScalingLazyColumn
+import com.thewizrd.simplewear.ui.components.HorizontalPagerScreen
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
import com.thewizrd.simplewear.ui.theme.activityViewModel
import com.thewizrd.simplewear.ui.theme.findActivity
-import com.thewizrd.simplewear.ui.tools.WearPreviewDevices
+import com.thewizrd.simplewear.ui.utils.rememberFocusRequester
import com.thewizrd.simplewear.viewmodels.ConfirmationData
import com.thewizrd.simplewear.viewmodels.ConfirmationViewModel
import com.thewizrd.simplewear.viewmodels.TimedActionUiViewModel
+import com.thewizrd.simplewear.viewmodels.WearableListenerViewModel
import com.thewizrd.simplewear.viewmodels.WearableListenerViewModel.Companion.EXTRA_STATUS
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
@@ -113,7 +108,7 @@ fun TimedActionSetupUi(
delay(15000)
if (isActive) {
lifecycleOwner.lifecycleScope.launch {
- activity.showConfirmationOverlay(false)
+ confirmationViewModel.showFailure()
navController.popBackStack()
}
}
@@ -134,7 +129,8 @@ fun TimedActionSetupUi(
timedActionUiViewModel.eventFlow.collect { event ->
when (event.eventType) {
WearableHelper.TimedActionAddPath -> {
- val status = event.data.getSerializable(EXTRA_STATUS) as ActionStatus
+ val status =
+ event.data.getSerializableCompat(EXTRA_STATUS, ActionStatus::class.java)
when (status) {
ActionStatus.SUCCESS -> {
@@ -144,16 +140,13 @@ fun TimedActionSetupUi(
}
ActionStatus.PERMISSION_DENIED -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_permissiondenied)
+ confirmationViewModel.showOpenOnPhoneForFailure(
+ message = context.getString(
+ R.string.error_permissiondenied_wear
)
)
- timedActionUiViewModel.openAppOnPhone(
- activity,
- showAnimation = false
- )
+ timedActionUiViewModel.openAppOnPhone(showAnimation = false)
}
else -> {
@@ -163,6 +156,15 @@ fun TimedActionSetupUi(
}
}
}
+
+ WearableListenerViewModel.ACTION_SHOWCONFIRMATION -> {
+ val jsonData =
+ event.data.getString(WearableListenerViewModel.EXTRA_ACTIONDATA)
+
+ JSONParser.deserializer(jsonData, ConfirmationData::class.java)?.let {
+ confirmationViewModel.showConfirmation(it)
+ }
+ }
}
}
}
@@ -177,7 +179,8 @@ private fun TimedActionSetupUi(
onAddAction: (Action?, TimedAction) -> Unit = { _, _ -> },
onCancel: () -> Unit = {}
) {
- val lifecycleOwner = LocalLifecycleOwner.current
+ val isPreview = LocalInspectionMode.current
+
val compositionScope = rememberCoroutineScope()
var scheduledTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
@@ -187,47 +190,63 @@ private fun TimedActionSetupUi(
var shouldSetInitialAction by remember { mutableStateOf(false) }
var initialAction: Action? by remember { mutableStateOf(null) }
- val focusRequester = remember { FocusRequester() }
+ val focusRequester = rememberFocusRequester()
- PagerScaffold(modifier = modifier) {
- HorizontalPager(
- state = pagerState,
- userScrollEnabled = false
- ) { index ->
- when (index) {
+ HorizontalPagerScreen(
+ modifier = modifier,
+ pagerState = pagerState,
+ userScrollEnabled = false,
+ hidePagerIndicator = true
+ ) { pageIdx ->
+ AnimatedPage(pageIdx, pagerState) {
+ when (pageIdx) {
// Time
0 -> {
- TimePicker(
- onTimeConfirm = {
- scheduledTime = System.currentTimeMillis() +
- TimeUnit.HOURS.toMillis(it.hour.toLong()) +
- TimeUnit.MINUTES.toMillis(it.minute.toLong())
- compositionScope.launch {
- pagerState.animateScrollToPage(index + 1)
- }
- },
- time = LocalTime.of(0, 15),
- showSeconds = false
- )
+ if (!isPreview) {
+ TimePicker(
+ onTimePicked = {
+ scheduledTime = System.currentTimeMillis() +
+ TimeUnit.HOURS.toMillis(it.hour.toLong()) +
+ TimeUnit.MINUTES.toMillis(it.minute.toLong())
+ compositionScope.launch {
+ pagerState.animateScrollToPage(pageIdx + 1)
+ }
+ },
+ initialTime = LocalTime.of(0, 15),
+ timePickerType = TimePickerType.HoursMinutes24H
+ )
+ } else {
+ com.google.android.horologist.composables.TimePicker(
+ onTimeConfirm = {},
+ time = LocalTime.of(0, 15),
+ showSeconds = false
+ )
+ }
}
// Actions
1 -> {
- val scrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Text,
- last = ScalingLazyColumnDefaults.ItemType.Chip
- )
+ val columnState = rememberTransformingLazyColumnState()
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
)
-
- Box {
- TimeText(modifier = Modifier.scrollAway { scrollState })
-
- ScalingLazyColumn(
- scrollState = scrollState,
- focusRequester = focusRequester
+ val transformationSpec = rememberTransformationSpec()
+
+ ScreenScaffold(
+ scrollState = columnState,
+ contentPadding = contentPadding
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ state = columnState,
+ contentPadding = contentPadding
) {
item {
- ResponsiveListHeader {
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec)
+ ) {
Text(text = stringResource(id = R.string.title_actions))
}
}
@@ -239,19 +258,24 @@ private fun TimedActionSetupUi(
ActionButtonViewModel.getViewModelFromAction(it)
}
- Chip(
- label = stringResource(id = model.actionLabelResId),
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ label = {
+ Text(text = stringResource(id = model.actionLabelResId))
+ },
icon = {
Icon(
painter = painterResource(id = model.drawableResId),
contentDescription = stringResource(id = model.actionLabelResId)
)
},
- colors = ChipDefaults.secondaryChipColors(),
onClick = {
selectedAction = Action.getDefaultAction(it)
compositionScope.launch {
- pagerState.animateScrollToPage(index + 1)
+ pagerState.animateScrollToPage(pageIdx + 1)
}
}
)
@@ -259,7 +283,7 @@ private fun TimedActionSetupUi(
}
LaunchedEffect(pagerState, pagerState.targetPage) {
- if (pagerState.targetPage == index) {
+ if (pagerState.targetPage == pageIdx) {
focusRequester.requestFocus()
}
}
@@ -267,12 +291,12 @@ private fun TimedActionSetupUi(
}
// State
2 -> {
- val scrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Text,
- last = ScalingLazyColumnDefaults.ItemType.Chip
- )
+ val columnState = rememberTransformingLazyColumnState()
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
)
+ val transformationSpec = rememberTransformationSpec()
var actionState by remember { mutableStateOf(null) }
var initActionState by remember { mutableStateOf(null) }
@@ -281,63 +305,47 @@ private fun TimedActionSetupUi(
ActionButtonViewModel(selectedAction)
}
- Box {
- TimeText(modifier = Modifier.scrollAway { scrollState })
-
- ScalingLazyColumn(
- scrollState = scrollState,
- focusRequester = focusRequester
+ ScreenScaffold(
+ scrollState = columnState,
+ contentPadding = contentPadding
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ state = columnState,
+ contentPadding = contentPadding
) {
item {
- ResponsiveListHeader {
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec)
+ ) {
Text(text = stringResource(id = R.string.title_confirm_action))
}
}
item {
- androidx.wear.compose.material.Chip(
- modifier = Modifier.fillMaxWidth(),
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
enabled = false,
- colors = ChipDefaults.secondaryChipColors(),
- border = ChipDefaults.chipBorder(),
+ icon = {
+ Icon(
+ painter = painterResource(id = model.drawableResId),
+ contentDescription = stringResource(id = model.actionLabelResId)
+ )
+ },
+ label = {
+ Text(text = stringResource(id = R.string.label_action))
+ },
+ secondaryLabel = {
+ Text(text = stringResource(id = model.actionLabelResId))
+ },
+ colors = disabledButtonColors(),
onClick = {}
- ) {
- Row(
- modifier = Modifier.fillMaxSize()
- ) {
- Box(
- modifier = Modifier.align(Alignment.CenterVertically)
- ) {
- Icon(
- modifier = Modifier.align(Alignment.Center),
- painter = painterResource(id = model.drawableResId),
- contentDescription = stringResource(id = model.actionLabelResId),
- tint = MaterialTheme.colors.onSurface
- )
- }
- Spacer(modifier = Modifier.size(6.dp))
- Column(
- modifier = Modifier.align(Alignment.CenterVertically)
- ) {
- Row {
- Text(
- text = stringResource(id = R.string.label_action),
- style = MaterialTheme.typography.button,
- color = MaterialTheme.colors.onSurface
- )
- }
- Row {
- Text(
- text = stringResource(id = model.actionLabelResId),
- style = MaterialTheme.typography.caption2,
- color = MaterialTheme.colors.onSurface.copy(
- alpha = 0.75f
- )
- )
- }
- }
- }
- }
+ )
}
item {
@@ -350,70 +358,60 @@ private fun TimedActionSetupUi(
)
}
- androidx.wear.compose.material.Chip(
- modifier = Modifier.fillMaxWidth(),
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
enabled = false,
- colors = ChipDefaults.secondaryChipColors(),
- border = ChipDefaults.chipBorder(),
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_alarm_white_24dp),
+ contentDescription = stringResource(id = R.string.label_time),
+ )
+ },
+ label = {
+ Text(text = stringResource(id = R.string.label_time))
+ },
+ secondaryLabel = {
+ Text(text = timeString)
+ },
+ colors = disabledButtonColors(),
onClick = {}
- ) {
- Row(
- modifier = Modifier.fillMaxSize()
- ) {
- Box(
- modifier = Modifier.align(Alignment.CenterVertically)
- ) {
- Icon(
- modifier = Modifier.align(Alignment.Center),
- painter = painterResource(id = R.drawable.ic_alarm_white_24dp),
- contentDescription = stringResource(id = R.string.label_time),
- tint = MaterialTheme.colors.onSurface
- )
- }
- Spacer(modifier = Modifier.size(6.dp))
- Column(
- modifier = Modifier.align(Alignment.CenterVertically)
- ) {
- Row {
- Text(
- text = stringResource(id = R.string.label_time),
- style = MaterialTheme.typography.button,
- color = MaterialTheme.colors.onSurface
- )
- }
- Row {
- Text(
- text = timeString,
- style = MaterialTheme.typography.caption2,
- color = MaterialTheme.colors.onSurface.copy(
- alpha = 0.75f
- )
- )
- }
- }
- }
- }
+ )
}
- item {
- ToggleChip(
- checked = shouldSetInitialAction,
- onCheckedChanged = {
- initialAction =
- Action.getDefaultAction(selectedAction.actionType)
- shouldSetInitialAction = it
- },
- label = stringResource(id = R.string.title_set_initial_state),
- toggleControl = ToggleChipToggleControl.Switch
- )
+ if (selectedAction.actionType != Actions.SLEEPTIMER) {
+ item {
+ SwitchButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ checked = shouldSetInitialAction,
+ onCheckedChange = {
+ initialAction =
+ Action.getDefaultAction(selectedAction.actionType)
+ shouldSetInitialAction = it
+ },
+ label = {
+ Text(text = stringResource(id = R.string.title_set_initial_state))
+ }
+ )
+ }
}
if (shouldSetInitialAction) {
item {
- ListHeader {
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
Text(
text = stringResource(id = R.string.title_initial_state),
- style = MaterialTheme.typography.caption1
+ style = MaterialTheme.typography.labelSmall
)
}
}
@@ -428,14 +426,21 @@ private fun TimedActionSetupUi(
ActionButtonViewModel(tA)
}
- ToggleChip(
+ SwitchButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(
+ transformationSpec
+ ),
checked = tA.isEnabled,
- onCheckedChanged = {
+ onCheckedChange = {
tA.isEnabled = it
initActionState = it
},
- label = stringResource(id = actionModel.stateLabelResId),
- toggleControl = ToggleChipToggleControl.Switch
+ label = {
+ Text(text = stringResource(id = actionModel.stateLabelResId))
+ }
)
}
}
@@ -454,14 +459,21 @@ private fun TimedActionSetupUi(
)
}
- ToggleChip(
- checked = mA.choice == choice,
- onCheckedChanged = {
+ RadioButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(
+ transformationSpec
+ ),
+ selected = mA.choice == choice,
+ onSelect = {
mA.choice = choice
- initActionState = it
+ initActionState = true
},
- label = stringResource(id = multiActionModel.stateLabelResId),
- toggleControl = ToggleChipToggleControl.Radio
+ label = {
+ Text(text = stringResource(id = multiActionModel.stateLabelResId))
+ }
)
}
}
@@ -470,67 +482,96 @@ private fun TimedActionSetupUi(
}
}
- item {
- ListHeader {
- Text(
- text = stringResource(id = R.string.title_scheduled_state),
- style = MaterialTheme.typography.caption1
- )
- }
- }
-
- when (selectedAction) {
- is ToggleAction -> {
- item {
- val tA = remember(selectedAction, actionState) {
- selectedAction as ToggleAction
- }
-
- ToggleChip(
- checked = tA.isEnabled,
- onCheckedChanged = {
- tA.isEnabled = it
- actionState = it
- },
- label = stringResource(id = model.stateLabelResId),
- toggleControl = ToggleChipToggleControl.Switch
+ if (selectedAction.actionType != Actions.SLEEPTIMER) {
+ item {
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
+ Text(
+ text = stringResource(id = R.string.title_scheduled_state),
+ style = MaterialTheme.typography.labelSmall
)
}
}
- is MultiChoiceAction -> {
- items((selectedAction as MultiChoiceAction).numberOfStates) { choice ->
- val mA = remember(selectedAction, actionState) {
- selectedAction as MultiChoiceAction
+ when (selectedAction) {
+ is ToggleAction -> {
+ item {
+ val tA = remember(selectedAction, actionState) {
+ selectedAction as ToggleAction
+ }
+
+ SwitchButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(
+ transformationSpec
+ ),
+ checked = tA.isEnabled,
+ onCheckedChange = {
+ tA.isEnabled = it
+ actionState = it
+ },
+ label = {
+ Text(text = stringResource(id = model.stateLabelResId))
+ }
+ )
}
- val multiActionModel = remember(mA, choice) {
- ActionButtonViewModel(
- MultiChoiceAction(
- mA.actionType,
- choice
+ }
+
+ is MultiChoiceAction -> {
+ items((selectedAction as MultiChoiceAction).numberOfStates) { choice ->
+ val mA = remember(selectedAction, actionState) {
+ selectedAction as MultiChoiceAction
+ }
+ val multiActionModel = remember(mA, choice) {
+ ActionButtonViewModel(
+ MultiChoiceAction(
+ mA.actionType,
+ choice
+ )
)
+ }
+
+ RadioButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(
+ transformationSpec
+ ),
+ selected = mA.choice == choice,
+ onSelect = {
+ mA.choice = choice
+ actionState = choice
+ },
+ label = {
+ Text(text = stringResource(id = multiActionModel.stateLabelResId))
+ }
)
}
-
- ToggleChip(
- checked = mA.choice == choice,
- onCheckedChanged = {
- mA.choice = choice
- actionState = choice
- },
- label = stringResource(id = multiActionModel.stateLabelResId),
- toggleControl = ToggleChipToggleControl.Radio
- )
}
- }
- else -> {
- item {
- Chip(
- label = stringResource(id = R.string.label_action_not_supported),
- onClick = {},
- enabled = false
- )
+ else -> {
+ item {
+ Button(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(
+ transformationSpec
+ ),
+ label = {
+ Text(text = stringResource(id = R.string.label_action_not_supported))
+ },
+ onClick = {},
+ enabled = false
+ )
+ }
}
}
}
@@ -539,34 +580,40 @@ private fun TimedActionSetupUi(
Spacer(modifier = Modifier.size(16.dp))
}
- if (selectedAction is ToggleAction || selectedAction is MultiChoiceAction) {
+ if (selectedAction is ToggleAction || selectedAction is MultiChoiceAction || selectedAction.actionType == Actions.SLEEPTIMER) {
item {
- Button(
- id = R.drawable.ic_check_white_24dp,
- contentDescription = stringResource(id = android.R.string.ok),
+ FilledIconButton(
+ content = {
+ Icon(
+ painter = painterResource(R.drawable.ic_check_white_24dp),
+ contentDescription = stringResource(id = android.R.string.ok),
+ )
+ },
onClick = {
onAddAction.invoke(
initialAction,
TimedAction(scheduledTime, selectedAction)
)
- },
- colors = ButtonDefaults.primaryButtonColors()
+ }
)
}
} else {
item {
- Button(
- id = R.drawable.ic_close_white_24dp,
- contentDescription = stringResource(id = android.R.string.cancel),
- onClick = onCancel,
- colors = ButtonDefaults.secondaryButtonColors()
+ FilledTonalIconButton(
+ content = {
+ Icon(
+ painter = painterResource(R.drawable.ic_close_white_24dp),
+ contentDescription = stringResource(id = android.R.string.cancel),
+ )
+ },
+ onClick = onCancel
)
}
}
}
LaunchedEffect(pagerState, pagerState.targetPage) {
- if (pagerState.targetPage == index) {
+ if (pagerState.targetPage == pageIdx) {
focusRequester.requestFocus()
}
}
@@ -577,6 +624,15 @@ private fun TimedActionSetupUi(
}
}
+@Composable
+private fun disabledButtonColors(): ButtonColors {
+ return ButtonDefaults.filledTonalButtonColors(
+ disabledContentColor = MaterialTheme.colorScheme.onSurface,
+ disabledSecondaryContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
+ disabledIconColor = MaterialTheme.colorScheme.onSurface,
+ )
+}
+
@WearPreviewDevices
@WearPreviewFontScales
@Composable
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/TimedActionUi.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/TimedActionUi.kt
index 9fa90441..14390161 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/TimedActionUi.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/TimedActionUi.kt
@@ -1,28 +1,21 @@
-@file:OptIn(
- ExperimentalHorologistApi::class,
- ExperimentalWearFoundationApi::class,
- ExperimentalWearMaterialApi::class
-)
-
package com.thewizrd.simplewear.ui.simplewear
import android.text.format.DateFormat
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.material.LocalContentColor
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Add
+import androidx.compose.material.icons.rounded.DeleteOutline
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -31,7 +24,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -44,43 +36,33 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.items
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
-import androidx.wear.compose.foundation.rememberRevealState
-import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
-import androidx.wear.compose.foundation.rotary.rotaryScrollable
-import androidx.wear.compose.material.Button
-import androidx.wear.compose.material.ButtonDefaults
-import androidx.wear.compose.material.Chip
-import androidx.wear.compose.material.ChipDefaults
-import androidx.wear.compose.material.ExperimentalWearMaterialApi
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.ListHeader
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.PositionIndicator
-import androidx.wear.compose.material.Scaffold
-import androidx.wear.compose.material.SwipeToRevealChip
-import androidx.wear.compose.material.SwipeToRevealDefaults
-import androidx.wear.compose.material.SwipeToRevealPrimaryAction
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.TimeText
-import androidx.wear.compose.material.Vignette
-import androidx.wear.compose.material.VignettePosition
-import androidx.wear.compose.material.dialog.Dialog
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.Dialog
+import androidx.wear.compose.material3.EdgeButton
+import androidx.wear.compose.material3.FilledIconButton
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.IconButtonDefaults
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.RadioButton
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.SwipeToReveal
+import androidx.wear.compose.material3.SwitchButton
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TimePicker
+import androidx.wear.compose.material3.TimePickerType
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.composables.TimePicker
-import com.google.android.horologist.composables.TimePickerWith12HourClock
-import com.google.android.horologist.compose.layout.ScalingLazyColumn
-import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
-import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults.listTextPadding
+import com.google.android.horologist.compose.layout.ColumnItemType
import com.google.android.horologist.compose.layout.fillMaxRectangle
-import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
-import com.google.android.horologist.compose.layout.scrollAway
-import com.google.android.horologist.compose.material.ResponsiveListHeader
-import com.google.android.horologist.compose.material.ToggleChip
-import com.google.android.horologist.compose.material.ToggleChipToggleControl
+import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding
import com.thewizrd.shared_resources.actions.ActionStatus
import com.thewizrd.shared_resources.actions.Actions
import com.thewizrd.shared_resources.actions.DNDChoice
@@ -90,16 +72,20 @@ import com.thewizrd.shared_resources.actions.TimedAction
import com.thewizrd.shared_resources.actions.ToggleAction
import com.thewizrd.shared_resources.controls.ActionButtonViewModel
import com.thewizrd.shared_resources.helpers.WearableHelper
+import com.thewizrd.shared_resources.utils.JSONParser
+import com.thewizrd.shared_resources.utils.getSerializableCompat
import com.thewizrd.simplewear.R
import com.thewizrd.simplewear.ui.components.ConfirmationOverlay
import com.thewizrd.simplewear.ui.components.LoadingContent
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
import com.thewizrd.simplewear.ui.navigation.Screen
+import com.thewizrd.simplewear.ui.theme.WearAppTheme
import com.thewizrd.simplewear.ui.theme.activityViewModel
import com.thewizrd.simplewear.ui.theme.findActivity
-import com.thewizrd.simplewear.ui.tools.WearPreviewDevices
import com.thewizrd.simplewear.viewmodels.ConfirmationData
import com.thewizrd.simplewear.viewmodels.ConfirmationViewModel
import com.thewizrd.simplewear.viewmodels.TimedActionUiViewModel
+import com.thewizrd.simplewear.viewmodels.WearableListenerViewModel
import com.thewizrd.simplewear.viewmodels.WearableListenerViewModel.Companion.EXTRA_STATUS
import kotlinx.coroutines.launch
import java.time.Instant
@@ -169,7 +155,8 @@ fun TimedActionUi(
when (event.eventType) {
WearableHelper.TimedActionAddPath,
WearableHelper.TimedActionDeletePath -> {
- val status = event.data.getSerializable(EXTRA_STATUS) as ActionStatus
+ val status =
+ event.data.getSerializableCompat(EXTRA_STATUS, ActionStatus::class.java)
when (status) {
ActionStatus.SUCCESS -> {
@@ -177,16 +164,13 @@ fun TimedActionUi(
}
ActionStatus.PERMISSION_DENIED -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_permissiondenied)
+ confirmationViewModel.showOpenOnPhoneForFailure(
+ message = context.getString(
+ R.string.error_permissiondenied_wear
)
)
- timedActionUiViewModel.openAppOnPhone(
- activity,
- showAnimation = false
- )
+ timedActionUiViewModel.openAppOnPhone(showAnimation = false)
}
else -> {
@@ -196,6 +180,15 @@ fun TimedActionUi(
}
}
}
+
+ WearableListenerViewModel.ACTION_SHOWCONFIRMATION -> {
+ val jsonData =
+ event.data.getString(WearableListenerViewModel.EXTRA_ACTIONDATA)
+
+ JSONParser.deserializer(jsonData, ConfirmationData::class.java)?.let {
+ confirmationViewModel.showConfirmation(it)
+ }
+ }
}
}
}
@@ -210,37 +203,38 @@ private fun TimedActionUi(
onActionDelete: (TimedAction) -> Unit = {},
onAddAction: () -> Unit = {}
) {
- val scrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Text,
- last = ScalingLazyColumnDefaults.ItemType.SingleButton
- )
+ val columnState = rememberTransformingLazyColumnState()
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
)
+ val transformationSpec = rememberTransformationSpec()
- Scaffold(
- modifier = modifier.background(MaterialTheme.colors.background),
- timeText = { TimeText(modifier = Modifier.scrollAway { scrollState }) },
- vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
- positionIndicator = { PositionIndicator(scalingLazyListState = scrollState.state) }
+ ScreenScaffold(
+ modifier = modifier,
+ scrollState = columnState,
+ contentPadding = contentPadding,
+ edgeButton = {
+ EdgeButton(onClick = onAddAction) {
+ Icon(
+ imageVector = Icons.Rounded.Add,
+ contentDescription = stringResource(id = R.string.label_add_action)
+ )
+ }
+ }
) {
- ScalingLazyColumn(
- modifier = modifier
- .fillMaxSize()
- .rotaryScrollable(
- focusRequester = rememberActiveFocusRequester(),
- behavior = RotaryScrollableDefaults.behavior(scrollState)
- ),
- columnState = scrollState
+ TransformingLazyColumn(
+ state = columnState,
+ contentPadding = contentPadding
) {
item {
- Box(
- modifier = Modifier.padding(bottom = 12.dp)
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec)
) {
- Text(
- modifier = Modifier.listTextPadding(),
- text = stringResource(id = R.string.title_actions),
- style = MaterialTheme.typography.button
- )
+ Text(text = stringResource(id = R.string.title_actions))
}
}
items(
@@ -253,17 +247,6 @@ private fun TimedActionUi(
onActionDelete = onActionDelete
)
}
- item {
- Spacer(modifier = Modifier.height(16.dp))
- }
- item {
- Button(onClick = onAddAction) {
- Icon(
- painter = painterResource(id = R.drawable.ic_add_white_24dp),
- contentDescription = stringResource(id = R.string.label_add_action)
- )
- }
- }
}
}
}
@@ -273,13 +256,13 @@ private fun EmptyTimedActionUi(
modifier: Modifier = Modifier,
onAddAction: () -> Unit = {}
) {
- Scaffold(
- modifier = modifier.background(MaterialTheme.colors.background),
- timeText = { TimeText() },
- vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) }
- ) {
+ ScreenScaffold(
+ modifier = modifier
+ ) { contentPadding ->
Column(
- modifier = Modifier.fillMaxRectangle()
+ modifier = Modifier
+ .padding(contentPadding)
+ .fillMaxRectangle()
) {
Box(
modifier = Modifier
@@ -292,8 +275,7 @@ private fun EmptyTimedActionUi(
) {
Text(
text = stringResource(id = R.string.title_schedule_action),
- textAlign = TextAlign.Center,
- color = MaterialTheme.colors.onBackground
+ textAlign = TextAlign.Center
)
}
@@ -305,7 +287,7 @@ private fun EmptyTimedActionUi(
onClick = onAddAction
) {
Icon(
- painter = painterResource(id = R.drawable.ic_add_white_24dp),
+ imageVector = Icons.Rounded.Add,
contentDescription = stringResource(R.string.label_add_action)
)
}
@@ -332,37 +314,32 @@ private fun TimedActionChip(
)
)
}
- val revealState = rememberRevealState()
- val chipColors = ChipDefaults.secondaryChipColors()
+ val buttonColors = ButtonDefaults.filledTonalButtonColors()
- SwipeToRevealChip(
+ SwipeToReveal(
primaryAction = {
- SwipeToRevealPrimaryAction(
- revealState = revealState,
+ PrimaryActionButton(
onClick = {
onActionDelete.invoke(timedAction)
},
icon = {
Icon(
- imageVector = SwipeToRevealDefaults.Delete,
+ imageVector = Icons.Rounded.DeleteOutline,
contentDescription = stringResource(id = R.string.action_delete)
)
},
- label = {
+ text = {
Text(text = stringResource(id = R.string.action_delete))
}
)
},
- revealState = revealState,
- onFullSwipe = {
+ onSwipePrimaryAction = {
onActionDelete.invoke(timedAction)
}
) {
- Chip(
+ FilledTonalButton(
modifier = Modifier.fillMaxWidth(),
- colors = chipColors,
- border = ChipDefaults.chipBorder(),
onClick = {
onActionClicked.invoke(timedAction)
}
@@ -384,7 +361,7 @@ private fun TimedActionChip(
) {
model.getDescription(context)
},
- tint = chipColors.iconColor(enabled = true).value
+ tint = buttonColors.iconColor
)
}
Spacer(modifier = Modifier.size(6.dp))
@@ -394,8 +371,8 @@ private fun TimedActionChip(
Row {
Text(
text = stringResource(id = model.actionLabelResId),
- style = MaterialTheme.typography.button,
- color = chipColors.contentColor(enabled = true).value,
+ style = MaterialTheme.typography.labelMedium,
+ color = buttonColors.contentColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@@ -404,8 +381,8 @@ private fun TimedActionChip(
Row {
Text(
text = stringResource(id = model.stateLabelResId),
- style = MaterialTheme.typography.caption1,
- color = chipColors.secondaryContentColor(enabled = true).value,
+ style = MaterialTheme.typography.labelSmall,
+ color = buttonColors.secondaryContentColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@@ -414,8 +391,8 @@ private fun TimedActionChip(
Row {
Text(
text = timeString,
- style = MaterialTheme.typography.caption2,
- color = chipColors.secondaryContentColor(enabled = true).value,
+ style = MaterialTheme.typography.bodySmall,
+ color = buttonColors.secondaryContentColor,
maxLines = 1
)
}
@@ -462,7 +439,8 @@ fun TimedActionDetailUi(
when (event.eventType) {
WearableHelper.TimedActionUpdatePath,
WearableHelper.TimedActionDeletePath -> {
- val status = event.data.getSerializable(EXTRA_STATUS) as ActionStatus
+ val status =
+ event.data.getSerializableCompat(EXTRA_STATUS, ActionStatus::class.java)
when (status) {
ActionStatus.SUCCESS -> {
@@ -471,16 +449,13 @@ fun TimedActionDetailUi(
}
ActionStatus.PERMISSION_DENIED -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- title = context.getString(R.string.error_permissiondenied)
+ confirmationViewModel.showOpenOnPhoneForFailure(
+ message = context.getString(
+ R.string.error_permissiondenied_wear
)
)
- timedActionUiViewModel.openAppOnPhone(
- activity,
- showAnimation = false
- )
+ timedActionUiViewModel.openAppOnPhone(showAnimation = false)
}
else -> {
@@ -490,6 +465,15 @@ fun TimedActionDetailUi(
}
}
}
+
+ WearableListenerViewModel.ACTION_SHOWCONFIRMATION -> {
+ val jsonData =
+ event.data.getString(WearableListenerViewModel.EXTRA_ACTIONDATA)
+
+ JSONParser.deserializer(jsonData, ConfirmationData::class.java)?.let {
+ confirmationViewModel.showConfirmation(it)
+ }
+ }
}
}
}
@@ -503,13 +487,13 @@ private fun TimedActionDetailUi(
onActionUpdate: (TimedAction) -> Unit = {},
onActionDelete: (TimedAction) -> Unit = {}
) {
- val scrollState = rememberResponsiveColumnState(
- contentPadding = ScalingLazyColumnDefaults.padding(
- first = ScalingLazyColumnDefaults.ItemType.Text,
- last = ScalingLazyColumnDefaults.ItemType.SingleButton,
- ),
- verticalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterVertically)
+ val columnState = rememberTransformingLazyColumnState()
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
)
+ val transformationSpec = rememberTransformationSpec()
+
val context = LocalContext.current
val is24Hour = remember(context) {
DateFormat.is24HourFormat(context)
@@ -531,122 +515,87 @@ private fun TimedActionDetailUi(
)
}
- Scaffold(
- modifier = modifier.background(MaterialTheme.colors.background),
- timeText = { TimeText(modifier = Modifier.scrollAway { scrollState }) },
- vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
- positionIndicator = { PositionIndicator(scalingLazyListState = scrollState.state) }
- ) {
- ScalingLazyColumn(
- columnState = scrollState
+ ScreenScaffold(
+ scrollState = columnState,
+ contentPadding = contentPadding
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ state = columnState,
+ contentPadding = contentPadding
) {
item {
- ResponsiveListHeader {
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec)
+ ) {
Text(text = stringResource(id = R.string.title_edit_action))
}
}
item {
- Chip(
- modifier = Modifier.fillMaxWidth(),
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
enabled = false,
- colors = ChipDefaults.secondaryChipColors(),
- border = ChipDefaults.chipBorder(),
- onClick = {}
- ) {
- Row(
- modifier = Modifier.fillMaxSize()
- ) {
- Box(
- modifier = Modifier.align(Alignment.CenterVertically)
- ) {
- Icon(
- modifier = Modifier.align(Alignment.Center),
- painter = painterResource(id = model.drawableResId),
- contentDescription = remember(
- context,
- model.actionLabelResId,
- model.stateLabelResId
- ) {
- model.getDescription(context)
- },
- tint = MaterialTheme.colors.onSurface
- )
- }
- Spacer(modifier = Modifier.size(6.dp))
- Column(
- modifier = Modifier.align(Alignment.CenterVertically)
- ) {
- Row {
- Text(
- text = stringResource(id = R.string.label_action),
- style = MaterialTheme.typography.button,
- color = MaterialTheme.colors.onSurface
- )
- }
- Row {
- Text(
- text = stringResource(id = model.actionLabelResId),
- style = MaterialTheme.typography.caption2,
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.75f)
- )
+ icon = {
+ Icon(
+ modifier = Modifier.align(Alignment.Center),
+ painter = painterResource(id = model.drawableResId),
+ contentDescription = remember(
+ context,
+ model.actionLabelResId,
+ model.stateLabelResId
+ ) {
+ model.getDescription(context)
}
- }
- }
- }
+ )
+ },
+ label = {
+ Text(text = stringResource(id = R.string.label_action))
+ },
+ secondaryLabel = {
+ Text(text = stringResource(id = model.actionLabelResId))
+ },
+ onClick = {}
+ )
}
item {
- Chip(
- modifier = Modifier.fillMaxWidth(),
- colors = ChipDefaults.secondaryChipColors(),
- border = ChipDefaults.chipBorder(),
+ FilledTonalButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
onClick = {
showTimePicker = true
+ },
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_alarm_white_24dp),
+ contentDescription = stringResource(id = R.string.label_time)
+ )
+ },
+ label = {
+ Text(text = stringResource(id = R.string.label_time))
+ },
+ secondaryLabel = {
+ Text(text = timeString)
}
- ) {
- Row(
- modifier = Modifier.fillMaxSize()
- ) {
- Box(
- modifier = Modifier.align(Alignment.CenterVertically)
- ) {
- Icon(
- modifier = Modifier.align(Alignment.Center),
- painter = painterResource(id = R.drawable.ic_alarm_white_24dp),
- contentDescription = stringResource(id = R.string.label_time),
- tint = MaterialTheme.colors.onSurface
- )
- }
- Spacer(modifier = Modifier.size(6.dp))
- Column(
- modifier = Modifier.align(Alignment.CenterVertically)
- ) {
- Row {
- Text(
- text = stringResource(id = R.string.label_time),
- style = MaterialTheme.typography.button,
- color = MaterialTheme.colors.onSurface
- )
- }
- Row {
- Text(
- text = timeString,
- style = MaterialTheme.typography.caption2,
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.75f)
- )
- }
- }
- }
- }
+ )
}
item {
- ListHeader {
- Text(
- text = stringResource(id = R.string.label_state),
- style = MaterialTheme.typography.caption1
- )
+ ListHeader(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec)
+ ) {
+ Text(text = stringResource(id = R.string.label_state))
}
}
@@ -657,14 +606,19 @@ private fun TimedActionDetailUi(
action.action as ToggleAction
}
- ToggleChip(
+ SwitchButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
checked = tA.isEnabled,
- onCheckedChanged = {
+ onCheckedChange = {
tA.isEnabled = it
actionState = it
},
- label = stringResource(id = model.stateLabelResId),
- toggleControl = ToggleChipToggleControl.Switch
+ label = {
+ Text(text = stringResource(id = model.stateLabelResId))
+ }
)
}
}
@@ -678,22 +632,33 @@ private fun TimedActionDetailUi(
ActionButtonViewModel(MultiChoiceAction(mA.actionType, choice))
}
- ToggleChip(
- checked = mA.choice == choice,
- onCheckedChanged = {
+ RadioButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ selected = mA.choice == choice,
+ onSelect = {
mA.choice = choice
actionState = choice
},
- label = stringResource(id = multiActionModel.stateLabelResId),
- toggleControl = ToggleChipToggleControl.Radio
+ label = {
+ Text(text = stringResource(id = multiActionModel.stateLabelResId))
+ }
)
}
}
else -> {
item {
- com.google.android.horologist.compose.material.Chip(
- label = stringResource(id = R.string.label_action_not_supported),
+ Button(
+ modifier = Modifier
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ label = {
+ Text(text = stringResource(id = R.string.label_action_not_supported))
+ },
onClick = {},
enabled = false
)
@@ -702,7 +667,9 @@ private fun TimedActionDetailUi(
}
item {
- Spacer(modifier = Modifier.size(16.dp))
+ Spacer(
+ modifier = Modifier.size(16.dp)
+ )
}
item {
@@ -712,20 +679,27 @@ private fun TimedActionDetailUi(
Alignment.CenterHorizontally
)
) {
- com.google.android.horologist.compose.material.Button(
- id = R.drawable.ic_check_white_24dp,
- contentDescription = stringResource(id = android.R.string.ok),
+ FilledIconButton(
+ content = {
+ Icon(
+ painter = painterResource(R.drawable.ic_check_white_24dp),
+ contentDescription = stringResource(id = android.R.string.ok),
+ )
+ },
onClick = {
onActionUpdate.invoke(action)
- },
- colors = ButtonDefaults.primaryButtonColors()
+ }
)
- com.google.android.horologist.compose.material.Button(
- id = R.drawable.ic_delete_outline,
- contentDescription = stringResource(id = R.string.action_delete),
- colors = ButtonDefaults.primaryButtonColors(
- backgroundColor = MaterialTheme.colors.error,
- contentColor = MaterialTheme.colors.onError
+ FilledIconButton(
+ content = {
+ Icon(
+ painter = painterResource(R.drawable.ic_delete_outline),
+ contentDescription = stringResource(id = R.string.action_delete),
+ )
+ },
+ colors = IconButtonDefaults.iconButtonColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer,
),
onClick = {
onActionDelete.invoke(action)
@@ -737,7 +711,7 @@ private fun TimedActionDetailUi(
}
Dialog(
- showDialog = showTimePicker,
+ visible = showTimePicker,
onDismissRequest = {
showTimePicker = false
}
@@ -748,42 +722,27 @@ private fun TimedActionDetailUi(
.toLocalTime()
}
- if (is24Hour) {
- TimePicker(
- time = localTime,
- onTimeConfirm = {
- val today = LocalDate.now()
- val now = LocalTime.now()
-
- action.timeInMillis = if (it < now) {
- today.plusDays(1).atTime(it).atZone(ZoneId.systemDefault()).toInstant()
- .toEpochMilli()
- } else {
- today.atTime(it).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
- }
-
- showTimePicker = false
- },
- showSeconds = false
- )
- } else {
- TimePickerWith12HourClock(
- time = localTime,
- onTimeConfirm = {
- val today = LocalDate.now()
- val now = LocalTime.now()
-
- action.timeInMillis = if (it < now) {
- today.plusDays(1).atTime(it).atZone(ZoneId.systemDefault()).toInstant()
- .toEpochMilli()
- } else {
- today.atTime(it).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
- }
-
- showTimePicker = false
+ TimePicker(
+ initialTime = localTime,
+ onTimePicked = {
+ val today = LocalDate.now()
+ val now = LocalTime.now()
+
+ action.timeInMillis = if (it < now) {
+ today.plusDays(1).atTime(it).atZone(ZoneId.systemDefault()).toInstant()
+ .toEpochMilli()
+ } else {
+ today.atTime(it).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
}
- )
- }
+
+ showTimePicker = false
+ },
+ timePickerType = if (is24Hour) {
+ TimePickerType.HoursMinutes24H
+ } else {
+ TimePickerType.HoursMinutesAmPm12H
+ }
+ )
}
}
@@ -810,9 +769,7 @@ private fun PreviewTimedActionUi() {
)
}
- CompositionLocalProvider(
- LocalContentColor provides Color.White
- ) {
+ WearAppTheme {
TimedActionUi(
actions = actions
)
@@ -834,9 +791,7 @@ private fun PreviewTimedActionDetailUi() {
)
}
- CompositionLocalProvider(
- LocalContentColor provides Color.White
- ) {
+ WearAppTheme {
TimedActionDetailUi(
action = action
)
@@ -847,9 +802,7 @@ private fun PreviewTimedActionDetailUi() {
@WearPreviewFontScales
@Composable
private fun PreviewEmptyTimedActionUi() {
- CompositionLocalProvider(
- LocalContentColor provides Color.White
- ) {
+ WearAppTheme {
EmptyTimedActionUi()
}
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/ValueActionScreen.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/ValueActionScreen.kt
index 071a66aa..faa388cf 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/ValueActionScreen.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/ValueActionScreen.kt
@@ -1,11 +1,12 @@
-@file:OptIn(ExperimentalWearFoundationApi::class, ExperimentalHorologistApi::class)
-
package com.thewizrd.simplewear.ui.simplewear
import android.content.Intent
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Add
+import androidx.compose.material.icons.rounded.Remove
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -19,24 +20,16 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
+import androidx.wear.compose.foundation.requestFocusOnHierarchyActive
import androidx.wear.compose.foundation.rotary.rotaryScrollable
-import androidx.wear.compose.material.Chip
-import androidx.wear.compose.material.ChipDefaults
-import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.Scaffold
-import androidx.wear.compose.material.Stepper
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.TimeText
-import androidx.wear.compose.material.Vignette
-import androidx.wear.compose.material.VignettePosition
-import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.audio.ui.VolumePositionIndicator
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.Stepper
+import androidx.wear.compose.material3.Text
import com.google.android.horologist.audio.ui.VolumeUiState
-import com.google.android.horologist.audio.ui.volumeRotaryBehavior
+import com.google.android.horologist.audio.ui.material3.VolumeLevelIndicator
+import com.google.android.horologist.audio.ui.material3.volumeRotaryBehavior
import com.thewizrd.shared_resources.actions.Action
import com.thewizrd.shared_resources.actions.ActionStatus
import com.thewizrd.shared_resources.actions.Actions
@@ -46,10 +39,13 @@ import com.thewizrd.shared_resources.controls.ActionButtonViewModel
import com.thewizrd.shared_resources.helpers.WearConnectionStatus
import com.thewizrd.shared_resources.helpers.WearableHelper
import com.thewizrd.shared_resources.utils.JSONParser
+import com.thewizrd.shared_resources.utils.getSerializableCompat
import com.thewizrd.simplewear.PhoneSyncActivity
import com.thewizrd.simplewear.R
import com.thewizrd.simplewear.ui.components.ConfirmationOverlay
+import com.thewizrd.simplewear.ui.compose.tools.WearPreviewDevices
import com.thewizrd.simplewear.ui.theme.findActivity
+import com.thewizrd.simplewear.ui.utils.rememberFocusRequester
import com.thewizrd.simplewear.viewmodels.ConfirmationData
import com.thewizrd.simplewear.viewmodels.ConfirmationViewModel
import com.thewizrd.simplewear.viewmodels.ValueActionUiState
@@ -77,14 +73,12 @@ fun ValueActionScreen(
val confirmationViewModel = viewModel()
val confirmationData by confirmationViewModel.confirmationEventsFlow.collectAsState()
- Scaffold(
- modifier = modifier.background(MaterialTheme.colors.background),
- vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
- timeText = {
- TimeText()
- },
- ) {
- ValueActionScreen(valueActionViewModel, volumeViewModel)
+ ScreenScaffold { contentPadding ->
+ ValueActionScreen(
+ modifier = modifier.padding(contentPadding),
+ valueActionViewModel = valueActionViewModel,
+ volumeViewModel = volumeViewModel
+ )
}
ConfirmationOverlay(
@@ -126,7 +120,7 @@ fun ValueActionScreen(
WearConnectionStatus.APPNOTINSTALLED -> {
// Open store on remote device
- valueActionViewModel.openPlayStore(activity)
+ valueActionViewModel.openPlayStore()
// Navigate
activity.startActivity(
@@ -154,33 +148,27 @@ fun ValueActionScreen(
lifecycleOwner.lifecycleScope.launch {
when (actionStatus) {
ActionStatus.UNKNOWN, ActionStatus.FAILURE -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- iconResId = R.drawable.ws_full_sad,
- title = context.getString(R.string.error_actionfailed),
+ confirmationViewModel.showFailure(
+ message = context.getString(
+ R.string.error_actionfailed
)
)
}
ActionStatus.PERMISSION_DENIED -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- iconResId = R.drawable.ws_full_sad,
- title = context.getString(R.string.error_permissiondenied),
+ confirmationViewModel.showFailure(
+ message = context.getString(
+ R.string.error_permissiondenied_wear
)
)
- valueActionViewModel.openAppOnPhone(
- activity,
- false
- )
+ valueActionViewModel.openAppOnPhone(false)
}
ActionStatus.TIMEOUT -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- iconResId = R.drawable.ws_full_sad,
- title = context.getString(R.string.error_sendmessage)
+ confirmationViewModel.showFailure(
+ message = context.getString(
+ R.string.error_sendmessage
)
)
}
@@ -194,32 +182,34 @@ fun ValueActionScreen(
WearableHelper.AudioVolumePath, WearableHelper.ValueStatusSetPath -> {
val status =
- event.data.getSerializable(WearableListenerViewModel.EXTRA_STATUS) as ActionStatus
+ event.data.getSerializableCompat(
+ WearableListenerViewModel.EXTRA_STATUS,
+ ActionStatus::class.java
+ )
when (status) {
ActionStatus.UNKNOWN, ActionStatus.FAILURE -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- iconResId = R.drawable.ws_full_sad,
- title = context.getString(R.string.error_actionfailed)
- )
- )
+ confirmationViewModel.showFailure(message = context.getString(R.string.error_actionfailed))
}
ActionStatus.PERMISSION_DENIED -> {
- confirmationViewModel.showConfirmation(
- ConfirmationData(
- iconResId = R.drawable.ws_full_sad,
- title = context.getString(R.string.error_permissiondenied)
- )
- )
+ confirmationViewModel.showFailure(message = context.getString(R.string.error_permissiondenied_wear))
- valueActionViewModel.openAppOnPhone(activity, false)
+ valueActionViewModel.openAppOnPhone(false)
}
else -> {}
}
}
+
+ WearableListenerViewModel.ACTION_SHOWCONFIRMATION -> {
+ val jsonData =
+ event.data.getString(WearableListenerViewModel.EXTRA_ACTIONDATA)
+
+ JSONParser.deserializer(jsonData, ConfirmationData::class.java)?.let {
+ confirmationViewModel.showConfirmation(it)
+ }
+ }
}
}
}
@@ -233,23 +223,23 @@ fun ValueActionScreen(
@Composable
fun ValueActionScreen(
+ modifier: Modifier = Modifier,
valueActionViewModel: ValueActionViewModel,
volumeViewModel: ValueActionVolumeViewModel
) {
- val lifecycleOwner = LocalLifecycleOwner.current
- val activityCtx = LocalContext.current.findActivity()
-
val uiState by valueActionViewModel.uiState.collectAsState()
val progressUiState by volumeViewModel.volumeUiState.collectAsState()
ValueActionScreen(
- modifier = Modifier.rotaryScrollable(
- focusRequester = rememberActiveFocusRequester(),
- behavior = volumeRotaryBehavior(
- volumeUiStateProvider = { progressUiState },
- onRotaryVolumeInput = { newValue -> volumeViewModel.setVolume(newValue) }
- )
- ),
+ modifier = modifier
+ .requestFocusOnHierarchyActive()
+ .rotaryScrollable(
+ focusRequester = rememberFocusRequester(),
+ behavior = volumeRotaryBehavior(
+ volumeUiStateProvider = { progressUiState },
+ onRotaryVolumeInput = { newValue -> volumeViewModel.setVolume(newValue) }
+ )
+ ),
uiState = uiState,
volumeUiState = progressUiState,
onValueChanged = { newValue -> volumeViewModel.setVolume(newValue) },
@@ -286,12 +276,12 @@ fun ValueActionScreen(
if (uiState.action == Actions.VOLUME) {
Icon(
painter = painterResource(id = R.drawable.ic_volume_up_white_24dp),
- contentDescription = stringResource(id = R.string.horologist_stepper_increase_content_description)
+ contentDescription = stringResource(id = R.string.horologist_volume_screen_volume_up_content_description)
)
} else {
Icon(
- painter = painterResource(id = R.drawable.ic_add_white_24dp),
- contentDescription = stringResource(id = R.string.horologist_stepper_increase_content_description)
+ imageVector = Icons.Rounded.Add,
+ contentDescription = stringResource(id = R.string.wear_m3c_slider_increase_content_description)
)
}
},
@@ -299,17 +289,17 @@ fun ValueActionScreen(
if (uiState.action == Actions.VOLUME) {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_volume_down_24),
- contentDescription = stringResource(id = R.string.horologist_stepper_decrease_content_description)
+ contentDescription = stringResource(id = R.string.horologist_volume_screen_volume_down_content_description)
)
} else {
Icon(
- painter = painterResource(id = R.drawable.ic_remove_white_24dp),
- contentDescription = stringResource(id = R.string.horologist_stepper_decrease_content_description)
+ imageVector = Icons.Rounded.Remove,
+ contentDescription = stringResource(id = R.string.wear_m3c_slider_decrease_content_description)
)
}
}
) {
- Chip(
+ Button(
label = {
when (uiState.action) {
Actions.VOLUME -> {
@@ -345,7 +335,7 @@ fun ValueActionScreen(
id = when (uiState.streamType) {
AudioStreamType.MUSIC -> R.drawable.ic_music_note_white_24dp
AudioStreamType.RINGTONE -> R.drawable.ic_baseline_ring_volume_24dp
- AudioStreamType.VOICE_CALL -> R.drawable.ic_baseline_call_24dp
+ AudioStreamType.VOICE_CALL -> R.drawable.ic_phone_24dp
AudioStreamType.ALARM -> R.drawable.ic_alarm_white_24dp
null -> R.drawable.ic_volume_up_white_24dp
}
@@ -381,11 +371,10 @@ fun ValueActionScreen(
}
}
},
- colors = ChipDefaults.secondaryChipColors(),
onClick = onActionChange
)
}
- VolumePositionIndicator(
+ VolumeLevelIndicator(
volumeUiState = { volumeUiState }
)
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Color.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Color.kt
index 35b6b539..ff97d84d 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Color.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Color.kt
@@ -1,11 +1,80 @@
package com.thewizrd.simplewear.ui.theme
import androidx.compose.ui.graphics.Color
-import androidx.wear.compose.material.Colors
+import androidx.compose.ui.graphics.toArgb
+import androidx.wear.compose.material3.ColorScheme
+import androidx.wear.protolayout.types.LayoutColor
-internal val wearColorPalette: Colors = Colors(
- primary = Color(0xFF9FCAFF),
- primaryVariant = Color(0xFF004881),
- secondary = Color(0xFF8BCEFF),
- secondaryVariant = Color(0xFF004B71)
+internal val wearColorScheme: ColorScheme = ColorScheme(
+ primary = Color(0xFFA2C9FD),
+ primaryContainer = Color(0xFF1C4975),
+ primaryDim = Color(0xFFA2C9FD),
+ onPrimary = Color(0xFF00325A),
+ onPrimaryContainer = Color(0xFFD2E4FF),
+
+ secondary = Color(0xFF95CDF7),
+ secondaryContainer = Color(0xFF004B6F),
+ secondaryDim = Color(0xFF95CDF7),
+ onSecondary = Color(0xFF00344E),
+ onSecondaryContainer = Color(0xFFC9E6FF),
+
+ tertiary = Color(0xFFA9C7FF),
+ tertiaryContainer = Color(0xFF264777),
+ tertiaryDim = Color(0xFFA9C7FF),
+ onTertiary = Color(0xFF07305F),
+ onTertiaryContainer = Color(0xFFD6E3FF),
+
+ surfaceContainer = Color(0xFF1D2024),
+ surfaceContainerLow = Color(0xFF191C20),
+ surfaceContainerHigh = Color(0xFF272A2F),
+ onSurface = Color(0xFFE1E2E8),
+ onSurfaceVariant = Color(0xFFC3C6CF),
+
+ error = Color(0xFFFFB4AB),
+ errorDim = Color(0xFFBA1B1B),
+ errorContainer = Color(0xFF93000A),
+ onError = Color(0xFF690005),
+ onErrorContainer = Color(0xFFFFDAD6),
+
+ //background = Color(0xFF111418),
+ //onBackground = Color(0xFFE1E2E8),
+
+ outline = Color(0xFF8D9199),
+ outlineVariant = Color(0xFF43474E),
+)
+
+val wearTileColorScheme = androidx.wear.protolayout.material3.ColorScheme(
+ primary = LayoutColor(wearColorScheme.primary.toArgb()),
+ onPrimary = LayoutColor(wearColorScheme.onPrimary.toArgb()),
+ primaryContainer = LayoutColor(wearColorScheme.primaryContainer.toArgb()),
+ onPrimaryContainer = LayoutColor(wearColorScheme.onPrimaryContainer.toArgb()),
+ primaryDim = LayoutColor(wearColorScheme.primaryDim.toArgb()),
+
+ secondary = LayoutColor(wearColorScheme.secondary.toArgb()),
+ onSecondary = LayoutColor(wearColorScheme.onSecondary.toArgb()),
+ secondaryContainer = LayoutColor(wearColorScheme.secondaryContainer.toArgb()),
+ onSecondaryContainer = LayoutColor(wearColorScheme.onSecondaryContainer.toArgb()),
+ secondaryDim = LayoutColor(wearColorScheme.secondaryDim.toArgb()),
+
+ tertiary = LayoutColor(wearColorScheme.tertiary.toArgb()),
+ onTertiary = LayoutColor(wearColorScheme.onTertiary.toArgb()),
+ tertiaryContainer = LayoutColor(wearColorScheme.tertiaryContainer.toArgb()),
+ onTertiaryContainer = LayoutColor(wearColorScheme.onTertiaryContainer.toArgb()),
+ tertiaryDim = LayoutColor(wearColorScheme.tertiaryDim.toArgb()),
+
+ surfaceContainer = LayoutColor(wearColorScheme.surfaceContainer.toArgb()),
+ surfaceContainerHigh = LayoutColor(wearColorScheme.surfaceContainerHigh.toArgb()),
+ surfaceContainerLow = LayoutColor(wearColorScheme.surfaceContainerLow.toArgb()),
+ onSurface = LayoutColor(wearColorScheme.onSurface.toArgb()),
+ onSurfaceVariant = LayoutColor(wearColorScheme.onSurfaceVariant.toArgb()),
+
+ outline = LayoutColor(wearColorScheme.outline.toArgb()),
+ outlineVariant = LayoutColor(wearColorScheme.outlineVariant.toArgb()),
+
+ error = LayoutColor(wearColorScheme.error.toArgb()),
+ onError = LayoutColor(wearColorScheme.onError.toArgb()),
+ errorContainer = LayoutColor(wearColorScheme.errorContainer.toArgb()),
+ onErrorContainer = LayoutColor(wearColorScheme.onErrorContainer.toArgb()),
+
+ errorDim = LayoutColor(wearColorScheme.errorDim.toArgb()),
)
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Theme.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Theme.kt
index fe8b3889..14c47fc4 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Theme.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Theme.kt
@@ -1,14 +1,16 @@
package com.thewizrd.simplewear.ui.theme
import androidx.compose.runtime.Composable
-import androidx.wear.compose.material.MaterialTheme
+import androidx.compose.ui.platform.LocalContext
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.dynamicColorScheme
@Composable
fun WearAppTheme(
content: @Composable () -> Unit
) {
MaterialTheme(
- colors = wearColorPalette,
+ colorScheme = dynamicColorScheme(LocalContext.current) ?: wearColorScheme,
typography = WearTypography,
content = content
)
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Typography.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Typography.kt
index 05689261..f8e326b4 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Typography.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/theme/Typography.kt
@@ -1,15 +1,5 @@
package com.thewizrd.simplewear.ui.theme
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.sp
-import androidx.wear.compose.material.Typography
+import androidx.wear.compose.material3.Typography
-val WearTypography = Typography(
- body1 = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp
- )
-)
\ No newline at end of file
+val WearTypography = Typography()
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/tiles/tools/WearTilePreviewDevices.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/tiles/tools/WearTilePreviewDevices.kt
new file mode 100644
index 00000000..af2a0803
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/tiles/tools/WearTilePreviewDevices.kt
@@ -0,0 +1,22 @@
+package com.thewizrd.simplewear.ui.tiles.tools
+
+import androidx.wear.tiles.tooling.preview.Preview
+import androidx.wear.tooling.preview.devices.WearDevices
+
+@Preview(
+ device = WearDevices.LARGE_ROUND,
+ name = "Wear - Large Round"
+)
+@Preview(
+ device = WearDevices.SMALL_ROUND,
+ name = "Wear - Small Round"
+)
+@Preview(
+ device = WearDevices.SQUARE,
+ name = "Wear - Square"
+)
+@Preview(
+ device = WearDevices.RECT,
+ name = "Wear - Rect"
+)
+annotation class WearPreviewDevices
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/tools/WearTilePreviewDevices.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/tools/WearTilePreviewDevices.kt
deleted file mode 100644
index b6fb3a84..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/tools/WearTilePreviewDevices.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.thewizrd.simplewear.ui.tools
-
-import androidx.wear.tiles.tooling.preview.Preview
-import androidx.wear.tooling.preview.devices.WearDevices
-
-@Preview(
- device = WearDevices.LARGE_ROUND,
- group = "Devices - Large Round"
-)
-@Preview(
- device = WearDevices.SMALL_ROUND,
- group = "Devices - Small Round"
-)
-@Preview(
- device = WearDevices.SQUARE,
- group = "Devices - Square"
-)
-@Preview(
- device = WearDevices.SMALL_ROUND,
- group = "Devices - Small Round",
- fontScale = 1.5f
-)
-public annotation class WearTilePreviewDevices
-
-@Preview(
- device = WearDevices.SMALL_ROUND,
- group = "Devices - Small Round"
-)
-public annotation class WearSmallRoundDeviceTilePreview
-
-@Preview(
- device = WearDevices.LARGE_ROUND,
- group = "Devices - Large Round"
-)
-public annotation class WearLargeRoundDeviceTilePreview
-
-@Preview(
- device = WearDevices.SQUARE,
- group = "Devices - Square"
-)
-public annotation class WearSquareDeviceTilePreview
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/utils/Colors.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/utils/Colors.kt
new file mode 100644
index 00000000..f7c634fa
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/utils/Colors.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thewizrd.simplewear.ui.utils
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.graphics.luminance
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * This is the minimum amount of calculated contrast for a color to be used on top of the
+ * surface color. These values are defined within the WCAG AA guidelines, and we use a value of
+ * 3:1 which is the minimum for user-interface components.
+ */
+const val MinContrastOfPrimaryVsBackground = 3f
+
+fun Color.contrastAgainst(background: Color): Float {
+ val fg = if (alpha < 1f) compositeOver(background) else this
+
+ val fgLuminance = fg.luminance() + 0.05f
+ val bgLuminance = background.luminance() + 0.05f
+
+ return max(fgLuminance, bgLuminance) / min(fgLuminance, bgLuminance)
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/utils/DynamicTheming.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/utils/DynamicTheming.kt
new file mode 100644
index 00000000..ed489d3e
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/utils/DynamicTheming.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thewizrd.simplewear.ui.utils
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Bitmap
+import androidx.collection.LruCache
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.palette.graphics.Palette
+import androidx.wear.compose.material3.MaterialTheme
+import com.google.android.material.color.utilities.Hct
+import com.google.android.material.color.utilities.SchemeContent
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+@Composable
+fun rememberDominantColorState(
+ context: Context = LocalContext.current,
+ defaultColor: Color = MaterialTheme.colorScheme.primary,
+ defaultOnColor: Color = MaterialTheme.colorScheme.onPrimary,
+ cacheSize: Int = 12,
+ isColorValid: (Color) -> Boolean = { true }
+): DominantColorState = remember {
+ DominantColorState(context, defaultColor, defaultOnColor, cacheSize, isColorValid)
+}
+
+/**
+ * A composable which allows dynamic theming of the [androidx.compose.material.Colors.primary]
+ * color from an image.
+ */
+@SuppressLint("RestrictedApi")
+@Composable
+fun DynamicThemePrimaryColorsFromImage(
+ dominantColorState: DominantColorState = rememberDominantColorState(),
+ content: @Composable () -> Unit
+) {
+ val color = animateColorAsState(
+ dominantColorState.color,
+ spring(stiffness = Spring.StiffnessLow), label = "primary"
+ ).value
+
+ val scheme = remember(color) {
+ SchemeContent(Hct.fromInt(color.toArgb()), true, 0.0)
+ }
+
+ val colors = MaterialTheme.colorScheme.copy(
+ primary = Color(scheme.primary),
+ onPrimary = Color(scheme.onPrimary),
+ primaryContainer = Color(scheme.primaryContainer),
+ onPrimaryContainer = Color(scheme.onPrimaryContainer),
+ primaryDim = Color(scheme.primaryFixedDim),
+
+ secondary = Color(scheme.secondary),
+ onSecondary = Color(scheme.onSecondary),
+ secondaryContainer = Color(scheme.secondaryContainer),
+ onSecondaryContainer = Color(scheme.onSecondaryContainer),
+ secondaryDim = Color(scheme.secondaryFixedDim),
+
+ tertiary = Color(scheme.tertiary),
+ onTertiary = Color(scheme.onTertiary),
+ tertiaryContainer = Color(scheme.tertiaryContainer),
+ onTertiaryContainer = Color(scheme.onTertiaryContainer),
+ tertiaryDim = Color(scheme.tertiaryFixedDim),
+
+ surfaceContainer = Color(scheme.surfaceContainer),
+ surfaceContainerHigh = Color(scheme.surfaceContainerHigh),
+ surfaceContainerLow = Color(scheme.surfaceContainerLow),
+ onSurface = Color(scheme.onSurface),
+ onSurfaceVariant = Color(scheme.onSurfaceVariant),
+
+ outline = Color(scheme.outline),
+ outlineVariant = Color(scheme.outlineVariant),
+
+ error = Color(scheme.error),
+ errorContainer = Color(scheme.errorContainer),
+ onError = Color(scheme.onError),
+ onErrorContainer = Color(scheme.onErrorContainer),
+
+ background = Color(scheme.background),
+ onBackground = Color(scheme.onBackground),
+ )
+
+ MaterialTheme(colorScheme = colors, content = content)
+}
+
+/**
+ * A class which stores and caches the result of any calculated dominant colors
+ * from images.
+ *
+ * @param context Android context
+ * @param defaultColor The default color, which will be used if [calculateDominantColor] fails to
+ * calculate a dominant color
+ * @param defaultOnColor The default foreground 'on color' for [defaultColor].
+ * @param cacheSize The size of the [LruCache] used to store recent results. Pass `0` to
+ * disable the cache.
+ * @param isColorValid A lambda which allows filtering of the calculated image colors.
+ */
+@Stable
+class DominantColorState(
+ private val context: Context,
+ private val defaultColor: Color,
+ private val defaultOnColor: Color,
+ cacheSize: Int = 12,
+ private val isColorValid: (Color) -> Boolean = { true }
+) {
+ var color by mutableStateOf(defaultColor)
+ private set
+ var onColor by mutableStateOf(defaultOnColor)
+ private set
+
+ private val cache = when {
+ cacheSize > 0 -> LruCache(cacheSize)
+ else -> null
+ }
+
+ suspend fun updateColorsFromImage(key: String, bitmap: Bitmap?, useCache: Boolean = true) {
+ val result = calculateDominantColor(key, bitmap, useCache)
+ color = result?.color ?: defaultColor
+ onColor = result?.onColor ?: defaultOnColor
+ }
+
+ private suspend fun calculateDominantColor(
+ key: String,
+ bitmap: Bitmap?,
+ useCache: Boolean = true
+ ): DominantColors? {
+ if (useCache) {
+ val cached = cache?.get(key)
+ if (cached != null) {
+ // If we already have the result cached, return early now...
+ return cached
+ }
+ }
+
+ // Otherwise we calculate the swatches in the image, and return the first valid color
+ return calculateSwatchesInImage(context, bitmap)
+ // First we want to sort the list by the color's population
+ .sortedByDescending { swatch -> swatch.population }
+ // Then we want to find the first valid color
+ .firstOrNull { swatch -> isColorValid(Color(swatch.rgb)) }
+ // If we found a valid swatch, wrap it in a [DominantColors]
+ ?.let { swatch ->
+ DominantColors(
+ color = Color(swatch.rgb),
+ onColor = Color(swatch.bodyTextColor).copy(alpha = 1f)
+ )
+ }
+ // Cache the resulting [DominantColors]
+ ?.also { result -> cache?.put(key, result) }
+ }
+
+ /**
+ * Reset the color values to [defaultColor].
+ */
+ fun reset() {
+ color = defaultColor
+ onColor = defaultColor
+ }
+}
+
+@Immutable
+private data class DominantColors(val color: Color, val onColor: Color)
+
+/**
+ * Uses [Palette] to calculate the dominant color.
+ */
+private suspend fun calculateSwatchesInImage(
+ context: Context,
+ bitmap: Bitmap?
+): List {
+ return bitmap?.let {
+ withContext(Dispatchers.Default) {
+ val palette = Palette.Builder(bitmap)
+ // Disable any bitmap resizing in Palette. We've already loaded an appropriately
+ // sized bitmap through Coil
+ .resizeBitmapArea(0)
+ // Clear any built-in filters. We want the unfiltered dominant color
+ .clearFilters()
+ // We reduce the maximum color count down to 8
+ .maximumColorCount(8)
+ .generate()
+
+ palette.swatches
+ }
+ } ?: emptyList()
+}
+
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/utils/Reorderable.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/utils/Reorderable.kt
new file mode 100644
index 00000000..c6af0eaa
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/utils/Reorderable.kt
@@ -0,0 +1,52 @@
+package com.thewizrd.simplewear.ui.utils
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.HapticFeedbackConstantsCompat
+import androidx.core.view.ViewCompat
+
+enum class ReorderHapticFeedbackType {
+ START,
+ MOVE,
+ END,
+}
+
+open class ReorderHapticFeedback {
+ open fun performHapticFeedback(type: ReorderHapticFeedbackType) {
+ // no-op
+ }
+}
+
+@Composable
+fun rememberReorderHapticFeedback(): ReorderHapticFeedback {
+ val view = LocalView.current
+
+ val reorderHapticFeedback = remember {
+ object : ReorderHapticFeedback() {
+ override fun performHapticFeedback(type: ReorderHapticFeedbackType) {
+ when (type) {
+ ReorderHapticFeedbackType.START ->
+ ViewCompat.performHapticFeedback(
+ view,
+ HapticFeedbackConstantsCompat.DRAG_START
+ )
+
+ ReorderHapticFeedbackType.MOVE ->
+ ViewCompat.performHapticFeedback(
+ view,
+ HapticFeedbackConstantsCompat.SEGMENT_FREQUENT_TICK
+ )
+
+ ReorderHapticFeedbackType.END ->
+ ViewCompat.performHapticFeedback(
+ view,
+ HapticFeedbackConstantsCompat.GESTURE_END
+ )
+ }
+ }
+ }
+ }
+
+ return reorderHapticFeedback
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/utils/Utils.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/utils/Utils.kt
index 1eb4bae4..561f6666 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/ui/utils/Utils.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/ui/utils/Utils.kt
@@ -1,10 +1,40 @@
package com.thewizrd.simplewear.ui.utils
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlin.math.sqrt
@Composable
fun rememberFocusRequester(): FocusRequester {
return remember { FocusRequester() }
+}
+
+@Stable
+fun Modifier.fillDashboard(): Modifier = composed {
+ val isRound = LocalConfiguration.current.isScreenRound
+ val screenHeightDp = LocalConfiguration.current.screenHeightDp
+
+ var bottomInset = Dp(screenHeightDp - (screenHeightDp * 0.8733032f))
+
+ if (isRound) {
+ val screenWidthDp = LocalConfiguration.current.smallestScreenWidthDp
+ val maxSquareEdge = (sqrt(((screenHeightDp * screenWidthDp) / 2).toFloat()))
+ bottomInset = Dp((screenHeightDp - (maxSquareEdge * 0.8733032f)) / 2)
+ }
+
+ fillMaxSize().padding(
+ start = if (isRound) 14.dp else 8.dp,
+ end = if (isRound) 14.dp else 8.dp,
+ top = if (isRound) 36.dp else 8.dp,
+ bottom = bottomInset
+ )
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/utils/ResourcesUtils.kt b/wear/src/main/java/com/thewizrd/simplewear/utils/ResourcesUtils.kt
deleted file mode 100644
index a4340e03..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/utils/ResourcesUtils.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-/*
- * ResourcesUtil.java
- * platform/frameworks/support
- * branch: pie-release
- */
-package com.thewizrd.simplewear.utils
-
-import android.content.Context
-import androidx.annotation.FractionRes
-
-/**
- * Utility methods to help with resource calculations.
- *
- * @hide
- */
-object ResourcesUtils {
- /**
- * Returns the screen width in pixels.
- */
- fun getScreenWidthPx(context: Context): Int {
- return context.resources.displayMetrics.widthPixels
- }
-
- /**
- * Returns the screen height in pixels.
- */
- fun getScreenHeightPx(context: Context): Int {
- return context.resources.displayMetrics.heightPixels
- }
-
- /**
- * Returns the number of pixels equivalent to the percentage of `resId` to the current
- * screen.
- */
- fun getFractionOfScreenPx(context: Context, screenPx: Int, @FractionRes resId: Int): Int {
- val marginPercent = context.resources.getFraction(resId, 1, 1)
- return (marginPercent * screenPx).toInt()
- }
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/viewmodels/AppLauncherViewModel.kt b/wear/src/main/java/com/thewizrd/simplewear/viewmodels/AppLauncherViewModel.kt
index 86d2d8e0..5dc44536 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/viewmodels/AppLauncherViewModel.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/viewmodels/AppLauncherViewModel.kt
@@ -1,6 +1,5 @@
package com.thewizrd.simplewear.viewmodels
-import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.util.Log
@@ -16,10 +15,10 @@ import com.thewizrd.shared_resources.data.AppItemSerializer
import com.thewizrd.shared_resources.helpers.WearConnectionStatus
import com.thewizrd.shared_resources.helpers.WearableHelper
import com.thewizrd.shared_resources.utils.ImageUtils.toBitmap
+import com.thewizrd.shared_resources.utils.JSONParser
import com.thewizrd.shared_resources.utils.Logger
import com.thewizrd.shared_resources.utils.bytesToString
import com.thewizrd.simplewear.controls.AppItemViewModel
-import com.thewizrd.simplewear.helpers.showConfirmationOverlay
import com.thewizrd.simplewear.preferences.Settings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -61,7 +60,7 @@ class AppLauncherViewModel(app: Application) : WearableListenerViewModel(app) {
viewModelState.update { state ->
state.copy(
- appsList = createAppsList(items ?: emptyList()),
+ appsList = createAppsList(items),
isLoading = false
)
}
@@ -164,7 +163,7 @@ class AppLauncherViewModel(app: Application) : WearableListenerViewModel(app) {
}
}
- fun openRemoteApp(activity: Activity, item: AppItemViewModel) {
+ fun openRemoteApp(item: AppItemViewModel) {
viewModelScope.launch {
val success = runCatching {
val intent = WearableHelper.createRemoteActivityIntent(
@@ -174,7 +173,21 @@ class AppLauncherViewModel(app: Application) : WearableListenerViewModel(app) {
startRemoteActivity(intent)
}.getOrDefault(false)
- activity.showConfirmationOverlay(success)
+ _eventsFlow.tryEmit(
+ WearableEvent(
+ ACTION_SHOWCONFIRMATION,
+ Bundle().apply {
+ putString(
+ EXTRA_ACTIONDATA,
+ JSONParser.serializer(
+ ConfirmationData(
+ confirmationType = if (success) ConfirmationType.OpenOnPhone else ConfirmationType.Failure
+ ), ConfirmationData::class.java
+ )
+ )
+ }
+ )
+ )
}
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/viewmodels/CallManagerViewModel.kt b/wear/src/main/java/com/thewizrd/simplewear/viewmodels/CallManagerViewModel.kt
index e37b631d..91b127b1 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/viewmodels/CallManagerViewModel.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/viewmodels/CallManagerViewModel.kt
@@ -3,6 +3,7 @@ package com.thewizrd.simplewear.viewmodels
import android.app.Application
import android.graphics.Bitmap
import android.os.Bundle
+import android.telephony.TelephonyManager
import androidx.lifecycle.viewModelScope
import com.google.android.gms.wearable.MessageEvent
import com.thewizrd.shared_resources.actions.ActionStatus
@@ -32,11 +33,17 @@ data class CallManagerUiState(
// InCallUi
val callerName: String? = null,
val callerBitmap: Bitmap? = null,
+ val callStartTime: Long = -1L,
val supportsSpeaker: Boolean = false,
val canSendDTMFKeys: Boolean = false,
val isCallActive: Boolean = false,
+ val callUiState: CallUiState = CallUiState.IDLE
)
+enum class CallUiState {
+ IDLE, INCOMING, ONGOING
+}
+
class CallManagerViewModel(app: Application) : WearableListenerViewModel(app) {
private val viewModelState = MutableStateFlow(CallManagerUiState(isLoading = true))
@@ -152,10 +159,16 @@ class CallManagerViewModel(app: Application) : WearableListenerViewModel(app) {
val callActive = callState?.callActive ?: false
val callerName = callState?.callerName
val callerBmp = callState?.callerBitmap?.toBitmap()
+ val callStartTime = callState?.callStartTime ?: -1L
val inCallFeatures = callState?.supportedFeatures ?: 0
val supportsSpeakerToggle =
inCallFeatures and InCallUIHelper.INCALL_FEATURE_SPEAKERPHONE != 0
val canSendDTMFKey = inCallFeatures and InCallUIHelper.INCALL_FEATURE_DTMF != 0
+ val callUiState = when (callState?.callState) {
+ TelephonyManager.CALL_STATE_RINGING -> CallUiState.INCOMING
+ TelephonyManager.CALL_STATE_OFFHOOK -> CallUiState.ONGOING
+ else -> CallUiState.IDLE
+ }
viewModelState.update {
it.copy(
@@ -163,9 +176,11 @@ class CallManagerViewModel(app: Application) : WearableListenerViewModel(app) {
callerName = callerName?.takeIf { name -> name.isNotBlank() }
?: appContext.getString(R.string.message_callactive),
callerBitmap = if (callActive) callerBmp else null,
+ callStartTime = callStartTime,
supportsSpeaker = callActive && supportsSpeakerToggle,
canSendDTMFKeys = callActive && canSendDTMFKey,
- isCallActive = callActive
+ isCallActive = callActive,
+ callUiState = callUiState
)
}
}
@@ -226,6 +241,14 @@ class CallManagerViewModel(app: Application) : WearableListenerViewModel(app) {
}
}
+ fun answerCall() {
+ viewModelScope.launch {
+ if (connect()) {
+ sendMessage(mPhoneNodeWithApp!!.id, InCallUIHelper.AnswerCallPath, null)
+ }
+ }
+ }
+
fun endCall() {
viewModelScope.launch {
if (connect()) {
diff --git a/wear/src/main/java/com/thewizrd/simplewear/viewmodels/ConfirmationViewModel.kt b/wear/src/main/java/com/thewizrd/simplewear/viewmodels/ConfirmationViewModel.kt
index 42c6d0e1..ffa8307d 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/viewmodels/ConfirmationViewModel.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/viewmodels/ConfirmationViewModel.kt
@@ -3,7 +3,7 @@ package com.thewizrd.simplewear.viewmodels
import androidx.annotation.DrawableRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import androidx.wear.compose.material.dialog.DialogDefaults
+import androidx.wear.compose.material3.ConfirmationDialogDefaults
import com.thewizrd.simplewear.R
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -30,7 +30,8 @@ class ConfirmationViewModel : ViewModel() {
_confirmationEventsFlow.update {
ConfirmationData(
animatedVectorResId = R.drawable.confirmation_animation,
- title = message
+ confirmationType = ConfirmationType.Success,
+ message = message
)
}
}
@@ -39,7 +40,8 @@ class ConfirmationViewModel : ViewModel() {
_confirmationEventsFlow.update {
ConfirmationData(
animatedVectorResId = R.drawable.failure_animation,
- title = message
+ confirmationType = ConfirmationType.Failure,
+ message = message
)
}
}
@@ -48,7 +50,18 @@ class ConfirmationViewModel : ViewModel() {
_confirmationEventsFlow.update {
ConfirmationData(
animatedVectorResId = R.drawable.open_on_phone_animation,
- title = message
+ confirmationType = ConfirmationType.OpenOnPhone,
+ message = message
+ )
+ }
+ }
+
+ fun showOpenOnPhoneForFailure(message: String? = null) {
+ _confirmationEventsFlow.update {
+ ConfirmationData(
+ animatedVectorResId = R.drawable.open_on_phone_animation,
+ confirmationType = ConfirmationType.Custom,
+ message = message
)
}
}
@@ -59,8 +72,13 @@ class ConfirmationViewModel : ViewModel() {
}
data class ConfirmationData(
- val title: String? = null,
- @DrawableRes val iconResId: Int? = R.drawable.ws_full_sad,
+ val message: String? = null,
+ @DrawableRes val iconResId: Int? = null,
@DrawableRes val animatedVectorResId: Int? = null,
- val durationMs: Long = DialogDefaults.ShortDurationMillis
-)
\ No newline at end of file
+ val confirmationType: ConfirmationType = ConfirmationType.Custom,
+ val durationMs: Long = ConfirmationDialogDefaults.DurationMillis
+)
+
+enum class ConfirmationType {
+ Success, Failure, OpenOnPhone, Custom
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/viewmodels/DashboardViewModel.kt b/wear/src/main/java/com/thewizrd/simplewear/viewmodels/DashboardViewModel.kt
index 4d13e0d0..228549b0 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/viewmodels/DashboardViewModel.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/viewmodels/DashboardViewModel.kt
@@ -78,7 +78,8 @@ class DashboardViewModel(app: Application) : WearableListenerViewModel(app) {
EXTRA_ACTIONDATA,
JSONParser.serializer(
ConfirmationData(
- title = it.getString(R.string.error_sendmessage)
+ confirmationType = ConfirmationType.Failure,
+ message = it.getString(R.string.error_sendmessage)
), ConfirmationData::class.java
)
)
@@ -157,6 +158,18 @@ class DashboardViewModel(app: Application) : WearableListenerViewModel(app) {
}
}
+ viewModelScope.launch {
+ Settings.getDashboardConfigFlow().collect {
+ updateActions(it)
+ }
+ }
+
+ viewModelScope.launch {
+ Settings.isShowBatStatusFlow().collect {
+ showBatteryState(it)
+ }
+ }
+
resetDashboard()
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/viewmodels/WearableListenerViewModel.kt b/wear/src/main/java/com/thewizrd/simplewear/viewmodels/WearableListenerViewModel.kt
index 01ef6036..cae7a1b8 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/viewmodels/WearableListenerViewModel.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/viewmodels/WearableListenerViewModel.kt
@@ -11,6 +11,7 @@ import android.os.Bundle
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
+import androidx.concurrent.futures.await
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.wear.phone.interactions.PhoneTypeHelper
@@ -35,12 +36,12 @@ import com.thewizrd.shared_resources.utils.JSONParser
import com.thewizrd.shared_resources.utils.Logger
import com.thewizrd.shared_resources.utils.bytesToString
import com.thewizrd.shared_resources.utils.stringToBytes
-import com.thewizrd.simplewear.helpers.showConfirmationOverlay
+import com.thewizrd.simplewear.R
import com.thewizrd.simplewear.utils.ErrorMessage
+import com.thewizrd.simplewear.viewmodels.WearableListenerViewModel.Companion.ACTION_OPENONPHONE
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import kotlin.coroutines.cancellation.CancellationException
@@ -94,47 +95,44 @@ abstract class WearableListenerViewModel(private val app: Application) : Android
activityContext = null
}
- fun openAppOnPhone(activity: Activity, showAnimation: Boolean = true) {
+ fun openAppOnPhone(showAnimation: Boolean = true) {
viewModelScope.launch {
connect()
if (mPhoneNodeWithApp == null) {
- _errorMessagesFlow.tryEmit(ErrorMessage.String("Device is not connected or app is not installed on device..."))
+ _errorMessagesFlow.tryEmit(ErrorMessage.Resource(R.string.status_device_disconnected_notinstalled))
when (PhoneTypeHelper.getPhoneDeviceType(appContext)) {
PhoneTypeHelper.DEVICE_TYPE_ANDROID -> {
- openPlayStore(activity, showAnimation)
+ openPlayStore(showAnimation)
}
PhoneTypeHelper.DEVICE_TYPE_IOS -> {
- _errorMessagesFlow.tryEmit(ErrorMessage.String("Connected device is not supported"))
+ _errorMessagesFlow.tryEmit(ErrorMessage.Resource(R.string.status_unsupported_device))
}
else -> {
- _errorMessagesFlow.tryEmit(ErrorMessage.String("Connected device is not supported"))
+ _errorMessagesFlow.tryEmit(ErrorMessage.Resource(R.string.status_unsupported_device))
}
}
} else {
// Send message to device to start activity
- val result = sendMessage(
- mPhoneNodeWithApp!!.id,
- WearableHelper.StartActivityPath,
- ByteArray(0)
- )
+ val success = runCatching {
+ val intent = WearableHelper.createRemoteActivityIntent(
+ WearableHelper.getPackageName(),
+ "${WearableHelper.PACKAGE_NAME}.MainActivity"
+ )
+ startRemoteActivity(intent)
+ }.getOrDefault(false)
if (showAnimation) {
- activity.showConfirmationOverlay(result != -1)
+ sendConfirmationEvent(success)
}
-
- _eventsFlow.tryEmit(WearableEvent(ACTION_OPENONPHONE, Bundle().apply {
- putBoolean(EXTRA_SUCCESS, result != -1)
- putBoolean(EXTRA_SHOWANIMATION, showAnimation)
- }))
}
}
}
- suspend fun openPlayStore(activity: Activity, showAnimation: Boolean = true) {
+ suspend fun openPlayStore(showAnimation: Boolean = true) {
// Open store on remote device
val intentAndroid = Intent(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
@@ -145,11 +143,11 @@ abstract class WearableListenerViewModel(private val app: Application) : Android
.await()
if (showAnimation) {
- activity.showConfirmationOverlay(true)
+ sendConfirmationEvent(true)
}
}.onFailure {
if (it !is CancellationException && showAnimation) {
- activity.showConfirmationOverlay(false)
+ sendConfirmationEvent(false)
}
}
}
@@ -392,6 +390,32 @@ abstract class WearableListenerViewModel(private val app: Application) : Android
}
}
+ protected fun sendConfirmationEvent(success: Boolean) {
+ if (success) {
+ sendConfirmationEvent(ConfirmationType.OpenOnPhone)
+ } else {
+ sendConfirmationEvent(ConfirmationType.Failure)
+ }
+ }
+
+ protected fun sendConfirmationEvent(confirmationType: ConfirmationType) {
+ _eventsFlow.tryEmit(
+ WearableEvent(
+ ACTION_SHOWCONFIRMATION,
+ Bundle().apply {
+ putString(
+ EXTRA_ACTIONDATA,
+ JSONParser.serializer(
+ ConfirmationData(
+ confirmationType = confirmationType
+ ), ConfirmationData::class.java
+ )
+ )
+ }
+ )
+ )
+ }
+
/*
* There should only ever be one phone in a node set (much less w/ the correct capability), so
* I am just grabbing the first one (which should be the only one).
@@ -447,6 +471,32 @@ abstract class WearableListenerViewModel(private val app: Application) : Android
return -1
}
+ protected suspend fun sendRequest(nodeID: String, path: String, data: ByteArray?): ByteArray {
+ try {
+ return Wearable.getMessageClient(appContext)
+ .sendRequest(nodeID, path, data).await()
+ } catch (e: Exception) {
+ if (e is ApiException || e.cause is ApiException) {
+ val apiException = e.cause as? ApiException ?: e as? ApiException
+ if (apiException?.statusCode == WearableStatusCodes.TARGET_NODE_NOT_CONNECTED) {
+ mConnectionStatus = WearConnectionStatus.DISCONNECTED
+
+ _eventsFlow.tryEmit(
+ WearableEvent(
+ ACTION_UPDATECONNECTIONSTATUS,
+ Bundle().apply {
+ putInt(EXTRA_CONNECTIONSTATUS, mConnectionStatus.value)
+ })
+ )
+ }
+ }
+
+ Logger.writeLine(Log.ERROR, e)
+ }
+
+ return byteArrayOf()
+ }
+
@Throws(ApiException::class)
protected suspend fun sendPing(nodeID: String) {
try {
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt
index 6ae2c467..501fde38 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/WearableDataListenerService.kt
@@ -37,6 +37,7 @@ import com.thewizrd.shared_resources.actions.BatteryStatus
import com.thewizrd.shared_resources.actions.ToggleAction
import com.thewizrd.shared_resources.appLib
import com.thewizrd.shared_resources.data.AppItemData
+import com.thewizrd.shared_resources.data.CallState
import com.thewizrd.shared_resources.helpers.InCallUIHelper
import com.thewizrd.shared_resources.helpers.MediaHelper
import com.thewizrd.shared_resources.helpers.WearableHelper
@@ -61,6 +62,7 @@ import com.thewizrd.simplewear.viewmodels.WearableListenerViewModel
import com.thewizrd.simplewear.wearable.complications.BatteryStatusComplicationService
import com.thewizrd.simplewear.wearable.tiles.DashboardTileProviderService
import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileProviderService
+import com.thewizrd.simplewear.wearable.tiles.NowPlayingTileProviderService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.firstOrNull
@@ -126,9 +128,15 @@ class WearableDataListenerService : WearableListenerService() {
messageEvent.path == InCallUIHelper.CallStateBridgePath -> {
val enable = messageEvent.data.bytesToBool()
+ val callState = messageEvent.data.takeIf { it.size > 1 }?.let {
+ JSONParser.deserializer(
+ it.copyOfRange(1, it.size).bytesToString(),
+ CallState::class.java
+ )
+ }
if (enable) {
- createCallOngoingActivity()
+ createCallOngoingActivity(callState)
} else {
dismissCallOngoingActivity()
}
@@ -171,6 +179,7 @@ class WearableDataListenerService : WearableListenerService() {
if (!mLegacyTilesEnabled && (playerState?.key != currentState?.mediaPlayerState?.key || (playerState?.playbackState == PlaybackState.PLAYING && playerState.mediaMetaData?.positionState != currentState?.mediaPlayerState?.mediaMetaData?.positionState))) {
MediaPlayerTileProviderService.requestTileUpdate(appLib.context)
+ NowPlayingTileProviderService.requestTileUpdate(appLib.context)
}
}.onFailure {
Logger.error(TAG, it)
@@ -191,6 +200,7 @@ class WearableDataListenerService : WearableListenerService() {
if (!mLegacyTilesEnabled && !artworkBytes.contentEquals(currentState)) {
MediaPlayerTileProviderService.requestTileUpdate(appLib.context)
+ NowPlayingTileProviderService.requestTileUpdate(appLib.context)
}
}.onFailure {
Logger.error(TAG, it)
@@ -220,6 +230,7 @@ class WearableDataListenerService : WearableListenerService() {
if (!mLegacyTilesEnabled && appInfo?.key != currentState?.key) {
MediaPlayerTileProviderService.requestTileUpdate(appLib.context)
+ NowPlayingTileProviderService.requestTileUpdate(appLib.context)
}
}.onFailure {
Logger.error(TAG, it)
@@ -350,7 +361,9 @@ class WearableDataListenerService : WearableListenerService() {
Actions.LOCATION,
Actions.LOCKSCREEN,
Actions.PHONE,
- Actions.HOTSPOT -> {
+ Actions.HOTSPOT,
+ Actions.NFC,
+ Actions.BATTERYSAVER -> {
appLib.appScope.launch {
runCatching {
val dashboardDataStore = appLib.context.dashboardDataStore
@@ -447,6 +460,17 @@ class WearableDataListenerService : WearableListenerService() {
}
}
+ protected suspend fun sendRequest(nodeID: String, path: String, data: ByteArray?): ByteArray {
+ try {
+ return Wearable.getMessageClient(this@WearableDataListenerService)
+ .sendRequest(nodeID, path, data).await()
+ } catch (e: Exception) {
+ Logger.writeLine(Log.ERROR, e)
+ }
+
+ return byteArrayOf()
+ }
+
override fun onDataChanged(dataEventBuffer: DataEventBuffer) {
super.onDataChanged(dataEventBuffer)
@@ -466,12 +490,13 @@ class WearableDataListenerService : WearableListenerService() {
}
}
- private fun createCallOngoingActivity() {
+ private fun createCallOngoingActivity(callState: CallState? = null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
initCallControllerNotifChannel()
}
- val notifTitle = getString(R.string.message_callactive)
+ val notifTitle: String = callState?.callerName?.takeIf { it.isNotEmpty() }
+ ?: getString(R.string.message_callactive)
val notifBuilder = NotificationCompat.Builder(this, CALLS_NOT_CHANNEL_ID)
.setStyle(
@@ -493,9 +518,16 @@ class WearableDataListenerService : WearableListenerService() {
getCallControllerIntent()
)
.setLocusId(LocusIdCompat(CALLS_LOCUS_ID))
+ .apply {
+ callState?.callStartTime?.takeIf { it > 0 }?.let { callStartTime ->
+ this.setUsesChronometer(true)
+ .setWhen(callStartTime)
+ .setShowWhen(false)
+ }
+ }
val ongoingActivityStatus = Status.Builder()
- .addTemplate(getString(R.string.message_callactive))
+ .addTemplate(notifTitle)
.build()
val ongoingActivity = OngoingActivity.Builder(applicationContext, 1000, notifBuilder)
@@ -551,6 +583,7 @@ class WearableDataListenerService : WearableListenerService() {
val ongoingActivity = OngoingActivity.Builder(applicationContext, 1001, notifBuilder)
.setStaticIcon(R.drawable.ic_music_note_white_24dp)
+ .setAnimatedIcon(R.drawable.music_note_bounce_animated)
.setTouchIntent(getMediaControllerIntent())
//.setStatus(ongoingActivityStatus) // Uses content text from notif
.setTitle(notifTitle)
@@ -636,6 +669,13 @@ class WearableDataListenerService : WearableListenerService() {
// Disconnect or dismiss any ongoing activity
dismissMediaOngoingActivity()
dismissCallOngoingActivity()
+ } else {
+ if (DashboardTileProviderService.isInFocus) {
+ DashboardTileProviderService.requestTileUpdate(this)
+ }
+ if (MediaPlayerTileProviderService.isInFocus) {
+ MediaPlayerTileProviderService.requestTileUpdate(this)
+ }
}
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/complications/BatteryStatusComplicationService.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/complications/BatteryStatusComplicationService.kt
index 03e11c2c..ddf4ac4f 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/complications/BatteryStatusComplicationService.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/complications/BatteryStatusComplicationService.kt
@@ -93,7 +93,7 @@ class BatteryStatusComplicationService : SuspendingComplicationDataSourceService
return scope.async {
val batteryStatus = latestStatus() ?: return@async NoDataComplicationData()
- val batteryLvl = batteryStatus.batteryLevel
+ val batteryLvl = batteryStatus.batteryLevel.coerceIn(0, 100)
val statusText = if (batteryStatus.isCharging) {
getString(R.string.batt_state_charging)
} else {
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileMessenger.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileMessenger.kt
index ce687cb3..6602a053 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileMessenger.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileMessenger.kt
@@ -199,6 +199,28 @@ class DashboardTileMessenger(
requestAction(ToggleAction(Actions.HOTSPOT, !hotspotAction.isEnabled))
}
+ Actions.NFC -> run {
+ val nfcAction = state.getAction(Actions.NFC) as? ToggleAction
+
+ if (nfcAction == null) {
+ requestUpdate()
+ return@run
+ }
+
+ requestAction(ToggleAction(Actions.NFC, !nfcAction.isEnabled))
+ }
+
+ Actions.BATTERYSAVER -> run {
+ val battSaverAction = state.getAction(Actions.BATTERYSAVER) as? ToggleAction
+
+ if (battSaverAction == null) {
+ requestUpdate()
+ return@run
+ }
+
+ requestAction(ToggleAction(Actions.BATTERYSAVER, !battSaverAction.isEnabled))
+ }
+
else -> {
// ignore unsupported actions
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileProviderService.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileProviderService.kt
index 07fa0eb4..2e56610a 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileProviderService.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileProviderService.kt
@@ -6,13 +6,15 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.SystemClock
+import androidx.concurrent.futures.SuspendToFutureAdapter
import androidx.lifecycle.lifecycleScope
import androidx.wear.protolayout.ResourceBuilders
-import androidx.wear.tiles.EventBuilders
+import androidx.wear.tiles.EventBuilders.TileInteractionEvent
import androidx.wear.tiles.RequestBuilders
import androidx.wear.tiles.TileBuilders
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.tiles.SuspendingTileService
+import com.google.common.util.concurrent.ListenableFuture
import com.thewizrd.shared_resources.actions.Action
import com.thewizrd.shared_resources.actions.Actions
import com.thewizrd.shared_resources.actions.NormalAction
@@ -114,9 +116,7 @@ class DashboardTileProviderService : SuspendingTileService() {
super.onDestroy()
}
- override fun onTileEnterEvent(requestParams: EventBuilders.TileEnterEvent) {
- super.onTileEnterEvent(requestParams)
-
+ private fun onTileInteractionEnterEvent(requestParams: TileInteractionEvent) {
Logger.debug(TAG, "onTileEnterEvent called with: tileId = ${requestParams.tileId}")
AnalyticsLogger.logEvent("on_tile_enter", Bundle().apply {
putString("tile", TAG)
@@ -134,12 +134,32 @@ class DashboardTileProviderService : SuspendingTileService() {
}
}
- override fun onTileLeaveEvent(requestParams: EventBuilders.TileLeaveEvent) {
- super.onTileLeaveEvent(requestParams)
+ private fun onTileInteractionLeaveEvent(requestParams: TileInteractionEvent) {
Logger.debug(TAG, "$TAG: onTileLeaveEvent called with: tileId = ${requestParams.tileId}")
isInFocus = false
}
+ override fun onRecentInteractionEventsAsync(events: List): ListenableFuture {
+ return SuspendToFutureAdapter.launchFuture {
+ val lastEvent = events.lastOrNull()
+
+ when (lastEvent?.eventType) {
+ TileInteractionEvent.ENTER -> {
+ onTileInteractionEnterEvent(lastEvent)
+ }
+
+ TileInteractionEvent.LEAVE -> {
+ onTileInteractionLeaveEvent(lastEvent)
+ }
+
+ TileInteractionEvent.UNKNOWN -> { /* no-op */
+ }
+ }
+
+ null
+ }
+ }
+
override suspend fun tileRequest(requestParams: RequestBuilders.TileRequest): TileBuilders.Tile {
Logger.debug(TAG, "tileRequest: ${requestParams.currentState}")
val startTime = SystemClock.elapsedRealtimeNanos()
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileRenderer.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileRenderer.kt
index 267474b0..6bb277f0 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileRenderer.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileRenderer.kt
@@ -2,9 +2,7 @@ package com.thewizrd.simplewear.wearable.tiles
import android.content.ComponentName
import android.content.Context
-import androidx.core.content.ContextCompat
import androidx.wear.protolayout.ActionBuilders
-import androidx.wear.protolayout.ColorBuilders
import androidx.wear.protolayout.DeviceParametersBuilders
import androidx.wear.protolayout.DimensionBuilders.expand
import androidx.wear.protolayout.LayoutElementBuilders
@@ -15,10 +13,6 @@ import androidx.wear.protolayout.ResourceBuilders
import androidx.wear.protolayout.StateBuilders
import androidx.wear.protolayout.expression.AppDataKey
import androidx.wear.protolayout.expression.DynamicDataBuilders
-import androidx.wear.protolayout.material.CompactChip
-import androidx.wear.protolayout.material.Text
-import androidx.wear.protolayout.material.Typography
-import androidx.wear.protolayout.material.layouts.PrimaryLayout
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.tiles.images.drawableResToImageResource
import com.google.android.horologist.tiles.render.SingleTileLayoutRendererWithState
@@ -26,6 +20,7 @@ import com.thewizrd.shared_resources.utils.Logger
import com.thewizrd.simplewear.PhoneSyncActivity
import com.thewizrd.simplewear.R
import com.thewizrd.simplewear.wearable.tiles.layouts.DashboardTileLayout
+import com.thewizrd.simplewear.wearable.tiles.layouts.LoadingTileLayout
@OptIn(ExperimentalHorologistApi::class)
class DashboardTileRenderer(context: Context, debugResourceMode: Boolean = false) :
@@ -35,6 +30,7 @@ class DashboardTileRenderer(context: Context, debugResourceMode: Boolean = false
internal const val ID_OPENONPHONE = "open_on_phone"
internal const val ID_PHONEDISCONNECTED = "phone_disconn"
internal const val ID_BATTERY = "batt"
+ internal const val ID_BATTERY_CHARGING = "batt_chg"
// Actions
// VOLUME, MUSIC, SLEEPTIMER, APPS, PHONE, BRIGHTNESS unavailable
@@ -58,10 +54,19 @@ class DashboardTileRenderer(context: Context, debugResourceMode: Boolean = false
internal const val ID_RINGER_SOUND = "ringer_sound"
internal const val ID_RINGER_SILENT = "ringer_silent"
internal const val ID_HOTSPOT = "hotspot"
+ internal const val ID_NFC_ON = "nfc_on"
+ internal const val ID_NFC_OFF = "nfc_off"
+ internal const val ID_BATTERY_SAVER = "battery_saver"
// Background drawables
internal const val ID_BUTTON_ENABLED = "round_button_enabled"
internal const val ID_BUTTON_DISABLED = "round_button_disabled"
+
+ fun getTapAction(context: Context): ActionBuilders.Action {
+ return ActionBuilders.launchAction(
+ ComponentName(context, PhoneSyncActivity::class.java)
+ )
+ }
}
override fun createState(state: DashboardTileState): StateBuilders.State {
@@ -99,33 +104,11 @@ class DashboardTileRenderer(context: Context, debugResourceMode: Boolean = false
)
.addContent(
if (state.isEmpty) {
- PrimaryLayout.Builder(deviceParameters)
- .setContent(
- Text.Builder(context, context.getString(R.string.state_loading))
- .setTypography(Typography.TYPOGRAPHY_CAPTION1)
- .setColor(
- ColorBuilders.argb(
- ContextCompat.getColor(context, R.color.colorSecondary)
- )
- )
- .setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_CENTER)
- .setMaxLines(1)
- .build()
- )
- .setPrimaryChipContent(
- CompactChip.Builder(
- context,
- context.getString(R.string.action_refresh),
- Clickable.Builder()
- .setOnClick(
- ActionBuilders.LoadAction.Builder().build()
- )
- .build(),
- deviceParameters
- )
- .build()
- )
- .build()
+ LoadingTileLayout(
+ context,
+ deviceParameters,
+ title = context.getString(R.string.title_activity_dashboard)
+ )
} else {
DashboardTileLayout(context, deviceParameters, state)
}
@@ -144,6 +127,7 @@ class DashboardTileRenderer(context: Context, debugResourceMode: Boolean = false
ID_OPENONPHONE to R.drawable.common_full_open_on_phone,
ID_PHONEDISCONNECTED to R.drawable.ic_phonelink_erase_white_24dp,
ID_BATTERY to R.drawable.ic_battery_std_white_24dp,
+ ID_BATTERY_CHARGING to R.drawable.ic_battery_charging_white_24dp,
ID_WIFI_ON to R.drawable.ic_network_wifi_white_24dp,
ID_WIFI_OFF to R.drawable.ic_signal_wifi_off_white_24dp,
@@ -161,12 +145,12 @@ class DashboardTileRenderer(context: Context, debugResourceMode: Boolean = false
ID_FLASHLIGHT to R.drawable.ic_lightbulb_outline_white_24dp,
- ID_LOCK to R.drawable.ic_lock_outline_white_24dp,
+ ID_LOCK to R.drawable.ic_lock_white_24dp,
ID_DND_OFF to R.drawable.ic_do_not_disturb_off_white_24dp,
ID_DND_PRIORITY to R.drawable.ic_error_white_24dp,
ID_DND_ALARMS to R.drawable.ic_alarm_white_24dp,
- ID_DND_SILENCE to R.drawable.ic_notifications_off_white_24dp,
+ ID_DND_SILENCE to R.drawable.ic_do_not_disturb_silence_white_24dp,
ID_RINGER_VIB to R.drawable.ic_vibration_white_24dp,
ID_RINGER_SOUND to R.drawable.ic_notifications_active_white_24dp,
@@ -174,6 +158,11 @@ class DashboardTileRenderer(context: Context, debugResourceMode: Boolean = false
ID_HOTSPOT to R.drawable.ic_wifi_tethering,
+ ID_NFC_ON to R.drawable.ic_nfc_on,
+ ID_NFC_OFF to R.drawable.ic_nfc_off,
+
+ ID_BATTERY_SAVER to R.drawable.ic_battery_saver,
+
ID_BUTTON_ENABLED to R.drawable.round_button_enabled,
ID_BUTTON_DISABLED to R.drawable.round_button_disabled
)
@@ -184,10 +173,4 @@ class DashboardTileRenderer(context: Context, debugResourceMode: Boolean = false
}
}
}
-
- private fun getTapAction(context: Context): ActionBuilders.Action {
- return ActionBuilders.launchAction(
- ComponentName(context, PhoneSyncActivity::class.java)
- )
- }
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileState.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileState.kt
index 871d785c..cc8527c7 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileState.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/DashboardTileState.kt
@@ -23,7 +23,7 @@ data class DashboardTileState(
fun isActionEnabled(action: Actions): Boolean {
return when (action) {
- Actions.WIFI, Actions.BLUETOOTH, Actions.MOBILEDATA, Actions.TORCH, Actions.HOTSPOT -> {
+ Actions.WIFI, Actions.BLUETOOTH, Actions.MOBILEDATA, Actions.TORCH, Actions.HOTSPOT, Actions.NFC, Actions.BATTERYSAVER -> {
(getAction(action) as? ToggleAction)?.isEnabled == true
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileMessenger.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileMessenger.kt
index 9166b445..7a5e0663 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileMessenger.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileMessenger.kt
@@ -5,7 +5,6 @@ import android.util.Log
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.CapabilityInfo
-import com.google.android.gms.wearable.DataClient
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.Node
@@ -240,7 +239,7 @@ class MediaPlayerTileMessenger(
.addListener(
listener,
WearableHelper.getWearDataUri("*", MediaHelper.MediaPlayerStatePath),
- DataClient.FILTER_LITERAL
+ MessageClient.FILTER_LITERAL
)
.await()
@@ -265,7 +264,7 @@ class MediaPlayerTileMessenger(
.addListener(
listener,
WearableHelper.getWearDataUri("*", MediaHelper.MediaPlayerArtPath),
- DataClient.FILTER_LITERAL
+ MessageClient.FILTER_LITERAL
)
.await()
@@ -332,7 +331,7 @@ class MediaPlayerTileMessenger(
"*",
MediaHelper.MediaVolumeStatusPath
),
- DataClient.FILTER_LITERAL
+ MessageClient.FILTER_LITERAL
)
}
@@ -343,7 +342,7 @@ class MediaPlayerTileMessenger(
"*",
MediaHelper.MediaPlayerStatePath
),
- DataClient.FILTER_LITERAL
+ MessageClient.FILTER_LITERAL
)
}
}
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileProviderService.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileProviderService.kt
index e8fd85ac..7fb78e62 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileProviderService.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileProviderService.kt
@@ -4,13 +4,15 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.SystemClock
+import androidx.concurrent.futures.SuspendToFutureAdapter
import androidx.lifecycle.lifecycleScope
import androidx.wear.protolayout.ResourceBuilders
-import androidx.wear.tiles.EventBuilders
+import androidx.wear.tiles.EventBuilders.TileInteractionEvent
import androidx.wear.tiles.RequestBuilders
import androidx.wear.tiles.TileBuilders
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.tiles.SuspendingTileService
+import com.google.common.util.concurrent.ListenableFuture
import com.thewizrd.shared_resources.appLib
import com.thewizrd.shared_resources.utils.AnalyticsLogger
import com.thewizrd.shared_resources.utils.Logger
@@ -113,9 +115,7 @@ class MediaPlayerTileProviderService : SuspendingTileService() {
super.onDestroy()
}
- override fun onTileEnterEvent(requestParams: EventBuilders.TileEnterEvent) {
- super.onTileEnterEvent(requestParams)
-
+ private fun onTileInteractionEnterEvent(requestParams: TileInteractionEvent) {
Logger.debug(TAG, "onTileEnterEvent called with: tileId = ${requestParams.tileId}")
AnalyticsLogger.logEvent("on_tile_enter", Bundle().apply {
putString("tile", TAG)
@@ -136,8 +136,7 @@ class MediaPlayerTileProviderService : SuspendingTileService() {
}
}
- override fun onTileLeaveEvent(requestParams: EventBuilders.TileLeaveEvent) {
- super.onTileLeaveEvent(requestParams)
+ private fun onTileInteractionLeaveEvent(requestParams: TileInteractionEvent) {
Logger.debug(TAG, "onTileLeaveEvent called with: tileId = ${requestParams.tileId}")
isInFocus = false
@@ -146,6 +145,27 @@ class MediaPlayerTileProviderService : SuspendingTileService() {
}
}
+ override fun onRecentInteractionEventsAsync(events: List): ListenableFuture {
+ return SuspendToFutureAdapter.launchFuture {
+ val lastEvent = events.lastOrNull()
+
+ when (lastEvent?.eventType) {
+ TileInteractionEvent.ENTER -> {
+ onTileInteractionEnterEvent(lastEvent)
+ }
+
+ TileInteractionEvent.LEAVE -> {
+ onTileInteractionLeaveEvent(lastEvent)
+ }
+
+ TileInteractionEvent.UNKNOWN -> { /* no-op */
+ }
+ }
+
+ null
+ }
+ }
+
override suspend fun tileRequest(requestParams: RequestBuilders.TileRequest): TileBuilders.Tile {
Logger.debug(TAG, "tileRequest: ${requestParams.currentState}")
val startTime = SystemClock.elapsedRealtimeNanos()
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileRenderer.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileRenderer.kt
index 90bb8558..f5ecd7ce 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileRenderer.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileRenderer.kt
@@ -43,6 +43,14 @@ class MediaPlayerTileRenderer(context: Context, debugResourceMode: Boolean = fal
internal const val ID_VOL_UP = "vol_up"
internal const val ID_VOL_DOWN = "vol_down"
internal const val ID_APPICON = "app_icon"
+
+ fun getTapAction(context: Context): ActionBuilders.Action {
+ return ActionBuilders.launchAction(
+ ComponentName(context.packageName, context.packageName.run {
+ if (BuildConfig.DEBUG) removeSuffix(".debug") else this
+ } + ".MediaControllerActivity")
+ )
+ }
}
override fun renderTile(
@@ -148,12 +156,4 @@ class MediaPlayerTileRenderer(context: Context, debugResourceMode: Boolean = fal
super.getFreshnessIntervalMillis(state)
}
}
-
- private fun getTapAction(context: Context): ActionBuilders.Action {
- return ActionBuilders.launchAction(
- ComponentName(context.packageName, context.packageName.run {
- if (BuildConfig.DEBUG) removeSuffix(".debug") else this
- } + ".MediaControllerActivity")
- )
- }
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/NowPlayingTileProviderService.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/NowPlayingTileProviderService.kt
new file mode 100644
index 00000000..acbc6806
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/NowPlayingTileProviderService.kt
@@ -0,0 +1,268 @@
+@file:OptIn(ExperimentalHorologistApi::class)
+
+package com.thewizrd.simplewear.wearable.tiles
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.SystemClock
+import androidx.concurrent.futures.SuspendToFutureAdapter
+import androidx.lifecycle.lifecycleScope
+import androidx.wear.protolayout.ResourceBuilders
+import androidx.wear.tiles.EventBuilders.TileInteractionEvent
+import androidx.wear.tiles.RequestBuilders
+import androidx.wear.tiles.TileBuilders
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.tiles.SuspendingTileService
+import com.google.common.util.concurrent.ListenableFuture
+import com.thewizrd.shared_resources.appLib
+import com.thewizrd.shared_resources.utils.AnalyticsLogger
+import com.thewizrd.shared_resources.utils.Logger
+import com.thewizrd.simplewear.PhoneSyncActivity
+import com.thewizrd.simplewear.datastore.media.appInfoDataStore
+import com.thewizrd.simplewear.datastore.media.artworkDataStore
+import com.thewizrd.simplewear.datastore.media.mediaDataStore
+import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileMessenger.PlayerAction
+import com.thewizrd.simplewear.wearable.tiles.NowPlayingTileRenderer.Companion.ID_OPENONPHONE
+import com.thewizrd.simplewear.wearable.tiles.NowPlayingTileRenderer.Companion.ID_PHONEDISCONNECTED
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.supervisorScope
+import kotlinx.coroutines.withTimeoutOrNull
+import java.time.Duration
+import kotlin.coroutines.cancellation.CancellationException
+import kotlin.coroutines.coroutineContext
+
+class NowPlayingTileProviderService : SuspendingTileService() {
+ companion object {
+ private const val TAG = "NowPlayingTileProviderService"
+
+ fun requestTileUpdate(context: Context) {
+ updateJob?.cancel()
+
+ // Defer update to prevent spam
+ updateJob = appLib.appScope.launch {
+ delay(1000)
+ if (isActive) {
+ Logger.debug(TAG, "requesting tile update")
+ getUpdater(context).requestUpdate(NowPlayingTileProviderService::class.java)
+ }
+ }
+ }
+
+ @JvmStatic
+ @Volatile
+ var isInFocus: Boolean = false
+ private set
+
+ @JvmStatic
+ @Volatile
+ var isUpdating: Boolean = false
+ private set
+
+ private var updateJob: Job? = null
+ }
+
+ private lateinit var tileMessenger: MediaPlayerTileMessenger
+ private lateinit var tileStateFlow: StateFlow
+ private lateinit var tileRenderer: NowPlayingTileRenderer
+
+ override fun onCreate() {
+ super.onCreate()
+ Logger.debug(TAG, "creating service...")
+
+ tileMessenger = MediaPlayerTileMessenger(this)
+ tileRenderer = NowPlayingTileRenderer(this)
+
+ tileMessenger.register()
+ tileStateFlow = combine(
+ this.mediaDataStore.data,
+ this.artworkDataStore.data,
+ this.appInfoDataStore.data,
+ tileMessenger.connectionState
+ ) { mediaCache, artwork, appInfo, connectionStatus ->
+ MediaPlayerTileState(
+ connectionStatus = connectionStatus,
+ title = mediaCache.mediaPlayerState?.mediaMetaData?.title,
+ artist = mediaCache.mediaPlayerState?.mediaMetaData?.artist,
+ artwork = artwork,
+ playbackState = mediaCache.mediaPlayerState?.playbackState,
+ positionState = mediaCache.mediaPlayerState?.mediaMetaData?.positionState,
+ audioStreamState = mediaCache.audioStreamState,
+ appIcon = appInfo.iconBitmap
+ )
+ }
+ .stateIn(
+ lifecycleScope,
+ started = SharingStarted.WhileSubscribed(2000),
+ initialValue = null
+ )
+ }
+
+ override fun onDestroy() {
+ isUpdating = false
+ Logger.debug(TAG, "destroying service...")
+ tileMessenger.unregister()
+ super.onDestroy()
+ }
+
+ private fun onTileInteractionEnterEvent(requestParams: TileInteractionEvent) {
+ Logger.debug(TAG, "onTileEnterEvent called with: tileId = ${requestParams.tileId}")
+ AnalyticsLogger.logEvent("on_tile_enter", Bundle().apply {
+ putString("tile", TAG)
+ })
+ isInFocus = true
+
+ appLib.appScope.launch {
+ tileMessenger.checkConnectionStatus()
+ tileMessenger.requestPlayerConnect()
+ tileMessenger.requestUpdatePlayerState()
+ }.invokeOnCompletion {
+ if (it is CancellationException || !isUpdating) {
+ // If update timed out
+ requestTileUpdate(this)
+ }
+ }
+ }
+
+ private fun onTileInteractionLeaveEvent(requestParams: TileInteractionEvent) {
+ Logger.debug(TAG, "onTileLeaveEvent called with: tileId = ${requestParams.tileId}")
+ isInFocus = false
+
+ appLib.appScope.launch {
+ tileMessenger.requestPlayerDisconnect()
+ }
+ }
+
+ override fun onRecentInteractionEventsAsync(events: List): ListenableFuture {
+ return SuspendToFutureAdapter.launchFuture {
+ val lastEvent = events.lastOrNull()
+
+ when (lastEvent?.eventType) {
+ TileInteractionEvent.ENTER -> {
+ onTileInteractionEnterEvent(lastEvent)
+ }
+
+ TileInteractionEvent.LEAVE -> {
+ onTileInteractionLeaveEvent(lastEvent)
+ }
+
+ TileInteractionEvent.UNKNOWN -> { /* no-op */
+ }
+ }
+
+ null
+ }
+ }
+
+ override suspend fun tileRequest(requestParams: RequestBuilders.TileRequest): TileBuilders.Tile {
+ Logger.debug(TAG, "tileRequest: ${requestParams.currentState}")
+ val startTime = SystemClock.elapsedRealtimeNanos()
+ isUpdating = true
+
+ tileMessenger.checkConnectionStatus()
+
+ if (requestParams.currentState.lastClickableId.isNotEmpty()) {
+ if (ID_OPENONPHONE == requestParams.currentState.lastClickableId || ID_PHONEDISCONNECTED == requestParams.currentState.lastClickableId) {
+ runCatching {
+ startActivity(Intent(applicationContext, PhoneSyncActivity::class.java))
+ }
+ } else {
+ // Process action
+ runCatching {
+ Logger.debug(
+ TAG,
+ "lastClickableId = ${requestParams.currentState.lastClickableId}"
+ )
+ val action = PlayerAction.valueOf(requestParams.currentState.lastClickableId)
+
+ val state = latestTileState()
+
+ withTimeoutOrNull(5000) {
+ val ret = tileMessenger.requestPlayerActionAsync(action)
+ Logger.debug(TAG, "requestPlayerActionAsync = $ret")
+ }
+
+ // Try to await for full metadata change
+ withTimeoutOrNull(5000) {
+ supervisorScope {
+ var songChanged = false
+ tileStateFlow.collectLatest { newState ->
+ if (!songChanged && newState?.title != state.title && newState?.artist != state.artist) {
+ // new song; wait for artwork
+ songChanged = true
+ } else if (songChanged && !newState?.artwork.contentEquals(state.artwork)) {
+ coroutineContext.cancel()
+ } else if (newState?.playbackState != state.playbackState) {
+ // only playstate change
+ coroutineContext.cancel()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ isUpdating = false
+ val tileState = latestTileState()
+
+ if (tileState.isEmpty) {
+ AnalyticsLogger.logEvent("mediatile_state_empty", Bundle().apply {
+ putBoolean("isCoroutineActive", coroutineContext.isActive)
+ })
+ }
+
+ val endTime = SystemClock.elapsedRealtimeNanos()
+ Logger.debug(TAG, "Current State - ${tileState.title}:${tileState.artist}")
+ Logger.debug(TAG, "Duration - ${Duration.ofNanos(endTime - startTime)}")
+ Logger.debug(TAG, "Rendering timeline...")
+ return tileRenderer.renderTimeline(tileState, requestParams)
+ }
+
+ private suspend fun latestTileState(): MediaPlayerTileState {
+ var tileState = tileStateFlow.filterNotNull().first()
+
+ if (tileState.isEmpty) {
+ Logger.debug(TAG, "No tile state available. loading from remote...")
+ tileMessenger.updatePlayerStateFromRemote()
+
+ // Try to await for full metadata change
+ runCatching {
+ withTimeoutOrNull(5000) {
+ supervisorScope {
+ var songChanged = false
+
+ tileStateFlow.filterNotNull().collectLatest { newState ->
+ if (!songChanged && newState.title != tileState.title && newState.artist != tileState.artist) {
+ // new song; wait for artwork
+ tileState = newState
+ songChanged = true
+ } else if (songChanged && !newState.artwork.contentEquals(tileState.artwork)) {
+ tileState = newState
+ coroutineContext.cancel()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return tileState
+ }
+
+ override suspend fun resourcesRequest(requestParams: RequestBuilders.ResourcesRequest): ResourceBuilders.Resources {
+ val tileState = latestTileState()
+ return tileRenderer.produceRequestedResources(tileState, requestParams)
+ }
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/NowPlayingTileRenderer.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/NowPlayingTileRenderer.kt
new file mode 100644
index 00000000..4b18a271
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/NowPlayingTileRenderer.kt
@@ -0,0 +1,162 @@
+@file:OptIn(ExperimentalHorologistApi::class)
+
+package com.thewizrd.simplewear.wearable.tiles
+
+import android.content.ComponentName
+import android.content.Context
+import androidx.wear.protolayout.ActionBuilders
+import androidx.wear.protolayout.DeviceParametersBuilders
+import androidx.wear.protolayout.DimensionBuilders.expand
+import androidx.wear.protolayout.LayoutElementBuilders
+import androidx.wear.protolayout.LayoutElementBuilders.Box
+import androidx.wear.protolayout.ModifiersBuilders
+import androidx.wear.protolayout.ModifiersBuilders.Clickable
+import androidx.wear.protolayout.ResourceBuilders
+import androidx.wear.protolayout.ResourceBuilders.IMAGE_FORMAT_UNDEFINED
+import androidx.wear.protolayout.ResourceBuilders.ImageResource
+import androidx.wear.protolayout.ResourceBuilders.InlineImageResource
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.tiles.images.drawableResToImageResource
+import com.google.android.horologist.tiles.render.SingleTileLayoutRendererWithState
+import com.thewizrd.shared_resources.media.PlaybackState
+import com.thewizrd.shared_resources.utils.ContextUtils.dpToPx
+import com.thewizrd.shared_resources.utils.Logger
+import com.thewizrd.simplewear.BuildConfig
+import com.thewizrd.simplewear.R
+import com.thewizrd.simplewear.wearable.tiles.layouts.NowPlayingTileLayout
+import kotlin.math.min
+
+class NowPlayingTileRenderer(context: Context, debugResourceMode: Boolean = false) :
+ SingleTileLayoutRendererWithState(
+ context,
+ debugResourceMode
+ ) {
+ companion object {
+ // Resource identifiers for images
+ internal const val ID_OPENONPHONE = "open_on_phone"
+ internal const val ID_PHONEDISCONNECTED = "phone_disconn"
+
+ internal const val ID_ARTWORK = "artwork"
+ internal const val ID_APPICON = "app_icon"
+ internal const val ID_PLAYINGICON = "playing_icon"
+
+ fun getTapAction(context: Context): ActionBuilders.Action {
+ return ActionBuilders.launchAction(
+ ComponentName(context.packageName, context.packageName.run {
+ if (BuildConfig.DEBUG) removeSuffix(".debug") else this
+ } + ".MediaControllerActivity")
+ )
+ }
+ }
+
+ override fun renderTile(
+ state: MediaPlayerTileState,
+ deviceParameters: DeviceParametersBuilders.DeviceParameters
+ ): LayoutElementBuilders.LayoutElement {
+ return Box.Builder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .setModifiers(
+ ModifiersBuilders.Modifiers.Builder()
+ .setClickable(
+ Clickable.Builder()
+ .setOnClick(
+ getTapAction(context)
+ )
+ .build()
+ )
+ .build()
+ )
+ .addContent(
+ NowPlayingTileLayout(context, deviceParameters, state)
+ )
+ .build()
+ }
+
+ @androidx.annotation.OptIn(ProtoLayoutExperimental::class)
+ override fun ResourceBuilders.Resources.Builder.produceRequestedResources(
+ resourceState: MediaPlayerTileState,
+ deviceParameters: DeviceParametersBuilders.DeviceParameters,
+ resourceIds: List
+ ) {
+ Logger.debug(this::class.java.name, "produceRequestedResources: resIds = $resourceIds")
+
+ val resources = mapOf(
+ ID_OPENONPHONE to R.drawable.common_full_open_on_phone,
+ ID_PHONEDISCONNECTED to R.drawable.ic_phonelink_erase_white_24dp
+ )
+
+ (resourceIds.takeIf { it.isNotEmpty() } ?: resources.keys).forEach { key ->
+ resources[key]?.let { resId ->
+ addIdToImageMapping(key, drawableResToImageResource(resId))
+ }
+ }
+
+ resourceState.artwork?.let { bitmap ->
+ if (resourceIds.isEmpty() || resourceIds.contains(ID_ARTWORK)) {
+ addIdToImageMapping(
+ ID_ARTWORK,
+ ImageResource.Builder()
+ .setInlineResource(
+ InlineImageResource.Builder()
+ .setData(bitmap)
+ .setWidthPx(300)
+ .setHeightPx(300)
+ .setFormat(IMAGE_FORMAT_UNDEFINED)
+ .build()
+ )
+ .build()
+ )
+ }
+ }
+
+ resourceState.appIcon?.let { bitmap ->
+ if (resourceIds.isEmpty() || resourceIds.contains(ID_APPICON)) {
+ val size = context.dpToPx(24f).toInt()
+
+ addIdToImageMapping(
+ ID_APPICON,
+ ImageResource.Builder()
+ .setInlineResource(
+ InlineImageResource.Builder()
+ .setData(bitmap)
+ .setWidthPx(size)
+ .setHeightPx(size)
+ .setFormat(IMAGE_FORMAT_UNDEFINED)
+ .build()
+ )
+ .build()
+ )
+ }
+ }
+
+ if (resourceIds.isEmpty() || resourceIds.contains(ID_PLAYINGICON)) {
+ addIdToImageMapping(
+ ID_PLAYINGICON,
+ ImageResource.Builder()
+ .setAndroidResourceByResId(
+ ResourceBuilders.AndroidImageResourceByResId.Builder()
+ .setResourceId(R.drawable.equalizer_animated)
+ .build()
+ )
+ .build()
+ )
+ }
+ }
+
+ override fun getResourcesVersionForTileState(state: MediaPlayerTileState): String {
+ return "${state.title}:${state.artist}:${state.artwork?.size}"
+ }
+
+ override fun getFreshnessIntervalMillis(state: MediaPlayerTileState): Long {
+ return if (state.playbackState == PlaybackState.PLAYING && state.positionState != null) {
+ val elapsedTime = System.currentTimeMillis() - state.positionState.currentTimeMs
+ val estimatedPosition =
+ (state.positionState.currentPositionMs + (elapsedTime * state.positionState.playbackSpeed)).toLong()
+ state.positionState.durationMs - min(estimatedPosition, state.positionState.durationMs)
+ } else {
+ super.getFreshnessIntervalMillis(state)
+ }
+ }
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/TilePreviews.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/TilePreviews.kt
deleted file mode 100644
index 9dd1fbf3..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/TilePreviews.kt
+++ /dev/null
@@ -1,243 +0,0 @@
-package com.thewizrd.simplewear.wearable.tiles
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
-import androidx.core.content.ContextCompat
-import androidx.core.graphics.drawable.toBitmapOrNull
-import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
-import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.compose.tools.TileLayoutPreview
-import com.thewizrd.shared_resources.actions.Actions
-import com.thewizrd.shared_resources.actions.AudioStreamState
-import com.thewizrd.shared_resources.actions.AudioStreamType
-import com.thewizrd.shared_resources.actions.BatteryStatus
-import com.thewizrd.shared_resources.actions.DNDChoice
-import com.thewizrd.shared_resources.actions.MultiChoiceAction
-import com.thewizrd.shared_resources.actions.NormalAction
-import com.thewizrd.shared_resources.actions.RingerChoice
-import com.thewizrd.shared_resources.actions.ToggleAction
-import com.thewizrd.shared_resources.helpers.WearConnectionStatus
-import com.thewizrd.shared_resources.media.PlaybackState
-import com.thewizrd.shared_resources.media.PositionState
-import com.thewizrd.shared_resources.utils.ImageUtils.toByteArray
-import com.thewizrd.simplewear.R
-import kotlinx.coroutines.runBlocking
-
-@OptIn(ExperimentalHorologistApi::class)
-@WearPreviewDevices
-@Composable
-fun DashboardTilePreview() {
- val context = LocalContext.current
- val state = remember {
- DashboardTileState(
- connectionStatus = WearConnectionStatus.CONNECTED,
- batteryStatus = BatteryStatus(100, true),
- actions = mapOf(
- Actions.WIFI to ToggleAction(Actions.WIFI, true),
- Actions.BLUETOOTH to ToggleAction(Actions.BLUETOOTH, true),
- Actions.LOCKSCREEN to NormalAction(Actions.LOCKSCREEN),
- Actions.DONOTDISTURB to MultiChoiceAction(
- Actions.DONOTDISTURB,
- DNDChoice.OFF.value
- ),
- Actions.RINGER to MultiChoiceAction(Actions.RINGER, RingerChoice.VIBRATION.value),
- Actions.TORCH to NormalAction(Actions.TORCH)
- )
- )
- }
- val renderer = remember {
- DashboardTileRenderer(context, debugResourceMode = true)
- }
-
- TileLayoutPreview(
- state = state,
- resourceState = Unit,
- renderer = renderer
- )
-}
-
-@OptIn(ExperimentalHorologistApi::class)
-@WearPreviewDevices
-@Composable
-fun DashboardLoadingTilePreview() {
- val context = LocalContext.current
- val state = remember {
- DashboardTileState(
- connectionStatus = WearConnectionStatus.CONNECTED,
- batteryStatus = null,
- actions = emptyMap()
- )
- }
- val renderer = remember {
- DashboardTileRenderer(context, debugResourceMode = true)
- }
-
- TileLayoutPreview(
- state = state,
- resourceState = Unit,
- renderer = renderer
- )
-}
-
-@OptIn(ExperimentalHorologistApi::class)
-@WearPreviewDevices
-@WearPreviewFontScales
-@Composable
-fun DashboardDisconnectTilePreview() {
- val context = LocalContext.current
- val state = remember {
- DashboardTileState(
- connectionStatus = WearConnectionStatus.DISCONNECTED,
- batteryStatus = BatteryStatus(100, true),
- actions = mapOf(
- Actions.WIFI to ToggleAction(Actions.WIFI, true),
- Actions.BLUETOOTH to ToggleAction(Actions.BLUETOOTH, true),
- Actions.LOCKSCREEN to NormalAction(Actions.LOCKSCREEN),
- Actions.DONOTDISTURB to MultiChoiceAction(
- Actions.DONOTDISTURB,
- DNDChoice.OFF.value
- ),
- Actions.RINGER to MultiChoiceAction(Actions.RINGER, RingerChoice.VIBRATION.value),
- Actions.TORCH to NormalAction(Actions.TORCH)
- )
- )
- }
- val renderer = remember {
- DashboardTileRenderer(context, debugResourceMode = true)
- }
-
- TileLayoutPreview(
- state = state,
- resourceState = Unit,
- renderer = renderer
- )
-}
-
-@OptIn(ExperimentalHorologistApi::class)
-@WearPreviewDevices
-@WearPreviewFontScales
-@Composable
-fun DashboardNotInstalledTilePreview() {
- val context = LocalContext.current
- val state = remember {
- DashboardTileState(
- connectionStatus = WearConnectionStatus.APPNOTINSTALLED,
- batteryStatus = BatteryStatus(100, true),
- actions = mapOf(
- Actions.WIFI to ToggleAction(Actions.WIFI, true),
- Actions.BLUETOOTH to ToggleAction(Actions.BLUETOOTH, true),
- Actions.LOCKSCREEN to NormalAction(Actions.LOCKSCREEN),
- Actions.DONOTDISTURB to MultiChoiceAction(
- Actions.DONOTDISTURB,
- DNDChoice.OFF.value
- ),
- Actions.RINGER to MultiChoiceAction(Actions.RINGER, RingerChoice.VIBRATION.value),
- Actions.TORCH to NormalAction(Actions.TORCH)
- )
- )
- }
- val renderer = remember {
- DashboardTileRenderer(context, debugResourceMode = true)
- }
-
- TileLayoutPreview(
- state = state,
- resourceState = Unit,
- renderer = renderer
- )
-}
-
-@OptIn(ExperimentalHorologistApi::class)
-@WearPreviewDevices
-@WearPreviewFontScales
-@Composable
-fun MediaPlayerTilePreview() {
- val context = LocalContext.current
- val state = remember {
- MediaPlayerTileState(
- connectionStatus = WearConnectionStatus.CONNECTED,
- title = "Title",
- artist = "Artist",
- playbackState = PlaybackState.PAUSED,
- audioStreamState = AudioStreamState(3, 0, 5, AudioStreamType.MUSIC),
- positionState = PositionState(100, 50),
- artwork = runBlocking {
- ContextCompat.getDrawable(context, R.drawable.ws_full_sad)?.toBitmapOrNull()
- ?.toByteArray()
- },
- appIcon = runBlocking {
- ContextCompat.getDrawable(context, R.drawable.ic_play_circle_simpleblue)
- ?.toBitmapOrNull()
- ?.toByteArray()
- }
- )
- }
- val renderer = remember {
- MediaPlayerTileRenderer(context, debugResourceMode = true)
- }
-
- TileLayoutPreview(
- state = state,
- resourceState = state,
- renderer = renderer
- )
-}
-
-@OptIn(ExperimentalHorologistApi::class)
-@WearPreviewDevices
-@Composable
-fun MediaPlayerEmptyTilePreview() {
- val context = LocalContext.current
- val state = remember {
- MediaPlayerTileState(
- connectionStatus = WearConnectionStatus.CONNECTED,
- title = null,
- artist = null,
- playbackState = null,
- audioStreamState = null,
- artwork = null
- )
- }
- val renderer = remember {
- MediaPlayerTileRenderer(context, debugResourceMode = true)
- }
-
- TileLayoutPreview(
- state = state,
- resourceState = state,
- renderer = renderer
- )
-}
-
-@OptIn(ExperimentalHorologistApi::class)
-@WearPreviewDevices
-@Composable
-fun MediaPlayerNotPlayingTilePreview() {
- val context = LocalContext.current
- val state = remember {
- MediaPlayerTileState(
- connectionStatus = WearConnectionStatus.CONNECTED,
- title = null,
- artist = null,
- playbackState = PlaybackState.NONE,
- audioStreamState = AudioStreamState(3, 0, 5, AudioStreamType.MUSIC),
- artwork = null,
- appIcon = runBlocking {
- ContextCompat.getDrawable(context, R.drawable.ic_play_circle_simpleblue)
- ?.toBitmapOrNull()
- ?.toByteArray()
- }
- )
- }
- val renderer = remember {
- MediaPlayerTileRenderer(context, debugResourceMode = true)
- }
-
- TileLayoutPreview(
- state = state,
- resourceState = state,
- renderer = renderer
- )
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/DashboardTileLayout.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/DashboardTileLayout.kt
index 74210b39..cf425fb9 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/DashboardTileLayout.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/DashboardTileLayout.kt
@@ -1,51 +1,68 @@
+@file:OptIn(ProtoLayoutExperimental::class)
+@file:kotlin.OptIn(ExperimentalHorologistApi::class)
+@file:Suppress("FunctionName")
+
package com.thewizrd.simplewear.wearable.tiles.layouts
import android.content.Context
-import android.graphics.Color
import androidx.annotation.OptIn
-import androidx.core.content.ContextCompat
import androidx.wear.protolayout.ActionBuilders
-import androidx.wear.protolayout.ColorBuilders
import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters
-import androidx.wear.protolayout.DimensionBuilders
import androidx.wear.protolayout.DimensionBuilders.dp
+import androidx.wear.protolayout.DimensionBuilders.expand
import androidx.wear.protolayout.DimensionBuilders.sp
+import androidx.wear.protolayout.LayoutElementBuilders.Column
import androidx.wear.protolayout.LayoutElementBuilders.FONT_VARIANT_BODY
import androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_MEDIUM
import androidx.wear.protolayout.LayoutElementBuilders.FontStyle
import androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
-import androidx.wear.protolayout.LayoutElementBuilders.Spacer
import androidx.wear.protolayout.LayoutElementBuilders.SpanImage
import androidx.wear.protolayout.LayoutElementBuilders.SpanText
import androidx.wear.protolayout.LayoutElementBuilders.Spannable
-import androidx.wear.protolayout.LayoutElementBuilders.TEXT_ALIGN_CENTER
import androidx.wear.protolayout.LayoutElementBuilders.TEXT_OVERFLOW_MARQUEE
-import androidx.wear.protolayout.ModifiersBuilders.Clickable
import androidx.wear.protolayout.StateBuilders
import androidx.wear.protolayout.expression.AppDataKey
import androidx.wear.protolayout.expression.DynamicBuilders
import androidx.wear.protolayout.expression.DynamicDataBuilders
import androidx.wear.protolayout.expression.ProtoLayoutExperimental
-import androidx.wear.protolayout.material.Button
-import androidx.wear.protolayout.material.ButtonColors
-import androidx.wear.protolayout.material.ChipColors
-import androidx.wear.protolayout.material.Colors
-import androidx.wear.protolayout.material.CompactChip
-import androidx.wear.protolayout.material.Text
-import androidx.wear.protolayout.material.Typography
-import androidx.wear.protolayout.material.layouts.MultiButtonLayout
-import androidx.wear.protolayout.material.layouts.MultiButtonLayout.FIVE_BUTTON_DISTRIBUTION_TOP_HEAVY
-import androidx.wear.protolayout.material.layouts.PrimaryLayout
+import androidx.wear.protolayout.material3.ButtonDefaults.filledTonalButtonColors
+import androidx.wear.protolayout.material3.ButtonGroupDefaults.DEFAULT_SPACER_BETWEEN_BUTTON_GROUPS
+import androidx.wear.protolayout.material3.CardDefaults.filledTonalCardColors
+import androidx.wear.protolayout.material3.DataCardStyle
+import androidx.wear.protolayout.material3.MaterialScope
+import androidx.wear.protolayout.material3.Typography.BODY_MEDIUM
+import androidx.wear.protolayout.material3.buttonGroup
+import androidx.wear.protolayout.material3.icon
+import androidx.wear.protolayout.material3.iconButton
+import androidx.wear.protolayout.material3.iconEdgeButton
+import androidx.wear.protolayout.material3.materialScope
+import androidx.wear.protolayout.material3.primaryLayout
+import androidx.wear.protolayout.material3.text
+import androidx.wear.protolayout.material3.textDataCard
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.clickable
+import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.types.layoutString
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.tools.tileRendererPreviewData
import com.thewizrd.shared_resources.actions.Actions
+import com.thewizrd.shared_resources.actions.BatteryStatus
import com.thewizrd.shared_resources.actions.DNDChoice
import com.thewizrd.shared_resources.actions.LocationState
import com.thewizrd.shared_resources.actions.MultiChoiceAction
+import com.thewizrd.shared_resources.actions.NormalAction
import com.thewizrd.shared_resources.actions.RingerChoice
import com.thewizrd.shared_resources.actions.ToggleAction
import com.thewizrd.shared_resources.helpers.WearConnectionStatus
import com.thewizrd.simplewear.R
+import com.thewizrd.simplewear.ui.theme.wearTileColorScheme
+import com.thewizrd.simplewear.ui.tiles.tools.WearPreviewDevices
+import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer
import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_BATTERY
+import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_BATTERY_CHARGING
+import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_BATTERY_SAVER
import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_BT_OFF
import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_BT_ON
import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_DATA_OFF
@@ -61,6 +78,8 @@ import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID
import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_LOCATION_OFF
import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_LOCATION_SENSORSONLY
import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_LOCK
+import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_NFC_OFF
+import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_NFC_ON
import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_OPENONPHONE
import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_PHONEDISCONNECTED
import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID_RINGER_SILENT
@@ -71,94 +90,100 @@ import com.thewizrd.simplewear.wearable.tiles.DashboardTileRenderer.Companion.ID
import com.thewizrd.simplewear.wearable.tiles.DashboardTileState
import java.util.Locale
-private val CIRCLE_SIZE = dp(48f)
-private val SMALL_CIRCLE_SIZE = dp(40f)
-
-private val ICON_SIZE = dp(24f)
-private val SMALL_ICON_SIZE = dp(20f)
-
-private val COLORS = Colors(
- 0xff91cfff.toInt(), 0xff000000.toInt(),
- 0xff202124.toInt(), 0xffffffff.toInt()
-)
-
-@OptIn(ProtoLayoutExperimental::class)
internal fun DashboardTileLayout(
context: Context,
deviceParameters: DeviceParameters,
state: DashboardTileState
-): LayoutElement {
- return if (state.connectionStatus != WearConnectionStatus.CONNECTED) {
- PrimaryLayout.Builder(deviceParameters)
- .apply {
- when (state.connectionStatus) {
- WearConnectionStatus.APPNOTINSTALLED -> {
- setContent(
- Text.Builder(context, context.getString(R.string.error_notinstalled))
- .setTypography(Typography.TYPOGRAPHY_CAPTION1)
- .setColor(
- ColorBuilders.argb(
- ContextCompat.getColor(context, R.color.colorSecondary)
+): LayoutElement =
+ materialScope(context, deviceParameters, defaultColorScheme = wearTileColorScheme) {
+ if (state.connectionStatus != WearConnectionStatus.CONNECTED) {
+ when (state.connectionStatus) {
+ WearConnectionStatus.APPNOTINSTALLED -> {
+ primaryLayout(
+ titleSlot = {
+ text(text = context.getString(R.string.title_activity_dashboard).layoutString)
+ },
+ mainSlot = {
+ textDataCard(
+ onClick = clickable(
+ action = DashboardTileRenderer.getTapAction(
+ context
)
- )
- .setMultilineAlignment(TEXT_ALIGN_CENTER)
- .setMaxLines(3)
- .build()
- )
-
- setPrimaryChipContent(
- IconButton(
- context,
- ID_OPENONPHONE,
- context.getString(R.string.common_open_on_phone),
- Clickable.Builder()
- .setId(ID_OPENONPHONE)
- .setOnClick(
- ActionBuilders.LoadAction.Builder()
- .build()
+ ),
+ width = expand(),
+ height = expand(),
+ title = {
+ text(
+ text = context.getString(R.string.error_notinstalled).layoutString,
+ typography = BODY_MEDIUM,
+ maxLines = 3
)
- .build(),
- size = SMALL_CIRCLE_SIZE,
- iconSize = SMALL_ICON_SIZE
+ },
+ colors = filledTonalCardColors(),
+ style = DataCardStyle.smallDataCardStyle()
)
- )
- }
+ },
+ bottomSlot = {
+ iconEdgeButton(
+ modifier = LayoutModifier.contentDescription(context.getString(R.string.common_open_on_phone)),
+ onClick = clickable(id = ID_OPENONPHONE),
+ iconContent = {
+ icon(ID_OPENONPHONE)
+ }
+ )
+ }
+ )
+ }
- else -> {
- setContent(
- Text.Builder(context, context.getString(R.string.status_disconnected))
- .setTypography(Typography.TYPOGRAPHY_CAPTION1)
- .setColor(
- ColorBuilders.argb(
- ContextCompat.getColor(context, R.color.colorSecondary)
+ else -> {
+ primaryLayout(
+ titleSlot = {
+ text(text = context.getString(R.string.title_activity_dashboard).layoutString)
+ },
+ mainSlot = {
+ textDataCard(
+ onClick = clickable(
+ action = DashboardTileRenderer.getTapAction(
+ context
)
- )
- .setMultilineAlignment(TEXT_ALIGN_CENTER)
- .setMaxLines(3)
- .build()
- )
-
- setPrimaryChipContent(
- IconButton(
- context,
- resourceId = ID_PHONEDISCONNECTED,
- contentDescription = context.getString(R.string.status_disconnected),
- size = SMALL_CIRCLE_SIZE,
- iconSize = SMALL_ICON_SIZE
+ ),
+ width = expand(),
+ height = expand(),
+ title = {
+ text(
+ text = context.getString(R.string.status_disconnected).layoutString,
+ typography = BODY_MEDIUM,
+ maxLines = 3
+ )
+ },
+ colors = filledTonalCardColors(),
+ style = DataCardStyle.smallDataCardStyle()
)
- )
- }
+ },
+ bottomSlot = {
+ iconEdgeButton(
+ modifier = LayoutModifier.contentDescription(context.getString(R.string.status_disconnected)),
+ onClick = clickable(id = ID_PHONEDISCONNECTED),
+ iconContent = {
+ icon(ID_PHONEDISCONNECTED)
+ }
+ )
+ }
+ )
}
}
- .build()
} else {
- return PrimaryLayout.Builder(deviceParameters)
- .setPrimaryLabelTextContent(
- if (state.showBatteryStatus) {
+ primaryLayout(
+ titleSlot = state.takeIf { it.showBatteryStatus }?.let { state ->
+ {
Spannable.Builder()
.addSpan(
SpanImage.Builder()
- .setResourceId(ID_BATTERY)
+ .setResourceId(
+ state.batteryStatus?.let { status ->
+ if (status.isCharging) ID_BATTERY_CHARGING else ID_BATTERY
+ } ?: ID_BATTERY
+ )
.setWidth(dp(16f))
.setHeight(dp(16f))
.build()
@@ -192,55 +217,46 @@ internal fun DashboardTileLayout(
.setMultilineAlignment(HORIZONTAL_ALIGN_CENTER)
.setOverflow(TEXT_OVERFLOW_MARQUEE)
.build()
- } else {
- Spacer.Builder().build()
}
- )
- .setContent(
- MultiButtonLayout.Builder()
- .setFiveButtonDistribution(FIVE_BUTTON_DISTRIBUTION_TOP_HEAVY)
+ },
+ mainSlot = {
+ Column.Builder()
+ .setWidth(expand())
+ .setHeight(expand())
.apply {
- state.actions.forEach { (actionType, _) ->
- addButtonContent(
- ActionButton(context, deviceParameters, state, actionType)
+ val chunks = state.actions.toList().chunked(3)
+ chunks.forEachIndexed { index, chunk ->
+ addContent(
+ buttonGroup {
+ chunk.forEach { (actionType, _) ->
+ buttonGroupItem {
+ ActionButton(state, actionType)
+ }
+ }
+ }
)
+
+ if (index < chunks.size - 1) {
+ addContent(DEFAULT_SPACER_BETWEEN_BUTTON_GROUPS)
+ }
}
}
- .build()
+ .build()
+ }
)
- .build()
- }
-}
-
-private fun IconButton(
- context: Context,
- resourceId: String,
- contentDescription: String = "",
- clickable: Clickable = Clickable.Builder().build(),
- size: DimensionBuilders.DpProp? = CIRCLE_SIZE,
- iconSize: DimensionBuilders.DpProp = ICON_SIZE
-) = Button.Builder(context, clickable)
- .setContentDescription(contentDescription)
- .setButtonColors(ButtonColors.primaryButtonColors(COLORS))
- .setIconContent(resourceId, iconSize)
- .apply {
- if (size != null) {
- setSize(size)
}
}
- .build()
-private fun ActionButton(
- context: Context,
- deviceParameters: DeviceParameters,
+private fun MaterialScope.ActionButton(
state: DashboardTileState,
action: Actions
-) = Button.Builder(
- context,
- Clickable.Builder()
- .setId(action.name)
- .setOnClick(
- ActionBuilders.LoadAction.Builder()
+): LayoutElement {
+ val isEnabled = state.isActionEnabled(action)
+
+ return iconButton(
+ onClick = clickable(
+ id = action.name,
+ action = ActionBuilders.LoadAction.Builder()
.setRequestState(
StateBuilders.State.Builder()
.addKeyToValueMapping(
@@ -252,53 +268,44 @@ private fun ActionButton(
.build()
)
.build()
- )
- .build()
-)
- .setButtonColors(
- ButtonColors(
- ColorBuilders.ColorProp.Builder(
- ContextCompat.getColor(context, R.color.buttonDisabled)
+ ),
+ width = expand(),
+ height = expand(),
+ iconContent = {
+ icon(protoLayoutResourceId = getResourceIdForAction(state, action))
+ },
+ colors = filledTonalButtonColors().copy(
+ containerColor = LayoutColor(
+ staticArgb = if (isEnabled) {
+ colorScheme.primaryContainer.staticArgb
+ } else {
+ colorScheme.surfaceContainer.staticArgb
+ },
+ dynamicArgb = DynamicBuilders.DynamicColor
+ .onCondition(
+ DynamicBuilders.DynamicBool.from(AppDataKey(action.name))
+ )
+ .use(colorScheme.primaryContainer.staticArgb)
+ .elseUse(colorScheme.surfaceContainer.staticArgb)
+ .animate()
+ ),
+ iconColor = LayoutColor(
+ staticArgb = if (isEnabled) {
+ colorScheme.onPrimaryContainer.staticArgb
+ } else {
+ colorScheme.onSurface.staticArgb
+ },
+ dynamicArgb = DynamicBuilders.DynamicColor
+ .onCondition(
+ DynamicBuilders.DynamicBool.from(AppDataKey(action.name))
+ )
+ .use(colorScheme.onPrimaryContainer.staticArgb)
+ .elseUse(colorScheme.onSurface.staticArgb)
+ .animate()
)
- .setDynamicValue(
- DynamicBuilders.DynamicColor
- .onCondition(
- DynamicBuilders.DynamicBool.from(AppDataKey(action.name))
- )
- .use(ContextCompat.getColor(context, R.color.colorPrimary))
- .elseUse(ContextCompat.getColor(context, R.color.buttonDisabled))
- .animate()
- )
- .build(),
- ColorBuilders.argb(Color.WHITE)
)
)
- .apply {
- val isSmol =
- minOf(deviceParameters.screenHeightDp, deviceParameters.screenWidthDp) <= 192f
- setIconContent(
- getResourceIdForAction(state, action),
- if (isSmol) SMALL_ICON_SIZE else ICON_SIZE
- )
- setSize(if (isSmol) SMALL_CIRCLE_SIZE else CIRCLE_SIZE)
- }
- .build()
-
-private fun CompactChipButton(
- context: Context,
- deviceParameters: DeviceParameters,
- text: String,
- iconResourceId: String? = null,
- clickable: Clickable = Clickable.Builder().build()
-) = CompactChip.Builder(
- context, text, clickable, deviceParameters
-).setChipColors(
- ChipColors.primaryChipColors(COLORS)
-).apply {
- if (iconResourceId != null) {
- setIconContent(iconResourceId)
- }
-}.build()
+}
private fun getResourceIdForAction(state: DashboardTileState, action: Actions): String {
return when (action) {
@@ -368,6 +375,87 @@ private fun getResourceIdForAction(state: DashboardTileState, action: Actions):
}
Actions.HOTSPOT -> ID_HOTSPOT
+
+ Actions.NFC -> {
+ if ((state.getAction(action) as? ToggleAction)?.isEnabled == true) ID_NFC_ON else ID_NFC_OFF
+ }
+
+ Actions.BATTERYSAVER -> ID_BATTERY_SAVER
+
else -> ""
}
-}
\ No newline at end of file
+}
+
+@WearPreviewDevices
+private fun DashboardTilePreview(context: Context) = tileRendererPreviewData(
+ renderer = DashboardTileRenderer(context, debugResourceMode = true),
+ tileState = DashboardTileState(
+ connectionStatus = WearConnectionStatus.CONNECTED,
+ batteryStatus = BatteryStatus(100, true),
+ actions = mapOf(
+ Actions.WIFI to ToggleAction(Actions.WIFI, true),
+ Actions.BLUETOOTH to ToggleAction(Actions.BLUETOOTH, true),
+ Actions.LOCKSCREEN to NormalAction(Actions.LOCKSCREEN),
+ Actions.DONOTDISTURB to MultiChoiceAction(
+ Actions.DONOTDISTURB,
+ DNDChoice.OFF.value
+ ),
+ Actions.RINGER to MultiChoiceAction(Actions.RINGER, RingerChoice.VIBRATION.value),
+ Actions.TORCH to NormalAction(Actions.TORCH)
+ )
+ ),
+ resourceState = Unit
+)
+
+@WearPreviewDevices
+private fun DashboardLoadingTilePreview(context: Context) = tileRendererPreviewData(
+ renderer = DashboardTileRenderer(context, debugResourceMode = true),
+ tileState = DashboardTileState(
+ connectionStatus = WearConnectionStatus.CONNECTED,
+ batteryStatus = null,
+ actions = emptyMap()
+ ),
+ resourceState = Unit
+)
+
+@WearPreviewDevices
+private fun DashboardDisconnectTilePreview(context: Context) = tileRendererPreviewData(
+ renderer = DashboardTileRenderer(context, debugResourceMode = true),
+ tileState = DashboardTileState(
+ connectionStatus = WearConnectionStatus.DISCONNECTED,
+ batteryStatus = BatteryStatus(100, true),
+ actions = mapOf(
+ Actions.WIFI to ToggleAction(Actions.WIFI, true),
+ Actions.BLUETOOTH to ToggleAction(Actions.BLUETOOTH, true),
+ Actions.LOCKSCREEN to NormalAction(Actions.LOCKSCREEN),
+ Actions.DONOTDISTURB to MultiChoiceAction(
+ Actions.DONOTDISTURB,
+ DNDChoice.OFF.value
+ ),
+ Actions.RINGER to MultiChoiceAction(Actions.RINGER, RingerChoice.VIBRATION.value),
+ Actions.TORCH to NormalAction(Actions.TORCH)
+ )
+ ),
+ resourceState = Unit
+)
+
+@WearPreviewDevices
+private fun DashboardNotInstalledTilePreview(context: Context) = tileRendererPreviewData(
+ renderer = DashboardTileRenderer(context, debugResourceMode = true),
+ tileState = DashboardTileState(
+ connectionStatus = WearConnectionStatus.APPNOTINSTALLED,
+ batteryStatus = BatteryStatus(100, true),
+ actions = mapOf(
+ Actions.WIFI to ToggleAction(Actions.WIFI, true),
+ Actions.BLUETOOTH to ToggleAction(Actions.BLUETOOTH, true),
+ Actions.LOCKSCREEN to NormalAction(Actions.LOCKSCREEN),
+ Actions.DONOTDISTURB to MultiChoiceAction(
+ Actions.DONOTDISTURB,
+ DNDChoice.OFF.value
+ ),
+ Actions.RINGER to MultiChoiceAction(Actions.RINGER, RingerChoice.VIBRATION.value),
+ Actions.TORCH to NormalAction(Actions.TORCH)
+ )
+ ),
+ resourceState = Unit
+)
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/LoadingTileLayout.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/LoadingTileLayout.kt
new file mode 100644
index 00000000..18a8929a
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/LoadingTileLayout.kt
@@ -0,0 +1,70 @@
+package com.thewizrd.simplewear.wearable.tiles.layouts
+
+import android.content.Context
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters
+import androidx.wear.protolayout.DimensionBuilders.expand
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
+import androidx.wear.protolayout.material3.CardDefaults.filledTonalCardColors
+import androidx.wear.protolayout.material3.DataCardStyle
+import androidx.wear.protolayout.material3.Typography.BODY_MEDIUM
+import androidx.wear.protolayout.material3.materialScope
+import androidx.wear.protolayout.material3.primaryLayout
+import androidx.wear.protolayout.material3.text
+import androidx.wear.protolayout.material3.textDataCard
+import androidx.wear.protolayout.material3.textEdgeButton
+import androidx.wear.protolayout.modifiers.clickable
+import androidx.wear.protolayout.modifiers.loadAction
+import androidx.wear.protolayout.types.layoutString
+import androidx.wear.tiles.tooling.preview.TilePreviewData
+import androidx.wear.tiles.tooling.preview.TilePreviewHelper
+import com.thewizrd.simplewear.R
+import com.thewizrd.simplewear.ui.theme.wearTileColorScheme
+import com.thewizrd.simplewear.ui.tiles.tools.WearPreviewDevices
+
+internal fun LoadingTileLayout(
+ context: Context,
+ deviceParameters: DeviceParameters,
+ title: String? = null
+): LayoutElement =
+ materialScope(context, deviceParameters, defaultColorScheme = wearTileColorScheme) {
+ primaryLayout(
+ titleSlot = title?.let {
+ {
+ text(text = it.layoutString)
+ }
+ },
+ mainSlot = {
+ textDataCard(
+ onClick = clickable(loadAction()),
+ width = expand(),
+ height = expand(),
+ title = {
+ text(
+ text = context.getString(R.string.state_loading).layoutString,
+ typography = BODY_MEDIUM,
+ maxLines = 3
+ )
+ },
+ colors = filledTonalCardColors(),
+ style = DataCardStyle.smallDataCardStyle()
+ )
+ },
+ bottomSlot = {
+ textEdgeButton(
+ onClick = clickable(loadAction()),
+ labelContent = {
+ text(context.getString(R.string.action_refresh).layoutString)
+ }
+ )
+ }
+ )
+ }
+
+@WearPreviewDevices
+private fun LoadingTilePreview(context: Context) = TilePreviewData(
+ onTileRequest = { request ->
+ TilePreviewHelper.singleTimelineEntryTileBuilder(
+ LoadingTileLayout(context, request.deviceConfiguration)
+ ).build()
+ }
+)
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/MediaPlayerTileLayout.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/MediaPlayerTileLayout.kt
index d47d14d8..527eb16a 100644
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/MediaPlayerTileLayout.kt
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/MediaPlayerTileLayout.kt
@@ -1,19 +1,24 @@
+@file:OptIn(ProtoLayoutExperimental::class)
+@file:kotlin.OptIn(ExperimentalHorologistApi::class)
+@file:Suppress("FunctionName")
+
package com.thewizrd.simplewear.wearable.tiles.layouts
+import android.annotation.SuppressLint
import android.content.Context
-import android.graphics.Color
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
+import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toBitmapOrNull
import androidx.wear.protolayout.ActionBuilders
-import androidx.wear.protolayout.ColorBuilders
import androidx.wear.protolayout.ColorBuilders.ColorProp
import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters
import androidx.wear.protolayout.DeviceParametersBuilders.SCREEN_SHAPE_ROUND
import androidx.wear.protolayout.DimensionBuilders
+import androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp
import androidx.wear.protolayout.DimensionBuilders.dp
import androidx.wear.protolayout.DimensionBuilders.expand
-import androidx.wear.protolayout.DimensionBuilders.wrap
+import androidx.wear.protolayout.DimensionBuilders.weight
import androidx.wear.protolayout.LayoutElementBuilders.Box
import androidx.wear.protolayout.LayoutElementBuilders.CONTENT_SCALE_MODE_FIT
import androidx.wear.protolayout.LayoutElementBuilders.Column
@@ -24,34 +29,50 @@ import androidx.wear.protolayout.LayoutElementBuilders.Row
import androidx.wear.protolayout.LayoutElementBuilders.Spacer
import androidx.wear.protolayout.LayoutElementBuilders.TEXT_ALIGN_CENTER
import androidx.wear.protolayout.LayoutElementBuilders.TEXT_OVERFLOW_MARQUEE
+import androidx.wear.protolayout.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM
import androidx.wear.protolayout.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
+import androidx.wear.protolayout.LayoutElementBuilders.VERTICAL_ALIGN_TOP
import androidx.wear.protolayout.ModifiersBuilders.Background
import androidx.wear.protolayout.ModifiersBuilders.Clickable
import androidx.wear.protolayout.ModifiersBuilders.Corner
import androidx.wear.protolayout.ModifiersBuilders.Modifiers
import androidx.wear.protolayout.ModifiersBuilders.Padding
-import androidx.wear.protolayout.TypeBuilders.FloatProp
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant
import androidx.wear.protolayout.expression.ProtoLayoutExperimental
-import androidx.wear.protolayout.material.Button
-import androidx.wear.protolayout.material.ButtonColors
-import androidx.wear.protolayout.material.CircularProgressIndicator
-import androidx.wear.protolayout.material.Colors
-import androidx.wear.protolayout.material.CompactChip
-import androidx.wear.protolayout.material.ProgressIndicatorColors
-import androidx.wear.protolayout.material.Text
-import androidx.wear.protolayout.material.Typography
-import androidx.wear.protolayout.material.layouts.MultiSlotLayout
-import androidx.wear.protolayout.material.layouts.PrimaryLayout
+import androidx.wear.protolayout.material3.CardDefaults.filledTonalCardColors
+import androidx.wear.protolayout.material3.DataCardStyle
+import androidx.wear.protolayout.material3.MaterialScope
+import androidx.wear.protolayout.material3.ProgressIndicatorColors
+import androidx.wear.protolayout.material3.Typography.BODY_MEDIUM
+import androidx.wear.protolayout.material3.Typography.TITLE_MEDIUM
+import androidx.wear.protolayout.material3.buttonGroup
+import androidx.wear.protolayout.material3.circularProgressIndicator
+import androidx.wear.protolayout.material3.icon
+import androidx.wear.protolayout.material3.iconButton
+import androidx.wear.protolayout.material3.iconEdgeButton
+import androidx.wear.protolayout.material3.materialScope
+import androidx.wear.protolayout.material3.primaryLayout
+import androidx.wear.protolayout.material3.text
+import androidx.wear.protolayout.material3.textDataCard
+import androidx.wear.protolayout.material3.textEdgeButton
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.clickable
+import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.modifiers.padding
+import androidx.wear.protolayout.types.layoutString
import androidx.wear.tiles.tooling.preview.TilePreviewData
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.tools.tileRendererPreviewData
import com.thewizrd.shared_resources.actions.AudioStreamState
import com.thewizrd.shared_resources.actions.AudioStreamType
import com.thewizrd.shared_resources.helpers.WearConnectionStatus
import com.thewizrd.shared_resources.media.PlaybackState
+import com.thewizrd.shared_resources.media.PositionState
import com.thewizrd.shared_resources.utils.ImageUtils.toByteArray
import com.thewizrd.simplewear.R
-import com.thewizrd.simplewear.ui.tools.WearTilePreviewDevices
+import com.thewizrd.simplewear.ui.theme.wearTileColorScheme
+import com.thewizrd.simplewear.ui.tiles.tools.WearPreviewDevices
import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileMessenger.PlayerAction
import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileRenderer
import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileRenderer.Companion.ID_APPICON
@@ -67,490 +88,702 @@ import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileRenderer.Companion.
import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileState
import kotlinx.coroutines.runBlocking
import java.time.Instant
+import kotlin.math.ceil
+import kotlin.math.max
-private val CIRCLE_SIZE = dp(48f)
-private val SMALL_CIRCLE_SIZE = dp(40f)
-
-private val ICON_SIZE = dp(24f)
-private val SMALL_ICON_SIZE = dp(20f)
-
-private val COLORS = Colors(
- 0xff91cfff.toInt(), 0xff000000.toInt(),
- 0xff202124.toInt(), 0xffffffff.toInt()
-)
-
+@SuppressLint("ProtoLayoutPrimaryLayoutResponsive")
@OptIn(ProtoLayoutExperimental::class)
internal fun MediaPlayerTileLayout(
context: Context,
deviceParameters: DeviceParameters,
state: MediaPlayerTileState
-): LayoutElement {
- return if (state.connectionStatus != WearConnectionStatus.CONNECTED) {
- PrimaryLayout.Builder(deviceParameters)
- .apply {
- when (state.connectionStatus) {
- WearConnectionStatus.APPNOTINSTALLED -> {
- setContent(
- Text.Builder(context, context.getString(R.string.error_notinstalled))
- .setTypography(Typography.TYPOGRAPHY_CAPTION1)
- .setColor(
- ColorBuilders.argb(
- ContextCompat.getColor(context, R.color.colorSecondary)
+): LayoutElement =
+ materialScope(context, deviceParameters, defaultColorScheme = wearTileColorScheme) {
+ if (state.connectionStatus != WearConnectionStatus.CONNECTED) {
+ when (state.connectionStatus) {
+ WearConnectionStatus.APPNOTINSTALLED -> {
+ primaryLayout(
+ titleSlot = {
+ text(text = context.getString(R.string.title_media_controller).layoutString)
+ },
+ mainSlot = {
+ textDataCard(
+ onClick = clickable(
+ action = MediaPlayerTileRenderer.getTapAction(context)
+ ),
+ width = expand(),
+ height = expand(),
+ title = {
+ text(
+ text = context.getString(R.string.error_notinstalled).layoutString,
+ typography = BODY_MEDIUM,
+ maxLines = 3
)
- )
- .setMultilineAlignment(TEXT_ALIGN_CENTER)
- .setMaxLines(3)
- .build()
- )
+ },
+ colors = filledTonalCardColors(),
+ style = DataCardStyle.smallDataCardStyle()
+ )
+ },
+ bottomSlot = {
+ iconEdgeButton(
+ modifier = LayoutModifier.contentDescription(context.getString(R.string.common_open_on_phone)),
+ onClick = clickable(id = ID_OPENONPHONE),
+ iconContent = {
+ icon(ID_OPENONPHONE)
+ }
+ )
+ }
+ )
+ }
- setPrimaryChipContent(
- IconButton(
- context,
- ID_OPENONPHONE,
- context.getString(R.string.common_open_on_phone),
- Clickable.Builder()
- .setId(ID_OPENONPHONE)
- .setOnClick(
- ActionBuilders.LoadAction.Builder()
- .build()
+ else -> {
+ primaryLayout(
+ titleSlot = {
+ text(text = context.getString(R.string.title_media_controller).layoutString)
+ },
+ mainSlot = {
+ textDataCard(
+ onClick = clickable(
+ action = MediaPlayerTileRenderer.getTapAction(context)
+ ),
+ width = expand(),
+ height = expand(),
+ title = {
+ text(
+ text = context.getString(R.string.status_disconnected).layoutString,
+ typography = BODY_MEDIUM,
+ maxLines = 3
)
- .build(),
- size = SMALL_CIRCLE_SIZE,
- iconSize = SMALL_ICON_SIZE
+ },
+ colors = filledTonalCardColors(),
+ style = DataCardStyle.smallDataCardStyle()
)
- )
- }
-
- else -> {
- setContent(
- Text.Builder(context, context.getString(R.string.status_disconnected))
- .setTypography(Typography.TYPOGRAPHY_CAPTION1)
- .setColor(
- ColorBuilders.argb(
- ContextCompat.getColor(context, R.color.colorSecondary)
- )
- )
- .setMultilineAlignment(TEXT_ALIGN_CENTER)
- .setMaxLines(3)
- .build()
- )
-
- setPrimaryChipContent(
- IconButton(
- context,
- resourceId = ID_PHONEDISCONNECTED,
- contentDescription = context.getString(R.string.status_disconnected),
- size = SMALL_CIRCLE_SIZE,
- iconSize = SMALL_ICON_SIZE
+ },
+ bottomSlot = {
+ iconEdgeButton(
+ modifier = LayoutModifier.contentDescription(context.getString(R.string.status_disconnected)),
+ onClick = clickable(id = ID_PHONEDISCONNECTED),
+ iconContent = {
+ icon(ID_PHONEDISCONNECTED)
+ }
)
- )
- }
+ }
+ )
}
}
- .build()
- } else if (state.isEmpty || state.playbackState == null || state.playbackState == PlaybackState.NONE) {
- return PrimaryLayout.Builder(deviceParameters)
- .setContent(
- Text.Builder(context, context.getString(R.string.message_playback_stopped))
- .setMaxLines(1)
- .setMultilineAlignment(TEXT_ALIGN_CENTER)
- .setOverflow(TEXT_OVERFLOW_MARQUEE)
- .setTypography(Typography.TYPOGRAPHY_CAPTION1)
- .setColor(
- ColorBuilders.argb(
- ContextCompat.getColor(context, R.color.colorSecondary)
- )
+ } else if (state.isEmpty || state.playbackState == null || state.playbackState == PlaybackState.NONE) {
+ primaryLayout(
+ titleSlot = {
+ text(text = context.getString(R.string.title_media_controller).layoutString)
+ },
+ mainSlot = {
+ textDataCard(
+ onClick = clickable(
+ action = MediaPlayerTileRenderer.getTapAction(context)
+ ),
+ width = expand(),
+ height = expand(),
+ title = {
+ text(
+ text = context.getString(R.string.message_playback_stopped).layoutString,
+ typography = BODY_MEDIUM,
+ maxLines = 3
+ )
+ },
+ colors = filledTonalCardColors(),
+ style = DataCardStyle.smallDataCardStyle()
)
- .build()
+ },
+ bottomSlot = {
+ textEdgeButton(
+ onClick = clickable(id = PlayerAction.PLAY.name),
+ labelContent = {
+ text(context.getString(R.string.action_play).layoutString)
+ }
+ )
+ }
)
- .setPrimaryChipContent(
- CompactChip.Builder(
- context,
- context.getString(R.string.action_play),
- Clickable.Builder()
- .setId(PlayerAction.PLAY.name)
- .setOnClick(
- ActionBuilders.LoadAction.Builder()
+ } else {
+ Box.Builder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContent(
+ Image.Builder()
+ .setResourceId(ID_ARTWORK)
+ .setWidth(expand())
+ .setHeight(expand())
+ .setContentScaleMode(CONTENT_SCALE_MODE_FIT)
+ .build()
+ )
+ .addContent(
+ Box.Builder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .setModifiers(
+ Modifiers.Builder()
+ .setBackground(
+ Background.Builder()
+ .setColor(
+ ColorProp.Builder(0xAA000000.toInt())
+ .build()
+ )
+ .build()
+ )
.build()
)
- .build(),
- deviceParameters
+ .addContent(
+ Column.Builder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContent(
+ Box.Builder()
+ .setWidth(expand())
+ .setHeight(
+ WrappedDimensionProp.Builder()
+ .apply {
+ if (deviceParameters.isLargeHeight()) {
+ setMinimumSize(dp(0f))
+ } else {
+ setMinimumSize(dp(68f))
+ }
+ }
+ .build()
+ )
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setVerticalAlignment(VERTICAL_ALIGN_BOTTOM)
+ .setModifiers(
+ Modifiers.Builder()
+ .setPadding(
+ padding(
+ top = deviceParameters.getScreenSizeInDpFromPercentage(
+ if (deviceParameters.isLargeHeight()) {
+ 13.2f
+ } else {
+ 12f
+ }
+ ),
+ bottom = deviceParameters.getScreenSizeInDpFromPercentage(
+ if (deviceParameters.isLargeHeight()) {
+ 6f
+ } else {
+ 2f
+ }
+ ),
+ )
+ )
+ .build()
+ )
+ .addContent(
+ Column.Builder()
+ .setHeight(dp(38f))
+ .apply {
+ if (!state.title.isNullOrBlank()) {
+ addContent(
+ Box.Builder()
+ .setWidth(
+ dp(
+ deviceParameters.getScreenWidthInDpFromPercentage(
+ 66.72f
+ )
+ )
+ )
+ .setHeight(dp(20f))
+ .setHorizontalAlignment(
+ HORIZONTAL_ALIGN_CENTER
+ )
+ .addContent(
+ text(
+ text = state.title.layoutString,
+ maxLines = 1,
+ alignment = TEXT_ALIGN_CENTER,
+ overflow = TEXT_OVERFLOW_MARQUEE,
+ typography = TITLE_MEDIUM,
+ color = colorScheme.onSurface
+ )
+ )
+ .build()
+ )
+ }
+
+ if (!state.artist.isNullOrBlank()) {
+ addContent(
+ Box.Builder()
+ .setWidth(
+ dp(
+ deviceConfiguration.getScreenWidthInDpFromPercentage(
+ if (deviceConfiguration.isLargeWidth()) {
+ 71f
+ } else {
+ 75f
+ }
+ )
+ )
+ )
+ .setHeight(dp(18f))
+ .setHorizontalAlignment(
+ HORIZONTAL_ALIGN_CENTER
+ )
+ .addContent(
+ text(
+ text = state.artist.layoutString,
+ maxLines = 1,
+ alignment = TEXT_ALIGN_CENTER,
+ overflow = TEXT_OVERFLOW_MARQUEE,
+ typography = BODY_MEDIUM,
+ color = colorScheme.onSurface
+ )
+ )
+ .build()
+ )
+ }
+ }
+ .build()
+ )
+ .build()
+ )
+ .addContent(
+ Box.Builder()
+ .setWidth(expand())
+ .setHeight(
+ WrappedDimensionProp.Builder()
+ .apply {
+ if (deviceParameters.isLargeHeight()) {
+ setMinimumSize(dp(80f))
+ } else {
+ setMinimumSize(dp(64f))
+ }
+ }
+ .build()
+ )
+ .addContent(
+ buttonGroup(
+ height = middleButtonSize(),
+ width = expand(),
+ spacing = 0f
+ ) {
+ buttonGroupItem {
+ PlayerButton(action = PlayerAction.PREVIOUS)
+ }
+
+ buttonGroupItem {
+ PlayPauseButton(state)
+ }
+
+ buttonGroupItem {
+ PlayerButton(action = PlayerAction.NEXT)
+ }
+ }
+ )
+ .build()
+ )
+ .addContent(
+ Box.Builder()
+ .setWidth(expand())
+ .setHeight(weight(1f))
+ .addContent(SettingsButtonLayout(state))
+ .build()
+ )
+ .build()
+ )
+ .build()
)
- .build()
- )
- .build()
- } else {
- return Box.Builder()
+ .build()
+ }
+ }
+
+private fun MaterialScope.SettingsButtonLayout(
+ state: MediaPlayerTileState
+): LayoutElement {
+ return if (deviceConfiguration.screenShape == SCREEN_SHAPE_ROUND || deviceConfiguration.squareNotSupported()) {
+ val isLargeWidth = deviceConfiguration.isLargeWidth()
+ val horizontalSpacerPercentage = if (isLargeWidth) 11f else 14.5f
+
+ Box.Builder()
.setWidth(expand())
.setHeight(expand())
+ .setVerticalAlignment(VERTICAL_ALIGN_CENTER)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
.addContent(
- Image.Builder()
- .setResourceId(ID_ARTWORK)
- .setWidth(expand())
- .setHeight(expand())
- .setContentScaleMode(CONTENT_SCALE_MODE_FIT)
- .build()
- )
- .addContent(
- Box.Builder()
+ Row.Builder()
.setWidth(expand())
.setHeight(expand())
+ .setVerticalAlignment(VERTICAL_ALIGN_CENTER)
.setModifiers(
Modifiers.Builder()
- .setBackground(
- Background.Builder()
- .setColor(
- ColorProp.Builder(0xAA000000.toInt())
- .build()
+ .setPadding(
+ padding(
+ bottom = deviceConfiguration.getScreenSizeInDpFromPercentage(
+ 1.2f
)
- .build()
+ )
)
.build()
)
.addContent(
- PrimaryLayout.Builder(deviceParameters)
- .setPrimaryLabelTextContent(
- Column.Builder()
- .setWidth(expand())
- .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
- .apply {
- val isRound =
- deviceParameters.screenShape == SCREEN_SHAPE_ROUND
-
- if (!state.title.isNullOrBlank()) {
- addContent(
- Text.Builder(context, state.title)
- .setTypography(Typography.TYPOGRAPHY_BUTTON)
- .setColor(
- ColorProp.Builder(Color.WHITE)
- .build()
- )
- .setMaxLines(1)
- .setMultilineAlignment(TEXT_ALIGN_CENTER)
- .setOverflow(TEXT_OVERFLOW_MARQUEE)
- .setModifiers(
- Modifiers.Builder()
- .setPadding(
- Padding.Builder()
- .setTop(dp(2f))
- .setBottom(dp(0.8f))
- .setStart(
- if (isRound) dp(32f) else dp(
- 8f
- )
- )
- .setEnd(
- if (isRound) dp(32f) else dp(
- 8f
- )
- )
- .build()
- )
- .build()
- )
- .build()
- )
- }
-
- if (!state.artist.isNullOrBlank()) {
- addContent(
- Text.Builder(context, state.artist)
- .setTypography(Typography.TYPOGRAPHY_BODY2)
- .setColor(
- ColorProp.Builder(Color.WHITE).build()
- )
- .setMaxLines(1)
- .setMultilineAlignment(TEXT_ALIGN_CENTER)
- .setOverflow(TEXT_OVERFLOW_MARQUEE)
- .setModifiers(
- Modifiers.Builder()
- .setPadding(
- Padding.Builder()
- .setTop(dp(2f))
- .setBottom(dp(0.6f))
- .setStart(
- if (isRound) dp(32f) else dp(
- 8f
- )
- )
- .setEnd(
- if (isRound) dp(32f) else dp(
- 8f
- )
- )
- .build()
- )
- .build()
- )
- .build()
- )
- }
- }
- .build()
- )
- .setContent(
- MultiSlotLayout.Builder()
- .addSlotContent(
- PlayerButton(deviceParameters, PlayerAction.PREVIOUS)
- )
- .apply {
- val playerButtonContent =
- if (state.playbackState != PlaybackState.PLAYING) {
- PlayerButton(deviceParameters, PlayerAction.PLAY)
- } else {
- PlayerButton(deviceParameters, PlayerAction.PAUSE)
- }
-
- addSlotContent(
- if (deviceParameters.supportsDynamicValue() && state.positionState != null) {
- val actualPercent =
- state.positionState.currentPositionMs.toFloat() / state.positionState.durationMs.toFloat()
-
- Box.Builder()
- .setWidth(dp(56f))
- .setHeight(dp(56f))
- .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
- .setVerticalAlignment(VERTICAL_ALIGN_CENTER)
- .addContent(playerButtonContent)
- .addContent(
- CircularProgressIndicator.Builder()
- .setStartAngle(0f)
- .setEndAngle(360f)
- .setCircularProgressIndicatorColors(
- ProgressIndicatorColors(
- Colors.DEFAULT.primary,
- 0x25FFFFFF.toInt()
- )
- )
- .setStrokeWidth(dp(3f))
- .setOuterMarginApplied(false)
- .setProgress(
- FloatProp.Builder(actualPercent)
- .apply {
- if (state.playbackState == PlaybackState.PLAYING) {
- val durationFloat =
- state.positionState.durationMs.toFloat() / 1000f
-
- val positionFractional =
- DynamicInstant.withSecondsPrecision(
- Instant.ofEpochMilli(
- state.positionState.currentTimeMs
- )
- ).durationUntil(
- DynamicInstant.platformTimeWithSecondsPrecision()
- )
- .toIntSeconds()
- .asFloat()
- .times(state.positionState.playbackSpeed)
- .plus(state.positionState.currentPositionMs.toFloat() / 1000f)
-
- val predictedPercent =
- DynamicFloat.onCondition(
- positionFractional.gt(
- durationFloat
- )
- )
- .use(
- durationFloat
- )
- .elseUse(
- positionFractional
- )
- .div(
- durationFloat
- )
-
- setDynamicValue(
- DynamicFloat.onCondition(
- predictedPercent.gt(
- 0f
- )
- )
- .use(
- predictedPercent
- )
- .elseUse(0f)
- .animate()
- )
- }
- }
- .build()
- )
- .build()
- )
- .build()
- } else {
- playerButtonContent
- }
- )
- }
- .addSlotContent(
- PlayerButton(deviceParameters, PlayerAction.NEXT)
+ Spacer.Builder()
+ .setWidth(
+ dp(
+ deviceConfiguration.getScreenWidthInDpFromPercentage(
+ horizontalSpacerPercentage
)
- .build()
+ )
)
- .setPrimaryChipContent(
- Row.Builder()
- .setWidth(wrap())
- .setHeight(wrap())
- .setVerticalAlignment(VERTICAL_ALIGN_CENTER)
- .addContent(
- VolumeButton(PlayerAction.VOL_DOWN)
- )
- .apply {
- if (state.appIcon != null) {
- addContent(
- Spacer.Builder()
- .setWidth(dp(12f))
- .build()
- )
- addContent(
- Image.Builder()
- .setResourceId(ID_APPICON)
- .setWidth(dp(24f))
- .setHeight(dp(24f))
- .setContentScaleMode(CONTENT_SCALE_MODE_FIT)
- .build()
- )
- addContent(
- Spacer.Builder()
- .setWidth(dp(12f))
- .build()
- )
- } else {
- addContent(
- Spacer.Builder()
- .setWidth(dp(24f))
- .build()
- )
- }
- }
- .addContent(
- VolumeButton(PlayerAction.VOL_UP)
+ .build()
+ )
+ .addContent(VolumeButton(PlayerAction.VOL_DOWN))
+ .addContent(BrandIcon(ID_APPICON))
+ .addContent(VolumeButton(PlayerAction.VOL_UP))
+ .addContent(
+ Spacer.Builder()
+ .setWidth(
+ dp(
+ deviceConfiguration.getScreenWidthInDpFromPercentage(
+ horizontalSpacerPercentage
)
- .build()
+ )
)
.build()
)
.build()
)
.build()
+ } else {
+ Box.Builder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .setVerticalAlignment(VERTICAL_ALIGN_CENTER)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .addContent(
+ Row.Builder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .setVerticalAlignment(VERTICAL_ALIGN_CENTER)
+ .addContent(
+ Spacer.Builder()
+ .setWidth(dp(deviceConfiguration.getScreenWidthInDpFromPercentage(11f)))
+ .build()
+ )
+ .addContent(VolumeButton(PlayerAction.VOL_DOWN))
+ .addContent(BrandIcon(ID_APPICON))
+ .addContent(VolumeButton(PlayerAction.VOL_UP))
+ .addContent(
+ Spacer.Builder()
+ .setWidth(dp(deviceConfiguration.getScreenWidthInDpFromPercentage(11f)))
+ .build()
+ )
+ .build()
+ )
+ .build()
}
}
-private fun PlayerButton(
- deviceParameters: DeviceParameters,
+private fun MaterialScope.middleButtonSize(): DimensionBuilders.DpProp =
+ if (deviceConfiguration.isLargeHeight()) {
+ dp(80f)
+ } else {
+ dp(64f)
+ }
+
+private fun MaterialScope.getSideButtonsPadding(
+ isLeftButton: Boolean
+): Padding {
+ val isLargeScreen = deviceConfiguration.isLargeHeight()
+
+ val buttonGroupSpacingPct = if (isLargeScreen) {
+ 3f
+ } else {
+ 5.2f
+ }
+
+ val adj =
+ if (deviceConfiguration.screenShape == SCREEN_SHAPE_ROUND || deviceConfiguration.squareNotSupported()) {
+ 0f
+ } else {
+ if (deviceConfiguration.isLargeWidth()) 2f else 4f
+ }
+
+ return padding(
+ start = max(
+ if (isLeftButton) {
+ deviceConfiguration.getScreenSizeInDpFromPercentage(7.1f)
+ } else {
+ deviceConfiguration.getScreenSizeInDpFromPercentage(buttonGroupSpacingPct)
+ } - adj,
+ 0f
+ ),
+ end = max(
+ if (!isLeftButton) {
+ deviceConfiguration.getScreenSizeInDpFromPercentage(7.1f)
+ } else {
+ deviceConfiguration.getScreenSizeInDpFromPercentage(buttonGroupSpacingPct)
+ } - adj,
+ 0f
+ ),
+ top = deviceConfiguration.getScreenSizeInDpFromPercentage(
+ if (isLargeScreen) {
+ 5.2f
+ } else {
+ 4.16f
+ }
+ ),
+ bottom = deviceConfiguration.getScreenSizeInDpFromPercentage(
+ if (isLargeScreen) {
+ 5.2f
+ } else {
+ 4.16f
+ }
+ ),
+ rtlAware = false
+ )
+}
+
+private fun DeviceParameters.getScreenSizeInDpFromPercentage(
+ percent: Float
+): Float {
+ return ceil(screenHeightDp * percent / 100f)
+}
+
+private fun DeviceParameters.getScreenWidthInDpFromPercentage(
+ percent: Float
+): Float {
+ return ceil(screenWidthDp * percent / 100f)
+}
+
+private fun MaterialScope.PlayerButton(
action: PlayerAction
): LayoutElement {
- val isPlayPause = action == PlayerAction.PAUSE || action == PlayerAction.PLAY
- val size = dp(50f)
+ val buttonPadding = getSideButtonsPadding(
+ isLeftButton = action == PlayerAction.PREVIOUS
+ )
+
return Box.Builder()
- .setHeight(size)
- .setWidth(size)
.setModifiers(
Modifiers.Builder()
- .setBackground(
- Background.Builder()
- .setColor(
- ColorProp.Builder(
- if (isPlayPause) {
- 0x19FFFFFF
- } else {
- Color.TRANSPARENT
- }
- ).build()
- )
- .setCorner(
- Corner.Builder()
- .setRadius(size)
- .build()
- )
- .build()
- )
- .setClickable(
- Clickable.Builder()
- .setId(action.name)
- .setOnClick(
- ActionBuilders.LoadAction.Builder()
- .build()
- )
- .build()
- )
+ .setPadding(buttonPadding)
.build()
)
- .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
- .setVerticalAlignment(VERTICAL_ALIGN_CENTER)
+ .setWidth(expand())
+ .setHeight(expand())
.addContent(
- Image.Builder()
- .setWidth(dp(32f))
- .setHeight(dp(32f))
- .setResourceId(getResourceIdForPlayerAction(action))
- .setContentScaleMode(CONTENT_SCALE_MODE_FIT)
+ iconButton(
+ onClick = clickable(id = action.name),
+ width = expand(),
+ height = expand(),
+ iconContent = {
+ icon(getResourceIdForPlayerAction(action))
+ }
+ )
+ )
+ .build()
+}
+
+private fun MaterialScope.PlayPauseButton(
+ state: MediaPlayerTileState
+): LayoutElement {
+ val middleButtonSize = middleButtonSize()
+
+ val action = if (state.playbackState != PlaybackState.PLAYING) {
+ PlayerAction.PLAY
+ } else {
+ PlayerAction.PAUSE
+ }
+
+ val contentSize = if (deviceConfiguration.isLargeHeight()) {
+ 80f
+ } else {
+ 64f
+ }
+
+ val playerButtonContent = Box.Builder()
+ .setModifiers(
+ Modifiers.Builder()
.build()
)
+ .addContent(
+ iconButton(
+ onClick = clickable(id = action.name),
+ width = dp(middleButtonSize.value - 14f),
+ height = dp(middleButtonSize.value - 14f),
+ iconContent = {
+ icon(getResourceIdForPlayerAction(action))
+ }
+ )
+ )
.build()
+
+ return if (deviceConfiguration.supportsDynamicValue() && state.positionState != null) {
+ val actualPercent =
+ state.positionState.currentPositionMs.toFloat() / state.positionState.durationMs.toFloat()
+
+ Box.Builder()
+ .setWidth(WrappedDimensionProp.Builder().setMinimumSize(middleButtonSize).build())
+ .setHeight(WrappedDimensionProp.Builder().setMinimumSize(middleButtonSize).build())
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setVerticalAlignment(VERTICAL_ALIGN_CENTER)
+ .addContent(playerButtonContent)
+ .addContent(
+ circularProgressIndicator(
+ staticProgress = actualPercent,
+ dynamicProgress = if (state.playbackState == PlaybackState.PLAYING) {
+ val durationFloat =
+ state.positionState.durationMs.toFloat() / 1000f
+
+ val positionFractional =
+ DynamicInstant.withSecondsPrecision(
+ Instant.ofEpochMilli(
+ state.positionState.currentTimeMs
+ )
+ ).durationUntil(
+ DynamicInstant.platformTimeWithSecondsPrecision()
+ )
+ .toIntSeconds()
+ .asFloat()
+ .times(state.positionState.playbackSpeed)
+ .plus(state.positionState.currentPositionMs.toFloat() / 1000f)
+
+ val predictedPercent =
+ DynamicFloat.onCondition(
+ positionFractional.gt(
+ durationFloat
+ )
+ )
+ .use(
+ durationFloat
+ )
+ .elseUse(
+ positionFractional
+ )
+ .div(
+ durationFloat
+ )
+
+ DynamicFloat.onCondition(
+ predictedPercent.gt(
+ 0f
+ )
+ )
+ .use(
+ predictedPercent
+ )
+ .elseUse(0f)
+ .animate()
+ } else {
+ null
+ },
+ startAngleDegrees = 0f,
+ endAngleDegrees = 360f,
+ strokeWidth = 4f,
+ colors = ProgressIndicatorColors(
+ colorScheme.onSecondaryContainer,
+ colorScheme.outline
+ )
+ )
+ )
+ .build()
+ } else {
+ playerButtonContent
+ }
}
-private fun VolumeButton(
+private fun MaterialScope.VolumeButton(
action: PlayerAction
): LayoutElement = Box.Builder()
- .setHeight(dp(26f))
- .setWidth(dp(26f))
- .setModifiers(
- Modifiers.Builder()
- .setBackground(
- Background.Builder()
- .setColor(
- ColorProp.Builder(Color.TRANSPARENT).build()
- )
- .setCorner(
- Corner.Builder()
- .setRadius(dp(26f))
+ .setWidth(weight(1f))
+ .setHeight(expand())
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setVerticalAlignment(
+ if (deviceConfiguration.screenShape == SCREEN_SHAPE_ROUND || deviceConfiguration.squareNotSupported()) {
+ VERTICAL_ALIGN_TOP
+ } else {
+ VERTICAL_ALIGN_CENTER
+ }
+ )
+ .addContent(
+ Box.Builder()
+ .setWidth(getSettingsIconWidth())
+ .setHeight(getSettingsIconHeight())
+ .setModifiers(
+ Modifiers.Builder()
+ .setBackground(
+ Background.Builder()
+ .setColor(
+ ColorProp.Builder(
+ ColorUtils.setAlphaComponent(
+ colorScheme.onSurface.staticArgb,
+ (0xFF * 0.24f).toInt()
+ )
+ ).build()
+ )
+ .setCorner(
+ Corner.Builder()
+ .setRadius(dp(22f))
+ .build()
+ )
.build()
)
- .build()
- )
- .setClickable(
- Clickable.Builder()
- .setId(action.name)
- .setOnClick(
- ActionBuilders.LoadAction.Builder()
+ .setClickable(
+ Clickable.Builder()
+ .setId(action.name)
+ .setOnClick(
+ ActionBuilders.LoadAction.Builder()
+ .build()
+ )
.build()
)
.build()
)
- .build()
- )
- .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
- .setVerticalAlignment(VERTICAL_ALIGN_CENTER)
- .addContent(
- Image.Builder()
- .setWidth(dp(26f))
- .setHeight(dp(26f))
- .setResourceId(getResourceIdForPlayerAction(action))
- .setContentScaleMode(CONTENT_SCALE_MODE_FIT)
+ .addContent(
+ icon(
+ protoLayoutResourceId = getResourceIdForPlayerAction(action),
+ width = dp(20f),
+ height = dp(20f),
+ tintColor = colorScheme.onSurface
+ )
+ )
.build()
)
.build()
-private fun IconButton(
- context: Context,
- resourceId: String,
- contentDescription: String = "",
- clickable: Clickable = Clickable.Builder().build(),
- size: DimensionBuilders.DpProp? = CIRCLE_SIZE,
- iconSize: DimensionBuilders.DpProp = ICON_SIZE
-) = Button.Builder(context, clickable)
- .setContentDescription(contentDescription)
- .setButtonColors(ButtonColors.primaryButtonColors(COLORS))
- .setIconContent(resourceId, iconSize)
+private fun MaterialScope.BrandIcon(
+ resourceId: String
+): LayoutElement = Box.Builder()
+ .setWidth(weight(1f))
+ .setHeight(expand())
.apply {
- if (size != null) {
- setSize(size)
+ if (deviceConfiguration.screenShape == SCREEN_SHAPE_ROUND || deviceConfiguration.squareNotSupported()) {
+ setModifiers(
+ Modifiers.Builder()
+ .setPadding(padding(bottom = deviceConfiguration.screenHeightDp * 0.03f))
+ .build()
+ )
+ setVerticalAlignment(VERTICAL_ALIGN_BOTTOM)
}
}
+ .addContent(
+ icon(
+ protoLayoutResourceId = resourceId,
+ width = getSettingsIconHeight(),
+ height = getSettingsIconHeight()
+ )
+ )
.build()
+private fun MaterialScope.getSettingsIconHeight(): DimensionBuilders.DpProp {
+ return if (!deviceConfiguration.isSmallWatch()) {
+ dp(32f)
+ } else {
+ dp(24f)
+ }
+}
+
+private fun MaterialScope.getSettingsIconWidth(): DimensionBuilders.DpProp {
+ return if (!deviceConfiguration.isSmallWatch()) {
+ dp(40f)
+ } else {
+ dp(32f)
+ }
+}
+
private fun getResourceIdForPlayerAction(action: PlayerAction): String {
return when (action) {
PlayerAction.PLAY -> ID_PLAY
@@ -562,7 +795,7 @@ private fun getResourceIdForPlayerAction(action: PlayerAction): String {
}
}
-@WearTilePreviewDevices
+@WearPreviewDevices
private fun MediaPlayerTilePreview(context: Context): TilePreviewData {
val state = MediaPlayerTileState(
connectionStatus = WearConnectionStatus.CONNECTED,
@@ -570,8 +803,9 @@ private fun MediaPlayerTilePreview(context: Context): TilePreviewData {
artist = "Artist",
playbackState = PlaybackState.PAUSED,
audioStreamState = AudioStreamState(3, 0, 5, AudioStreamType.MUSIC),
+ positionState = PositionState(100, 50),
artwork = runBlocking {
- ContextCompat.getDrawable(context, R.drawable.ws_full_sad)?.toBitmapOrNull()
+ ContextCompat.getDrawable(context, R.drawable.sample_image)?.toBitmapOrNull()
?.toByteArray()
},
appIcon = runBlocking {
@@ -580,10 +814,51 @@ private fun MediaPlayerTilePreview(context: Context): TilePreviewData {
?.toByteArray()
}
)
- val renderer = MediaPlayerTileRenderer(context, debugResourceMode = true)
- return TilePreviewData(
- onTileRequest = { renderer.renderTimeline(state, it) },
- onTileResourceRequest = { renderer.produceRequestedResources(state, it) }
+ return tileRendererPreviewData(
+ renderer = MediaPlayerTileRenderer(context, debugResourceMode = true),
+ tileState = state,
+ resourceState = state
+ )
+}
+
+@WearPreviewDevices
+private fun MediaPlayerEmptyTilePreview(context: Context): TilePreviewData {
+ val state = MediaPlayerTileState(
+ connectionStatus = WearConnectionStatus.DISCONNECTED,
+ title = null,
+ artist = null,
+ playbackState = null,
+ audioStreamState = null,
+ artwork = null
+ )
+
+ return tileRendererPreviewData(
+ renderer = MediaPlayerTileRenderer(context, debugResourceMode = true),
+ tileState = state,
+ resourceState = state
+ )
+}
+
+@WearPreviewDevices
+private fun MediaPlayerNotPlayingTilePreview(context: Context): TilePreviewData {
+ val state = MediaPlayerTileState(
+ connectionStatus = WearConnectionStatus.CONNECTED,
+ title = null,
+ artist = null,
+ playbackState = PlaybackState.NONE,
+ audioStreamState = AudioStreamState(3, 0, 5, AudioStreamType.MUSIC),
+ artwork = null,
+ appIcon = runBlocking {
+ ContextCompat.getDrawable(context, R.drawable.ic_play_circle_simpleblue)
+ ?.toBitmapOrNull()
+ ?.toByteArray()
+ }
+ )
+
+ return tileRendererPreviewData(
+ renderer = MediaPlayerTileRenderer(context, debugResourceMode = true),
+ tileState = state,
+ resourceState = state,
)
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/NowPlayingTileLayout.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/NowPlayingTileLayout.kt
new file mode 100644
index 00000000..1aa0119a
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/NowPlayingTileLayout.kt
@@ -0,0 +1,324 @@
+@file:kotlin.OptIn(ExperimentalHorologistApi::class)
+
+package com.thewizrd.simplewear.wearable.tiles.layouts
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.annotation.OptIn
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.toBitmapOrNull
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters
+import androidx.wear.protolayout.DimensionBuilders.dp
+import androidx.wear.protolayout.DimensionBuilders.expand
+import androidx.wear.protolayout.LayoutElementBuilders.CONTENT_SCALE_MODE_CROP
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental
+import androidx.wear.protolayout.material3.CardDefaults.filledTonalCardColors
+import androidx.wear.protolayout.material3.DataCardStyle
+import androidx.wear.protolayout.material3.TitleCardStyle
+import androidx.wear.protolayout.material3.Typography.BODY_MEDIUM
+import androidx.wear.protolayout.material3.backgroundImage
+import androidx.wear.protolayout.material3.icon
+import androidx.wear.protolayout.material3.iconEdgeButton
+import androidx.wear.protolayout.material3.materialScope
+import androidx.wear.protolayout.material3.primaryLayout
+import androidx.wear.protolayout.material3.text
+import androidx.wear.protolayout.material3.textDataCard
+import androidx.wear.protolayout.material3.textEdgeButton
+import androidx.wear.protolayout.material3.titleCard
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.clickable
+import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.types.layoutString
+import androidx.wear.tiles.tooling.preview.TilePreviewData
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.tools.tileRendererPreviewData
+import com.thewizrd.shared_resources.actions.AudioStreamState
+import com.thewizrd.shared_resources.actions.AudioStreamType
+import com.thewizrd.shared_resources.helpers.WearConnectionStatus
+import com.thewizrd.shared_resources.media.PlaybackState
+import com.thewizrd.shared_resources.media.PositionState
+import com.thewizrd.shared_resources.utils.ImageUtils.toByteArray
+import com.thewizrd.simplewear.R
+import com.thewizrd.simplewear.ui.theme.wearTileColorScheme
+import com.thewizrd.simplewear.ui.tiles.tools.WearPreviewDevices
+import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileMessenger.PlayerAction
+import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileState
+import com.thewizrd.simplewear.wearable.tiles.NowPlayingTileRenderer
+import com.thewizrd.simplewear.wearable.tiles.NowPlayingTileRenderer.Companion.ID_APPICON
+import com.thewizrd.simplewear.wearable.tiles.NowPlayingTileRenderer.Companion.ID_ARTWORK
+import com.thewizrd.simplewear.wearable.tiles.NowPlayingTileRenderer.Companion.ID_OPENONPHONE
+import com.thewizrd.simplewear.wearable.tiles.NowPlayingTileRenderer.Companion.ID_PHONEDISCONNECTED
+import com.thewizrd.simplewear.wearable.tiles.NowPlayingTileRenderer.Companion.ID_PLAYINGICON
+import kotlinx.coroutines.runBlocking
+
+@SuppressLint("ProtoLayoutPrimaryLayoutResponsive")
+@OptIn(ProtoLayoutExperimental::class)
+internal fun NowPlayingTileLayout(
+ context: Context,
+ deviceParameters: DeviceParameters,
+ state: MediaPlayerTileState
+): LayoutElement =
+ materialScope(context, deviceParameters, defaultColorScheme = wearTileColorScheme) {
+ if (state.connectionStatus != WearConnectionStatus.CONNECTED) {
+ when (state.connectionStatus) {
+ WearConnectionStatus.APPNOTINSTALLED -> {
+ primaryLayout(
+ titleSlot = {
+ text(text = context.getString(R.string.title_nowplaying).layoutString)
+ },
+ mainSlot = {
+ textDataCard(
+ onClick = clickable(
+ action = NowPlayingTileRenderer.getTapAction(context)
+ ),
+ width = expand(),
+ height = expand(),
+ title = {
+ text(
+ text = context.getString(R.string.error_notinstalled).layoutString,
+ typography = BODY_MEDIUM,
+ maxLines = 3
+ )
+ },
+ colors = filledTonalCardColors(),
+ style = DataCardStyle.smallDataCardStyle()
+ )
+ },
+ bottomSlot = {
+ iconEdgeButton(
+ modifier = LayoutModifier.contentDescription(context.getString(R.string.common_open_on_phone)),
+ onClick = clickable(id = ID_OPENONPHONE),
+ iconContent = {
+ icon(ID_OPENONPHONE)
+ }
+ )
+ }
+ )
+ }
+
+ else -> {
+ primaryLayout(
+ titleSlot = {
+ text(text = context.getString(R.string.title_nowplaying).layoutString)
+ },
+ mainSlot = {
+ textDataCard(
+ onClick = clickable(
+ action = NowPlayingTileRenderer.getTapAction(context)
+ ),
+ width = expand(),
+ height = expand(),
+ title = {
+ text(
+ text = context.getString(R.string.status_disconnected).layoutString,
+ typography = BODY_MEDIUM,
+ maxLines = 3
+ )
+ },
+ colors = filledTonalCardColors(),
+ style = DataCardStyle.smallDataCardStyle()
+ )
+ },
+ bottomSlot = {
+ iconEdgeButton(
+ modifier = LayoutModifier.contentDescription(context.getString(R.string.status_disconnected)),
+ onClick = clickable(id = ID_PHONEDISCONNECTED),
+ iconContent = {
+ icon(ID_PHONEDISCONNECTED)
+ }
+ )
+ }
+ )
+ }
+ }
+ } else if (state.isEmpty || state.playbackState == null || state.playbackState == PlaybackState.NONE) {
+ primaryLayout(
+ titleSlot = {
+ text(text = context.getString(R.string.title_nowplaying).layoutString)
+ },
+ mainSlot = {
+ textDataCard(
+ onClick = clickable(
+ action = NowPlayingTileRenderer.getTapAction(context)
+ ),
+ width = expand(),
+ height = expand(),
+ title = {
+ text(
+ text = context.getString(R.string.message_playback_stopped).layoutString,
+ typography = BODY_MEDIUM,
+ maxLines = 3
+ )
+ },
+ colors = filledTonalCardColors(),
+ style = DataCardStyle.smallDataCardStyle()
+ )
+ },
+ bottomSlot = {
+ textEdgeButton(
+ onClick = clickable(id = PlayerAction.PLAY.name),
+ labelContent = {
+ text(context.getString(R.string.action_play).layoutString)
+ }
+ )
+ }
+ )
+ } else {
+ primaryLayout(
+ titleSlot = {
+ text(text = context.getString(R.string.title_nowplaying).layoutString)
+ },
+ mainSlot = {
+ titleCard(
+ onClick = clickable(NowPlayingTileRenderer.getTapAction(context)),
+ height = expand(),
+ backgroundContent = {
+ backgroundImage(
+ protoLayoutResourceId = ID_ARTWORK,
+ width = expand(),
+ height = expand(),
+ contentScaleMode = CONTENT_SCALE_MODE_CROP
+ )
+ },
+ title = {
+ text(
+ text = (state.title ?: "").layoutString,
+ color = colorScheme.onSurface
+ )
+ },
+ content = state.artist?.let {
+ {
+ text(
+ text = it.layoutString,
+ color = colorScheme.onSurfaceVariant
+ )
+ }
+ },
+ time = state.appIcon?.let {
+ {
+ icon(
+ protoLayoutResourceId = ID_APPICON,
+ width = dp(24f),
+ height = dp(24f)
+ )
+ }
+ },
+ style = TitleCardStyle.largeTitleCardStyle()
+ )
+ },
+ bottomSlot = {
+ if (state.playbackState == PlaybackState.PLAYING) {
+ iconEdgeButton(
+ onClick = clickable(id = PlayerAction.PAUSE.name),
+ iconContent = {
+ icon(protoLayoutResourceId = ID_PLAYINGICON)
+ }
+ )
+ } else {
+ textEdgeButton(
+ onClick = clickable(id = PlayerAction.PLAY.name),
+ labelContent = {
+ text(context.getString(R.string.action_play).layoutString)
+ }
+ )
+ }
+ }
+ )
+ }
+ }
+
+@WearPreviewDevices
+private fun NowPlayingPausedTilePreview(context: Context): TilePreviewData {
+ val state = MediaPlayerTileState(
+ connectionStatus = WearConnectionStatus.CONNECTED,
+ title = "Title",
+ artist = "Artist",
+ playbackState = PlaybackState.PAUSED,
+ audioStreamState = AudioStreamState(3, 0, 5, AudioStreamType.MUSIC),
+ positionState = PositionState(100, 50),
+ artwork = runBlocking {
+ ContextCompat.getDrawable(context, R.drawable.sample_image)?.toBitmapOrNull()
+ ?.toByteArray()
+ },
+ appIcon = runBlocking {
+ ContextCompat.getDrawable(context, R.drawable.ic_play_circle_simpleblue)
+ ?.toBitmapOrNull()
+ ?.toByteArray()
+ }
+ )
+
+ return tileRendererPreviewData(
+ renderer = NowPlayingTileRenderer(context, debugResourceMode = true),
+ tileState = state,
+ resourceState = state
+ )
+}
+
+@WearPreviewDevices
+private fun NowPlayingTilePreview(context: Context): TilePreviewData {
+ val state = MediaPlayerTileState(
+ connectionStatus = WearConnectionStatus.CONNECTED,
+ title = "Title",
+ artist = "Artist",
+ playbackState = PlaybackState.PLAYING,
+ audioStreamState = AudioStreamState(3, 0, 5, AudioStreamType.MUSIC),
+ positionState = PositionState(100, 50),
+ artwork = runBlocking {
+ ContextCompat.getDrawable(context, R.drawable.sample_image)?.toBitmapOrNull()
+ ?.toByteArray()
+ },
+ appIcon = runBlocking {
+ ContextCompat.getDrawable(context, R.drawable.ic_play_circle_simpleblue)
+ ?.toBitmapOrNull()
+ ?.toByteArray()
+ }
+ )
+
+ return tileRendererPreviewData(
+ renderer = NowPlayingTileRenderer(context, debugResourceMode = true),
+ tileState = state,
+ resourceState = state
+ )
+}
+
+@WearPreviewDevices
+private fun NowPlayingEmptyTilePreview(context: Context): TilePreviewData {
+ val state = MediaPlayerTileState(
+ connectionStatus = WearConnectionStatus.DISCONNECTED,
+ title = null,
+ artist = null,
+ playbackState = null,
+ audioStreamState = null,
+ artwork = null
+ )
+
+ return tileRendererPreviewData(
+ renderer = NowPlayingTileRenderer(context, debugResourceMode = true),
+ tileState = state,
+ resourceState = state
+ )
+}
+
+@WearPreviewDevices
+private fun NotPlayingTilePreview(context: Context): TilePreviewData {
+ val state = MediaPlayerTileState(
+ connectionStatus = WearConnectionStatus.CONNECTED,
+ title = null,
+ artist = null,
+ playbackState = PlaybackState.NONE,
+ audioStreamState = AudioStreamState(3, 0, 5, AudioStreamType.MUSIC),
+ artwork = null,
+ appIcon = runBlocking {
+ ContextCompat.getDrawable(context, R.drawable.ic_play_circle_simpleblue)
+ ?.toBitmapOrNull()
+ ?.toByteArray()
+ }
+ )
+
+ return tileRendererPreviewData(
+ renderer = NowPlayingTileRenderer(context, debugResourceMode = true),
+ tileState = state,
+ resourceState = state,
+ )
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/ProtoLayoutVersionUtils.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/ProtoLayoutVersionUtils.kt
deleted file mode 100644
index bbd16fc3..00000000
--- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/ProtoLayoutVersionUtils.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.thewizrd.simplewear.wearable.tiles.layouts
-
-import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters
-import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
-
-fun DeviceParameters.supportsTransformation(): Boolean {
- // @RequiresSchemaVersion(major = 1, minor = 400)
- val supportedVersion = VersionInfo.Builder()
- .setMajor(1).setMinor(400)
- .build()
-
- return this.rendererSchemaVersion >= supportedVersion
-}
-
-fun DeviceParameters.supportsDynamicValue(): Boolean {
- // @RequiresSchemaVersion(major = 1, minor = 200)
- val supportedVersion = VersionInfo.Builder()
- .setMajor(1).setMinor(200)
- .build()
-
- return this.rendererSchemaVersion >= supportedVersion
-}
\ No newline at end of file
diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/TileUtils.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/TileUtils.kt
new file mode 100644
index 00000000..588b9024
--- /dev/null
+++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/TileUtils.kt
@@ -0,0 +1,56 @@
+package com.thewizrd.simplewear.wearable.tiles.layouts
+
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
+import kotlin.math.max
+
+fun DeviceParameters.supportsTransformation(): Boolean {
+ // @RequiresSchemaVersion(major = 1, minor = 400)
+ val supportedVersion = VersionInfo.Builder()
+ .setMajor(1).setMinor(400)
+ .build()
+
+ return this.rendererSchemaVersion >= supportedVersion
+}
+
+fun DeviceParameters.supportsDynamicValue(): Boolean {
+ // @RequiresSchemaVersion(major = 1, minor = 200)
+ val supportedVersion = VersionInfo.Builder()
+ .setMajor(1).setMinor(200)
+ .build()
+
+ return this.rendererSchemaVersion >= supportedVersion
+}
+
+fun DeviceParameters.squareNotSupported(): Boolean {
+ // @RequiresSchemaVersion(major = 1, minor = 400)
+ val supportedVersion = VersionInfo.Builder()
+ .setMajor(1).setMinor(400)
+ .build()
+
+ return this.rendererSchemaVersion >= supportedVersion
+}
+
+fun DeviceParameters.isSmallWatch(): Boolean {
+ return max(screenHeightDp, screenWidthDp) < 225
+}
+
+fun DeviceParameters.isLargeWatch(): Boolean {
+ return max(screenHeightDp, screenWidthDp) >= 225
+}
+
+fun DeviceParameters.isSmallHeight(): Boolean {
+ return screenHeightDp < 225
+}
+
+fun DeviceParameters.isLargeHeight(): Boolean {
+ return screenHeightDp >= 225
+}
+
+fun DeviceParameters.isSmallWidth(): Boolean {
+ return screenWidthDp < 225
+}
+
+fun DeviceParameters.isLargeWidth(): Boolean {
+ return screenWidthDp >= 225
+}
\ No newline at end of file
diff --git a/wear/src/main/res/color/button_background_checkable.xml b/wear/src/main/res/color/button_background_checkable.xml
deleted file mode 100644
index 7d21ddce..00000000
--- a/wear/src/main/res/color/button_background_checkable.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/color/wear_chip_primary_text_color.xml b/wear/src/main/res/color/wear_chip_primary_text_color.xml
deleted file mode 100644
index a90edd6c..00000000
--- a/wear/src/main/res/color/wear_chip_primary_text_color.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/color/wear_chip_seconary_text_color.xml b/wear/src/main/res/color/wear_chip_seconary_text_color.xml
deleted file mode 100644
index 42578d63..00000000
--- a/wear/src/main/res/color/wear_chip_seconary_text_color.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/drawable-hdpi/ws_full_sad.png b/wear/src/main/res/drawable-hdpi/ws_full_sad.png
deleted file mode 100644
index 5b6eeeeb..00000000
Binary files a/wear/src/main/res/drawable-hdpi/ws_full_sad.png and /dev/null differ
diff --git a/wear/src/main/res/drawable-mdpi/ws_full_sad.png b/wear/src/main/res/drawable-mdpi/ws_full_sad.png
deleted file mode 100644
index 27777bfc..00000000
Binary files a/wear/src/main/res/drawable-mdpi/ws_full_sad.png and /dev/null differ
diff --git a/wear/src/main/res/drawable-nodpi/mediatile_preview.png b/wear/src/main/res/drawable-nodpi/mediatile_preview.png
index ca884946..7a0e08b0 100644
Binary files a/wear/src/main/res/drawable-nodpi/mediatile_preview.png and b/wear/src/main/res/drawable-nodpi/mediatile_preview.png differ
diff --git a/wear/src/main/res/drawable-nodpi/nowplaying_preview.png b/wear/src/main/res/drawable-nodpi/nowplaying_preview.png
new file mode 100644
index 00000000..f02dc928
Binary files /dev/null and b/wear/src/main/res/drawable-nodpi/nowplaying_preview.png differ
diff --git a/wear/src/main/res/drawable-nodpi/tile_preview.png b/wear/src/main/res/drawable-nodpi/tile_preview.png
index 8f1e169b..34e1ff78 100644
Binary files a/wear/src/main/res/drawable-nodpi/tile_preview.png and b/wear/src/main/res/drawable-nodpi/tile_preview.png differ
diff --git a/wear/src/main/res/drawable-round-nodpi/mediatile_preview.png b/wear/src/main/res/drawable-round-nodpi/mediatile_preview.png
index 49b0acb0..0f595766 100644
Binary files a/wear/src/main/res/drawable-round-nodpi/mediatile_preview.png and b/wear/src/main/res/drawable-round-nodpi/mediatile_preview.png differ
diff --git a/wear/src/main/res/drawable-round-nodpi/nowplaying_preview.png b/wear/src/main/res/drawable-round-nodpi/nowplaying_preview.png
new file mode 100644
index 00000000..f3b9a0c3
Binary files /dev/null and b/wear/src/main/res/drawable-round-nodpi/nowplaying_preview.png differ
diff --git a/wear/src/main/res/drawable-round-nodpi/tile_preview.png b/wear/src/main/res/drawable-round-nodpi/tile_preview.png
index ad496d8a..29eac39d 100644
Binary files a/wear/src/main/res/drawable-round-nodpi/tile_preview.png and b/wear/src/main/res/drawable-round-nodpi/tile_preview.png differ
diff --git a/wear/src/main/res/drawable-xhdpi/ws_full_sad.png b/wear/src/main/res/drawable-xhdpi/ws_full_sad.png
deleted file mode 100644
index 5cac3062..00000000
Binary files a/wear/src/main/res/drawable-xhdpi/ws_full_sad.png and /dev/null differ
diff --git a/wear/src/main/res/drawable-xxhdpi/ws_full_sad.png b/wear/src/main/res/drawable-xxhdpi/ws_full_sad.png
deleted file mode 100644
index 734c0eda..00000000
Binary files a/wear/src/main/res/drawable-xxhdpi/ws_full_sad.png and /dev/null differ
diff --git a/wear/src/main/res/drawable-xxxhdpi/ws_full_sad.png b/wear/src/main/res/drawable-xxxhdpi/ws_full_sad.png
deleted file mode 100644
index 9b3b2081..00000000
Binary files a/wear/src/main/res/drawable-xxxhdpi/ws_full_sad.png and /dev/null differ
diff --git a/wear/src/main/res/drawable/button_background_accent.xml b/wear/src/main/res/drawable/button_background_accent.xml
deleted file mode 100644
index bc7c3d5e..00000000
--- a/wear/src/main/res/drawable/button_background_accent.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/wear/src/main/res/drawable/button_background_accent_onsurface.xml b/wear/src/main/res/drawable/button_background_accent_onsurface.xml
deleted file mode 100644
index c4de6b6f..00000000
--- a/wear/src/main/res/drawable/button_background_accent_onsurface.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/wear/src/main/res/drawable/button_background_gradient_end.xml b/wear/src/main/res/drawable/button_background_gradient_end.xml
deleted file mode 100644
index 6b2c0c7f..00000000
--- a/wear/src/main/res/drawable/button_background_gradient_end.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/drawable/button_background_gradient_end_rtl.xml b/wear/src/main/res/drawable/button_background_gradient_end_rtl.xml
deleted file mode 100644
index 86e94e01..00000000
--- a/wear/src/main/res/drawable/button_background_gradient_end_rtl.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/drawable/button_background_gradient_start.xml b/wear/src/main/res/drawable/button_background_gradient_start.xml
deleted file mode 100644
index 955888f3..00000000
--- a/wear/src/main/res/drawable/button_background_gradient_start.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/drawable/button_background_gradient_start_rtl.xml b/wear/src/main/res/drawable/button_background_gradient_start_rtl.xml
deleted file mode 100644
index 551015a9..00000000
--- a/wear/src/main/res/drawable/button_background_gradient_start_rtl.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/drawable/button_background_onsurface.xml b/wear/src/main/res/drawable/button_background_onsurface.xml
deleted file mode 100644
index 5f0e70dc..00000000
--- a/wear/src/main/res/drawable/button_background_onsurface.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/drawable/ic_phone_simpleblue.xml b/wear/src/main/res/drawable/ic_phone_simpleblue.xml
index e3caa7b7..c9432ec1 100644
--- a/wear/src/main/res/drawable/ic_phone_simpleblue.xml
+++ b/wear/src/main/res/drawable/ic_phone_simpleblue.xml
@@ -1,19 +1,13 @@
-
-
- -
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
diff --git a/wear/src/main/res/drawable/ic_wear_checkbox_off.xml b/wear/src/main/res/drawable/ic_wear_checkbox_off.xml
deleted file mode 100644
index 32026c5b..00000000
--- a/wear/src/main/res/drawable/ic_wear_checkbox_off.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/wear/src/main/res/drawable/ic_wear_checkbox_on.xml b/wear/src/main/res/drawable/ic_wear_checkbox_on.xml
deleted file mode 100644
index 3267018f..00000000
--- a/wear/src/main/res/drawable/ic_wear_checkbox_on.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/wear/src/main/res/drawable/ic_wear_radio_off.xml b/wear/src/main/res/drawable/ic_wear_radio_off.xml
deleted file mode 100644
index d7ae129a..00000000
--- a/wear/src/main/res/drawable/ic_wear_radio_off.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/wear/src/main/res/drawable/ic_wear_radio_on.xml b/wear/src/main/res/drawable/ic_wear_radio_on.xml
deleted file mode 100644
index fd1a56d6..00000000
--- a/wear/src/main/res/drawable/ic_wear_radio_on.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/wear/src/main/res/drawable/ic_wear_switch_off.xml b/wear/src/main/res/drawable/ic_wear_switch_off.xml
deleted file mode 100644
index b6e47214..00000000
--- a/wear/src/main/res/drawable/ic_wear_switch_off.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
diff --git a/wear/src/main/res/drawable/ic_wear_switch_on.xml b/wear/src/main/res/drawable/ic_wear_switch_on.xml
deleted file mode 100644
index adbd464d..00000000
--- a/wear/src/main/res/drawable/ic_wear_switch_on.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
diff --git a/wear/src/main/res/drawable/playpause_button.xml b/wear/src/main/res/drawable/playpause_button.xml
deleted file mode 100644
index 93e2b5fe..00000000
--- a/wear/src/main/res/drawable/playpause_button.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/drawable/playpause_button_ambient.xml b/wear/src/main/res/drawable/playpause_button_ambient.xml
deleted file mode 100644
index 830b87d4..00000000
--- a/wear/src/main/res/drawable/playpause_button_ambient.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/drawable/ring_progress.xml b/wear/src/main/res/drawable/ring_progress.xml
deleted file mode 100644
index 6167e6f3..00000000
--- a/wear/src/main/res/drawable/ring_progress.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/drawable/wear_checkbox_icon.xml b/wear/src/main/res/drawable/wear_checkbox_icon.xml
deleted file mode 100644
index 8c82672f..00000000
--- a/wear/src/main/res/drawable/wear_checkbox_icon.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/drawable/wear_radio_icon.xml b/wear/src/main/res/drawable/wear_radio_icon.xml
deleted file mode 100644
index 420dcbe1..00000000
--- a/wear/src/main/res/drawable/wear_radio_icon.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/drawable/wear_switch_icon.xml b/wear/src/main/res/drawable/wear_switch_icon.xml
deleted file mode 100644
index 8b30fab5..00000000
--- a/wear/src/main/res/drawable/wear_switch_icon.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/layout-round/accept_deny_dialog.xml b/wear/src/main/res/layout-round/accept_deny_dialog.xml
deleted file mode 100644
index 02e0ba4b..00000000
--- a/wear/src/main/res/layout-round/accept_deny_dialog.xml
+++ /dev/null
@@ -1,157 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/wear/src/main/res/layout/accept_deny_dialog.xml b/wear/src/main/res/layout/accept_deny_dialog.xml
deleted file mode 100644
index d96d2d8c..00000000
--- a/wear/src/main/res/layout/accept_deny_dialog.xml
+++ /dev/null
@@ -1,158 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/wear/src/main/res/layout/app_item.xml b/wear/src/main/res/layout/app_item.xml
deleted file mode 100644
index 63452ebd..00000000
--- a/wear/src/main/res/layout/app_item.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
diff --git a/wear/src/main/res/layout/dialog_addaction.xml b/wear/src/main/res/layout/dialog_addaction.xml
deleted file mode 100644
index d911db67..00000000
--- a/wear/src/main/res/layout/dialog_addaction.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/layout/layout_dash_add_button.xml b/wear/src/main/res/layout/layout_dash_add_button.xml
deleted file mode 100644
index ae95ecc9..00000000
--- a/wear/src/main/res/layout/layout_dash_add_button.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/layout/layout_dash_button.xml b/wear/src/main/res/layout/layout_dash_button.xml
deleted file mode 100644
index cfdec29f..00000000
--- a/wear/src/main/res/layout/layout_dash_button.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/layout/layout_dashboard_config.xml b/wear/src/main/res/layout/layout_dashboard_config.xml
deleted file mode 100644
index 84e1578d..00000000
--- a/wear/src/main/res/layout/layout_dashboard_config.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/layout/layout_tile_dashboard_config.xml b/wear/src/main/res/layout/layout_tile_dashboard_config.xml
deleted file mode 100644
index 5e2f9643..00000000
--- a/wear/src/main/res/layout/layout_tile_dashboard_config.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/layout/tile_layout_dashboard.xml b/wear/src/main/res/layout/tile_layout_dashboard.xml
index d81c0c8e..86bf8bcc 100644
--- a/wear/src/main/res/layout/tile_layout_dashboard.xml
+++ b/wear/src/main/res/layout/tile_layout_dashboard.xml
@@ -131,7 +131,7 @@
android:foreground="?android:selectableItemBackgroundBorderless"
android:padding="@dimen/tile_action_button_padding"
android:scaleType="fitCenter"
- tools:src="@drawable/ic_lock_outline_white_24dp" />
+ tools:src="@drawable/ic_lock_white_24dp" />
diff --git a/wear/src/main/res/layout/wear_chip_button_layout.xml b/wear/src/main/res/layout/wear_chip_button_layout.xml
deleted file mode 100644
index 83013c92..00000000
--- a/wear/src/main/res/layout/wear_chip_button_layout.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/layout/ws_customoverlay_confirmation.xml b/wear/src/main/res/layout/ws_customoverlay_confirmation.xml
deleted file mode 100644
index 77b0535d..00000000
--- a/wear/src/main/res/layout/ws_customoverlay_confirmation.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/wear/src/main/res/values-de/strings.xml b/wear/src/main/res/values-de/strings.xml
new file mode 100644
index 00000000..95deb644
--- /dev/null
+++ b/wear/src/main/res/values-de/strings.xml
@@ -0,0 +1,89 @@
+
+
+ "Auf dem Telefon geöffnet…"
+
+
+ "Gerät nicht verbunden"
+ "Verbinden…"
+ "Verbunden"
+ "Verbundenes Gerät wird nicht unterstützt"
+ "Gerät ist nicht verbunden oder die App ist nicht auf dem Gerät installiert..."
+
+
+ "Verbindungsstatus wird abgerufen…"
+ "App auf dem Gerät installieren und erneut versuchen"
+ "Daten konnten nicht synchronisiert werden"
+
+
+ "Dashboard"
+ "Aufladen"
+ "Entladen"
+ "Unbekannt"
+ "Aktionen"
+ "Synchronisieren…"
+ "Kameraberechtigung deaktiviert. Bitte überprüfen Sie die Berechtigung in der Telefon-App"
+ "Keine Antwort. Bitte überprüfen Sie die Verbindung"
+ "Aktion konnte nicht ausgeführt werden"
+ "Aktion konnte nicht ausgeführt werden. Bitte überprüfen Sie die Berechtigung in der Telefon-App"
+ "Aktionslayout"
+ "Raster"
+ "Liste"
+ "Mediensteuerung starten"
+ "Mediensteuerung automatisch starten"
+ "Medienwiedergabe fehlgeschlagen. Bitte versuche es erneut."
+ "Keine Media Player gefunden"
+ "Medien auf dem Telefon abspielen"
+ "SleepTimer-App nicht installiert.
+Bitte installieren, um fortzufahren."
+ "Apps filtern"
+ "Alle löschen"
+ "Wiedergabe gestoppt"
+ "Wiedergabe fehlgeschlagen. Bitte überprüfe die Medienwarteschlange"
+ "Zurück"
+ "Bitte aktiviere Bluetooth für eine zuverlässige Verbindung"
+ "Kein Anruf aktiv"
+ "Apps konnten nicht abgerufen werden"
+ "App-Symbole laden"
+ "Controller starten"
+ "Aktuelle Wiedergabe"
+ "Kachel konfigurieren"
+ "Auf Standardwerte zurücksetzen?"
+ "Kachel-Editor"
+ "Dashboard konfigurieren"
+ "Dashboard-Editor"
+ "Mediensteuerung zum Launcher hinzufügen"
+ "Telefon-Akkustatus"
+ "Akkuzustand"
+ "Lädt…"
+ "Aktualisieren"
+ "Wiederholen"
+ "Abspielen"
+ "Akkuzustand hinzufügen"
+ "Akkuzustand entfernen"
+ "Bedienungshilfendienst deaktiviert"
+
+
+ "Neue Aktion planen"
+ "Aktion bestätigen"
+ "Aktion bearbeiten"
+ "Anfangszustand festlegen"
+ "Anfangszustand"
+ "Geplanter Zustand"
+
+
+ "Stumm"
+ "Tastatur"
+ "Lautsprecher Ein"
+ "Lautsprecher Aus"
+ "Kontaktfoto"
+ "Gerätezustand"
+ "Pfeil nach oben"
+ "Pfeil nach unten"
+ "Pfeil nach links"
+ "Pfeil nach rechts"
+ "DPad Mitte"
+ "Player-Liste öffnen"
+ "Cover"
+ "Aktion hinzufügen"
+ "Berechtigung für Aktion verweigert. Bitte überprüfe die Berechtigung in der Telefon-App"
+
\ No newline at end of file
diff --git a/wear/src/main/res/values-es/strings.xml b/wear/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..84d46fa6
--- /dev/null
+++ b/wear/src/main/res/values-es/strings.xml
@@ -0,0 +1,89 @@
+
+
+ "Abierto en el teléfono…"
+
+
+ "Dispositivo no conectado"
+ "Conectando…"
+ "Conectado"
+ "El dispositivo conectado no es compatible"
+ "El dispositivo no está conectado o la aplicación no está instalada en el dispositivo..."
+
+
+ "Obteniendo el estado de la conexión…"
+ "Instale la aplicación en el dispositivo y vuelva a intentarlo"
+ "No se pueden sincronizar los datos"
+
+
+ "Panel de control"
+ "Cargando"
+ "Descargando"
+ "Desconocido"
+ "Acciones"
+ "Sincronizando…"
+ "Permiso de la cámara desactivado. Por favor, compruebe el permiso en la aplicación del teléfono"
+ "Sin respuesta. Por favor, compruebe la conexión"
+ "Error al realizar la acción"
+ "Error al realizar la acción. Por favor, compruebe el permiso en la aplicación del teléfono"
+ "Diseño de acciones"
+ "Cuadrícula"
+ "Lista"
+ "Iniciar Controles Multimedia"
+ "Iniciar automáticamente los controles multimedia"
+ "Error al reproducir el contenido. Inténtalo de nuevo."
+ "No se encontraron reproductores multimedia"
+ "Reproducir contenido en el teléfono"
+ "La aplicación SleepTimer no está instalada.
+Instálala para continuar."
+ "Filtrar aplicaciones"
+ "Borrar todo"
+ "Reproducción detenida"
+ "Error al reproducir. Comprueba la cola de reproducción"
+ "Atrás"
+ "Habilita Bluetooth para una conexión fiable"
+ "No hay ninguna llamada activa"
+ "No se pueden recuperar las aplicaciones"
+ "Cargar iconos de aplicaciones"
+ "Iniciar controlador"
+ "Reproduciendo ahora"
+ "Configurar mosaico"
+ "¿Restablecer a los valores predeterminados?"
+ "Editor de mosaicos"
+ "Configurar panel de control"
+ "Editor de panel de control"
+ "Añadir controlador multimedia al iniciador"
+ "Estado de la batería del teléfono"
+ "Estado de la batería"
+ "Cargando…"
+ "Actualizar"
+ "Reintentar"
+ "Reproducir"
+ "Añadir estado de la batería"
+ "Eliminar estado de la batería"
+ "Servicio de accesibilidad desactivado"
+
+
+ "Programar nueva acción"
+ "Confirmar acción"
+ "Editar acción"
+ "Establecer estado inicial"
+ "Estado inicial"
+ "Estado programado"
+
+
+ "Silenciar"
+ "Teclado"
+ "Altavoz activado"
+ "Altavoz Apagado"
+ "Foto de Contacto"
+ "Estado del Dispositivo"
+ "Flecha Arriba"
+ "Flecha Abajo"
+ "Flecha Izquierda"
+ "Flecha Derecha"
+ "Centro del D-pad"
+ "Abrir Lista de Reproductor"
+ "Ilustración"
+ "Añadir Acción"
+ "Permiso denegado para la acción. Por favor, compruebe el permiso en la aplicación del teléfono"
+
\ No newline at end of file
diff --git a/wear/src/main/res/values-fr/strings.xml b/wear/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..a0440699
--- /dev/null
+++ b/wear/src/main/res/values-fr/strings.xml
@@ -0,0 +1,89 @@
+
+
+ "Ouvert sur le téléphone…"
+
+
+ "Appareil non connecté"
+ "Connexion…"
+ "Connecté"
+ "L'appareil connecté n'est pas pris en charge"
+ "L'appareil n'est pas connecté ou l'application n'est pas installée sur l'appareil..."
+
+
+ "Obtention de l'état de la connexion…"
+ "Installer l'application sur l'appareil et réessayer"
+ "Impossible de synchroniser les données"
+
+
+ "Tableau de bord"
+ "En charge"
+ "Décharge"
+ "Inconnu"
+ "Actions"
+ "Synchronisation…"
+ "Autorisation d'accès à la caméra désactivée. Veuillez vérifier l'autorisation sur l'application téléphonique"
+ "Aucune réponse. Veuillez vérifier la connexion"
+ "Échec de l'exécution de l'action"
+ "Échec de l'exécution de l'action. Veuillez vérifier l'autorisation sur l'application téléphonique"
+ "Disposition des actions"
+ "Grille"
+ "Liste"
+ "Lancer les contrôles multimédias"
+ "Lancer automatiquement les contrôles multimédias"
+ "Impossible de lire le contenu multimédia. Veuillez réessayer."
+ "Impossible de trouver un lecteur multimédia"
+ "Lire le contenu multimédia sur le téléphone"
+ "L'application SleepTimer n'est pas installée.
+Veuillez l'installer pour continuer."
+ "Filtrer les applications"
+ "Tout effacer"
+ "Lecture arrêtée"
+ "Impossible de lire. Veuillez vérifier la file d'attente multimédia"
+ "Retour"
+ "Veuillez activer Bluetooth pour une connexion fiable"
+ "Aucun appel actif"
+ "Impossible de récupérer les applications"
+ "Charger les icônes d'application"
+ "Lancer le contrôleur"
+ "Lecture en cours"
+ "Configurer la vignette"
+ "Restaurer les paramètres par défaut ?"
+ "Éditeur de vignette"
+ "Configurer le tableau de bord"
+ "Éditeur de tableau de bord"
+ "Ajouter un contrôleur multimédia au lanceur"
+ "État de la batterie du téléphone"
+ "État de la batterie"
+ "Chargement…"
+ "Actualiser"
+ "Réessayer"
+ "Lecture"
+ "Ajouter l'état de la batterie"
+ "Supprimer l'état de la batterie"
+ "Service d'accessibilité désactivé"
+
+
+ "Planifier une nouvelle action"
+ "Confirmer l'action"
+ "Modifier l'action"
+ "Définir l'état initial"
+ "État initial"
+ "État planifié"
+
+
+ "Muet"
+ "Clavier"
+ "Haut-parleur activé"
+ "Haut-parleur désactivé"
+ "Photo du contact"
+ "État de l'appareil"
+ "Flèche vers le haut"
+ "Flèche vers le bas"
+ "Flèche vers la gauche"
+ "Flèche vers la droite"
+ "Centre du pavé directionnel"
+ "Ouvrir la liste des lecteurs"
+ "Illustration"
+ "Ajouter une action"
+ "Autorisation refusée pour l'action. Veuillez vérifier l'autorisation sur l'application du téléphone"
+
\ No newline at end of file
diff --git a/wear/src/main/res/values/actionbutton_styles.xml b/wear/src/main/res/values/actionbutton_styles.xml
deleted file mode 100644
index 051d37f6..00000000
--- a/wear/src/main/res/values/actionbutton_styles.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/values/attrs.xml b/wear/src/main/res/values/attrs.xml
deleted file mode 100644
index 583cdd48..00000000
--- a/wear/src/main/res/values/attrs.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/wear/src/main/res/values/dimens.xml b/wear/src/main/res/values/dimens.xml
index 9e63e810..4ded0e34 100644
--- a/wear/src/main/res/values/dimens.xml
+++ b/wear/src/main/res/values/dimens.xml
@@ -1,5 +1,5 @@
-
+
8dp
32dp
@@ -16,10 +16,6 @@
135
45
- 48dp
- 48dp
- 48dp
-
8dp
diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml
index 714b3f7c..b49a276f 100644
--- a/wear/src/main/res/values/strings.xml
+++ b/wear/src/main/res/values/strings.xml
@@ -7,6 +7,9 @@
Connecting…
Connected
+ Connected device is not supported
+ Device is not connected or app is not installed on device...
+
Getting connection status…
Install app on device and try again
@@ -21,7 +24,7 @@
Syncing…
Camera permission disabled. Please check permission on phone app
- Permission denied for action. Please check permission on phone app
+ Permission denied for action. Please check permission on phone app
No response. Please check connection
Failed to perform action
Failed to perform action. Please check permission on phone app
diff --git a/wear/src/main/res/values/styles.xml b/wear/src/main/res/values/styles.xml
index 1af9d000..43f8a10b 100644
--- a/wear/src/main/res/values/styles.xml
+++ b/wear/src/main/res/values/styles.xml
@@ -25,9 +25,6 @@
-
?android:selectableItemBackgroundBorderless
-
- - @style/Widget.Wear.WearChipButton
- - @style/Widget.Wear.ActionButton
-
-
\ No newline at end of file
diff --git a/wearsettings/build.gradle b/wearsettings/build.gradle
index b2213084..7b68b515 100644
--- a/wearsettings/build.gradle
+++ b/wearsettings/build.gradle
@@ -1,20 +1,20 @@
plugins {
id('com.android.application')
id('kotlin-android')
- id('dev.rikka.tools.refine') version "$refine_version"
+ id('dev.rikka.tools.refine') version libs.versions.refine.version
}
android {
- compileSdk rootProject.compileSdkVersion
+ compileSdk = libs.versions.compileSdkVersion.get().toInteger()
defaultConfig {
applicationId "com.thewizrd.wearsettings"
- minSdk rootProject.minSdkVersion
+ minSdk libs.versions.minSdkVersion.get().toInteger()
//noinspection ExpiredTargetSdkVersion
targetSdk 28
// NOTE: update SUPPORTED_VERSION_CODE if needed
- versionCode 1030002
- versionName "1.3.1"
+ versionCode 1040000
+ versionName "1.4.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -24,13 +24,13 @@ android {
applicationIdSuffix ".debug"
debuggable true
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
shrinkResources true
crunchPngs true
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
@@ -55,36 +55,37 @@ android {
dependencies {
implementation project(":shared_resources")
+ implementation project(":common")
compileOnly project(':hidden-api')
- coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_version"
+ coreLibraryDesugaring libs.desugar.jdk.libs
// Unit Testing
- androidTestImplementation "androidx.test:core:$test_core_version"
+ androidTestImplementation libs.test.core
// AndroidJUnitRunner and JUnit Rules
- androidTestImplementation "androidx.test:runner:$test_runner_version"
- androidTestImplementation "androidx.test:rules:$test_rules_version"
+ androidTestImplementation libs.test.runner
+ androidTestImplementation libs.test.rules
// Assertions
- androidTestImplementation "androidx.test.ext:junit:$junit_version"
- androidTestImplementation "androidx.test.ext:truth:$androidx_truth_version"
- androidTestImplementation "com.google.truth:truth:$google_truth_version"
+ androidTestImplementation libs.androidx.junit
+ androidTestImplementation libs.androidx.truth
+ androidTestImplementation libs.google.truth
- implementation "androidx.core:core-ktx:$core_version"
- implementation "androidx.appcompat:appcompat:$appcompat_version"
- implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version"
+ implementation libs.core.ktx
+ implementation libs.appcompat
+ implementation libs.constraintlayout
- implementation "com.google.android.material:material:$material_version"
+ implementation libs.material
- implementation "com.jakewharton.timber:timber:$timber_version"
+ implementation libs.timber
// Root
- implementation 'com.github.topjohnwu.libsu:core:3.1.2'
+ implementation libs.libsu.core
// Shizuku
- implementation "dev.rikka.shizuku:api:$shizuku_version"
- implementation "dev.rikka.shizuku:provider:$shizuku_version"
- implementation "dev.rikka.tools.refine:runtime:$refine_version"
- implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:6.1'
+ implementation libs.shizuki.api
+ implementation libs.shizuki.provider
+ implementation libs.refine.runtime
+ implementation libs.hiddenapibypass
}
\ No newline at end of file
diff --git a/wearsettings/src/main/AndroidManifest.xml b/wearsettings/src/main/AndroidManifest.xml
index c1db0ab9..d6d06446 100644
--- a/wearsettings/src/main/AndroidManifest.xml
+++ b/wearsettings/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
@@ -33,7 +34,8 @@
android:name=".App"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
- android:label="@string/app_name"
+ android:label="@string/app_name_settings"
+ android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:taskAffinity=""
diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/MainActivity.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/MainActivity.kt
index 19fd9fdf..90196999 100644
--- a/wearsettings/src/main/java/com/thewizrd/wearsettings/MainActivity.kt
+++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/MainActivity.kt
@@ -2,18 +2,27 @@ package com.thewizrd.wearsettings
import android.Manifest
import android.annotation.SuppressLint
+import android.app.NotificationManager
import android.content.ComponentName
+import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.content.res.Configuration.UI_MODE_NIGHT_MASK
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.graphics.Color
-import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
+import android.view.ViewGroup
+import androidx.activity.enableEdgeToEdge
+import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.PermissionChecker
+import androidx.core.graphics.ColorUtils
+import androidx.core.net.toUri
+import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -32,6 +41,11 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
companion object {
private const val BTCONNECT_REQCODE = 0
private const val SHIZUKU_REQCODE = 1
+
+ private const val CORNERS_FULL = 0
+ private const val CORNERS_TOP = 1
+ private const val CORNERS_CENTER = 2
+ private const val CORNERS_BOTTOM = 3
}
private lateinit var binding: ActivityMainBinding
@@ -40,6 +54,7 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
@SuppressLint("BatteryLife")
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
mPowerMgr = getSystemService(PowerManager::class.java)
@@ -47,12 +62,20 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
+ binding.scrollView.children.firstOrNull().let { root ->
+ val parent = root as ViewGroup
+
+ parent.viewTreeObserver.addOnGlobalLayoutListener {
+ updateRoundedBackground(parent)
+ }
+ }
+
binding.bgoptsPref.setOnClickListener {
if (!mPowerMgr.isIgnoringBatteryOptimizations(this.packageName)) {
runCatching {
startActivity(
Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
- data = Uri.parse("package:${packageName}")
+ data = "package:${packageName}".toUri()
}
)
}
@@ -67,7 +90,7 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
if (!checkSecureSettingsPermission(this)) {
// Show a dialog detailing how to set this permission
startActivity(Intent(Intent.ACTION_VIEW).apply {
- data = Uri.parse(getString(R.string.url_secure_settings_help))
+ data = getString(R.string.url_secure_settings_help).toUri()
})
}
}
@@ -80,7 +103,7 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
if (!RootHelper.isRootEnabled()) {
// Show dialog about root access
startActivity(Intent(Intent.ACTION_VIEW).apply {
- data = Uri.parse(getString(R.string.url_root_access_help))
+ data = getString(R.string.url_root_access_help).toUri()
})
} else {
SettingsHelper.setRootAccessEnabled(true)
@@ -111,6 +134,14 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
binding.btPref.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+ binding.dndPref.setOnClickListener {
+ if (!isNotificationAccessAllowed()) {
+ runCatching {
+ startActivity(Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS))
+ }
+ }
+ }
+
binding.shizukuPref.setOnClickListener {
runCatching {
val shizukuState = ShizukuUtils.getShizukuState(this)
@@ -137,7 +168,7 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
setAction(R.string.title_settings) {
runCatching {
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
- data = Uri.parse("package:${it.context.packageName}")
+ data = "package:${it.context.packageName}".toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}
@@ -180,6 +211,7 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
val rootEnabled = SettingsHelper.isRootAccessEnabled() && RootHelper.isRootEnabled()
val shizukuState = ShizukuUtils.getShizukuState(this@MainActivity)
updateBTPref(isBluetoothConnectPermGranted() || rootEnabled || shizukuState == ShizukuState.RUNNING)
+ updateDNDAccessText(isNotificationAccessAllowed() || rootEnabled || shizukuState == ShizukuState.RUNNING)
updateSecureSettingsPref(checkSecureSettingsPermission(this@MainActivity) || rootEnabled || shizukuState == ShizukuState.RUNNING)
updateRootAccessPref(rootEnabled)
updateShizukuPref(shizukuState)
@@ -197,14 +229,30 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
}
}
+ private fun isNotificationAccessAllowed(): Boolean {
+ val notMan =
+ applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ return notMan.isNotificationPolicyAccessGranted
+ }
+
private fun updateBgOptsPref(enabled: Boolean) {
binding.bgoptsPrefSummary.setText(if (enabled) R.string.message_bgopts_enabled else R.string.message_bgopts_disabled)
- binding.bgoptsPrefSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.bgoptsPrefSummary.setTextColor(
+ getTextColor(
+ binding.bgoptsPrefSummary.context,
+ enabled
+ )
+ )
}
private fun updateSecureSettingsPref(enabled: Boolean) {
binding.securesettingsPrefSummary.setText(if (enabled) R.string.message_securesettings_enabled else R.string.message_securesettings_disabled)
- binding.securesettingsPrefSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.securesettingsPrefSummary.setTextColor(
+ getTextColor(
+ binding.securesettingsPrefSummary.context,
+ enabled
+ )
+ )
}
private fun updateRootAccessPref(enabled: Boolean) {
@@ -218,7 +266,12 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
private fun updateBTPref(enabled: Boolean) {
binding.btPrefSummary.setText(if (enabled) R.string.permission_bt_enabled else R.string.permission_bt_disabled)
- binding.btPrefSummary.setTextColor(if (enabled) Color.GREEN else Color.RED)
+ binding.btPrefSummary.setTextColor(getTextColor(binding.btPrefSummary.context, enabled))
+ }
+
+ private fun updateDNDAccessText(enabled: Boolean) {
+ binding.dndSummary.setText(if (enabled) R.string.permission_dnd_enabled else R.string.permission_dnd_disabled)
+ binding.dndSummary.setTextColor(getTextColor(binding.dndSummary.context, enabled))
}
private fun updateShizukuPref(state: ShizukuState) {
@@ -230,7 +283,12 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
ShizukuState.RUNNING -> R.string.message_shizuku_running
}
)
- binding.shizukuPrefSummary.setTextColor(if (state == ShizukuState.RUNNING) Color.GREEN else Color.RED)
+ binding.shizukuPrefSummary.setTextColor(
+ getTextColor(
+ binding.shizukuPrefSummary.context,
+ state == ShizukuState.RUNNING
+ )
+ )
}
private fun isLauncherIconEnabled(): Boolean {
@@ -283,4 +341,51 @@ class MainActivity : AppCompatActivity(), Shizuku.OnRequestPermissionResultListe
Shizuku.removeRequestPermissionResultListener(this)
super.onDestroy()
}
+
+ @ColorInt
+ private fun getTextColor(context: Context, enabled: Boolean): Int {
+ return when (enabled) {
+ true -> if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) {
+ Color.GREEN
+ } else {
+ ColorUtils.blendARGB(Color.GREEN, Color.BLACK, 0.25f)
+ }
+
+ false -> if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) {
+ ColorUtils.blendARGB(Color.RED, Color.WHITE, 0.25f)
+ } else {
+ Color.RED
+ }
+ }
+ }
+
+ private fun updateRoundedBackground(parent: ViewGroup) {
+ val permissionsPreferences =
+ parent.children.filter { it.tag == "permissions" && it.isVisible }.toList()
+
+ permissionsPreferences.forEachIndexed { index, view ->
+ val cornerType = when {
+ permissionsPreferences.size <= 1 -> CORNERS_FULL
+ index == 0 -> CORNERS_TOP
+ index == permissionsPreferences.size - 1 -> CORNERS_BOTTOM
+ else -> CORNERS_CENTER
+ }
+
+ when (cornerType) {
+ CORNERS_FULL -> view.setBackgroundResource(
+ R.drawable.preference_round_background
+ )
+
+ CORNERS_TOP -> view.setBackgroundResource(
+ R.drawable.preference_round_background_top
+ )
+
+ CORNERS_BOTTOM -> view.setBackgroundResource(
+ R.drawable.preference_round_background_bottom
+ )
+
+ CORNERS_CENTER -> view.setBackgroundResource(R.drawable.preference_round_background_center)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/SettingsService.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/SettingsService.kt
index daf6288a..8f7dde47 100644
--- a/wearsettings/src/main/java/com/thewizrd/wearsettings/SettingsService.kt
+++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/SettingsService.kt
@@ -1,3 +1,5 @@
+@file:Suppress("DEPRECATION")
+
package com.thewizrd.wearsettings
import android.app.Activity.RESULT_CANCELED
@@ -6,7 +8,12 @@ import android.app.IntentService
import android.content.Intent
import android.os.Bundle
import android.os.ResultReceiver
-import com.thewizrd.shared_resources.actions.*
+import com.thewizrd.shared_resources.actions.Action
+import com.thewizrd.shared_resources.actions.ActionStatus
+import com.thewizrd.shared_resources.actions.EXTRA_ACTION_CALLINGPKG
+import com.thewizrd.shared_resources.actions.EXTRA_ACTION_DATA
+import com.thewizrd.shared_resources.actions.EXTRA_ACTION_ERROR
+import com.thewizrd.shared_resources.actions.RemoteAction
import com.thewizrd.shared_resources.utils.JSONParser
import com.thewizrd.shared_resources.wearsettings.PackageValidator
import com.thewizrd.wearsettings.actions.ActionHelper
@@ -14,11 +21,13 @@ import com.thewizrd.wearsettings.actions.ActionHelper
class SettingsService : IntentService("SettingsService") {
private lateinit var mPackageValidator: PackageValidator
+ @Deprecated("Deprecated in Java")
override fun onCreate() {
super.onCreate()
mPackageValidator = PackageValidator(this)
}
+ @Deprecated("Deprecated in Java")
override fun onHandleIntent(intent: Intent?) {
if (intent == null) return
diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/ActionHelper.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/ActionHelper.kt
index e25299c4..48e26d3c 100644
--- a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/ActionHelper.kt
+++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/ActionHelper.kt
@@ -42,6 +42,18 @@ object ActionHelper {
LockScreenAction.executeAction(context, action)
}
+ Actions.DONOTDISTURB -> {
+ DoNotDisturbAction.executeAction(context, action)
+ }
+
+ Actions.NFC -> {
+ NfcAction.executeAction(context, action)
+ }
+
+ Actions.BATTERYSAVER -> {
+ BatterySaverAction.executeAction(context, action)
+ }
+
else -> ActionStatus.FAILURE
}
}
diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/BatterySaverAction.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/BatterySaverAction.kt
new file mode 100644
index 00000000..97fd9f1f
--- /dev/null
+++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/BatterySaverAction.kt
@@ -0,0 +1,71 @@
+package com.thewizrd.wearsettings.actions
+
+import android.content.Context
+import android.os.Build
+import android.os.IPowerManager
+import android.provider.Settings
+import android.util.Log
+import com.thewizrd.shared_resources.actions.Action
+import com.thewizrd.shared_resources.actions.ActionStatus
+import com.thewizrd.shared_resources.actions.ToggleAction
+import com.thewizrd.shared_resources.utils.Logger
+import com.thewizrd.wearsettings.root.RootHelper
+import com.thewizrd.wearsettings.shizuku.ShizukuUtils
+import com.thewizrd.wearsettings.shizuku.grantSecureSettingsPermission
+import rikka.shizuku.ShizukuBinderWrapper
+import rikka.shizuku.SystemServiceHelper
+import com.thewizrd.wearsettings.Settings as SettingsHelper
+
+object BatterySaverAction {
+ fun executeAction(context: Context, action: Action): ActionStatus {
+ if (action is ToggleAction) {
+ return if (ShizukuUtils.isRunning(context)) {
+ context.grantSecureSettingsPermission()
+ setBatterySaverEnabledShizuku(context, action.isEnabled)
+ } else if (SettingsHelper.isRootAccessEnabled() && RootHelper.isRootEnabled()) {
+ setBatterySaverEnabledRoot(context, action.isEnabled)
+ } else if (checkSecureSettingsPermission(context)) {
+ setBatterySaverEnabled(context, action.isEnabled)
+ } else {
+ ActionStatus.REMOTE_PERMISSION_DENIED
+ }
+ }
+
+ return ActionStatus.UNKNOWN
+ }
+
+ private fun setBatterySaverEnabled(context: Context, enable: Boolean): ActionStatus {
+ val success = Settings.Global.putInt(
+ context.contentResolver, "low_power", if (enable) 1 else 0
+ )
+
+ return if (success) {
+ ActionStatus.SUCCESS
+ } else {
+ ActionStatus.REMOTE_FAILURE
+ }
+ }
+
+ private fun setBatterySaverEnabledRoot(context: Context, enable: Boolean): ActionStatus {
+ return GlobalSettingsAction.putSettingRoot("low_power", if (enable) "1" else "0")
+ }
+
+ private fun setBatterySaverEnabledShizuku(context: Context, enable: Boolean): ActionStatus {
+ return runCatching {
+ val powerMgr = SystemServiceHelper.getSystemService(Context.POWER_SERVICE)
+ .let(::ShizukuBinderWrapper)
+ .let(IPowerManager.Stub::asInterface)
+
+ val ret = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ powerMgr.setPowerSaveModeEnabled(enable)
+ } else {
+ powerMgr.setPowerSaveMode(enable)
+ }
+
+ if (ret) ActionStatus.SUCCESS else ActionStatus.REMOTE_FAILURE
+ }.getOrElse {
+ Logger.writeLine(Log.ERROR, it)
+ ActionStatus.REMOTE_FAILURE
+ }
+ }
+}
\ No newline at end of file
diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/DoNotDisturbAction.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/DoNotDisturbAction.kt
new file mode 100644
index 00000000..0cf9967a
--- /dev/null
+++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/DoNotDisturbAction.kt
@@ -0,0 +1,147 @@
+package com.thewizrd.wearsettings.actions
+
+import android.app.INotificationManager
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import com.thewizrd.shared_resources.actions.Action
+import com.thewizrd.shared_resources.actions.ActionStatus
+import com.thewizrd.shared_resources.actions.DNDChoice
+import com.thewizrd.shared_resources.actions.MultiChoiceAction
+import com.thewizrd.shared_resources.actions.ToggleAction
+import com.thewizrd.shared_resources.utils.Logger
+import com.thewizrd.wearsettings.root.RootHelper
+import com.thewizrd.wearsettings.shizuku.ShizukuUtils
+import com.topjohnwu.superuser.Shell
+import rikka.shizuku.ShizukuBinderWrapper
+import rikka.shizuku.SystemServiceHelper
+import com.thewizrd.wearsettings.Settings as SettingsHelper
+
+object DoNotDisturbAction {
+ fun executeAction(context: Context, action: Action): ActionStatus {
+ if (action is MultiChoiceAction) {
+ val state = DNDChoice.valueOf(action.choice)
+
+ return if (ShizukuUtils.isRunning(context)) {
+ setDNDStateShizuku(context, state)
+ } else if (SettingsHelper.isRootAccessEnabled() && RootHelper.isRootEnabled()) {
+ setDNDStateRoot(context, state)
+ } else if (isNotificationAccessAllowed(context)) {
+ setDNDState(context, state)
+ } else {
+ ActionStatus.REMOTE_PERMISSION_DENIED
+ }
+ } else if (action is ToggleAction) {
+ return if (ShizukuUtils.isRunning(context)) {
+ setDNDStateShizuku(context, action.isEnabled)
+ } else if (SettingsHelper.isRootAccessEnabled() && RootHelper.isRootEnabled()) {
+ setDNDStateRoot(context, action.isEnabled)
+ } else if (isNotificationAccessAllowed(context)) {
+ setDNDState(context, action.isEnabled)
+ } else {
+ ActionStatus.REMOTE_PERMISSION_DENIED
+ }
+ }
+
+ return ActionStatus.UNKNOWN
+ }
+
+ private fun isNotificationAccessAllowed(context: Context): Boolean {
+ val notMan =
+ context.applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ return notMan.isNotificationPolicyAccessGranted
+ }
+
+ private fun setDNDState(context: Context, enable: Boolean): ActionStatus {
+ return setDNDState(context, if (enable) DNDChoice.PRIORITY else DNDChoice.OFF)
+ }
+
+ private fun setDNDState(context: Context, state: DNDChoice): ActionStatus {
+ return try {
+ val notMan =
+ context.applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ when (state) {
+ DNDChoice.OFF -> notMan.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
+ DNDChoice.PRIORITY -> notMan.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
+ DNDChoice.ALARMS -> notMan.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALARMS)
+ DNDChoice.SILENCE -> notMan.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_NONE)
+ }
+ ActionStatus.SUCCESS
+ } catch (e: Exception) {
+ Logger.writeLine(Log.ERROR, e)
+ ActionStatus.REMOTE_FAILURE
+ }
+ }
+
+ private fun setDNDStateRoot(context: Context, enable: Boolean): ActionStatus {
+ return setDNDStateRoot(context, if (enable) DNDChoice.PRIORITY else DNDChoice.OFF)
+ }
+
+ private fun setDNDStateRoot(context: Context, state: DNDChoice): ActionStatus {
+ val dndValue = when (state) {
+ DNDChoice.OFF -> "off"
+ DNDChoice.PRIORITY -> "priority"
+ DNDChoice.ALARMS -> "alarms"
+ DNDChoice.SILENCE -> "on"
+ }
+
+ val result = Shell.su("cmd notification set_dnd $dndValue").exec()
+
+ return if (result.isSuccess) {
+ ActionStatus.SUCCESS
+ } else {
+ ActionStatus.REMOTE_FAILURE
+ }
+ }
+
+ private fun setDNDStateShizuku(context: Context, enable: Boolean): ActionStatus {
+ return setDNDStateShizuku(context, if (enable) DNDChoice.PRIORITY else DNDChoice.OFF)
+ }
+
+ private fun setDNDStateShizuku(context: Context, state: DNDChoice): ActionStatus {
+ val interruptionFilter = when (state) {
+ DNDChoice.OFF -> NotificationManager.INTERRUPTION_FILTER_ALL
+ DNDChoice.PRIORITY -> NotificationManager.INTERRUPTION_FILTER_PRIORITY
+ DNDChoice.ALARMS -> NotificationManager.INTERRUPTION_FILTER_ALARMS
+ DNDChoice.SILENCE -> NotificationManager.INTERRUPTION_FILTER_NONE
+ }
+
+ return runCatching {
+ val notificationMgr = SystemServiceHelper.getSystemService(Context.NOTIFICATION_SERVICE)
+ .let(::ShizukuBinderWrapper)
+ .let(INotificationManager.Stub::asInterface)
+
+ val ret = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ notificationMgr.setInterruptionFilter("com.android.shell", interruptionFilter, true)
+ true
+ } else if (Build.VERSION.SDK_INT_FULL >= (Build.VERSION_CODES_FULL.UPSIDE_DOWN_CAKE + 3)) {
+ runCatching {
+ notificationMgr.setInterruptionFilter("com.android.system", interruptionFilter)
+ }.onFailure {
+ if (it is NoSuchMethodError) {
+ // Android 14 - QPR3
+ notificationMgr.setInterruptionFilter(
+ "com.android.shell",
+ interruptionFilter,
+ true
+ )
+ } else {
+ throw it
+ }
+ }
+ true
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ notificationMgr.setInterruptionFilter("com.android.system", interruptionFilter)
+ true
+ } else {
+ false
+ }
+
+ if (ret) ActionStatus.SUCCESS else ActionStatus.REMOTE_FAILURE
+ }.getOrElse {
+ Logger.writeLine(Log.ERROR, it)
+ ActionStatus.REMOTE_FAILURE
+ }
+ }
+}
\ No newline at end of file
diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/LocationAction.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/LocationAction.kt
index 7d8bdb00..86c60350 100644
--- a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/LocationAction.kt
+++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/LocationAction.kt
@@ -38,6 +38,7 @@ object LocationAction {
}
} else if (action is ToggleAction) {
return if (ShizukuUtils.isRunning(context)) {
+ context.grantSecureSettingsPermission()
setLocationEnabledShizuku(context, action.isEnabled)
} else if (checkSecureSettingsPermission(context)) {
setLocationEnabled(context, action.isEnabled)
diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/NfcAction.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/NfcAction.kt
new file mode 100644
index 00000000..a10e2aee
--- /dev/null
+++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/NfcAction.kt
@@ -0,0 +1,88 @@
+package com.thewizrd.wearsettings.actions
+
+import android.content.Context
+import android.nfc.INfcAdapter
+import android.nfc.NfcAdapter
+import android.os.Build
+import android.util.Log
+import com.thewizrd.shared_resources.actions.Action
+import com.thewizrd.shared_resources.actions.ActionStatus
+import com.thewizrd.shared_resources.actions.ToggleAction
+import com.thewizrd.shared_resources.utils.Logger
+import com.thewizrd.wearsettings.Settings
+import com.thewizrd.wearsettings.root.RootHelper
+import com.thewizrd.wearsettings.shizuku.ShizukuUtils
+import com.thewizrd.wearsettings.shizuku.grantSecureSettingsPermission
+import com.topjohnwu.superuser.Shell
+import rikka.shizuku.ShizukuBinderWrapper
+import rikka.shizuku.SystemServiceHelper
+
+object NfcAction {
+ fun executeAction(context: Context, action: Action): ActionStatus {
+ if (action is ToggleAction) {
+ return if (ShizukuUtils.isRunning(context)) {
+ context.grantSecureSettingsPermission()
+ setNfcEnabledShizuku(context, action.isEnabled)
+ } else if (Settings.isRootAccessEnabled() && RootHelper.isRootEnabled()) {
+ setNfcEnabledRoot(action.isEnabled)
+ } else if (checkSecureSettingsPermission(context)) {
+ setNfcEnabled(context, action.isEnabled)
+ } else {
+ ActionStatus.REMOTE_PERMISSION_DENIED
+ }
+ }
+
+ return ActionStatus.UNKNOWN
+ }
+
+ private fun setNfcEnabled(context: Context, enable: Boolean): ActionStatus {
+ val nfcAdapter = NfcAdapter.getDefaultAdapter(context)
+ return nfcAdapter?.let {
+ try {
+ val success = if (enable) it.enable() else it.disable()
+ if (success) {
+ ActionStatus.SUCCESS
+ } else {
+ ActionStatus.REMOTE_FAILURE
+ }
+ } catch (e: SecurityException) {
+ Logger.writeLine(Log.ERROR, e)
+ ActionStatus.REMOTE_PERMISSION_DENIED
+ }
+ } ?: ActionStatus.REMOTE_FAILURE
+ }
+
+ private fun setNfcEnabledRoot(enable: Boolean): ActionStatus {
+ val arg = if (enable) "enable" else "disable"
+
+ val result = Shell.su("svc nfc $arg").exec()
+
+ return if (result.isSuccess) {
+ ActionStatus.SUCCESS
+ } else {
+ ActionStatus.REMOTE_FAILURE
+ }
+ }
+
+ private fun setNfcEnabledShizuku(context: Context, enable: Boolean): ActionStatus {
+ return runCatching {
+ val nfcManager = SystemServiceHelper.getSystemService(Context.NFC_SERVICE)
+ .let(::ShizukuBinderWrapper)
+ .let(INfcAdapter.Stub::asInterface)
+
+ val ret = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ if (enable) nfcManager.enable("com.android.shell") else nfcManager.disable(
+ false,
+ "com.android.shell"
+ )
+ } else {
+ if (enable) nfcManager.enable() else nfcManager.disable(false)
+ }
+
+ if (ret) ActionStatus.SUCCESS else ActionStatus.REMOTE_FAILURE
+ }.getOrElse {
+ Logger.writeLine(Log.ERROR, it)
+ ActionStatus.REMOTE_FAILURE
+ }
+ }
+}
\ No newline at end of file
diff --git a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/WifiHotspotAction.kt b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/WifiHotspotAction.kt
index 3a56ac18..4e56118c 100644
--- a/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/WifiHotspotAction.kt
+++ b/wearsettings/src/main/java/com/thewizrd/wearsettings/actions/WifiHotspotAction.kt
@@ -17,8 +17,13 @@ import com.thewizrd.shared_resources.actions.ActionStatus
import com.thewizrd.shared_resources.actions.ToggleAction
import com.thewizrd.shared_resources.utils.Logger
import com.thewizrd.wearsettings.shizuku.ShizukuUtils
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
import rikka.shizuku.ShizukuBinderWrapper
import rikka.shizuku.SystemServiceHelper
+import kotlin.coroutines.resume
@SuppressLint("PrivateApi")
object WifiHotspotAction {
@@ -64,29 +69,43 @@ object WifiHotspotAction {
.let(IConnectivityManager.Stub::asInterface)
if (enabled) {
- val resultReceiver = object : ResultReceiver(null) {
- override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
- when (resultCode) {
- TETHER_ERROR_NO_ERROR -> {
- Logger.info(TAG, "setHotspotEnabledShizukuPreR(true) - success")
- }
+ return@runCatching runBlocking {
+ withTimeout(10000) {
+ suspendCancellableCoroutine { continuation ->
+ val resultReceiver = object : ResultReceiver(null) {
+ override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
+ when (resultCode) {
+ TETHER_ERROR_NO_ERROR -> {
+ Logger.info(
+ TAG,
+ "setHotspotEnabledShizukuPreR(true) - success"
+ )
+ if (isActive) {
+ continuation.resume(ActionStatus.SUCCESS)
+ }
+ }
- else -> {
- Logger.error(
- TAG,
- "setHotspotEnabledShizukuPreR(true) - failed. code = $resultCode"
- )
+ else -> {
+ Logger.error(
+ TAG,
+ "setHotspotEnabledShizukuPreR(true) - failed. code = $resultCode"
+ )
+ if (isActive) {
+ continuation.resume(ActionStatus.REMOTE_FAILURE)
+ }
+ }
+ }
+ }
}
+
+ connMgr.startTethering(TETHERING_WIFI, resultReceiver, false)
}
}
}
-
- connMgr.startTethering(TETHERING_WIFI, resultReceiver, false)
} else {
connMgr.stopTethering(TETHERING_WIFI)
+ ActionStatus.SUCCESS
}
-
- ActionStatus.SUCCESS
}.getOrElse {
Logger.writeLine(Log.ERROR, it)
ActionStatus.REMOTE_FAILURE
@@ -97,7 +116,8 @@ object WifiHotspotAction {
private fun setHotspotEnabledShizuku(
enabled: Boolean,
exemptFromEntitlementCheck: Boolean = true,
- shouldShowEntitlementUi: Boolean = false
+ shouldShowEntitlementUi: Boolean = false,
+ retry: Boolean = true
): ActionStatus {
Logger.info(TAG, "entering setHotspotEnabledShizuku(enabled = ${enabled})...")
@@ -106,80 +126,94 @@ object WifiHotspotAction {
.let(::ShizukuBinderWrapper)
.let(ITetheringConnector.Stub::asInterface)
- if (enabled) {
- val resultListener = object : IIntResultListener.Stub() {
- override fun onResult(resultCode: Int) {
- when (resultCode) {
- TETHER_ERROR_NO_ERROR -> {
- Logger.info(TAG, "setHotspotEnabledShizuku(true) - success")
- }
+ runBlocking {
+ withTimeout(10000) {
+ suspendCancellableCoroutine { continuation ->
+ val resultListener = object : IIntResultListener.Stub() {
+ override fun onResult(resultCode: Int) {
+ when (resultCode) {
+ TETHER_ERROR_NO_ERROR -> {
+ Logger.info(
+ TAG,
+ "setHotspotEnabledShizuku(${enabled}) - success"
+ )
+ if (isActive) {
+ continuation.resume(ActionStatus.SUCCESS)
+ }
+ }
- TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION -> {
- // retry
- setHotspotEnabledShizuku(enabled, false, shouldShowEntitlementUi)
- }
+ TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION -> {
+ // retry
+ Logger.warn(
+ TAG,
+ "setHotspotEnabledShizuku(${enabled}) - TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION"
+ )
+ if (retry) {
+ setHotspotEnabledShizuku(
+ enabled,
+ false,
+ shouldShowEntitlementUi
+ )
+ } else {
+ if (isActive) {
+ continuation.resume(ActionStatus.REMOTE_PERMISSION_DENIED)
+ }
+ }
+ }
- else -> {
- Logger.error(
- TAG,
- "setHotspotEnabledShizuku(true) - failed. code = $resultCode"
- )
+ else -> {
+ Logger.error(
+ TAG,
+ "setHotspotEnabledShizuku(${enabled}) - failed. code = $resultCode"
+ )
+ if (isActive) {
+ continuation.resume(ActionStatus.REMOTE_FAILURE)
+ }
+ }
+ }
}
}
- }
- }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- tetheringMgr.startTethering(
- createTetheringRequestParcel(
- exemptFromEntitlementCheck,
- shouldShowEntitlementUi
- ) as TetheringRequestParcel,
- "com.android.shell",
- "",
- resultListener
- )
- } else {
- tetheringMgr.startTethering(
- createTetheringRequestParcel(
- exemptFromEntitlementCheck,
- shouldShowEntitlementUi
- ) as TetheringRequestParcel,
- "com.android.shell",
- resultListener
- )
- }
- } else {
- val resultListener = object : IIntResultListener.Stub() {
- override fun onResult(resultCode: Int) {
- when (resultCode) {
- TETHER_ERROR_NO_ERROR -> {
- Logger.info(TAG, "setHotspotEnabledShizuku(false) - success")
+ if (enabled) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ tetheringMgr.startTethering(
+ createTetheringRequestParcel(
+ exemptFromEntitlementCheck,
+ shouldShowEntitlementUi
+ ) as TetheringRequestParcel,
+ "com.android.shell",
+ "",
+ resultListener
+ )
+ } else {
+ tetheringMgr.startTethering(
+ createTetheringRequestParcel(
+ exemptFromEntitlementCheck,
+ shouldShowEntitlementUi
+ ) as TetheringRequestParcel,
+ "com.android.shell",
+ resultListener
+ )
}
-
- else -> {
- Logger.error(
- TAG,
- "setHotspotEnabledShizuku(false) - failed. code = $resultCode"
+ } else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ tetheringMgr.stopTethering(
+ TETHERING_WIFI,
+ "com.android.shell",
+ "",
+ resultListener
+ )
+ } else {
+ tetheringMgr.stopTethering(
+ TETHERING_WIFI,
+ "com.android.shell",
+ resultListener
)
}
}
}
}
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- tetheringMgr.stopTethering(
- TETHERING_WIFI,
- "com.android.shell",
- "",
- resultListener
- )
- } else {
- tetheringMgr.stopTethering(TETHERING_WIFI, "com.android.shell", resultListener)
- }
}
-
- ActionStatus.SUCCESS
}.getOrElse {
Logger.writeLine(Log.ERROR, it)
ActionStatus.REMOTE_FAILURE
diff --git a/wearsettings/src/main/res/drawable/preference_round_background.xml b/wearsettings/src/main/res/drawable/preference_round_background.xml
new file mode 100644
index 00000000..e5b42775
--- /dev/null
+++ b/wearsettings/src/main/res/drawable/preference_round_background.xml
@@ -0,0 +1,30 @@
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wearsettings/src/main/res/drawable/preference_round_background_bottom.xml b/wearsettings/src/main/res/drawable/preference_round_background_bottom.xml
new file mode 100644
index 00000000..208a469b
--- /dev/null
+++ b/wearsettings/src/main/res/drawable/preference_round_background_bottom.xml
@@ -0,0 +1,32 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wearsettings/src/main/res/drawable/preference_round_background_center.xml b/wearsettings/src/main/res/drawable/preference_round_background_center.xml
new file mode 100644
index 00000000..66cbcce3
--- /dev/null
+++ b/wearsettings/src/main/res/drawable/preference_round_background_center.xml
@@ -0,0 +1,28 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wearsettings/src/main/res/drawable/preference_round_background_top.xml b/wearsettings/src/main/res/drawable/preference_round_background_top.xml
new file mode 100644
index 00000000..053faa4c
--- /dev/null
+++ b/wearsettings/src/main/res/drawable/preference_round_background_top.xml
@@ -0,0 +1,32 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wearsettings/src/main/res/layout/activity_main.xml b/wearsettings/src/main/res/layout/activity_main.xml
index efeba37a..1e5fae96 100644
--- a/wearsettings/src/main/res/layout/activity_main.xml
+++ b/wearsettings/src/main/res/layout/activity_main.xml
@@ -1,38 +1,60 @@
+ tools:theme="@style/Theme.Material3Expressive.DayNight.NoActionBar">
-
-
+
+
+ android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
+ app:collapsedTitleTextAppearance="?textAppearanceTitleLargeEmphasized"
+ app:collapsedSubtitleTextAppearance="?textAppearanceTitleSmall"
+ app:expandedTitleTextAppearance="?textAppearanceDisplaySmallEmphasized"
+ app:expandedSubtitleTextAppearance="?textAppearanceTitleMedium"
+ style="?collapsingToolbarLayoutLargeStyle"
+ app:titleCollapseMode="scale"
+ app:expandedTitleMarginStart="24dp"
+ app:expandedTitleMarginBottom="32dp"
+ app:titleMaxLines="2">
+
+
+
+
-
+ app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
+ style="@style/Permissions.Preference.Category.Expressive">
+ style="@style/Permissions.Preference.Category.Title.Expressive"
+ android:text="@string/cat_title_permissions" />
-
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
+ style="@style/Permissions.Preference.Title.Expressive"
+ android:text="@string/permission_title_bgopts" />
+ style="@style/Permissions.Preference.Summary.Expressive"
+ android:text="@string/message_bgopts_disabled" />
-
+
-
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
+ style="@style/Permissions.Preference.Title.Expressive"
+ android:text="@string/permission_title_securesettings" />
+ style="@style/Permissions.Preference.Summary.Expressive"
+ android:text="@string/message_securesettings_disabled" />
-
+
-
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
+ style="@style/Permissions.Preference.Title.Expressive"
+ android:text="@string/permission_title_bt" />
+ style="@style/Permissions.Preference.Summary.Expressive"
+ android:text="@string/permission_bt_disabled" />
-
+
-
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
+ style="@style/Permissions.Preference.Title.Expressive"
+ android:text="@string/action_dnd" />
+ style="@style/Permissions.Preference.Summary.Expressive"
+ tools:text="@string/permission_dnd_disabled" />
-
+
-
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
+ style="@style/Permissions.Preference.Title.Expressive"
+ android:text="@string/permission_title_shizuku" />
+ style="@style/Permissions.Preference.Summary.Expressive"
+ android:text="@string/message_shizuku_not_installed" />
+
+
+
+
+
+
+
+
+
+
+
+
+ app:thumbIcon="@drawable/ic_check_white_24dp" />
-
+
-
+ android:orientation="horizontal"
+ style="@style/Permissions.Preference.Expressive"
+ android:tag="permissions">
-
+ android:layout_weight="1"
+ android:orientation="vertical">
-
+
+
+
+
+
+ app:thumbIcon="@drawable/ic_check_white_24dp" />
+
+
-
+
-
+
diff --git a/wearsettings/src/main/res/values-de/strings.xml b/wearsettings/src/main/res/values-de/strings.xml
new file mode 100644
index 00000000..c68a9953
--- /dev/null
+++ b/wearsettings/src/main/res/values-de/strings.xml
@@ -0,0 +1,20 @@
+
+
+ "Hintergrundzugriff zulassen"
+ "Erforderlich, um Aktionen im Hintergrund ordnungsgemäß auszuführen"
+ "Hintergrundzugriff aktiviert"
+ "Sichere Einstellungen festlegen"
+ "Erforderlich, um NFC-, mobile Daten- und Standorteinstellungen festzulegen"
+ "Einstellungszugriff aktiviert"
+ "Root-Zugriff aktivieren"
+ "Root-Zugriff deaktiviert"
+ "Root-Zugriff aktiviert"
+ "App-Symbol anzeigen"
+ "App-Symbol im Launcher anzeigen"
+ "Shizuku"
+ "Shizuku-Berechtigung deaktiviert"
+ "Shizuku nicht installiert"
+ "Shizuku-Dienst wird nicht ausgeführt"
+ "Shizuku-Dienst aktiviert"
+ "Shizuku kann als Option verwendet werden, um SimpleWear Systemberechtigungen für Geräte ohne Root-Zugriff zu gewähren. Da es sich um eine Drittanbieter-App handelt, verwenden Sie diese bitte nach eigenem Ermessen. Bitte beachten Sie, dass der Shizuku-Dienst nach jedem Start manuell neu gestartet werden muss."
+
\ No newline at end of file
diff --git a/wearsettings/src/main/res/values-es/strings.xml b/wearsettings/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..2568382b
--- /dev/null
+++ b/wearsettings/src/main/res/values-es/strings.xml
@@ -0,0 +1,20 @@
+
+
+ "Permitir acceso en segundo plano"
+ "Necesario para ejecutar las acciones correctamente en segundo plano"
+ "Acceso en segundo plano habilitado"
+ "Establecer ajustes seguros"
+ "Necesario para establecer ajustes de NFC, datos móviles y ubicación"
+ "Acceso a ajustes habilitado"
+ "Habilitar acceso Root"
+ "Acceso Root deshabilitado"
+ "Acceso Root habilitado"
+ "Mostrar icono de la aplicación"
+ "Mostrar el icono de la aplicación en el lanzador"
+ "Shizuku"
+ "Permiso Shizuku desactivado"
+ "Shizuku no está instalado"
+ "El servicio Shizuku no se está ejecutando"
+ "Servicio Shizuku activado"
+ "Shizuku puede usarse como una opción para otorgar permisos de sistema a SimpleWear en dispositivos sin acceso root. Dado que esta es una aplicación de terceros, úsela bajo su propia responsabilidad. Tenga en cuenta que el servicio Shizuku deberá reiniciarse manualmente cada vez que se inicie el dispositivo."
+
\ No newline at end of file
diff --git a/wearsettings/src/main/res/values-fr/strings.xml b/wearsettings/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..35ae2173
--- /dev/null
+++ b/wearsettings/src/main/res/values-fr/strings.xml
@@ -0,0 +1,20 @@
+
+
+ "Autoriser l'accès en arrière-plan"
+ "Nécessaire pour exécuter correctement les actions en arrière-plan"
+ "Accès en arrière-plan activé"
+ "Définir les paramètres sécurisés"
+ "Nécessaire pour définir les paramètres NFC, données mobiles et localisation"
+ "Accès aux paramètres activé"
+ "Activer l'accès Root"
+ "Accès Root désactivé"
+ "Accès Root activé"
+ "Afficher l'icône de l'application"
+ "Afficher l'icône de l'application dans le lanceur"
+ "Shizuku"
+ "Autorisation Shizuku désactivée"
+ "Shizuku non installé"
+ "Service Shizuku non exécuté"
+ "Service Shizuku activé"
+ "Shizuku peut être utilisé comme option pour fournir des autorisations système à SimpleWear pour les appareils non rootés. Comme il s'agit d'une application tierce, veuillez l'utiliser à votre propre discrétion. Veuillez noter que le service Shizuku devra être redémarré manuellement à chaque démarrage."
+
\ No newline at end of file
diff --git a/wearsettings/src/main/res/values/ic_launcher_background.xml b/wearsettings/src/main/res/values/ic_launcher_background.xml
deleted file mode 100644
index 787fba1b..00000000
--- a/wearsettings/src/main/res/values/ic_launcher_background.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- #0070C0
-
\ No newline at end of file
diff --git a/wearsettings/src/main/res/values/strings.xml b/wearsettings/src/main/res/values/strings.xml
index 597bf44c..d3cd97e4 100644
--- a/wearsettings/src/main/res/values/strings.xml
+++ b/wearsettings/src/main/res/values/strings.xml
@@ -1,12 +1,12 @@
- SimpleWear Settings
+ SimpleWear Settings
Allow Background Access
Needed to run actions properly in the background
Background access enabled
Set Secure Settings
- Needed to set mobile data and location settings
+ Needed to set NFC, mobile data and location settings
Settings access enabled
Enable Root Access
@@ -19,11 +19,7 @@
https://simpleappprojects.github.io/SimpleWear/root-access
https://simpleappprojects.github.io/SimpleWear/secure-settings-access
- Bluetooth
- Bluetooth permission enabled
- Bluetooth permission disabled
-
- Shizuku
+ Shizuku
Shizuku permission disabled
Shizuku not installed
Shizuku service not running
diff --git a/wearsettings/src/main/res/values/themes.xml b/wearsettings/src/main/res/values/themes.xml
index f80cae2a..045e125f 100644
--- a/wearsettings/src/main/res/values/themes.xml
+++ b/wearsettings/src/main/res/values/themes.xml
@@ -1,37 +1,3 @@
+
-
-
-
-
-
\ No newline at end of file
+