From add74ea848b0bbae9fa0a4b54a693a2a935b2676 Mon Sep 17 00:00:00 2001 From: IllusionMan1212 Date: Thu, 24 Feb 2022 21:56:38 +0200 Subject: [PATCH] feat: integrate the notification listener into the project --- android/app/src/main/AndroidManifest.xml | 2 +- .../lyricsgrabbr/lyricsgrabbr/MainActivity.kt | 49 +++++++ .../lyricsgrabbr/NotificationListener.kt | 78 ++++++++++++ .../lyricsgrabbr/NotificationStreamHandler.kt | 46 +++++++ lib/main.dart | 120 ++++++++---------- lib/notification_listener.dart | 76 +++++++++++ pubspec.lock | 7 - pubspec.yaml | 2 - 8 files changed, 306 insertions(+), 74 deletions(-) create mode 100644 android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/NotificationListener.kt create mode 100644 android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/NotificationStreamHandler.kt create mode 100644 lib/notification_listener.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6efeb21..103811b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ diff --git a/android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/MainActivity.kt b/android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/MainActivity.kt index 3956ec3..2a57bd1 100644 --- a/android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/MainActivity.kt +++ b/android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/MainActivity.kt @@ -1,6 +1,55 @@ package com.illusionware.lyricsgrabbr.lyricsgrabbr +import android.content.Intent +import android.text.TextUtils +import androidx.core.app.NotificationManagerCompat 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() { + companion object { + private const val ACTION_NOTIFICATION_LISTENER_SETTINGS = "android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS" + private const val EVENT_CHANNEL_NAME = "notifications.eventChannel" + private const val METHOD_CHANNEL_NAME = "notifications/method" + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + + val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL_NAME) + methodChannel.setMethodCallHandler { call, result -> + when (call.method) { + "plugin.hasPermission" -> { + result.success(isPermissionGranted()) + } + "plugin.openPermissionSettings" -> { + result.success(openPermissionSettings()) + } + else -> result.notImplemented() + } + } + + val eventChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL_NAME) + val streamHandler = NotificationStreamHandler(context) + eventChannel.setStreamHandler(streamHandler) + } + + private fun isPermissionGranted(): Boolean { + val packageName = context.packageName + val pkgs = NotificationManagerCompat.getEnabledListenerPackages(context) + for (name in pkgs) { + if (TextUtils.equals(packageName, name)) { + return true + } + } + + return false + } + + private fun openPermissionSettings(): Boolean { + context.startActivity(Intent(ACTION_NOTIFICATION_LISTENER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + return true + } } diff --git a/android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/NotificationListener.kt b/android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/NotificationListener.kt new file mode 100644 index 0000000..19e8d08 --- /dev/null +++ b/android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/NotificationListener.kt @@ -0,0 +1,78 @@ +package com.illusionware.lyricsgrabbr.lyricsgrabbr + +import android.app.Notification +import android.content.Intent +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import org.json.JSONException +import org.json.JSONObject +import android.os.Bundle + +/** + * Notification listening service. Intercepts notifications if permission is given to do so. + */ +class NotificationListener : NotificationListenerService() { + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + getCurrentNotifications() + return super.onStartCommand(intent, flags, startId) + } + + override fun onListenerConnected() { + getCurrentNotifications() + } + + override fun onNotificationPosted(sbn: StatusBarNotification) { + sendNotificationBroadcast(sbn) + } + + private fun getCurrentNotifications() { + 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") { + sendNotificationBroadcast(sbn) + } + } + } + + private fun sendNotificationBroadcast(sbn: StatusBarNotification) { + // TODO: get specific media player notification data + // such as onPlay and onNextSong or whatever tf + + // Retrieve package name to set as title. + val packageName = sbn.packageName + // Retrieve extra object from notification to extract payload. + val extras = sbn.notification.extras + val packageMessage = extras?.getCharSequence(Notification.EXTRA_TEXT).toString() + val packageText = extras?.getCharSequence("android.title").toString() + val packageExtra = convertBundleToJsonString(sbn.notification.extras) + // Pass data from one activity to another. + val intent = Intent(NOTIFICATION_INTENT) + intent.putExtra(NOTIFICATION_PACKAGE_NAME, packageName) + intent.putExtra(NOTIFICATION_PACKAGE_MESSAGE, packageMessage) + intent.putExtra(NOTIFICATION_PACKAGE_TEXT, packageText) + intent.putExtra(NOTIFICATION_PACKAGE_EXTRA, packageExtra) + sendBroadcast(intent) + } + + companion object { + const val NOTIFICATION_INTENT = "notification_event" + 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" + } + + 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() + } +} diff --git a/android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/NotificationStreamHandler.kt b/android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/NotificationStreamHandler.kt new file mode 100644 index 0000000..bf66b92 --- /dev/null +++ b/android/app/src/main/kotlin/com/illusionware/lyricsgrabbr/lyricsgrabbr/NotificationStreamHandler.kt @@ -0,0 +1,46 @@ +package com.illusionware.lyricsgrabbr.lyricsgrabbr + +import io.flutter.plugin.common.EventChannel.StreamHandler +import io.flutter.plugin.common.EventChannel.EventSink +import android.content.* + +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) + + context.startService(listenerIntent) + } + + // Called whenever the event channel subscription is cancelled in Flutter + override fun onCancel(o: Any?) { + eventSink = null + + context.stopService(listenerIntent) + context.unregisterReceiver(receiver) + } + + internal inner class NotificationReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + 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 packageExtra = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_EXTRA) + val map = HashMap() + map["packageName"] = packageName!! + map["packageMessage"] = packageMessage!! + map["packageText"] = packageText!! + map["packageExtra"] = packageExtra!! + eventSink?.success(map) + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 79eac7e..5807f68 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:android_notification_listener2/android_notification_listener2.dart'; +import 'notification_listener.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = @@ -17,13 +17,10 @@ Future main() async { // e.g: notification icon const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); - const InitializationSettings initializationSettings = InitializationSettings( - android: initializationSettingsAndroid - ); - await flutterLocalNotificationsPlugin.initialize( - initializationSettings, - onSelectNotification: onNotificationClick - ); + const InitializationSettings initializationSettings = + InitializationSettings(android: initializationSettingsAndroid); + await flutterLocalNotificationsPlugin.initialize(initializationSettings, + onSelectNotification: onNotificationClick); runApp(const LyricsGrabbr()); } @@ -61,9 +58,9 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with WidgetsBindingObserver { late AndroidNotificationListener _notifications; - late StreamSubscription _subscription; + late StreamSubscription _subscription; BuildContext? permissionDialogCtx; - final List _notifs = []; + final List _notifs = []; bool running = false; @override @@ -96,43 +93,39 @@ class _HomePageState extends State with WidgetsBindingObserver { } void showNotificationPermDialog() { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext buildCtx) { - permissionDialogCtx = buildCtx; + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext buildCtx) { + permissionDialogCtx = buildCtx; - return WillPopScope( - onWillPop: () { - SystemNavigator.pop(); - return Future.value(false); - }, - child: Expanded( - child: AlertDialog( - title: const Text( - 'Notifications Access Permission', - style: TextStyle( - fontWeight: FontWeight.bold - ), - ), - content: const Text( - 'This app requires permission to listen to notifications to function properly. Please grant permission in the next screen.', - style: TextStyle( - color: Colors.blueGrey - ), - ), - actions: [ - TextButton( - onPressed: () async { - await AndroidNotificationListener.openPermissionSettings(); - }, - child: const Text("OK") - ), - ], - ), + return WillPopScope( + onWillPop: () { + SystemNavigator.pop(); + return Future.value(false); + }, + child: Expanded( + child: AlertDialog( + title: const Text( + 'Notifications Access Permission', + style: TextStyle(fontWeight: FontWeight.bold), + ), + content: const Text( + 'This app requires permission to listen to notifications to function properly. Please grant permission in the next screen.', + style: TextStyle(color: Colors.blueGrey), + ), + actions: [ + TextButton( + onPressed: () async { + await AndroidNotificationListener + .openPermissionSettings(); + }, + child: const Text("OK")), + ], ), - ); - }); + ), + ); + }); } Future initPlatformState() async { @@ -145,10 +138,10 @@ class _HomePageState extends State with WidgetsBindingObserver { } } - void onData(NotificationEventV2 event) { + void onData(NotificationEvent event) { if (event.packageName == spotifyPackage) { // TODO: if the app is in the background, only update our notif - // if the app is in the foreground, update the notif, and + // if the app is in the foreground, update the notif, and // fetch the new lyrics and display them. var song = event.packageText; var artist = event.packageMessage; @@ -160,25 +153,25 @@ class _HomePageState extends State with WidgetsBindingObserver { } Future sendNotification(String title, String body) async { - const AndroidNotificationDetails androidNotification = AndroidNotificationDetails( - "Lyrics", - "Lyrics", - importance: Importance.low, - priority: Priority.min, - showWhen: false, - ongoing: true, - playSound: false, + const AndroidNotificationDetails androidNotification = + AndroidNotificationDetails( + "Lyrics", + "Lyrics", + importance: Importance.low, + priority: Priority.min, + showWhen: false, + ongoing: true, + playSound: false, ); - const NotificationDetails notificationDetails = NotificationDetails( - android: androidNotification - ); + const NotificationDetails notificationDetails = + NotificationDetails(android: androidNotification); await flutterLocalNotificationsPlugin.show( - notificationId, - title, - body, - notificationDetails, + notificationId, + title, + body, + notificationDetails, ); } @@ -214,8 +207,7 @@ class _HomePageState extends State with WidgetsBindingObserver { leading: Text(entry.packageText), trailing: Text(entry.packageMessage), ); - } - ), + }), floatingActionButton: FloatingActionButton( onPressed: running ? stopListening : startListening, tooltip: running ? 'Stop service' : "Start service", diff --git a/lib/notification_listener.dart b/lib/notification_listener.dart new file mode 100644 index 0000000..e49f1d6 --- /dev/null +++ b/lib/notification_listener.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'dart:io' show Platform; + +class NotificationException implements Exception { + final String _cause; + + NotificationException(this._cause); + + @override + String toString() { + return _cause; + } +} + +class NotificationEvent { + String packageMessage; + String packageName; + String packageExtra; + String packageText; + DateTime timeStamp; + + NotificationEvent({ + required this.packageName, + required this.packageMessage, + required this.timeStamp, + required this.packageExtra, + required this.packageText + }); + + factory NotificationEvent.fromMap(Map map) { + DateTime time = DateTime.now(); + String name = map['packageName']; + String message = map['packageMessage']; + String text = map['packageText']; + String extra = map['packageExtra']; + + return NotificationEvent(packageName: name, packageMessage: message, timeStamp: time,packageText: text , packageExtra: extra); + } + + @override + String toString() { + return "Notification Event \n Package Name: $packageName \n - Timestamp: $timeStamp \n - Package Message: $packageMessage"; + } +} + +NotificationEvent _notificationEvent(dynamic data) { + return NotificationEvent.fromMap(data); +} + +class AndroidNotificationListener { + static const EventChannel _notificationEventChannel = + EventChannel('notifications.eventChannel'); + static const MethodChannel _methodChannel = + MethodChannel('notifications/method'); + + Stream get notificationStream { + if (Platform.isAndroid) { + + Stream _notificationStream = _notificationEventChannel + .receiveBroadcastStream() + .map((event) => _notificationEvent(event)); + return _notificationStream; + } + throw NotificationException( + 'Notification API exclusively available on Android!'); + } + + static Future get hasPermission async { + return await _methodChannel.invokeMethod('plugin.hasPermission'); + } + + static Future openPermissionSettings() async { + return await _methodChannel.invokeMethod('plugin.openPermissionSettings'); + } +} diff --git a/pubspec.lock b/pubspec.lock index abb004f..2926e08 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,13 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - android_notification_listener2: - dependency: "direct main" - description: - path: "/home/illusion/Desktop/repos/AndroidNotificationListener" - relative: false - source: path - version: "2.0.0" args: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 70f4638..474a2c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,8 +34,6 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. http: ^0.13.4 - android_notification_listener2: - path: /home/illusion/Desktop/repos/AndroidNotificationListener flutter_local_notifications: ^9.3.2 dev_dependencies: