Skip to content

Commit 3e4a6ce

Browse files
1 parent 0ce9349 commit 3e4a6ce

File tree

4 files changed

+190
-47
lines changed

4 files changed

+190
-47
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ wear = "1.3.0"
7676
wearComposeFoundation = "1.5.0-rc02"
7777
wearComposeMaterial = "1.5.0-rc02"
7878
wearComposeMaterial3 = "1.5.0-rc02"
79-
wearOngoing = "1.0.0"
79+
wearOngoing = "1.1.0"
8080
wearToolingPreview = "1.0.0"
8181
webkit = "1.14.0"
8282

wear/src/main/AndroidManifest.xml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,21 @@
224224
</activity>
225225

226226
<service
227-
android:name=".snippets.alwayson.AlwaysOnService"
227+
android:name=".snippets.alwayson.AlwaysOnService1"
228+
android:foregroundServiceType="specialUse"
229+
android:exported="false">
230+
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
231+
android:value="For Ongoing Activity"/>
232+
</service>
233+
<service
234+
android:name=".snippets.alwayson.AlwaysOnService2"
235+
android:foregroundServiceType="specialUse"
236+
android:exported="false">
237+
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
238+
android:value="For Ongoing Activity"/>
239+
</service>
240+
<service
241+
android:name=".snippets.alwayson.AlwaysOnService3"
228242
android:foregroundServiceType="specialUse"
229243
android:exported="false">
230244
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"

wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.example.wear.snippets.alwayson
1818

