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,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: firebase-messaging
|
|
3
|
+
description: >
|
|
4
|
+
Firebase Cloud Messaging (FCM) setup and handling for Android.
|
|
5
|
+
Load this skill when implementing push notifications, handling FCM tokens,
|
|
6
|
+
processing foreground/background messages, or sending data payloads.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Firebase Messaging (FCM)
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
Firebase Cloud Messaging (FCM) is Google's cross-platform messaging solution for push notifications. It delivers notification messages (shown by system) and data messages (handled by app code) to Android devices. FCM tokens identify each device/app installation.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Core Principles
|
|
17
|
+
|
|
18
|
+
- **Notification messages** — shown automatically by system when app is in background
|
|
19
|
+
- **Data messages** — always delivered to `onMessageReceived()` for app to handle
|
|
20
|
+
- FCM token can change — always update the server when token refreshes
|
|
21
|
+
- Notification permission is required on **API 33+** — `POST_NOTIFICATIONS`
|
|
22
|
+
- Handle messages in a **short time** — `onMessageReceived()` has ~10 second limit
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
```kotlin
|
|
29
|
+
// build.gradle.kts
|
|
30
|
+
dependencies {
|
|
31
|
+
implementation(platform(libs.firebase.bom))
|
|
32
|
+
implementation(libs.firebase.messaging)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```xml
|
|
37
|
+
<!-- AndroidManifest.xml -->
|
|
38
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
39
|
+
|
|
40
|
+
<service
|
|
41
|
+
android:name=".messaging.MyFirebaseMessagingService"
|
|
42
|
+
android:exported="false">
|
|
43
|
+
<intent-filter>
|
|
44
|
+
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
45
|
+
</intent-filter>
|
|
46
|
+
</service>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## MessagingService Implementation
|
|
52
|
+
|
|
53
|
+
```kotlin
|
|
54
|
+
// ✅ Handle incoming messages and token refresh
|
|
55
|
+
class MyFirebaseMessagingService : FirebaseMessagingService() {
|
|
56
|
+
|
|
57
|
+
@Inject
|
|
58
|
+
lateinit var notificationManager: AppNotificationManager
|
|
59
|
+
|
|
60
|
+
@Inject
|
|
61
|
+
lateinit var tokenRepository: FcmTokenRepository
|
|
62
|
+
|
|
63
|
+
// ✅ Called when a new FCM token is generated
|
|
64
|
+
override fun onNewToken(token: String) {
|
|
65
|
+
super.onNewToken(token)
|
|
66
|
+
// Upload new token to your server
|
|
67
|
+
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
|
|
68
|
+
tokenRepository.updateToken(token)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ✅ Called for all messages when app is in foreground
|
|
73
|
+
// Called for data-only messages when app is in background
|
|
74
|
+
override fun onMessageReceived(message: RemoteMessage) {
|
|
75
|
+
super.onMessageReceived(message)
|
|
76
|
+
|
|
77
|
+
val title = message.notification?.title ?: message.data["title"] ?: return
|
|
78
|
+
val body = message.notification?.body ?: message.data["body"] ?: return
|
|
79
|
+
val type = message.data["type"]
|
|
80
|
+
val id = message.data["id"]
|
|
81
|
+
|
|
82
|
+
notificationManager.showNotification(
|
|
83
|
+
title = title,
|
|
84
|
+
body = body,
|
|
85
|
+
data = message.data
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## FCM Token Management
|
|
94
|
+
|
|
95
|
+
```kotlin
|
|
96
|
+
// ✅ Get current token
|
|
97
|
+
class FcmTokenRepository @Inject constructor(
|
|
98
|
+
private val api: UserApi,
|
|
99
|
+
private val prefs: DataStore<Preferences>
|
|
100
|
+
) {
|
|
101
|
+
suspend fun getToken(): String? = suspendCoroutine { cont ->
|
|
102
|
+
FirebaseMessaging.getInstance().token
|
|
103
|
+
.addOnSuccessListener { cont.resume(it) }
|
|
104
|
+
.addOnFailureListener { cont.resume(null) }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
suspend fun updateToken(token: String) {
|
|
108
|
+
val saved = prefs.data.first()[FCM_TOKEN_KEY]
|
|
109
|
+
if (saved != token) {
|
|
110
|
+
api.updateFcmToken(token)
|
|
111
|
+
prefs.edit { it[FCM_TOKEN_KEY] = token }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
suspend fun deleteToken() {
|
|
116
|
+
FirebaseMessaging.getInstance().deleteToken().await()
|
|
117
|
+
api.deleteFcmToken()
|
|
118
|
+
prefs.edit { it.remove(FCM_TOKEN_KEY) }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
companion object {
|
|
122
|
+
val FCM_TOKEN_KEY = stringPreferencesKey("fcm_token")
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Notification Permission (API 33+)
|
|
130
|
+
|
|
131
|
+
```kotlin
|
|
132
|
+
// ✅ Request POST_NOTIFICATIONS permission on Android 13+
|
|
133
|
+
@Composable
|
|
134
|
+
fun NotificationPermissionHandler(onGranted: () -> Unit) {
|
|
135
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
136
|
+
val permissionState = rememberPermissionState(
|
|
137
|
+
Manifest.permission.POST_NOTIFICATIONS
|
|
138
|
+
)
|
|
139
|
+
LaunchedEffect(Unit) {
|
|
140
|
+
if (!permissionState.status.isGranted) {
|
|
141
|
+
permissionState.launchPermissionRequest()
|
|
142
|
+
} else {
|
|
143
|
+
onGranted()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
LaunchedEffect(Unit) { onGranted() }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Showing Notifications
|
|
155
|
+
|
|
156
|
+
```kotlin
|
|
157
|
+
// ✅ NotificationManager wrapper
|
|
158
|
+
class AppNotificationManager @Inject constructor(
|
|
159
|
+
private val context: Context
|
|
160
|
+
) {
|
|
161
|
+
private val notificationManager =
|
|
162
|
+
context.getSystemService(NotificationManager::class.java)
|
|
163
|
+
|
|
164
|
+
init {
|
|
165
|
+
createChannels()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private fun createChannels() {
|
|
169
|
+
val channel = NotificationChannel(
|
|
170
|
+
CHANNEL_ID_GENERAL,
|
|
171
|
+
"General",
|
|
172
|
+
NotificationManager.IMPORTANCE_DEFAULT
|
|
173
|
+
).apply {
|
|
174
|
+
description = "General app notifications"
|
|
175
|
+
}
|
|
176
|
+
notificationManager.createNotificationChannel(channel)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fun showNotification(title: String, body: String, data: Map<String, String>) {
|
|
180
|
+
val intent = Intent(context, MainActivity::class.java).apply {
|
|
181
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
182
|
+
data.forEach { (key, value) -> putExtra(key, value) }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
val pendingIntent = PendingIntent.getActivity(
|
|
186
|
+
context, 0, intent,
|
|
187
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
val notification = NotificationCompat.Builder(context, CHANNEL_ID_GENERAL)
|
|
191
|
+
.setSmallIcon(R.drawable.ic_notification)
|
|
192
|
+
.setContentTitle(title)
|
|
193
|
+
.setContentText(body)
|
|
194
|
+
.setAutoCancel(true)
|
|
195
|
+
.setContentIntent(pendingIntent)
|
|
196
|
+
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
197
|
+
.build()
|
|
198
|
+
|
|
199
|
+
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
companion object {
|
|
203
|
+
const val CHANNEL_ID_GENERAL = "general"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Message Types
|
|
211
|
+
|
|
212
|
+
```kotlin
|
|
213
|
+
// Notification message (server-sent):
|
|
214
|
+
// {
|
|
215
|
+
// "notification": { "title": "Hello", "body": "World" },
|
|
216
|
+
// "to": "<fcm_token>"
|
|
217
|
+
// }
|
|
218
|
+
// → System shows notification automatically when app in background
|
|
219
|
+
// → onMessageReceived() called when app in foreground
|
|
220
|
+
|
|
221
|
+
// Data message (server-sent):
|
|
222
|
+
// {
|
|
223
|
+
// "data": { "title": "Hello", "body": "World", "type": "order_update" },
|
|
224
|
+
// "to": "<fcm_token>"
|
|
225
|
+
// }
|
|
226
|
+
// → onMessageReceived() always called regardless of app state
|
|
227
|
+
|
|
228
|
+
// ✅ Prefer data messages for full control over display
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Topic Subscription
|
|
234
|
+
|
|
235
|
+
```kotlin
|
|
236
|
+
// ✅ Subscribe to topics for broadcast messages
|
|
237
|
+
suspend fun subscribeToTopic(topic: String) {
|
|
238
|
+
FirebaseMessaging.getInstance().subscribeToTopic(topic).await()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
suspend fun unsubscribeFromTopic(topic: String) {
|
|
242
|
+
FirebaseMessaging.getInstance().unsubscribeFromTopic(topic).await()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Usage
|
|
246
|
+
subscribeToTopic("promotions")
|
|
247
|
+
subscribeToTopic("user_${userId}")
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Anti-Patterns
|
|
253
|
+
|
|
254
|
+
- Not updating token on `onNewToken()` — server sends to stale token, messages lost
|
|
255
|
+
- Long-running work in `onMessageReceived()` — only 10 seconds available; use WorkManager
|
|
256
|
+
- Notification messages for data that needs app processing — use data messages
|
|
257
|
+
- Not creating notification channels — notifications silently dropped on API 26+
|
|
258
|
+
- Not handling `POST_NOTIFICATIONS` permission on API 33+ — no notifications shown
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Related Skills
|
|
263
|
+
- `firebase` — Firebase core setup
|
|
264
|
+
- `notification` — notification channels and display
|
|
265
|
+
- `workmanager` — background work triggered by FCM
|
|
266
|
+
- `deep-link` — navigating to specific screen from notification tap
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audio
|
|
3
|
+
description: >
|
|
4
|
+
Android audio playback and recording patterns.
|
|
5
|
+
Load this skill when playing audio files, recording from microphone,
|
|
6
|
+
managing audio focus, or handling audio streams in Android.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Audio
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
Android provides multiple APIs for audio — `MediaPlayer` for simple playback, `AudioTrack` for low-latency PCM, `MediaRecorder` for recording, and `AudioRecord` for raw PCM capture. Choosing the right API depends on the use case: streaming, local file, or real-time processing.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Core Principles
|
|
17
|
+
|
|
18
|
+
- Always **request audio focus** before playback — respect other apps
|
|
19
|
+
- **Release resources** (`MediaPlayer`, `MediaRecorder`) when done — they hold hardware
|
|
20
|
+
- Audio operations that block belong on **Dispatchers.IO**
|
|
21
|
+
- Handle **audio output changes** (headphone unplug, Bluetooth) — pause when needed
|
|
22
|
+
- Declare `RECORD_AUDIO` permission for recording — runtime permission required
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Permissions
|
|
27
|
+
|
|
28
|
+
```xml
|
|
29
|
+
<!-- AndroidManifest.xml -->
|
|
30
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## MediaPlayer — Simple Playback
|
|
36
|
+
|
|
37
|
+
```kotlin
|
|
38
|
+
// ✅ Play from resource
|
|
39
|
+
class AudioPlayer(private val context: Context) {
|
|
40
|
+
|
|
41
|
+
private var mediaPlayer: MediaPlayer? = null
|
|
42
|
+
|
|
43
|
+
fun playFromResource(@RawRes resId: Int) {
|
|
44
|
+
release()
|
|
45
|
+
mediaPlayer = MediaPlayer.create(context, resId).apply {
|
|
46
|
+
setOnCompletionListener { release() }
|
|
47
|
+
start()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ✅ Play from URL (streaming)
|
|
52
|
+
fun playFromUrl(url: String) {
|
|
53
|
+
release()
|
|
54
|
+
mediaPlayer = MediaPlayer().apply {
|
|
55
|
+
setAudioAttributes(
|
|
56
|
+
AudioAttributes.Builder()
|
|
57
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
|
58
|
+
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
59
|
+
.build()
|
|
60
|
+
)
|
|
61
|
+
setDataSource(url)
|
|
62
|
+
setOnPreparedListener { start() }
|
|
63
|
+
setOnErrorListener { _, what, extra ->
|
|
64
|
+
Log.e("AudioPlayer", "Error: what=$what extra=$extra")
|
|
65
|
+
true
|
|
66
|
+
}
|
|
67
|
+
prepareAsync() // ✅ Non-blocking prepare for streams
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fun pause() { mediaPlayer?.pause() }
|
|
72
|
+
fun resume() { mediaPlayer?.start() }
|
|
73
|
+
fun stop() { mediaPlayer?.stop() }
|
|
74
|
+
|
|
75
|
+
// ✅ Always release — frees hardware resource
|
|
76
|
+
fun release() {
|
|
77
|
+
mediaPlayer?.release()
|
|
78
|
+
mediaPlayer = null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Audio Focus
|
|
86
|
+
|
|
87
|
+
```kotlin
|
|
88
|
+
// ✅ Request audio focus before playback
|
|
89
|
+
class AudioFocusManager(private val context: Context) {
|
|
90
|
+
|
|
91
|
+
private val audioManager = context.getSystemService(AudioManager::class.java)
|
|
92
|
+
private var focusRequest: AudioFocusRequest? = null
|
|
93
|
+
|
|
94
|
+
fun requestFocus(onGranted: () -> Unit, onLost: () -> Unit): Boolean {
|
|
95
|
+
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
|
96
|
+
.setAudioAttributes(
|
|
97
|
+
AudioAttributes.Builder()
|
|
98
|
+
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
99
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
|
100
|
+
.build()
|
|
101
|
+
)
|
|
102
|
+
.setOnAudioFocusChangeListener { focusChange ->
|
|
103
|
+
when (focusChange) {
|
|
104
|
+
AudioManager.AUDIOFOCUS_LOSS -> onLost()
|
|
105
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> onLost() // pause temporarily
|
|
106
|
+
AudioManager.AUDIOFOCUS_GAIN -> onGranted() // resume
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
.build()
|
|
110
|
+
|
|
111
|
+
focusRequest = request
|
|
112
|
+
val result = audioManager.requestAudioFocus(request)
|
|
113
|
+
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fun abandonFocus() {
|
|
117
|
+
focusRequest?.let { audioManager.abandonAudioFocusRequest(it) }
|
|
118
|
+
focusRequest = null
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Handle Headphone Unplug
|
|
126
|
+
|
|
127
|
+
```kotlin
|
|
128
|
+
// ✅ Pause when headphones are disconnected
|
|
129
|
+
class NoisyReceiver(private val onBecomingNoisy: () -> Unit) : BroadcastReceiver() {
|
|
130
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
131
|
+
if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
|
|
132
|
+
onBecomingNoisy() // pause playback
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Register/unregister with lifecycle
|
|
138
|
+
override fun onStart() {
|
|
139
|
+
super.onStart()
|
|
140
|
+
val filter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
|
|
141
|
+
registerReceiver(noisyReceiver, filter)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
override fun onStop() {
|
|
145
|
+
unregisterReceiver(noisyReceiver)
|
|
146
|
+
super.onStop()
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## MediaRecorder — Simple Recording
|
|
153
|
+
|
|
154
|
+
```kotlin
|
|
155
|
+
// ✅ Record audio to file
|
|
156
|
+
class AudioRecorder(private val context: Context) {
|
|
157
|
+
|
|
158
|
+
private var recorder: MediaRecorder? = null
|
|
159
|
+
|
|
160
|
+
fun startRecording(outputFile: File) {
|
|
161
|
+
recorder = MediaRecorder(context).apply {
|
|
162
|
+
setAudioSource(MediaRecorder.AudioSource.MIC)
|
|
163
|
+
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
|
164
|
+
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
|
|
165
|
+
setAudioSamplingRate(44100)
|
|
166
|
+
setAudioEncodingBitRate(128_000)
|
|
167
|
+
setOutputFile(outputFile.absolutePath)
|
|
168
|
+
prepare()
|
|
169
|
+
start()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fun stopRecording(): File? {
|
|
174
|
+
return try {
|
|
175
|
+
recorder?.apply {
|
|
176
|
+
stop()
|
|
177
|
+
release()
|
|
178
|
+
}
|
|
179
|
+
recorder = null
|
|
180
|
+
outputFile
|
|
181
|
+
} catch (e: Exception) {
|
|
182
|
+
null
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fun release() {
|
|
187
|
+
recorder?.release()
|
|
188
|
+
recorder = null
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## AudioRecord — Raw PCM Capture
|
|
196
|
+
|
|
197
|
+
```kotlin
|
|
198
|
+
// ✅ For real-time audio processing (speech recognition, audio analysis)
|
|
199
|
+
class RawAudioCapture {
|
|
200
|
+
|
|
201
|
+
private val sampleRate = 44100
|
|
202
|
+
private val bufferSize = AudioRecord.getMinBufferSize(
|
|
203
|
+
sampleRate,
|
|
204
|
+
AudioFormat.CHANNEL_IN_MONO,
|
|
205
|
+
AudioFormat.ENCODING_PCM_16BIT
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
private var audioRecord: AudioRecord? = null
|
|
209
|
+
private var isRecording = false
|
|
210
|
+
|
|
211
|
+
fun startCapture(onAudioData: (ShortArray) -> Unit) {
|
|
212
|
+
audioRecord = AudioRecord(
|
|
213
|
+
MediaRecorder.AudioSource.MIC,
|
|
214
|
+
sampleRate,
|
|
215
|
+
AudioFormat.CHANNEL_IN_MONO,
|
|
216
|
+
AudioFormat.ENCODING_PCM_16BIT,
|
|
217
|
+
bufferSize
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
isRecording = true
|
|
221
|
+
audioRecord?.startRecording()
|
|
222
|
+
|
|
223
|
+
Thread {
|
|
224
|
+
val buffer = ShortArray(bufferSize)
|
|
225
|
+
while (isRecording) {
|
|
226
|
+
val read = audioRecord?.read(buffer, 0, buffer.size) ?: 0
|
|
227
|
+
if (read > 0) onAudioData(buffer.copyOf(read))
|
|
228
|
+
}
|
|
229
|
+
}.start()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fun stopCapture() {
|
|
233
|
+
isRecording = false
|
|
234
|
+
audioRecord?.stop()
|
|
235
|
+
audioRecord?.release()
|
|
236
|
+
audioRecord = null
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Anti-Patterns
|
|
244
|
+
|
|
245
|
+
- Not releasing `MediaPlayer` or `MediaRecorder` — holds hardware, other apps can't use it
|
|
246
|
+
- Playing audio without requesting audio focus — overlaps with other apps' audio
|
|
247
|
+
- `prepare()` instead of `prepareAsync()` for network streams — blocks main thread
|
|
248
|
+
- Not handling `AudioManager.ACTION_AUDIO_BECOMING_NOISY` — audio leaks from speaker
|
|
249
|
+
- Recording without checking/requesting `RECORD_AUDIO` permission
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Related Skills
|
|
254
|
+
- `exoplayer` — advanced media playback
|
|
255
|
+
- `foreground-service` — background audio playback
|
|
256
|
+
- `lifecycle` — releasing audio resources with lifecycle
|
|
257
|
+
- `manifest` — audio permission declaration
|