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 + + + + + + +