I'm trying to add a notification listener to a Kotlin android compose project:
My AndroidManigest.xml
:
<application>
<!-- ... -->
<service
android:name=".MyNotificationListenerService"
android:exported="false"
android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="explanation_for_special_use" />
</service>
</application>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
My MainActivity.kt
:
class MainActivity : ComponentActivity() {
private lateinit var serviceIntent: Intent
private fun isServiceRunningInForeground(): Boolean {
val manager = this.getSystemService(ACTIVITY_SERVICE) as ActivityManager
@Suppress("DEPRECATION")
for (service in manager.getRunningServices(Int.MAX_VALUE)) {
if (MyNotificationListenerService::class.java.name == service.service.className) {
if (service.foreground) {
return true
}
}
}
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
serviceIntent = Intent(this, MyNotificationListenerService::class.java)
Log.d("NB", "MainActivity.onCreate")
setContent {
MyNotificationListenerTheme {
var isServiceRunning by remember { mutableStateOf(isServiceRunningInForeground()) }
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = {
if (isServiceRunning) {
stopService(serviceIntent)
} else {
if (!NotificationManagerCompat.getEnabledListenerPackages(this)
.contains(
packageName
)
) {
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
}
startForegroundService(
serviceIntent
)
}
isServiceRunning = !isServiceRunning
}
) {
Icon(
imageVector = if (isServiceRunning) Icons.Default.Close else Icons.Default.PlayArrow,
contentDescription = if (isServiceRunning) "Pause" else "Play"
)
}
}
) { innerPadding ->
Surface(
modifier = Modifier.padding(innerPadding),
color = MaterialTheme.colorScheme.background
) {
// Your UI content
}
}
}
}
}
}
My MyNotificationListenerService.kt
:
class MyNotificationListenerService : NotificationListenerService() {
private val channelId = "MyNotificationListenerService"
override fun onCreate() {
super.onCreate()
createNotificationChannel()
startForeground(1, getNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
Log.d("NB", "MyNotificationListenerService.onCreate")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("NB", "MyNotificationListenerService.onStartCommand")
return START_STICKY
}
private fun createNotificationChannel() {
val serviceChannel = NotificationChannel(
channelId,
"My Notification Listener Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
private fun getNotification(): Notification {
return Notification.Builder(this, channelId)
.setContentTitle("Running...")
.build()
}
override fun onBind(intent: Intent?): IBinder? {
Log.d("NB", "MyNotificationListenerService.onBind")
return super.onBind(intent)
}
override fun onDestroy() {
Log.d("NB", "MyNotificationListenerService.onDestroy")
return super.onDestroy()
}
override fun onNotificationPosted(sbn: StatusBarNotification) {
val text = sbn.notification?.extras?.getString("android.text");
Log.d("NB", "MyNotificationListenerService.onNotificationPosted - text: $text")
super.onNotificationPosted(sbn)
}
override fun onNotificationRemoved(sbn: StatusBarNotification) {
// Optionally handle notification removal
}
override fun onListenerConnected() {
Log.d("NB", "MyNotificationListenerService.onListenerConnected")
}
}
When I press "play" I do get the logs:
MainActivity.onCreate
MyNotificationListenerService.onCreate
MyNotificationListenerService.onStartCommand
But when my phone receives a notification, MyNotificationListenerService.onNotificationPosted
is not called.
What am I missing?
As suggested by Mike M. in the comments, this is not something you start yourself.
The user must manually enable your app as a listener, then your Service will be started automatically by the system.
It is possible to direct the user to the screen for them to enable it using startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
.
Removing the logic to start the service and all the foreground related code made it work:
AndroidManifest.xml
:
<application>
<!-- ... -->
<service
android:name=".MyNotificationListenerService"
android:exported="false"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
MainActivity.kt
:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyNotificationListenerTheme {
var isServiceRunning by remember { mutableStateOf(isServiceRunningInForeground()) }
Scaffold { innerPadding ->
Surface(
modifier = Modifier.padding(innerPadding),
color = MaterialTheme.colorScheme.background
) {
// Your UI content
}
}
}
}
}
}
MyNotificationListenerService.kt
:
class MyNotificationListenerService : NotificationListenerService() {
override fun onCreate() {
super.onCreate()
Log.d("NB", "MyNotificationListenerService.onCreate")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("NB", "MyNotificationListenerService.onStartCommand")
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? {
Log.d("NB", "MyNotificationListenerService.onBind")
return super.onBind(intent)
}
override fun onDestroy() {
Log.d("NB", "MyNotificationListenerService.onDestroy")
return super.onDestroy()
}
override fun onNotificationPosted(sbn: StatusBarNotification) {
val text = sbn.notification?.extras?.getString("android.text");
Log.d("NB", "MyNotificationListenerService.onNotificationPosted - text: $text")
super.onNotificationPosted(sbn)
}
override fun onNotificationRemoved(sbn: StatusBarNotification) {
// Optionally handle notification removal
}
override fun onListenerConnected() {
Log.d("NB", "MyNotificationListenerService.onListenerConnected")
}
}