feat: integrate the notification listener into the project

This commit is contained in:
2022-02-24 21:56:38 +02:00
parent a56a88efe4
commit add74ea848
8 changed files with 306 additions and 74 deletions
+1 -1
View File
@@ -27,7 +27,7 @@
</activity> </activity>
<service <service
android:label="Lyrics Grabbr Notifications Listener" android:label="Lyrics Grabbr Notifications Listener"
android:name="com.emilecode.android_notification_listener2.NotificationListener" android:name=".NotificationListener"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
@@ -1,6 +1,55 @@
package com.illusionware.lyricsgrabbr.lyricsgrabbr 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.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() { 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
}
} }
@@ -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()
}
}
@@ -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<String, Any>()
map["packageName"] = packageName!!
map["packageMessage"] = packageMessage!!
map["packageText"] = packageText!!
map["packageExtra"] = packageExtra!!
eventSink?.success(map)
}
}
}
+18 -26
View File
@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
@@ -17,13 +17,10 @@ Future<void> main() async {
// e.g: notification icon // e.g: notification icon
const AndroidInitializationSettings initializationSettingsAndroid = const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher'); AndroidInitializationSettings('@mipmap/ic_launcher');
const InitializationSettings initializationSettings = InitializationSettings( const InitializationSettings initializationSettings =
android: initializationSettingsAndroid InitializationSettings(android: initializationSettingsAndroid);
); await flutterLocalNotificationsPlugin.initialize(initializationSettings,
await flutterLocalNotificationsPlugin.initialize( onSelectNotification: onNotificationClick);
initializationSettings,
onSelectNotification: onNotificationClick
);
runApp(const LyricsGrabbr()); runApp(const LyricsGrabbr());
} }
@@ -61,9 +58,9 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WidgetsBindingObserver { class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
late AndroidNotificationListener _notifications; late AndroidNotificationListener _notifications;
late StreamSubscription<NotificationEventV2> _subscription; late StreamSubscription<NotificationEvent> _subscription;
BuildContext? permissionDialogCtx; BuildContext? permissionDialogCtx;
final List<NotificationEventV2> _notifs = []; final List<NotificationEvent> _notifs = [];
bool running = false; bool running = false;
@override @override
@@ -111,23 +108,19 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
child: AlertDialog( child: AlertDialog(
title: const Text( title: const Text(
'Notifications Access Permission', 'Notifications Access Permission',
style: TextStyle( style: TextStyle(fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold
),
), ),
content: const Text( content: const Text(
'This app requires permission to listen to notifications to function properly. Please grant permission in the next screen.', 'This app requires permission to listen to notifications to function properly. Please grant permission in the next screen.',
style: TextStyle( style: TextStyle(color: Colors.blueGrey),
color: Colors.blueGrey
),
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await AndroidNotificationListener.openPermissionSettings(); await AndroidNotificationListener
.openPermissionSettings();
}, },
child: const Text("OK") child: const Text("OK")),
),
], ],
), ),
), ),
@@ -145,7 +138,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
} }
} }
void onData(NotificationEventV2 event) { void onData(NotificationEvent event) {
if (event.packageName == spotifyPackage) { if (event.packageName == spotifyPackage) {
// TODO: if the app is in the background, only update our notif // 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
@@ -160,7 +153,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
} }
Future<void> sendNotification(String title, String body) async { Future<void> sendNotification(String title, String body) async {
const AndroidNotificationDetails androidNotification = AndroidNotificationDetails( const AndroidNotificationDetails androidNotification =
AndroidNotificationDetails(
"Lyrics", "Lyrics",
"Lyrics", "Lyrics",
importance: Importance.low, importance: Importance.low,
@@ -170,9 +164,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
playSound: false, playSound: false,
); );
const NotificationDetails notificationDetails = NotificationDetails( const NotificationDetails notificationDetails =
android: androidNotification NotificationDetails(android: androidNotification);
);
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
notificationId, notificationId,
@@ -214,8 +207,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
leading: Text(entry.packageText), leading: Text(entry.packageText),
trailing: Text(entry.packageMessage), trailing: Text(entry.packageMessage),
); );
} }),
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: running ? stopListening : startListening, onPressed: running ? stopListening : startListening,
tooltip: running ? 'Stop service' : "Start service", tooltip: running ? 'Stop service' : "Start service",
+76
View File
@@ -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<dynamic, dynamic> 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<NotificationEvent> get notificationStream {
if (Platform.isAndroid) {
Stream<NotificationEvent> _notificationStream = _notificationEventChannel
.receiveBroadcastStream()
.map((event) => _notificationEvent(event));
return _notificationStream;
}
throw NotificationException(
'Notification API exclusively available on Android!');
}
static Future<bool?> get hasPermission async {
return await _methodChannel.invokeMethod('plugin.hasPermission');
}
static Future<bool?> openPermissionSettings() async {
return await _methodChannel.invokeMethod('plugin.openPermissionSettings');
}
}
-7
View File
@@ -1,13 +1,6 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
android_notification_listener2:
dependency: "direct main"
description:
path: "/home/illusion/Desktop/repos/AndroidNotificationListener"
relative: false
source: path
version: "2.0.0"
args: args:
dependency: transitive dependency: transitive
description: description:
-2
View File
@@ -34,8 +34,6 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
http: ^0.13.4 http: ^0.13.4
android_notification_listener2:
path: /home/illusion/Desktop/repos/AndroidNotificationListener
flutter_local_notifications: ^9.3.2 flutter_local_notifications: ^9.3.2
dev_dependencies: dev_dependencies: