mirror of
https://github.com/IllusionMan1212/lyrics-grabbr.git
synced 2026-10-24 02:12:27 +00:00
WIP: make service foreground
This commit is contained in:
@@ -45,7 +45,7 @@ android {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.illusionware.lyricsgrabbr.lyricsgrabbr"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 31
|
||||
targetSdkVersion 30
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
@@ -65,4 +65,5 @@ flutter {
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
+37
-12
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+94
-40
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+15
-13
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user