android-sdd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +143 -0
- package/package.json +27 -0
- package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
- package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
- package/skills/Android Platform/Configuration/SKILL.md +201 -0
- package/skills/Android Platform/Filesystem/SKILL.md +216 -0
- package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
- package/skills/Android Platform/Manifest/SKILL.md +226 -0
- package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
- package/skills/Android Platform/Resources/SKILL.md +234 -0
- package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
- package/skills/Android Platform/State Restoration/SKILL.md +210 -0
- package/skills/Architecture/Bounded Context/SKILL.md +207 -0
- package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
- package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
- package/skills/Architecture/Entity Design/SKILL.md +243 -0
- package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
- package/skills/Architecture/MVI/SKILL.md +224 -0
- package/skills/Architecture/MVVM/SKILL.md +198 -0
- package/skills/Architecture/Modularization/SKILL.md +194 -0
- package/skills/Architecture/Offline First/SKILL.md +249 -0
- package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
- package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
- package/skills/Architecture/State Management/SKILL.md +229 -0
- package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
- package/skills/Architecture/Use Case Design/SKILL.md +244 -0
- package/skills/Architecture/Value Object/SKILL.md +226 -0
- package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
- package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
- package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
- package/skills/Build System/Build Cache/SKILL.md +233 -0
- package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
- package/skills/Build System/Build Variant/SKILL.md +215 -0
- package/skills/Build System/Convention Plugin/SKILL.md +288 -0
- package/skills/Build System/Dependency Management/SKILL.md +261 -0
- package/skills/Build System/Gradle/SKILL.md +284 -0
- package/skills/Build System/Incremental Build/SKILL.md +199 -0
- package/skills/Build System/KAPT/SKILL.md +198 -0
- package/skills/Build System/KSP/SKILL.md +263 -0
- package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
- package/skills/Build System/Specialized/C++/SKILL.md +308 -0
- package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
- package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
- package/skills/Build System/Version Catalog/SKILL.md +304 -0
- package/skills/Concurrency/Background Processing/SKILL.md +185 -0
- package/skills/Concurrency/Channel/SKILL.md +207 -0
- package/skills/Concurrency/Coroutine/SKILL.md +200 -0
- package/skills/Concurrency/Flow/SKILL.md +179 -0
- package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
- package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
- package/skills/Concurrency/StateFlow/SKILL.md +175 -0
- package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
- package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
- package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
- package/skills/Core Language/DSL/SKILL.md +186 -0
- package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
- package/skills/Core Language/Immutability/SKILL.md +156 -0
- package/skills/Core Language/KMP/SKILL.md +182 -0
- package/skills/Core Language/Kotlin/SKILL.md +187 -0
- package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
- package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
- package/skills/Core Language/Serialization/SKILL.md +191 -0
- package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
- package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
- package/skills/Data Layer/DAO/SKILL.md +225 -0
- package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
- package/skills/Data Layer/DataStore/SKILL.md +264 -0
- package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
- package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
- package/skills/Data Layer/File Storage/SKILL.md +247 -0
- package/skills/Data Layer/Indexing/SKILL.md +184 -0
- package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
- package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
- package/skills/Data Layer/Migration/SKILL.md +243 -0
- package/skills/Data Layer/Paging/SKILL.md +264 -0
- package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
- package/skills/Data Layer/Room/SKILL.md +244 -0
- package/skills/Data Layer/SQLite/SKILL.md +255 -0
- package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
- package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
- package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
- package/skills/Dependency Injection/Koin/SKILL.md +282 -0
- package/skills/Developer Experience/Detekt/SKILL.md +272 -0
- package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
- package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
- package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
- package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
- package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
- package/skills/Media/Audio/SKILL.md +257 -0
- package/skills/Media/Camera/SKILL.md +229 -0
- package/skills/Media/CameraX/SKILL.md +295 -0
- package/skills/Media/ExoPlayer/SKILL.md +258 -0
- package/skills/Media/Video/SKILL.md +228 -0
- package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
- package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
- package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
- package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
- package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
- package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
- package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
- package/skills/Navigation/Navigation/SKILL.md +215 -0
- package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
- package/skills/Networking/API Contract/SKILL.md +220 -0
- package/skills/Networking/Authentication/SKILL.md +210 -0
- package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
- package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
- package/skills/Networking/Ktor/SKILL.md +219 -0
- package/skills/Networking/Multipart Upload/SKILL.md +213 -0
- package/skills/Networking/OkHttp/SKILL.md +193 -0
- package/skills/Networking/REST/SKILL.md +178 -0
- package/skills/Networking/Rate Limiting/SKILL.md +170 -0
- package/skills/Networking/Retrofit/SKILL.md +241 -0
- package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
- package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
- package/skills/Networking/WebSocket/SKILL.md +224 -0
- package/skills/Observability/Crash Reporting/SKILL.md +219 -0
- package/skills/Observability/Logging/SKILL.md +168 -0
- package/skills/Observability/Metrics/SKILL.md +227 -0
- package/skills/Observability/Structured Logging/SKILL.md +234 -0
- package/skills/Performance/ANR Prevention/SKILL.md +192 -0
- package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
- package/skills/Performance/App Startup/SKILL.md +183 -0
- package/skills/Performance/Baseline Profile/SKILL.md +205 -0
- package/skills/Performance/Battery Optimization/SKILL.md +192 -0
- package/skills/Performance/Benchmark/SKILL.md +182 -0
- package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
- package/skills/Performance/Compose Optimization/SKILL.md +187 -0
- package/skills/Performance/Heap Management/SKILL.md +184 -0
- package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
- package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
- package/skills/Performance/Rendering Performance/SKILL.md +205 -0
- package/skills/Performance/Startup Optimization/SKILL.md +219 -0
- package/skills/Security/Biometric/SKILL.md +224 -0
- package/skills/Security/Certificate Transparency/SKILL.md +158 -0
- package/skills/Security/Cryptography/SKILL.md +244 -0
- package/skills/Security/Encrypted Storage/SKILL.md +273 -0
- package/skills/Security/Frida Detection/SKILL.md +230 -0
- package/skills/Security/Hook Detection/SKILL.md +197 -0
- package/skills/Security/Keystore/SKILL.md +272 -0
- package/skills/Security/Network Security Config/SKILL.md +186 -0
- package/skills/Security/Obfuscation/SKILL.md +226 -0
- package/skills/Security/Proguard/SKILL.md +202 -0
- package/skills/Security/R8/SKILL.md +234 -0
- package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
- package/skills/Security/Root Detection/SKILL.md +220 -0
- package/skills/Security/Secure Networking/SKILL.md +220 -0
- package/skills/System Integration/AlarmManager/SKILL.md +182 -0
- package/skills/System Integration/App Widget/SKILL.md +182 -0
- package/skills/System Integration/Deep Link/SKILL.md +187 -0
- package/skills/System Integration/Foreground Service/SKILL.md +212 -0
- package/skills/System Integration/Notification/SKILL.md +237 -0
- package/skills/System Integration/WorkManager/SKILL.md +256 -0
- package/skills/System Integration/clipboard/SKILL.md +155 -0
- package/skills/System Integration/share-intent/SKILL.md +182 -0
- package/skills/Testing/Compose Testing/SKILL.md +296 -0
- package/skills/Testing/Espresso/SKILL.md +292 -0
- package/skills/Testing/Fake Data/SKILL.md +245 -0
- package/skills/Testing/Integration Testing/SKILL.md +288 -0
- package/skills/Testing/Mocking/SKILL.md +229 -0
- package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
- package/skills/Testing/UI Testing/SKILL.md +293 -0
- package/skills/Testing/Unit Testing/SKILL.md +309 -0
- package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
- package/skills/UI System/Compose/SKILL.md +296 -0
- package/skills/UI System/Compose Animation/SKILL.md +281 -0
- package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
- package/skills/UI System/Compose Navigation/SKILL.md +255 -0
- package/skills/UI System/Compose Performance/SKILL.md +274 -0
- package/skills/UI System/Design System/SKILL.md +217 -0
- package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
- package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
- package/skills/UI System/Loading Strategy/SKILL.md +254 -0
- package/skills/UI System/Material 3/SKILL.md +279 -0
- package/skills/UI System/RTL/SKILL.md +179 -0
- package/src/index.ts +182 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: notification
|
|
3
|
+
description: >
|
|
4
|
+
Android notification system implementation.
|
|
5
|
+
Load this skill when creating notification channels, building
|
|
6
|
+
notifications, handling notification permissions, showing progress
|
|
7
|
+
notifications, or adding actions and deep links to notifications.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Notification
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Android notifications require a notification channel (API 26+), a POST_NOTIFICATIONS permission request (API 33+), and a properly built `Notification` object. Notifications can contain actions, deep link intents, progress indicators, and custom layouts.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Create notification channels **once** at app startup — not on every notification
|
|
20
|
+
- Request `POST_NOTIFICATIONS` permission on Android 13+ before showing notifications
|
|
21
|
+
- Use `NotificationCompat` — not the platform `Notification` directly
|
|
22
|
+
- Always provide a `contentIntent` — tapping the notification should open the relevant screen
|
|
23
|
+
- Group related notifications with a summary notification
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Permissions
|
|
28
|
+
|
|
29
|
+
```xml
|
|
30
|
+
<!-- AndroidManifest.xml -->
|
|
31
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```kotlin
|
|
35
|
+
// ✅ Request permission on Android 13+
|
|
36
|
+
class MainActivity : ComponentActivity() {
|
|
37
|
+
private val requestPermissionLauncher = registerForActivityResult(
|
|
38
|
+
ActivityResultContracts.RequestPermission()
|
|
39
|
+
) { isGranted ->
|
|
40
|
+
if (!isGranted) {
|
|
41
|
+
// Show rationale or disable notification features
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fun requestNotificationPermission() {
|
|
46
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
47
|
+
if (ContextCompat.checkSelfPermission(
|
|
48
|
+
this, Manifest.permission.POST_NOTIFICATIONS
|
|
49
|
+
) != PackageManager.PERMISSION_GRANTED
|
|
50
|
+
) {
|
|
51
|
+
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Notification Channels
|
|
61
|
+
|
|
62
|
+
```kotlin
|
|
63
|
+
// ✅ Create channels once at app startup
|
|
64
|
+
class NotificationChannelSetup @Inject constructor(
|
|
65
|
+
@ApplicationContext private val context: Context
|
|
66
|
+
) {
|
|
67
|
+
fun createChannels() {
|
|
68
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
69
|
+
|
|
70
|
+
val manager = context.getSystemService(NotificationManager::class.java)
|
|
71
|
+
|
|
72
|
+
manager.createNotificationChannel(
|
|
73
|
+
NotificationChannel(
|
|
74
|
+
CHANNEL_MESSAGES,
|
|
75
|
+
"Messages",
|
|
76
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
77
|
+
).apply {
|
|
78
|
+
description = "New message notifications"
|
|
79
|
+
enableVibration(true)
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
manager.createNotificationChannel(
|
|
84
|
+
NotificationChannel(
|
|
85
|
+
CHANNEL_SYNC,
|
|
86
|
+
"Sync",
|
|
87
|
+
NotificationManager.IMPORTANCE_LOW
|
|
88
|
+
).apply {
|
|
89
|
+
description = "Background sync status"
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
companion object {
|
|
95
|
+
const val CHANNEL_MESSAGES = "channel_messages"
|
|
96
|
+
const val CHANNEL_SYNC = "channel_sync"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Basic Notification
|
|
104
|
+
|
|
105
|
+
```kotlin
|
|
106
|
+
// ✅ Simple notification with deep link intent
|
|
107
|
+
fun showMessageNotification(context: Context, message: Message) {
|
|
108
|
+
val deepLinkIntent = Intent(
|
|
109
|
+
Intent.ACTION_VIEW,
|
|
110
|
+
Uri.parse("https://example.com/messages/${message.id}")
|
|
111
|
+
).apply {
|
|
112
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
val pendingIntent = TaskStackBuilder.create(context).run {
|
|
116
|
+
addNextIntentWithParentStack(deepLinkIntent)
|
|
117
|
+
getPendingIntent(
|
|
118
|
+
message.id.hashCode(),
|
|
119
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
val notification = NotificationCompat.Builder(context, CHANNEL_MESSAGES)
|
|
124
|
+
.setSmallIcon(R.drawable.ic_message)
|
|
125
|
+
.setContentTitle(message.senderName)
|
|
126
|
+
.setContentText(message.preview)
|
|
127
|
+
.setStyle(NotificationCompat.BigTextStyle().bigText(message.body))
|
|
128
|
+
.setContentIntent(pendingIntent)
|
|
129
|
+
.setAutoCancel(true)
|
|
130
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
131
|
+
.build()
|
|
132
|
+
|
|
133
|
+
NotificationManagerCompat.from(context)
|
|
134
|
+
.notify(message.id.hashCode(), notification)
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Notification with Actions
|
|
141
|
+
|
|
142
|
+
```kotlin
|
|
143
|
+
// ✅ Action buttons on notification
|
|
144
|
+
val replyIntent = PendingIntent.getBroadcast(
|
|
145
|
+
context,
|
|
146
|
+
0,
|
|
147
|
+
Intent(context, ReplyReceiver::class.java).putExtra("message_id", messageId),
|
|
148
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
val markReadIntent = PendingIntent.getBroadcast(
|
|
152
|
+
context,
|
|
153
|
+
1,
|
|
154
|
+
Intent(context, MarkReadReceiver::class.java).putExtra("message_id", messageId),
|
|
155
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
NotificationCompat.Builder(context, CHANNEL_MESSAGES)
|
|
159
|
+
.addAction(R.drawable.ic_reply, "Reply", replyIntent)
|
|
160
|
+
.addAction(R.drawable.ic_check, "Mark Read", markReadIntent)
|
|
161
|
+
.build()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Progress Notification
|
|
167
|
+
|
|
168
|
+
```kotlin
|
|
169
|
+
// ✅ Update progress notification
|
|
170
|
+
fun showProgressNotification(context: Context, progress: Int, done: Boolean) {
|
|
171
|
+
val notification = NotificationCompat.Builder(context, CHANNEL_SYNC)
|
|
172
|
+
.setSmallIcon(R.drawable.ic_sync)
|
|
173
|
+
.setContentTitle("Uploading file")
|
|
174
|
+
.setContentText(if (done) "Upload complete" else "$progress%")
|
|
175
|
+
.setProgress(100, progress, false)
|
|
176
|
+
.setOngoing(!done)
|
|
177
|
+
.setOnlyAlertOnce(true) // ✅ don't re-alert on every update
|
|
178
|
+
.build()
|
|
179
|
+
|
|
180
|
+
NotificationManagerCompat.from(context).notify(NOTIFICATION_ID_UPLOAD, notification)
|
|
181
|
+
|
|
182
|
+
if (done) {
|
|
183
|
+
// Remove progress after delay
|
|
184
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
185
|
+
NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID_UPLOAD)
|
|
186
|
+
}, 3_000)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Grouped Notifications
|
|
194
|
+
|
|
195
|
+
```kotlin
|
|
196
|
+
// ✅ Group multiple notifications with a summary
|
|
197
|
+
val GROUP_KEY = "com.example.MESSAGES"
|
|
198
|
+
|
|
199
|
+
// Individual notifications
|
|
200
|
+
messages.forEach { message ->
|
|
201
|
+
val notification = NotificationCompat.Builder(context, CHANNEL_MESSAGES)
|
|
202
|
+
.setSmallIcon(R.drawable.ic_message)
|
|
203
|
+
.setContentTitle(message.sender)
|
|
204
|
+
.setContentText(message.preview)
|
|
205
|
+
.setGroup(GROUP_KEY)
|
|
206
|
+
.build()
|
|
207
|
+
manager.notify(message.id.hashCode(), notification)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Summary notification
|
|
211
|
+
val summary = NotificationCompat.Builder(context, CHANNEL_MESSAGES)
|
|
212
|
+
.setSmallIcon(R.drawable.ic_message)
|
|
213
|
+
.setStyle(NotificationCompat.InboxStyle()
|
|
214
|
+
.setSummaryText("${messages.size} new messages"))
|
|
215
|
+
.setGroup(GROUP_KEY)
|
|
216
|
+
.setGroupSummary(true)
|
|
217
|
+
.build()
|
|
218
|
+
manager.notify(SUMMARY_ID, summary)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Anti-Patterns
|
|
224
|
+
|
|
225
|
+
- Creating notification channels on every `notify()` call — create once at startup
|
|
226
|
+
- Not providing a `contentIntent` — tapping the notification does nothing
|
|
227
|
+
- Using `IMPORTANCE_HIGH` for background sync notifications — disrupts the user
|
|
228
|
+
- Not setting `FLAG_IMMUTABLE` on `PendingIntent` — crashes on Android 12+
|
|
229
|
+
- Showing notifications without requesting `POST_NOTIFICATIONS` on Android 13+
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Related Skills
|
|
234
|
+
- `deep-navigation` — building deep link intents for notifications
|
|
235
|
+
- `foreground-service` — persistent service notification
|
|
236
|
+
- `alarmmanager` — triggering notifications at a specific time
|
|
237
|
+
- `firebase-messaging` — push notifications via FCM
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workmanager
|
|
3
|
+
description: >
|
|
4
|
+
WorkManager for guaranteed background task execution in Android.
|
|
5
|
+
Load this skill when scheduling deferrable work, running periodic tasks,
|
|
6
|
+
chaining workers, handling constraints, observing work state,
|
|
7
|
+
or running tasks that must survive app and device restarts.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# WorkManager
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
WorkManager is the recommended solution for deferrable, guaranteed background work in Android. It handles Doze mode, app death, and device restarts transparently. It supports one-time and periodic work, constraints, chaining, and progress reporting.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Use WorkManager for work that **must complete** even if the app is killed
|
|
20
|
+
- Use `CoroutineWorker` — not `Worker` — for suspend-friendly work
|
|
21
|
+
- Set **constraints** to avoid running on metered networks or low battery
|
|
22
|
+
- Use **unique work** to prevent duplicate tasks
|
|
23
|
+
- Observe work state via `WorkInfo` — never poll manually
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
# libs.versions.toml
|
|
31
|
+
[versions]
|
|
32
|
+
workmanager = "2.9.0"
|
|
33
|
+
|
|
34
|
+
[libraries]
|
|
35
|
+
workmanager = { module = "androidx.work:work-runtime-ktx", version.ref = "workmanager" }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## CoroutineWorker
|
|
41
|
+
|
|
42
|
+
```kotlin
|
|
43
|
+
// ✅ Standard worker
|
|
44
|
+
class SyncWorker(
|
|
45
|
+
context: Context,
|
|
46
|
+
params: WorkerParameters
|
|
47
|
+
) : CoroutineWorker(context, params) {
|
|
48
|
+
|
|
49
|
+
override suspend fun doWork(): Result {
|
|
50
|
+
return try {
|
|
51
|
+
val userId = inputData.getString(KEY_USER_ID)
|
|
52
|
+
?: return Result.failure()
|
|
53
|
+
|
|
54
|
+
syncRepository.syncUser(userId)
|
|
55
|
+
Result.success()
|
|
56
|
+
} catch (e: Exception) {
|
|
57
|
+
if (runAttemptCount < 3) Result.retry()
|
|
58
|
+
else Result.failure(
|
|
59
|
+
workDataOf("error" to (e.message ?: "Unknown"))
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
companion object {
|
|
65
|
+
const val KEY_USER_ID = "user_id"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## One-Time Work
|
|
73
|
+
|
|
74
|
+
```kotlin
|
|
75
|
+
// ✅ Enqueue one-time work
|
|
76
|
+
fun scheduleSyncUser(userId: String) {
|
|
77
|
+
val request = OneTimeWorkRequestBuilder<SyncWorker>()
|
|
78
|
+
.setInputData(workDataOf(SyncWorker.KEY_USER_ID to userId))
|
|
79
|
+
.setConstraints(
|
|
80
|
+
Constraints.Builder()
|
|
81
|
+
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
82
|
+
.build()
|
|
83
|
+
)
|
|
84
|
+
.setBackoffCriteria(
|
|
85
|
+
BackoffPolicy.EXPONENTIAL,
|
|
86
|
+
WorkRequest.MIN_BACKOFF_MILLIS,
|
|
87
|
+
TimeUnit.MILLISECONDS
|
|
88
|
+
)
|
|
89
|
+
.addTag("sync")
|
|
90
|
+
.build()
|
|
91
|
+
|
|
92
|
+
WorkManager.getInstance(context).enqueueUniqueWork(
|
|
93
|
+
"sync_user_$userId",
|
|
94
|
+
ExistingWorkPolicy.KEEP, // don't replace if already queued
|
|
95
|
+
request
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Periodic Work
|
|
103
|
+
|
|
104
|
+
```kotlin
|
|
105
|
+
// ✅ Periodic sync — minimum interval is 15 minutes
|
|
106
|
+
fun schedulePeriodicSync() {
|
|
107
|
+
val request = PeriodicWorkRequestBuilder<SyncWorker>(
|
|
108
|
+
repeatInterval = 1,
|
|
109
|
+
repeatIntervalTimeUnit = TimeUnit.HOURS
|
|
110
|
+
)
|
|
111
|
+
.setConstraints(
|
|
112
|
+
Constraints.Builder()
|
|
113
|
+
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
114
|
+
.setRequiresBatteryNotLow(true)
|
|
115
|
+
.build()
|
|
116
|
+
)
|
|
117
|
+
.build()
|
|
118
|
+
|
|
119
|
+
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
|
120
|
+
"periodic_sync",
|
|
121
|
+
ExistingPeriodicWorkPolicy.KEEP,
|
|
122
|
+
request
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Chaining Workers
|
|
130
|
+
|
|
131
|
+
```kotlin
|
|
132
|
+
// ✅ Sequential chain
|
|
133
|
+
WorkManager.getInstance(context)
|
|
134
|
+
.beginUniqueWork(
|
|
135
|
+
"upload_pipeline",
|
|
136
|
+
ExistingWorkPolicy.REPLACE,
|
|
137
|
+
OneTimeWorkRequestBuilder<CompressWorker>().build()
|
|
138
|
+
)
|
|
139
|
+
.then(OneTimeWorkRequestBuilder<UploadWorker>().build())
|
|
140
|
+
.then(OneTimeWorkRequestBuilder<NotifyWorker>().build())
|
|
141
|
+
.enqueue()
|
|
142
|
+
|
|
143
|
+
// ✅ Parallel then merge
|
|
144
|
+
val syncA = OneTimeWorkRequestBuilder<SyncAWorker>().build()
|
|
145
|
+
val syncB = OneTimeWorkRequestBuilder<SyncBWorker>().build()
|
|
146
|
+
val merge = OneTimeWorkRequestBuilder<MergeWorker>().build()
|
|
147
|
+
|
|
148
|
+
WorkManager.getInstance(context)
|
|
149
|
+
.beginWith(listOf(syncA, syncB))
|
|
150
|
+
.then(merge)
|
|
151
|
+
.enqueue()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Progress Reporting
|
|
157
|
+
|
|
158
|
+
```kotlin
|
|
159
|
+
// ✅ Report progress from worker
|
|
160
|
+
class UploadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
|
161
|
+
override suspend fun doWork(): Result {
|
|
162
|
+
val files = getFilesToUpload()
|
|
163
|
+
files.forEachIndexed { index, file ->
|
|
164
|
+
uploadFile(file)
|
|
165
|
+
val progress = ((index + 1) * 100) / files.size
|
|
166
|
+
setProgress(workDataOf("progress" to progress))
|
|
167
|
+
}
|
|
168
|
+
return Result.success()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ✅ Observe progress in ViewModel
|
|
173
|
+
val workInfo: Flow<WorkInfo?> = workManager
|
|
174
|
+
.getWorkInfosForUniqueWorkFlow("upload_pipeline")
|
|
175
|
+
.map { it.firstOrNull() }
|
|
176
|
+
|
|
177
|
+
// In composable
|
|
178
|
+
val progress = workInfo?.progress?.getInt("progress", 0) ?: 0
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Observing Work State
|
|
184
|
+
|
|
185
|
+
```kotlin
|
|
186
|
+
// ✅ Observe in ViewModel
|
|
187
|
+
class SyncViewModel @Inject constructor(
|
|
188
|
+
private val workManager: WorkManager
|
|
189
|
+
) : ViewModel() {
|
|
190
|
+
|
|
191
|
+
val syncState: StateFlow<SyncUiState> = workManager
|
|
192
|
+
.getWorkInfosForUniqueWorkFlow("periodic_sync")
|
|
193
|
+
.map { workInfos ->
|
|
194
|
+
when (workInfos.firstOrNull()?.state) {
|
|
195
|
+
WorkInfo.State.RUNNING -> SyncUiState.Syncing
|
|
196
|
+
WorkInfo.State.SUCCEEDED -> SyncUiState.Success
|
|
197
|
+
WorkInfo.State.FAILED -> SyncUiState.Failed
|
|
198
|
+
else -> SyncUiState.Idle
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), SyncUiState.Idle)
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Hilt Integration
|
|
208
|
+
|
|
209
|
+
```kotlin
|
|
210
|
+
// ✅ Inject dependencies into Worker with Hilt
|
|
211
|
+
@HiltWorker
|
|
212
|
+
class SyncWorker @AssistedInject constructor(
|
|
213
|
+
@Assisted context: Context,
|
|
214
|
+
@Assisted params: WorkerParameters,
|
|
215
|
+
private val syncRepository: SyncRepository
|
|
216
|
+
) : CoroutineWorker(context, params) {
|
|
217
|
+
|
|
218
|
+
override suspend fun doWork(): Result {
|
|
219
|
+
return try {
|
|
220
|
+
syncRepository.sync()
|
|
221
|
+
Result.success()
|
|
222
|
+
} catch (e: Exception) {
|
|
223
|
+
if (runAttemptCount < 3) Result.retry() else Result.failure()
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// In Application class
|
|
229
|
+
@HiltAndroidApp
|
|
230
|
+
class App : Application(), Configuration.Provider {
|
|
231
|
+
@Inject lateinit var workerFactory: HiltWorkerFactory
|
|
232
|
+
|
|
233
|
+
override val workManagerConfiguration
|
|
234
|
+
get() = Configuration.Builder()
|
|
235
|
+
.setWorkerFactory(workerFactory)
|
|
236
|
+
.build()
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Anti-Patterns
|
|
243
|
+
|
|
244
|
+
- Using `Worker` instead of `CoroutineWorker` — blocks the thread
|
|
245
|
+
- Not using unique work — creates duplicate background tasks
|
|
246
|
+
- Not setting network constraints for network-dependent workers
|
|
247
|
+
- Doing UI updates directly inside a Worker — use WorkInfo observation
|
|
248
|
+
- Using WorkManager for immediate short tasks — use coroutines instead
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Related Skills
|
|
253
|
+
- `background-processing` — choosing the right background tool
|
|
254
|
+
- `foreground-service` — long-running user-visible work
|
|
255
|
+
- `hilt` — injecting dependencies into workers
|
|
256
|
+
- `coroutine` — coroutine fundamentals used inside CoroutineWorker
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clipboard
|
|
3
|
+
description: >
|
|
4
|
+
Clipboard read and write operations in Android.
|
|
5
|
+
Load this skill when copying text or URIs to the clipboard,
|
|
6
|
+
reading clipboard content, showing copy confirmation feedback,
|
|
7
|
+
handling clipboard access restrictions on Android 10+,
|
|
8
|
+
or implementing paste functionality.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Clipboard
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
The Android clipboard allows apps to copy and paste text, URIs, and other data. Since Android 10, apps can only read the clipboard when they are in the foreground. Android 13 shows a visual confirmation when an app reads clipboard content. Best practice is to write to clipboard and show immediate in-app feedback rather than reading silently.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Always show **visual feedback** after copying — toast or snackbar
|
|
21
|
+
- Never read clipboard silently in background — it's restricted on Android 10+
|
|
22
|
+
- Use `ClipData.newPlainText` for text — `ClipData.newUri` for URIs
|
|
23
|
+
- On Android 13+, the system shows a clipboard toast automatically — avoid double-toasting
|
|
24
|
+
- Sensitive content (passwords, tokens) should set `ClipDescription.EXTRA_IS_SENSITIVE`
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Writing to Clipboard
|
|
29
|
+
|
|
30
|
+
```kotlin
|
|
31
|
+
// ✅ Copy text to clipboard
|
|
32
|
+
fun copyToClipboard(context: Context, text: String, label: String = "Copied text") {
|
|
33
|
+
val clipboard = context.getSystemService(ClipboardManager::class.java)
|
|
34
|
+
val clip = ClipData.newPlainText(label, text)
|
|
35
|
+
clipboard.setPrimaryClip(clip)
|
|
36
|
+
|
|
37
|
+
// ✅ Show feedback only on Android 12 and below
|
|
38
|
+
// Android 13+ shows system toast automatically
|
|
39
|
+
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
|
40
|
+
Toast.makeText(context, "Copied", Toast.LENGTH_SHORT).show()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ✅ Copy sensitive content (password, token)
|
|
45
|
+
fun copySensitiveToClipboard(context: Context, text: String) {
|
|
46
|
+
val clipboard = context.getSystemService(ClipboardManager::class.java)
|
|
47
|
+
val clip = ClipData.newPlainText("sensitive", text).apply {
|
|
48
|
+
description.extras = PersistableBundle().apply {
|
|
49
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
50
|
+
putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
clipboard.setPrimaryClip(clip)
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Reading from Clipboard
|
|
61
|
+
|
|
62
|
+
```kotlin
|
|
63
|
+
// ✅ Read clipboard — only when app is in foreground
|
|
64
|
+
fun readFromClipboard(context: Context): String? {
|
|
65
|
+
val clipboard = context.getSystemService(ClipboardManager::class.java)
|
|
66
|
+
val clip = clipboard.primaryClip ?: return null
|
|
67
|
+
if (clip.itemCount == 0) return null
|
|
68
|
+
return clip.getItemAt(0).coerceToText(context).toString()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ✅ Check if clipboard has text before reading
|
|
72
|
+
fun hasTextInClipboard(context: Context): Boolean {
|
|
73
|
+
val clipboard = context.getSystemService(ClipboardManager::class.java)
|
|
74
|
+
return clipboard.hasPrimaryClip() &&
|
|
75
|
+
clipboard.primaryClipDescription?.hasMimeType("text/*") == true
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Clipboard in Compose
|
|
82
|
+
|
|
83
|
+
```kotlin
|
|
84
|
+
// ✅ Copy on long press or button tap
|
|
85
|
+
@Composable
|
|
86
|
+
fun CopyableText(text: String) {
|
|
87
|
+
val context = LocalContext.current
|
|
88
|
+
val clipboardManager = LocalClipboardManager.current
|
|
89
|
+
|
|
90
|
+
Text(
|
|
91
|
+
text = text,
|
|
92
|
+
modifier = Modifier.clickable {
|
|
93
|
+
clipboardManager.setText(AnnotatedString(text))
|
|
94
|
+
// Show feedback on Android 12 and below
|
|
95
|
+
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
|
96
|
+
Toast.makeText(context, "Copied", Toast.LENGTH_SHORT).show()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ✅ Paste button in Compose
|
|
103
|
+
@Composable
|
|
104
|
+
fun PasteButton(onPaste: (String) -> Unit) {
|
|
105
|
+
val clipboardManager = LocalClipboardManager.current
|
|
106
|
+
|
|
107
|
+
Button(onClick = {
|
|
108
|
+
val text = clipboardManager.getText()?.text ?: return@Button
|
|
109
|
+
onPaste(text)
|
|
110
|
+
}) {
|
|
111
|
+
Text("Paste")
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Clipboard Change Listener
|
|
119
|
+
|
|
120
|
+
```kotlin
|
|
121
|
+
// ✅ Listen for clipboard changes (e.g. autofill OTP)
|
|
122
|
+
class OtpViewModel : ViewModel() {
|
|
123
|
+
|
|
124
|
+
private val clipboardListener = ClipboardManager.OnPrimaryClipChangedListener {
|
|
125
|
+
val text = readFromClipboard(context) ?: return@OnPrimaryClipChangedListener
|
|
126
|
+
if (text.matches(Regex("\\d{6}"))) {
|
|
127
|
+
_otpInput.value = text
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fun registerClipboardListener(clipboard: ClipboardManager) {
|
|
132
|
+
clipboard.addPrimaryClipChangedListener(clipboardListener)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fun unregisterClipboardListener(clipboard: ClipboardManager) {
|
|
136
|
+
clipboard.removePrimaryClipChangedListener(clipboardListener)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Anti-Patterns
|
|
144
|
+
|
|
145
|
+
- Reading clipboard in the background — restricted on Android 10+, throws SecurityException
|
|
146
|
+
- Not showing feedback after copy — user doesn't know the action succeeded
|
|
147
|
+
- Showing a toast on Android 13+ — the system already shows one, causing double notification
|
|
148
|
+
- Storing passwords in clipboard without `EXTRA_IS_SENSITIVE` — shown in clipboard history
|
|
149
|
+
- Reading clipboard on every resume — intrusive, user sees system clipboard access notification
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Related Skills
|
|
154
|
+
- `compose` — `LocalClipboardManager` in Compose
|
|
155
|
+
- `share-intent` — sharing content with other apps instead of clipboard
|