1919
import android.Manifest
20+
import android.content.Intent
2021
import android.content.pm.PackageManager
2122
import android.os.Build
2223
import android.os.Bundle
@@ -25,12 +26,14 @@ import android.util.Log
2526
import androidx.activity.ComponentActivity
2627
import androidx.activity.compose.setContent
2728
import androidx.activity.result.contract.ActivityResultContracts
28-
import androidx.compose.foundation.layout.Box
29-
import androidx.compose.foundation.layout.Column
3029
import androidx.compose.foundation.layout.PaddingValues
3130
import androidx.compose.foundation.layout.Spacer
3231
import androidx.compose.foundation.layout.fillMaxSize
3332
import androidx.compose.foundation.layout.height
33+
import androidx.wear.compose.foundation.lazy.AutoCenteringParams
34+
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
35+
import androidx.wear.compose.foundation.lazy.items
36+
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
3437
import androidx.compose.runtime.Composable
3538
import androidx.compose.runtime.getValue
3639
import androidx.compose.runtime.mutableStateOf
@@ -141,46 +144,68 @@ fun ElapsedTime(ambientState: AmbientState) {
141144
@Composable
142145
fun WearApp() {
143146
val context = LocalContext.current
144-
var isOngoingActivity by rememberSaveable { mutableStateOf(AlwaysOnService.isRunning) }
147+
var runningService by rememberSaveable { mutableStateOf<Class<*>?>(null) }
148+
val listState = rememberScalingLazyListState()
149+
145150
MaterialTheme(
146151
colorScheme = dynamicColorScheme(LocalContext.current) ?: MaterialTheme.colorScheme
147152
) {
148-
// [START android_wear_ongoing_activity_ambientaware]
149153
AmbientAware { ambientState ->
150-
// [START_EXCLUDE]
151-
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
152-
Column(horizontalAlignment = Alignment.CenterHorizontally) {
154+
ScalingLazyColumn(
155+
modifier = Modifier.fillMaxSize(),
156+
state = listState,
157+
horizontalAlignment = Alignment.CenterHorizontally,
158+
autoCentering = AutoCenteringParams(itemIndex = 0)
159+
) {
160+
item {
153161
Text(text = "Elapsed Time", style = MaterialTheme.typography.titleLarge)
162+
}
163+
item {
154164
Spacer(modifier = Modifier.height(8.dp))
155-
// [END_EXCLUDE]
165+
}
166+
item {
156167
ElapsedTime(ambientState = ambientState)
157-
// [START_EXCLUDE]
168+
}
169+
item {
158170
Spacer(modifier = Modifier.height(8.dp))
171+
}
172+
173+
val services = listOf(
174+
AlwaysOnService1::class.java,
175+
AlwaysOnService2::class.java,
176+
AlwaysOnService3::class.java
177+
)
178+
179+
items(services.size) { index ->
180+
val serviceClass = services[index]
181+
val isRunning = runningService == serviceClass
159182
SwitchButton(
160-
checked = isOngoingActivity,
183+
checked = isRunning,
161184
onCheckedChange = { newState ->
162-
Log.d(TAG, "Switch button changed: $newState")
163-
isOngoingActivity = newState
164-
165185
if (newState) {
166-
Log.d(TAG, "Starting AlwaysOnService")
167-
AlwaysOnService.startService(context)
186+
if (runningService != null) {
187+
Log.d(TAG, "Stopping ${runningService?.simpleName}")
188+
context.stopService(Intent(context, runningService))
189+
}
190+
Log.d(TAG, "Starting ${serviceClass.simpleName}")
191+
val intent = Intent(context, serviceClass)
192+
context.startForegroundService(intent)
193+
runningService = serviceClass
168194
} else {
169-
Log.d(TAG, "Stopping AlwaysOnService")
170-
AlwaysOnService.stopService(context)
195+
Log.d(TAG, "Stopping ${serviceClass.simpleName}")
196+
context.stopService(Intent(context, serviceClass))
197+
runningService = null
171198
}
172199
},
173200
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
174201
) {
175202
Text(
176-
text = "Ongoing Activity",
177-
style = MaterialTheme.typography.bodyExtraSmall,
203+
text = "Ongoing Activity ${index + 1}",
204+
style = MaterialTheme.typography.bodySmall,
178205
)
179206
}
180207
}
181208
}
182-
// [END_EXCLUDE]
183209
}
184-
// [END android_wear_ongoing_activity_ambientaware]
185210
}
186211
}

wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt

Lines changed: 128 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@
1616

1717
package com.example.wear.snippets.alwayson
1818

19-
import android.app.Notification
2019
import android.app.NotificationChannel
2120
import android.app.NotificationManager
2221
import android.app.PendingIntent
23-
import android.content.Context
2422
import android.content.Intent
23+
import android.os.SystemClock
2524
import android.util.Log
2625
import androidx.core.app.NotificationCompat
2726
import androidx.core.content.getSystemService
@@ -30,29 +29,19 @@ import androidx.wear.ongoing.OngoingActivity
3029
import androidx.wear.ongoing.Status
3130
import com.example.wear.R
3231

33-
class AlwaysOnService : LifecycleService() {
32+
abstract class AlwaysOnServiceBase : LifecycleService() {
3433

3534
private val notificationManager by lazy { getSystemService<NotificationManager>() }
3635

3736
companion object {
3837
private const val TAG = "AlwaysOnService"
39-
private const val NOTIFICATION_ID = 1001
40-
private const val CHANNEL_ID = "always_on_service_channel"
38+
const val NOTIFICATION_ID = 1001
39+
const val CHANNEL_ID = "always_on_service_channel"
4140
private const val CHANNEL_NAME = "Always On Service"
41+
4242
@Volatile
4343
var isRunning = false
4444
private set
45-
46-
fun startService(context: Context) {
47-
Log.d(TAG, "Starting AlwaysOnService")
48-
val intent = Intent(context, AlwaysOnService::class.java)
49-
context.startForegroundService(intent)
50-
}
51-
52-
fun stopService(context: Context) {
53-
Log.d(TAG, "Stopping AlwaysOnService")
54-
context.stopService(Intent(context, AlwaysOnService::class.java))
55-
}
5645
}
5746

5847
override fun onCreate() {
@@ -66,9 +55,7 @@ class AlwaysOnService : LifecycleService() {
6655
super.onStartCommand(intent, flags, startId)
6756
Log.d(TAG, "onStartCommand: Service started with startId: $startId")
6857

69-
// Create and start foreground notification
70-
val notification = createNotification()
71-
startForeground(NOTIFICATION_ID, notification)
58+
createNotification()
7259

7360
Log.d(TAG, "onStartCommand: Service is now running as foreground service")
7461

@@ -93,8 +80,13 @@ class AlwaysOnService : LifecycleService() {
9380
Log.d(TAG, "createNotificationChannel: Notification channel created")
9481
}
9582

96-
// [START android_wear_ongoing_activity_create_notification]
97-
private fun createNotification(): Notification {
83+
abstract fun createNotification()
84+
}
85+
86+
class AlwaysOnService1 : AlwaysOnServiceBase() {
87+
override fun createNotification() {
88+
// Creates an ongoing activity that demonstrates how to link the touch intent to the always-on activity.
89+
// [START android_wear_ongoing_activity_create_notification]
9890
val activityIntent =
9991
Intent(this, AlwaysOnActivity::class.java).apply {
10092
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
@@ -122,7 +114,6 @@ class AlwaysOnService : LifecycleService() {
122114
.setOngoing(true)
123115

124116
// [START_EXCLUDE]
125-
// Create an Ongoing Activity
126117
val ongoingActivityStatus = Status.Builder().addTemplate("Stopwatch running").build()
127118
// [END_EXCLUDE]
128119

@@ -139,7 +130,120 @@ class AlwaysOnService : LifecycleService() {
139130

140131
ongoingActivity.apply(applicationContext)
141132

142-
return notificationBuilder.build()
133+
val notification = notificationBuilder.build()
134+
// [END android_wear_ongoing_activity_create_notification]
135+
136+
startForeground(NOTIFICATION_ID, notification)
137+
}
138+
}
139+
140+
class AlwaysOnService2 : AlwaysOnServiceBase() {
141+
override fun createNotification() {
142+
// Creates an ongoing activity with a static status text
143+
144+
// [START android_wear_ongoing_activity_notification_builder]
145+
// Create a PendingIntent to pass to the notification builder
146+
val pendingIntent =
147+
PendingIntent.getActivity(
148+
this,
149+
0,
150+
Intent(this, AlwaysOnActivity::class.java).apply {
151+
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
152+
},
153+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
154+
)
155+
156+
val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
157+
.setContentTitle("Always On Service")
158+
.setContentText("Service is running in background")
159+
.setSmallIcon(R.drawable.animated_walk)
160+
// Category helps the system prioritize the ongoing activity
161+
.setCategory(NotificationCompat.CATEGORY_WORKOUT)
162+
.setContentIntent(pendingIntent)
163+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
164+
.setOngoing(true) // Important!
165+
// [END android_wear_ongoing_activity_notification_builder]
166+
167+
// [START android_wear_ongoing_activity_builder]
168+
val ongoingActivity =
169+
OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder)
170+
// Sets the icon that appears on the watch face in active mode.
171+
.setAnimatedIcon(R.drawable.animated_walk)
172+
// Sets the icon that appears on the watch face in ambient mode.
173+
.setStaticIcon(R.drawable.ic_walk)
174+
// Sets the tap target to bring the user back to the app.
175+
.setTouchIntent(pendingIntent)
176+
.build()
177+
// [END android_wear_ongoing_activity_builder]
178+
179+
// [START android_wear_ongoing_activity_post_notification]
180+
// This call modifies notificationBuilder to include the ongoing activity data.
181+
ongoingActivity.apply(applicationContext)
182+
183+
// Post the notification.
184+
startForeground(NOTIFICATION_ID, notificationBuilder.build())
185+
// [END android_wear_ongoing_activity_post_notification]
186+
}
187+
}
188+
189+
class AlwaysOnService3 : AlwaysOnServiceBase() {
190+
override fun createNotification() {
191+
// Creates an ongoing activity that demonstrates dynamic status text (a timer)
192+
val activityIntent =
193+
Intent(this, AlwaysOnActivity::class.java).apply {
194+
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
195+
}
196+
197+
val pendingIntent =
198+
PendingIntent.getActivity(
199+
this,
200+
0,
201+
activityIntent,
202+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
203+
)
204+
205+
val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
206+
.setContentTitle("Always On Service")
207+
.setContentText("Service is running in background")
208+
.setSmallIcon(R.drawable.animated_walk)
209+
// Category helps the system prioritize the ongoing activity
210+
.setCategory(NotificationCompat.CATEGORY_WORKOUT)
211+
.setContentIntent(pendingIntent)
212+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
213+
.setOngoing(true) // Important!
214+
215+
// [START android_wear_ongoing_activity_create_status]
216+
// Define a template with placeholders for the activity type and the timer.
217+
val statusTemplate = "#type# for #time#"
218+
219+
// Set the start time for a stopwatch.
220+
// Use SystemClock.elapsedRealtime() for time-based parts.
221+
val runStartTime = SystemClock.elapsedRealtime()
222+
223+
val ongoingActivityStatus = Status.Builder()
224+
// Sets the template string.
225+
.addTemplate(statusTemplate)
226+
// Fills the #type# placeholder with a static text part.
227+
.addPart("type", Status.TextPart("Run"))
228+
// Fills the #time# placeholder with a stopwatch part.
229+
.addPart("time", Status.StopwatchPart(runStartTime))
230+
.build()
231+
// [END android_wear_ongoing_activity_create_status]
232+
233+
// [START android_wear_ongoing_activity_set_status]
234+
val ongoingActivity =
235+
OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder)
236+
// [START_EXCLUDE]
237+
.setAnimatedIcon(R.drawable.animated_walk)
238+
.setStaticIcon(R.drawable.ic_walk)
239+
.setTouchIntent(pendingIntent)
240+
// [END_EXCLUDE]
241+
// Add the status to the OngoingActivity.
242+
.setStatus(ongoingActivityStatus)
243+
.build()
244+
// [END android_wear_ongoing_activity_set_status]
245+
246+
ongoingActivity.apply(applicationContext)
247+
startForeground(NOTIFICATION_ID, notificationBuilder.build())
143248
}
144-
// [END android_wear_ongoing_activity_create_notification]
145249
}

0 commit comments

Comments
 (0)