WIP: make service foreground

This commit is contained in:
2022-02-28 21:30:40 +02:00
parent 2cc0b4b75d
commit 10b7529f98
9 changed files with 208 additions and 132 deletions
+4 -1
View File
@@ -1,8 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.illusionware.lyricsgrabbr.lyricsgrabbr">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:label="Lyrics Grabbr"
android:name="${applicationName}"
android:name=".App"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
@@ -0,0 +1,23 @@
package com.illusionware.lyricsgrabbr.lyricsgrabbr
import android.app.Application
import android.os.StrictMode
class App : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll() // or .detectAll() for all detectable problems
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.penaltyDeath()
.build()
)
}
}
@@ -1,15 +1,20 @@
package com.illusionware.lyricsgrabbr.lyricsgrabbr
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
private var streamHandler : NotificationStreamHandler? = null
// private var streamHandler : NotificationStreamHandler? = null
private var listenerIntent : Intent? = null
companion object {
private const val ACTION_NOTIFICATION_LISTENER_SETTINGS = "android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"
@@ -17,10 +22,12 @@ class MainActivity: FlutterActivity() {
private const val METHOD_CHANNEL_NAME = "notifications/method"
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL_NAME)
listenerIntent = Intent(applicationContext, NotificationListener::class.java)
val methodChannel = MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, METHOD_CHANNEL_NAME)
methodChannel.setMethodCallHandler { call, result ->
when (call.method) {
"plugin.hasPermission" -> {
@@ -29,18 +36,19 @@ class MainActivity: FlutterActivity() {
"plugin.openPermissionSettings" -> {
result.success(openPermissionSettings())
}
"plugin.startService" -> {
result.success(startService())
}
"plugin.stopService" -> {
result.success(stopService())
}
else -> result.notImplemented()
}
}
val eventChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL_NAME)
streamHandler = NotificationStreamHandler(context)
eventChannel.setStreamHandler(streamHandler)
}
override fun onDestroy() {
super.onDestroy()
streamHandler?.cleanUp()
// val eventChannel = EventChannel(flutterEngine!!.dartExecutor.binaryMessenger, EVENT_CHANNEL_NAME)
// streamHandler = NotificationStreamHandler(context)
// eventChannel.setStreamHandler(streamHandler)
}
private fun isPermissionGranted(): Boolean {
@@ -59,4 +67,21 @@ class MainActivity: FlutterActivity() {
context.startActivity(Intent(ACTION_NOTIFICATION_LISTENER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
return true
}
private fun startService(): Boolean {
listenerIntent?.action = NotificationListener.START_FOREGROUND_SERVICE_ACTION
ContextCompat.startForegroundService(applicationContext, listenerIntent!!)
Log.d("startService", "starting")
return true
}
private fun stopService(): Boolean {
listenerIntent?.action = NotificationListener.STOP_FOREGROUND_SERVICE_ACTION
ContextCompat.startForegroundService(applicationContext, listenerIntent!!)
Log.d("stopService", "stopping")
//applicationContext.stopService(listenerIntent!!)
return true
}
}
@@ -1,26 +1,58 @@
package com.illusionware.lyricsgrabbr.lyricsgrabbr
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.MediaSession
import android.os.Build
import android.os.Handler
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import org.json.JSONException
import org.json.JSONObject
import android.os.Bundle
import java.io.File
import java.io.FileOutputStream
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
/**
* Notification listening service. Intercepts notifications if permission is given to do so.
*/
class NotificationListener : NotificationListenerService() {
private var notificationManager : NotificationManagerCompat? = null
override fun onCreate() {
super.onCreate()
Log.d("onCreate Service", "creating service")
initNotificationChannels(this)
notificationManager = NotificationManagerCompat.from(applicationContext)
}
override fun onDestroy() {
Log.d("onDestroy Service", "destroying service")
stopForeground(true)
stopSelf()
super.onDestroy()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
getCurrentNotifications(applicationContext)
if (intent != null) {
when (intent.action) {
START_FOREGROUND_SERVICE_ACTION -> {
Log.d("START", "starting foreground service")
startForegroundService()
getCurrentNotifications(applicationContext)
}
STOP_FOREGROUND_SERVICE_ACTION -> {
Log.d("STOP", "stopping foreground service")
stopForegroundService()
}
}
}
return super.onStartCommand(intent, flags, startId)
}
@@ -29,14 +61,46 @@ class NotificationListener : NotificationListenerService() {
}
override fun onNotificationPosted(sbn: StatusBarNotification) {
sendNotificationBroadcast(sbn, applicationContext)
if (sbn.packageName == SPOTIFY_PACKAGE) {
sendNotificationBroadcast(sbn, applicationContext)
}
}
private fun startForegroundService() {
val notification: Notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle("TITLE")
.setContentText("TEXT")
.setSmallIcon(R.drawable.ic_notification)
.build()
notification.flags = Notification.FLAG_FOREGROUND_SERVICE
startForeground(NOTIFICATION_ID, notification)
}
private fun stopForegroundService() {
stopForeground(true)
stopSelf()
}
private fun initNotificationChannels(context: Context) {
if (Build.VERSION.SDK_INT < 26) {
return
}
val notificationManager =
context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"Lyrics",
NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(channel)
}
private fun getCurrentNotifications(context: Context) {
val sbns = activeNotifications
for (sbn in sbns) {
// TODO: remove this check and figure out a way to send a fucking list instead
if (sbn.packageName == "com.spotify.music") {
if (sbn.packageName == SPOTIFY_PACKAGE) {
sendNotificationBroadcast(sbn, context)
}
}
@@ -48,19 +112,7 @@ class NotificationListener : NotificationListenerService() {
val mediaCtrl = mediaSessionToken?.let { MediaController(context, it) }
val duration = mediaCtrl?.metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.div(1000)
val playbackState = mediaCtrl?.playbackState?.state
var albumArtPath = ""
if (sbn.packageName == "com.spotify.music") {
val albumArt = mediaCtrl?.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
val tempArtFile = File(cacheDir, "albumart_temp.png")
tempArtFile.createNewFile()
val fileOutputStream = FileOutputStream(tempArtFile)
albumArt?.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)
fileOutputStream.flush()
fileOutputStream.close()
albumArtPath = tempArtFile.absolutePath
}
val albumArt = mediaCtrl?.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
// Retrieve package name to set as title.
val packageName = sbn.packageName
@@ -73,10 +125,22 @@ class NotificationListener : NotificationListenerService() {
intent.putExtra(NOTIFICATION_PACKAGE_NAME, packageName)
intent.putExtra(NOTIFICATION_PACKAGE_MESSAGE, packageMessage)
intent.putExtra(NOTIFICATION_PACKAGE_TEXT, packageText)
intent.putExtra(NOTIFICATION_PACKAGE_ART, albumArtPath)
intent.putExtra(NOTIFICATION_PACKAGE_DURATION, duration)
intent.putExtra(NOTIFICATION_PACKAGE_PLAYBACK_STATE, playbackState)
sendBroadcast(intent)
val notification: Notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle("Playing [${packageText}] By [${packageMessage}]")
.setContentText("Tap to get lyrics")
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(albumArt)
// .setContentIntent(pendingIntent) // launch intent when clicking on notification
.build()
notification.flags = Notification.FLAG_FOREGROUND_SERVICE
notificationManager?.notify(NOTIFICATION_ID, notification)
//sendBroadcast(intent)
}
companion object {
@@ -84,22 +148,12 @@ class NotificationListener : NotificationListenerService() {
const val NOTIFICATION_PACKAGE_NAME = "package_name"
const val NOTIFICATION_PACKAGE_MESSAGE = "package_message"
const val NOTIFICATION_PACKAGE_TEXT = "package_text"
const val NOTIFICATION_PACKAGE_EXTRA = "package_extra"
const val NOTIFICATION_PACKAGE_ART = "package_art"
const val NOTIFICATION_PACKAGE_PLAYBACK_STATE = "package_playback_state"
const val NOTIFICATION_PACKAGE_DURATION = "package_duration"
}
private fun convertBundleToJsonString(extra: Bundle): String {
val json = JSONObject()
val keys = extra.keySet()
for (key in keys) {
try {
json.put(key, JSONObject.wrap(extra.get(key)))
} catch (e: JSONException) {
//Handle exception here
}
}
return json.toString()
const val NOTIFICATION_ID = 12251999
const val NOTIFICATION_CHANNEL_ID = "Lyrics"
const val SPOTIFY_PACKAGE = "com.spotify.music"
const val START_FOREGROUND_SERVICE_ACTION = "START_SERVICE"
const val STOP_FOREGROUND_SERVICE_ACTION = "STOP_SERVICE"
}
}
@@ -3,34 +3,38 @@ package com.illusionware.lyricsgrabbr.lyricsgrabbr
import io.flutter.plugin.common.EventChannel.StreamHandler
import io.flutter.plugin.common.EventChannel.EventSink
import android.content.*
import android.util.Log
import androidx.core.content.ContextCompat
class NotificationStreamHandler(private val context: Context) : StreamHandler {
private var eventSink: EventSink? = null
private var receiver: NotificationReceiver? = null
private val listenerIntent = Intent(context, NotificationListener::class.java)
// Called whenever the event channel is subscribed to in Flutter
override fun onListen(o: Any?, eventSink: EventSink?) {
this.eventSink = eventSink
receiver = NotificationReceiver()
val intentFilter = IntentFilter()
intentFilter.addAction(NotificationListener.NOTIFICATION_INTENT)
context.registerReceiver(receiver, intentFilter)
val listenerIntent = Intent(context, NotificationListener::class.java)
listenerIntent.action = NotificationListener.START_FOREGROUND_SERVICE_ACTION
context.startService(listenerIntent)
//receiver = NotificationReceiver()
//val intentFilter = IntentFilter()
//intentFilter.addAction(NotificationListener.NOTIFICATION_INTENT)
//context.registerReceiver(receiver, intentFilter)
ContextCompat.startForegroundService(context, listenerIntent)
}
// Called whenever the event channel subscription is cancelled in Flutter
override fun onCancel(o: Any?) {
eventSink = null
Log.d("onCancel", "subscription cancelled, stopping service")
context.stopService(listenerIntent)
context.unregisterReceiver(receiver)
}
val listenerIntent = Intent(context, NotificationListener::class.java)
listenerIntent.action = NotificationListener.STOP_FOREGROUND_SERVICE_ACTION
fun cleanUp() {
context.unregisterReceiver(receiver)
ContextCompat.startForegroundService(context, listenerIntent)
//context.unregisterReceiver(receiver)
}
internal inner class NotificationReceiver : BroadcastReceiver() {
@@ -38,14 +42,12 @@ class NotificationStreamHandler(private val context: Context) : StreamHandler {
val packageName = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_NAME)
val packageMessage = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_MESSAGE)
val packageText = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_TEXT)
val packageArt = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_ART)
val packagePlaybackState = intent.getIntExtra(NotificationListener.NOTIFICATION_PACKAGE_PLAYBACK_STATE, -1)
val packageDuration = intent.getLongExtra(NotificationListener.NOTIFICATION_PACKAGE_DURATION, -1)
val map = HashMap<String, Any>()
map["packageName"] = packageName!!
map["packageMessage"] = packageMessage!!
map["packageText"] = packageText!!
map["packageArt"] = packageArt!!
map["packagePlaybackState"] = packagePlaybackState
map["packageDuration"] = packageDuration
eventSink?.success(map)