From 3927ef0257680319cb1cd739ea37f633f26b3a17 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 18 Jun 2025 18:57:55 +0100 Subject: [PATCH 01/18] Updates --- bluetoothle/build.gradle.kts | 4 ++++ compose/recomposehighlighter/build.gradle.kts | 3 +++ compose/snippets/build.gradle.kts | 3 +++ gradle/libs.versions.toml | 2 +- kotlin/build.gradle.kts | 4 ++++ misc/build.gradle.kts | 3 +++ wear/build.gradle.kts | 5 +++++ wear/src/main/AndroidManifest.xml | 11 +++++++++++ xr/build.gradle.kts | 1 + 9 files changed, 35 insertions(+), 1 deletion(-) diff --git a/bluetoothle/build.gradle.kts b/bluetoothle/build.gradle.kts index 5dc002c96..7ea67f9db 100644 --- a/bluetoothle/build.gradle.kts +++ b/bluetoothle/build.gradle.kts @@ -33,11 +33,15 @@ android { "proguard-rules.pro") } } + kotlinOptions { + jvmTarget = "17" + } } dependencies { implementation(libs.kotlin.stdlib) implementation(libs.androidx.appcompat) implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.core.ktx) testImplementation(libs.junit) androidTestImplementation(libs.junit) androidTestImplementation(libs.androidx.test.core) diff --git a/compose/recomposehighlighter/build.gradle.kts b/compose/recomposehighlighter/build.gradle.kts index fe8b79aff..a05a4ca75 100644 --- a/compose/recomposehighlighter/build.gradle.kts +++ b/compose/recomposehighlighter/build.gradle.kts @@ -40,6 +40,9 @@ android { // Disable unused AGP features viewBinding = true } + kotlinOptions { + jvmTarget = "17" + } } dependencies { diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index c8d728da0..4a00f91e1 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -73,6 +73,9 @@ android { lint { lintConfig = file("lint.xml") } + kotlinOptions { + jvmTarget = "17" + } } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index babb910f0..1de0130b0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -189,7 +189,7 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.1.21" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/kotlin/build.gradle.kts b/kotlin/build.gradle.kts index 12feddf2d..fd633d3a7 100644 --- a/kotlin/build.gradle.kts +++ b/kotlin/build.gradle.kts @@ -52,10 +52,14 @@ android { excludes += "/META-INF/AL2.0" excludes += "/META-INF/LGPL2.1" } + kotlinOptions { + jvmTarget = "17" + } } dependencies { implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.core.ktx) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.junit) } diff --git a/misc/build.gradle.kts b/misc/build.gradle.kts index e5cc3cc1d..f6db62b59 100644 --- a/misc/build.gradle.kts +++ b/misc/build.gradle.kts @@ -43,6 +43,9 @@ android { // Disable unused AGP features viewBinding = true } + kotlinOptions { + jvmTarget = "17" + } } dependencies { diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 888f3d434..840d51e97 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -46,9 +46,13 @@ android { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } + kotlinOptions { + jvmTarget = "17" + } } dependencies { + implementation(libs.androidx.core.ktx) val composeBom = platform(libs.androidx.compose.bom) implementation(composeBom) androidTestImplementation(composeBom) @@ -80,6 +84,7 @@ dependencies { implementation(libs.androidx.material.icons.core) androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.tooling.preview) debugImplementation(libs.androidx.compose.ui.test.manifest) testImplementation(libs.junit) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index c7209c3a7..e538593d7 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -163,6 +163,17 @@ android:resource="@drawable/tile_preview" /> + + + + + + + \ No newline at end of file diff --git a/xr/build.gradle.kts b/xr/build.gradle.kts index 74ad5bfe9..9cc7b2cba 100644 --- a/xr/build.gradle.kts +++ b/xr/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation(libs.kotlinx.coroutines.guava) implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.core.ktx) val composeBom = platform(libs.androidx.compose.bom) implementation(composeBom) From 2bdf4106bf79fdd2801fb92cca5da1fee4b060d1 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 18 Jun 2025 21:28:47 +0100 Subject: [PATCH 02/18] Updates --- .../snippets/alwayson/AlwaysOnActivity.kt | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt new file mode 100644 index 000000000..e8d8870b4 --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.wear.snippets.alwayson + +import android.os.Bundle +import android.os.SystemClock +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.dynamicColorScheme +import androidx.wear.tooling.preview.devices.WearDevices +import kotlinx.coroutines.delay + +class AlwaysOnActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setTheme(android.R.style.Theme_DeviceDefault) + + setContent { WearApp() } + } +} + +@Composable +fun ElapsedTime() { + val startTimeMs = rememberSaveable { SystemClock.elapsedRealtime() } + + val elapsedMs by + produceState(initialValue = 0L, key1 = startTimeMs) { + while (true) { + value = SystemClock.elapsedRealtime() - startTimeMs + delay(1_000L - (value % 1_000L)) + } + } + + val totalSeconds = elapsedMs / 1_000L + val minutes = totalSeconds / 60 + val seconds = totalSeconds % 60 + + Text( + text = "%02d:%02d".format(minutes, seconds), + style = MaterialTheme.typography.numeralMedium, + ) +} + +@Preview( + device = WearDevices.LARGE_ROUND, + backgroundColor = 0xff000000, + showBackground = true, + group = "Devices - Large Round", + showSystemUi = true, +) +@Composable +fun WearApp() { + MaterialTheme( + colorScheme = dynamicColorScheme(LocalContext.current) ?: MaterialTheme.colorScheme + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = "Elapsed Time", style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(8.dp)) + ElapsedTime() + } + } + } +} From 2a374a2619788b44f63fe5df75b36fd98b12ff61 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 18 Jun 2025 21:46:29 +0100 Subject: [PATCH 03/18] Add AmbientAware --- .../snippets/alwayson/AlwaysOnActivity.kt | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt index e8d8870b4..24cd14f44 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt @@ -39,6 +39,8 @@ import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.Text import androidx.wear.compose.material3.dynamicColorScheme import androidx.wear.tooling.preview.devices.WearDevices +import com.google.android.horologist.compose.ambient.AmbientAware +import com.google.android.horologist.compose.ambient.AmbientState import kotlinx.coroutines.delay class AlwaysOnActivity : ComponentActivity() { @@ -52,14 +54,16 @@ class AlwaysOnActivity : ComponentActivity() { } @Composable -fun ElapsedTime() { +fun ElapsedTime(ambientState: AmbientState) { val startTimeMs = rememberSaveable { SystemClock.elapsedRealtime() } val elapsedMs by produceState(initialValue = 0L, key1 = startTimeMs) { while (true) { value = SystemClock.elapsedRealtime() - startTimeMs - delay(1_000L - (value % 1_000L)) + // In ambient mode, update every minute instead of every second + val updateInterval = if (ambientState.isAmbient) 60_000L else 1_000L + delay(updateInterval - (value % updateInterval)) } } @@ -67,8 +71,16 @@ fun ElapsedTime() { val minutes = totalSeconds / 60 val seconds = totalSeconds % 60 + val timeText = if (ambientState.isAmbient) { + // Show "mm:--" format in ambient mode + "%02d:--".format(minutes) + } else { + // Show full "mm:ss" format in interactive mode + "%02d:%02d".format(minutes, seconds) + } + Text( - text = "%02d:%02d".format(minutes, seconds), + text = timeText, style = MaterialTheme.typography.numeralMedium, ) } @@ -85,11 +97,13 @@ fun WearApp() { MaterialTheme( colorScheme = dynamicColorScheme(LocalContext.current) ?: MaterialTheme.colorScheme ) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = "Elapsed Time", style = MaterialTheme.typography.titleLarge) - Spacer(modifier = Modifier.height(8.dp)) - ElapsedTime() + AmbientAware { ambientState -> + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = "Elapsed Time", style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(8.dp)) + ElapsedTime(ambientState = ambientState) + } } } } From db1a521b21c2d85e2f116192de6ecda54a8486b3 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 18 Jun 2025 22:10:51 +0100 Subject: [PATCH 04/18] Add OA switch --- .../snippets/alwayson/AlwaysOnActivity.kt | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt index 24cd14f44..7061c72e3 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt @@ -22,20 +22,23 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.SwitchButton import androidx.wear.compose.material3.Text import androidx.wear.compose.material3.dynamicColorScheme import androidx.wear.tooling.preview.devices.WearDevices @@ -71,18 +74,16 @@ fun ElapsedTime(ambientState: AmbientState) { val minutes = totalSeconds / 60 val seconds = totalSeconds % 60 - val timeText = if (ambientState.isAmbient) { - // Show "mm:--" format in ambient mode - "%02d:--".format(minutes) - } else { - // Show full "mm:ss" format in interactive mode - "%02d:%02d".format(minutes, seconds) - } + val timeText = + if (ambientState.isAmbient) { + // Show "mm:--" format in ambient mode + "%02d:--".format(minutes) + } else { + // Show full "mm:ss" format in interactive mode + "%02d:%02d".format(minutes, seconds) + } - Text( - text = timeText, - style = MaterialTheme.typography.numeralMedium, - ) + Text(text = timeText, style = MaterialTheme.typography.numeralMedium) } @Preview( @@ -94,6 +95,8 @@ fun ElapsedTime(ambientState: AmbientState) { ) @Composable fun WearApp() { + var isOngoingActivity by rememberSaveable { mutableStateOf(false) } + MaterialTheme( colorScheme = dynamicColorScheme(LocalContext.current) ?: MaterialTheme.colorScheme ) { @@ -103,6 +106,17 @@ fun WearApp() { Text(text = "Elapsed Time", style = MaterialTheme.typography.titleLarge) Spacer(modifier = Modifier.height(8.dp)) ElapsedTime(ambientState = ambientState) + Spacer(modifier = Modifier.height(8.dp)) + SwitchButton( + checked = isOngoingActivity, + onCheckedChange = { isOngoingActivity = it }, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), + ) { + Text( + text = "Ongoing Activity", + style = MaterialTheme.typography.bodyExtraSmall, + ) + } } } } From 0207fb72b2f4088e36f0ed312f1ecc9f23cc3abc Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 18 Jun 2025 22:19:26 +0100 Subject: [PATCH 05/18] Add service --- wear/src/main/AndroidManifest.xml | 4 +++ .../wear/snippets/alwayson/AlwaysOnService.kt | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index e538593d7..9ed3bea88 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -174,6 +174,10 @@ + + \ No newline at end of file diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt new file mode 100644 index 000000000..b128db0bb --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.wear.snippets.alwayson + +import androidx.lifecycle.LifecycleService + +class AlwaysOnService : LifecycleService() { + + override fun onCreate() { + super.onCreate() + // Initialize service resources here + } + + override fun onDestroy() { + super.onDestroy() + // Clean up resources here + } +} \ No newline at end of file From c428bca0ccc7cf795761c1699884f5d2323ebd17 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 18 Jun 2025 22:44:35 +0100 Subject: [PATCH 06/18] Start and stop foreground service --- gradle/libs.versions.toml | 4 + wear/build.gradle.kts | 2 + wear/src/main/AndroidManifest.xml | 1 + .../snippets/alwayson/AlwaysOnActivity.kt | 22 ++++- .../wear/snippets/alwayson/AlwaysOnService.kt | 95 ++++++++++++++++++- 5 files changed, 117 insertions(+), 7 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1de0130b0..f705a6920 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,7 @@ kotlinCoroutinesOkhttp = "1.0" kotlinxCoroutinesGuava = "1.10.2" kotlinxSerializationJson = "1.8.1" ksp = "2.1.20-2.0.1" +lifecycleService = "2.9.1" maps-compose = "6.6.0" material = "1.13.0-alpha13" material3-adaptive = "1.1.0" @@ -73,6 +74,7 @@ wear = "1.3.0" wearComposeFoundation = "1.5.0-beta01" wearComposeMaterial = "1.5.0-beta01" wearComposeMaterial3 = "1.5.0-beta01" +wearOngoing = "1.0.0" wearToolingPreview = "1.0.0" webkit = "1.13.0" @@ -126,6 +128,7 @@ androidx-graphics-shapes = "androidx.graphics:graphics-shapes:1.0.1" androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime-compose" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime-compose" } +androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycleService" } androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } @@ -149,6 +152,7 @@ androidx-tiles-testing = { module = "androidx.wear.tiles:tiles-testing", version androidx-tiles-tooling = { module = "androidx.wear.tiles:tiles-tooling", version.ref = "tiles" } androidx-tiles-tooling-preview = { module = "androidx.wear.tiles:tiles-tooling-preview", version.ref = "tiles" } androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" } +androidx-wear-ongoing = { module = "androidx.wear:wear-ongoing", version.ref = "wearOngoing" } androidx-wear-tooling-preview = { module = "androidx.wear:wear-tooling-preview", version.ref = "wearToolingPreview" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" } diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 840d51e97..6014762b8 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -61,6 +61,8 @@ dependencies { implementation(libs.play.services.wearable) implementation(libs.androidx.tiles) implementation(libs.androidx.wear) + implementation(libs.androidx.wear.ongoing) + implementation(libs.androidx.lifecycle.service) implementation(libs.androidx.protolayout) implementation(libs.androidx.protolayout.material) implementation(libs.androidx.protolayout.material3) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 9ed3bea88..53a7d2e9e 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt index 7061c72e3..cffcf4f73 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt @@ -18,6 +18,7 @@ package com.example.wear.snippets.alwayson import android.os.Bundle import android.os.SystemClock +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Box @@ -47,8 +48,13 @@ import com.google.android.horologist.compose.ambient.AmbientState import kotlinx.coroutines.delay class AlwaysOnActivity : ComponentActivity() { + companion object { + private const val TAG = "AlwaysOnActivity" + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + Log.d(TAG, "onCreate: Activity created") setTheme(android.R.style.Theme_DeviceDefault) @@ -95,7 +101,8 @@ fun ElapsedTime(ambientState: AmbientState) { ) @Composable fun WearApp() { - var isOngoingActivity by rememberSaveable { mutableStateOf(false) } + val context = LocalContext.current + var isOngoingActivity by rememberSaveable { mutableStateOf(AlwaysOnService.isRunning) } MaterialTheme( colorScheme = dynamicColorScheme(LocalContext.current) ?: MaterialTheme.colorScheme @@ -109,7 +116,18 @@ fun WearApp() { Spacer(modifier = Modifier.height(8.dp)) SwitchButton( checked = isOngoingActivity, - onCheckedChange = { isOngoingActivity = it }, + onCheckedChange = { newState -> + Log.d("AlwaysOnActivity", "Switch button changed: $newState") + isOngoingActivity = newState + + if (newState) { + Log.d("AlwaysOnActivity", "Starting AlwaysOnService") + AlwaysOnService.startService(context) + } else { + Log.d("AlwaysOnActivity", "Stopping AlwaysOnService") + AlwaysOnService.stopService(context) + } + }, contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), ) { Text( diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt index b128db0bb..ad1edf164 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt @@ -16,17 +16,102 @@ package com.example.wear.snippets.alwayson +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat import androidx.lifecycle.LifecycleService class AlwaysOnService : LifecycleService() { - + + companion object { + private const val TAG = "AlwaysOnService" + private const val NOTIFICATION_ID = 1001 + private const val CHANNEL_ID = "always_on_service_channel" + private const val CHANNEL_NAME = "Always On Service" + @Volatile + var isRunning = false + private set + + fun startService(context: Context) { + Log.d(TAG, "Starting AlwaysOnService") + val intent = Intent(context, AlwaysOnService::class.java) + context.startForegroundService(intent) + } + + fun stopService(context: Context) { + Log.d(TAG, "Stopping AlwaysOnService") + context.stopService(Intent(context, AlwaysOnService::class.java)) + } + } + override fun onCreate() { super.onCreate() - // Initialize service resources here + Log.d(TAG, "onCreate: Service created") + isRunning = true + createNotificationChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.d(TAG, "onStartCommand: Service started with startId: $startId") + + // Create and start foreground notification + val notification = createNotification() + startForeground(NOTIFICATION_ID, notification) + + Log.d(TAG, "onStartCommand: Service is now running as foreground service") + + return super.onStartCommand(intent, flags, startId) } - + override fun onDestroy() { + Log.d(TAG, "onDestroy: Service destroyed") + isRunning = false super.onDestroy() - // Clean up resources here } -} \ No newline at end of file + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW) + .apply { + description = "Always On Service notification channel" + setShowBadge(false) + } + + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + Log.d(TAG, "createNotificationChannel: Notification channel created") + } + } + + private fun createNotification(): Notification { + val intent = + Intent(this, AlwaysOnActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + + val pendingIntent = + PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Always On Service") + .setContentText("Service is running in background") + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + } +} From dd6f8fce866f9e6ad60711912763bda8d78e7097 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Thu, 19 Jun 2025 00:24:26 +0100 Subject: [PATCH 07/18] Add ongoing activity --- wear/src/main/AndroidManifest.xml | 2 + .../snippets/alwayson/AlwaysOnActivity.kt | 37 ++++++++++ .../wear/snippets/alwayson/AlwaysOnService.kt | 70 +++++++++++-------- 3 files changed, 80 insertions(+), 29 deletions(-) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 53a7d2e9e..250ac8fae 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + @@ -166,6 +167,7 @@ diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt index cffcf4f73..1457c218e 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt @@ -16,11 +16,15 @@ package com.example.wear.snippets.alwayson +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.os.SystemClock import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -38,6 +42,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.SwitchButton import androidx.wear.compose.material3.Text @@ -52,14 +57,46 @@ class AlwaysOnActivity : ComponentActivity() { private const val TAG = "AlwaysOnActivity" } + private val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + Log.d(TAG, "POST_NOTIFICATIONS permission granted") + } else { + Log.w(TAG, "POST_NOTIFICATIONS permission denied") + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d(TAG, "onCreate: Activity created") setTheme(android.R.style.Theme_DeviceDefault) + // Check and request notification permission + checkAndRequestNotificationPermission() + setContent { WearApp() } } + + private fun checkAndRequestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + when { + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED -> { + Log.d(TAG, "POST_NOTIFICATIONS permission already granted") + } + shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> { + Log.d(TAG, "Should show permission rationale") + // You could show a dialog here explaining why the permission is needed + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + else -> { + Log.d(TAG, "Requesting POST_NOTIFICATIONS permission") + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + } + } } @Composable diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt index ad1edf164..581236b52 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt @@ -22,10 +22,12 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.lifecycle.LifecycleService +import androidx.wear.ongoing.OngoingActivity +import androidx.wear.ongoing.Status +import com.example.wear.R class AlwaysOnService : LifecycleService() { @@ -76,42 +78,52 @@ class AlwaysOnService : LifecycleService() { } private fun createNotificationChannel() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = - NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW) - .apply { - description = "Always On Service notification channel" - setShowBadge(false) - } - - val notificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) - Log.d(TAG, "createNotificationChannel: Notification channel created") - } + val channel = + NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) + .apply { + description = "Always On Service notification channel" + setShowBadge(false) + } + + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + Log.d(TAG, "createNotificationChannel: Notification channel created") } private fun createNotification(): Notification { - val intent = - Intent(this, AlwaysOnActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - val pendingIntent = PendingIntent.getActivity( this, 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + Intent(this, AlwaysOnActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE, ) - return NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("Always On Service") - .setContentText("Service is running in background") - .setSmallIcon(android.R.drawable.ic_dialog_info) - .setContentIntent(pendingIntent) - .setOngoing(true) - .setPriority(NotificationCompat.PRIORITY_LOW) - .build() + val notificationBuilder = + NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Always On Service") + .setContentText("Service is running in background") + .setSmallIcon(R.drawable.animated_walk) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setCategory(NotificationCompat.CATEGORY_STOPWATCH) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + + // Create an Ongoing Activity + val ongoingActivityStatus = Status.Builder().addTemplate("Stopwatch running").build() + + val ongoingActivity = + OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder) + .setStaticIcon(R.drawable.animated_walk) + .setAnimatedIcon(R.drawable.animated_walk) + .setTouchIntent(pendingIntent) + .setStatus(ongoingActivityStatus) + .build() + + // Apply the ongoing activity to the notification builder + ongoingActivity.apply(applicationContext) + + return notificationBuilder.build() } } From ba5e050941489ad961a93b6aa4b00a049f7df08b Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Thu, 19 Jun 2025 17:29:12 +0100 Subject: [PATCH 08/18] Reuse existing activity and fix permissions --- wear/build.gradle.kts | 4 ++-- wear/src/main/AndroidManifest.xml | 11 ++++++++--- .../example/wear/snippets/alwayson/AlwaysOnService.kt | 8 ++++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 6014762b8..60cf27717 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -6,12 +6,12 @@ plugins { android { namespace = "com.example.wear" - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = "com.example.wear" minSdk = 26 - targetSdk = 33 + targetSdk = 36 versionCode = 1 versionName = "1.0" vectorDrawables { diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 250ac8fae..770b6d956 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ - + + @@ -167,7 +168,7 @@ @@ -179,7 +180,11 @@ + android:foregroundServiceType="specialUse" + android:exported="false"> + + diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt index 581236b52..4ad4b677a 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt @@ -92,12 +92,16 @@ class AlwaysOnService : LifecycleService() { } private fun createNotification(): Notification { + val intent = Intent(this, AlwaysOnActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + val pendingIntent = PendingIntent.getActivity( this, 0, - Intent(this, AlwaysOnActivity::class.java), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) val notificationBuilder = From dcbb68f04d0914169be67e5c10edda1bfed3cd17 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Thu, 19 Jun 2025 17:29:26 +0100 Subject: [PATCH 09/18] Add animated icon --- wear/src/main/res/drawable/animated_walk.xml | 682 +++++++++++++++++++ 1 file changed, 682 insertions(+) create mode 100644 wear/src/main/res/drawable/animated_walk.xml diff --git a/wear/src/main/res/drawable/animated_walk.xml b/wear/src/main/res/drawable/animated_walk.xml new file mode 100644 index 000000000..e94991e07 --- /dev/null +++ b/wear/src/main/res/drawable/animated_walk.xml @@ -0,0 +1,682 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 2eb1b0a080eeb37d8426bbdfa56a10e06372e4e2 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Thu, 19 Jun 2025 17:31:37 +0100 Subject: [PATCH 10/18] spotlessApply --- .../wear/snippets/alwayson/AlwaysOnActivity.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt index 1457c218e..cee8bac16 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt @@ -104,14 +104,14 @@ fun ElapsedTime(ambientState: AmbientState) { val startTimeMs = rememberSaveable { SystemClock.elapsedRealtime() } val elapsedMs by - produceState(initialValue = 0L, key1 = startTimeMs) { - while (true) { - value = SystemClock.elapsedRealtime() - startTimeMs - // In ambient mode, update every minute instead of every second - val updateInterval = if (ambientState.isAmbient) 60_000L else 1_000L - delay(updateInterval - (value % updateInterval)) - } + produceState(initialValue = 0L, key1 = startTimeMs) { + while (true) { + value = SystemClock.elapsedRealtime() - startTimeMs + // In ambient mode, update every minute instead of every second + val updateInterval = if (ambientState.isAmbient) 60_000L else 1_000L + delay(updateInterval - (value % updateInterval)) } + } val totalSeconds = elapsedMs / 1_000L val minutes = totalSeconds / 60 From a5eb9c933e330a4b5ed0bb209fa68385899ae35c Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Thu, 19 Jun 2025 23:16:09 +0100 Subject: [PATCH 11/18] Improvements --- .../wear/snippets/alwayson/AlwaysOnService.kt | 21 +++++++++++-------- wear/src/main/res/drawable/ic_walk.xml | 16 ++++++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 wear/src/main/res/drawable/ic_walk.xml diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt index 4ad4b677a..f3bc7b8db 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt @@ -24,6 +24,7 @@ import android.content.Context import android.content.Intent import android.util.Log import androidx.core.app.NotificationCompat +import androidx.core.content.getSystemService import androidx.lifecycle.LifecycleService import androidx.wear.ongoing.OngoingActivity import androidx.wear.ongoing.Status @@ -31,6 +32,8 @@ import com.example.wear.R class AlwaysOnService : LifecycleService() { + private val notificationManager by lazy { getSystemService() } + companion object { private const val TAG = "AlwaysOnService" private const val NOTIFICATION_ID = 1001 @@ -60,6 +63,7 @@ class AlwaysOnService : LifecycleService() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) Log.d(TAG, "onStartCommand: Service started with startId: $startId") // Create and start foreground notification @@ -68,7 +72,7 @@ class AlwaysOnService : LifecycleService() { Log.d(TAG, "onStartCommand: Service is now running as foreground service") - return super.onStartCommand(intent, flags, startId) + return START_STICKY } override fun onDestroy() { @@ -85,22 +89,21 @@ class AlwaysOnService : LifecycleService() { setShowBadge(false) } - val notificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) + notificationManager?.createNotificationChannel(channel) Log.d(TAG, "createNotificationChannel: Notification channel created") } private fun createNotification(): Notification { - val intent = Intent(this, AlwaysOnActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_SINGLE_TOP - } + val activityIntent = + Intent(this, AlwaysOnActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } val pendingIntent = PendingIntent.getActivity( this, 0, - intent, + activityIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) @@ -119,7 +122,7 @@ class AlwaysOnService : LifecycleService() { val ongoingActivity = OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder) - .setStaticIcon(R.drawable.animated_walk) + .setStaticIcon(R.drawable.ic_walk) .setAnimatedIcon(R.drawable.animated_walk) .setTouchIntent(pendingIntent) .setStatus(ongoingActivityStatus) diff --git a/wear/src/main/res/drawable/ic_walk.xml b/wear/src/main/res/drawable/ic_walk.xml new file mode 100644 index 000000000..6c226e943 --- /dev/null +++ b/wear/src/main/res/drawable/ic_walk.xml @@ -0,0 +1,16 @@ + + + + + From 6f267ee18dfa0ced12007b7e6c8bf80a00f62c4f Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 20 Jun 2025 00:09:04 +0100 Subject: [PATCH 12/18] Revert build script changes --- bluetoothle/build.gradle.kts | 4 ---- compose/recomposehighlighter/build.gradle.kts | 3 --- compose/snippets/build.gradle.kts | 3 --- kotlin/build.gradle.kts | 4 ---- misc/build.gradle.kts | 3 --- xr/build.gradle.kts | 1 - 6 files changed, 18 deletions(-) diff --git a/bluetoothle/build.gradle.kts b/bluetoothle/build.gradle.kts index 7ea67f9db..5dc002c96 100644 --- a/bluetoothle/build.gradle.kts +++ b/bluetoothle/build.gradle.kts @@ -33,15 +33,11 @@ android { "proguard-rules.pro") } } - kotlinOptions { - jvmTarget = "17" - } } dependencies { implementation(libs.kotlin.stdlib) implementation(libs.androidx.appcompat) implementation(libs.androidx.constraintlayout) - implementation(libs.androidx.core.ktx) testImplementation(libs.junit) androidTestImplementation(libs.junit) androidTestImplementation(libs.androidx.test.core) diff --git a/compose/recomposehighlighter/build.gradle.kts b/compose/recomposehighlighter/build.gradle.kts index a05a4ca75..fe8b79aff 100644 --- a/compose/recomposehighlighter/build.gradle.kts +++ b/compose/recomposehighlighter/build.gradle.kts @@ -40,9 +40,6 @@ android { // Disable unused AGP features viewBinding = true } - kotlinOptions { - jvmTarget = "17" - } } dependencies { diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index 4a00f91e1..c8d728da0 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -73,9 +73,6 @@ android { lint { lintConfig = file("lint.xml") } - kotlinOptions { - jvmTarget = "17" - } } dependencies { diff --git a/kotlin/build.gradle.kts b/kotlin/build.gradle.kts index fd633d3a7..12feddf2d 100644 --- a/kotlin/build.gradle.kts +++ b/kotlin/build.gradle.kts @@ -52,14 +52,10 @@ android { excludes += "/META-INF/AL2.0" excludes += "/META-INF/LGPL2.1" } - kotlinOptions { - jvmTarget = "17" - } } dependencies { implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.kotlinx.coroutines.android) - implementation(libs.androidx.core.ktx) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.junit) } diff --git a/misc/build.gradle.kts b/misc/build.gradle.kts index f6db62b59..e5cc3cc1d 100644 --- a/misc/build.gradle.kts +++ b/misc/build.gradle.kts @@ -43,9 +43,6 @@ android { // Disable unused AGP features viewBinding = true } - kotlinOptions { - jvmTarget = "17" - } } dependencies { diff --git a/xr/build.gradle.kts b/xr/build.gradle.kts index 9cc7b2cba..74ad5bfe9 100644 --- a/xr/build.gradle.kts +++ b/xr/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { implementation(libs.kotlinx.coroutines.guava) implementation(libs.androidx.media3.exoplayer) - implementation(libs.androidx.core.ktx) val composeBom = platform(libs.androidx.compose.bom) implementation(composeBom) From b9503a3e3061d22fb93e83b16ac608af51c78bb5 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 20 Jun 2025 00:16:03 +0100 Subject: [PATCH 13/18] Tag android_wear_ongoing_activity_create_notification --- .../wear/snippets/alwayson/AlwaysOnService.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt index f3bc7b8db..59ed0f8af 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt @@ -93,6 +93,7 @@ class AlwaysOnService : LifecycleService() { Log.d(TAG, "createNotificationChannel: Notification channel created") } + // [START android_wear_ongoing_activity_create_notification] private fun createNotification(): Notification { val activityIntent = Intent(this, AlwaysOnActivity::class.java).apply { @@ -109,28 +110,36 @@ class AlwaysOnService : LifecycleService() { val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID) + // ... + // [START_EXCLUDE] .setContentTitle("Always On Service") .setContentText("Service is running in background") .setSmallIcon(R.drawable.animated_walk) .setContentIntent(pendingIntent) - .setOngoing(true) .setCategory(NotificationCompat.CATEGORY_STOPWATCH) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + // [END_EXCLUDE] + .setOngoing(true) + // [START_EXCLUDE] // Create an Ongoing Activity val ongoingActivityStatus = Status.Builder().addTemplate("Stopwatch running").build() + // [END_EXCLUDE] val ongoingActivity = OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder) + // ... + // [START_EXCLUDE] .setStaticIcon(R.drawable.ic_walk) .setAnimatedIcon(R.drawable.animated_walk) - .setTouchIntent(pendingIntent) .setStatus(ongoingActivityStatus) + // [END_EXCLUDE] + .setTouchIntent(pendingIntent) .build() - // Apply the ongoing activity to the notification builder ongoingActivity.apply(applicationContext) return notificationBuilder.build() } + // [END android_wear_ongoing_activity_create_notification] } From 4a8b717ddc0b7e633a11ab8876d1a0d8dddee0e9 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 20 Jun 2025 00:24:19 +0100 Subject: [PATCH 14/18] Add more snippet markers --- .../wear/snippets/alwayson/AlwaysOnActivity.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt index cee8bac16..42436a3c4 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt @@ -100,7 +100,9 @@ class AlwaysOnActivity : ComponentActivity() { } @Composable +// [START android_wear_ongoing_activity_elapsedtime] fun ElapsedTime(ambientState: AmbientState) { + // [START_EXCLUDE] val startTimeMs = rememberSaveable { SystemClock.elapsedRealtime() } val elapsedMs by @@ -117,6 +119,7 @@ fun ElapsedTime(ambientState: AmbientState) { val minutes = totalSeconds / 60 val seconds = totalSeconds % 60 + // [END_EXCLUDE] val timeText = if (ambientState.isAmbient) { // Show "mm:--" format in ambient mode @@ -128,6 +131,7 @@ fun ElapsedTime(ambientState: AmbientState) { Text(text = timeText, style = MaterialTheme.typography.numeralMedium) } +// [END android_wear_ongoing_activity_elapsedtime] @Preview( device = WearDevices.LARGE_ROUND, @@ -140,16 +144,19 @@ fun ElapsedTime(ambientState: AmbientState) { fun WearApp() { val context = LocalContext.current var isOngoingActivity by rememberSaveable { mutableStateOf(AlwaysOnService.isRunning) } - MaterialTheme( colorScheme = dynamicColorScheme(LocalContext.current) ?: MaterialTheme.colorScheme ) { + // [START android_wear_ongoing_activity_ambientaware] AmbientAware { ambientState -> + // [START_EXCLUDE] Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = "Elapsed Time", style = MaterialTheme.typography.titleLarge) Spacer(modifier = Modifier.height(8.dp)) + // [END_EXCLUDE] ElapsedTime(ambientState = ambientState) + // [START_EXCLUDE] Spacer(modifier = Modifier.height(8.dp)) SwitchButton( checked = isOngoingActivity, @@ -174,6 +181,9 @@ fun WearApp() { } } } + // [END_EXCLUDE] } + // [END android_wear_ongoing_activity_ambientaware] } + } From dfbc6bdbc6b3020dab57710694de2367ffe0780d Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 20 Jun 2025 00:32:54 +0100 Subject: [PATCH 15/18] ./gradlew :wear:spotlessApply --init-script gradle/init.gradle.kts --- .../java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt index 42436a3c4..a441210ea 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt @@ -185,5 +185,4 @@ fun WearApp() { } // [END android_wear_ongoing_activity_ambientaware] } - } From c83b5230efe1014b87658883aea9084f61f844ff Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 20 Jun 2025 11:10:10 +0100 Subject: [PATCH 16/18] Fix lint error: $ ./gradlew :wear:lintReportDebug --init-script gradle/init.gradle.kts --- wear/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 60cf27717..eb7c66445 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -76,6 +76,7 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.fragment.ktx) implementation(libs.wear.compose.material) implementation(libs.wear.compose.material3) implementation(libs.compose.foundation) From 929ba2b350e17395efdd34f2f6a1e7cd71614cb1 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 20 Jun 2025 11:19:12 +0100 Subject: [PATCH 17/18] Share TAG --- .../wear/snippets/alwayson/AlwaysOnActivity.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt index a441210ea..7a95e6b1d 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt @@ -52,11 +52,9 @@ import com.google.android.horologist.compose.ambient.AmbientAware import com.google.android.horologist.compose.ambient.AmbientState import kotlinx.coroutines.delay -class AlwaysOnActivity : ComponentActivity() { - companion object { - private const val TAG = "AlwaysOnActivity" - } +private const val TAG = "AlwaysOnActivity" +class AlwaysOnActivity : ComponentActivity() { private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (isGranted) { @@ -161,14 +159,14 @@ fun WearApp() { SwitchButton( checked = isOngoingActivity, onCheckedChange = { newState -> - Log.d("AlwaysOnActivity", "Switch button changed: $newState") + Log.d(TAG, "Switch button changed: $newState") isOngoingActivity = newState if (newState) { - Log.d("AlwaysOnActivity", "Starting AlwaysOnService") + Log.d(TAG, "Starting AlwaysOnService") AlwaysOnService.startService(context) } else { - Log.d("AlwaysOnActivity", "Stopping AlwaysOnService") + Log.d(TAG, "Stopping AlwaysOnService") AlwaysOnService.stopService(context) } }, From cc6e20c8931ad931838a0532f0a2438c6d307ebb Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 20 Jun 2025 11:21:28 +0100 Subject: [PATCH 18/18] Explain the while (true) --- .../java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt index 7a95e6b1d..17b18d8af 100644 --- a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt @@ -105,7 +105,7 @@ fun ElapsedTime(ambientState: AmbientState) { val elapsedMs by produceState(initialValue = 0L, key1 = startTimeMs) { - while (true) { + while (true) { // time doesn't stop! value = SystemClock.elapsedRealtime() - startTimeMs // In ambient mode, update every minute instead of every second val updateInterval = if (ambientState.isAmbient) 60_000L else 1_000L