├── .gitattributes ├── android ├── settings.gradle ├── .gitignore ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── notification │ │ └── listener │ │ └── service │ │ ├── models │ │ ├── ActionCache.java │ │ ├── RemoteInputParcel.java │ │ └── Action.java │ │ ├── NotificationConstants.java │ │ ├── NotificationReceiver.java │ │ ├── NotificationUtils.java │ │ ├── NotificationListenerServicePlugin.java │ │ └── NotificationListener.java ├── build.gradle ├── gradlew.bat └── gradlew ├── example ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── java │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── notification_listener_service_example │ │ │ │ │ │ └── MainActivity.java │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle.kts │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle.kts │ └── settings.gradle.kts ├── .metadata ├── README.md ├── .gitignore ├── test │ └── widget_test.dart ├── analysis_options.yaml ├── pubspec.yaml ├── lib │ └── main.dart └── pubspec.lock ├── analysis_options.yaml ├── .metadata ├── CHANGELOG.md ├── .gitignore ├── LICENSE ├── lib ├── notification_listener_service.dart └── notification_event.dart ├── test └── notification_listener_service_test.dart ├── pubspec.yaml └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.java linguist-language=Dart 2 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'notification_listener_service' 2 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X-SLAYER/notification_listener_service/main/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X-SLAYER/notification_listener_service/main/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X-SLAYER/notification_listener_service/main/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X-SLAYER/notification_listener_service/main/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X-SLAYER/notification_listener_service/main/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X-SLAYER/notification_listener_service/main/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/notification_listener_service_example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.notification_listener_service_example; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity { 6 | } 7 | -------------------------------------------------------------------------------- /android/src/main/java/notification/listener/service/models/ActionCache.java: -------------------------------------------------------------------------------- 1 | package notification.listener.service.models; 2 | 3 | import java.util.HashMap; 4 | 5 | abstract public class ActionCache { 6 | public static HashMap cachedNotifications = new HashMap<>(); 7 | } 8 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 7 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 5464c5bac742001448fe4fc0597be939379f88ea 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 5464c5bac742001448fe4fc0597be939379f88ea 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.5 2 | 3 | - Add onGoing flag for Notification 4 | 5 | ## 0.3.4 6 | 7 | - Add namespace for android build.gradle 8 | 9 | ## 0.3.3 10 | 11 | - Added support for Android 14 12 | 13 | ## 0.3.2 14 | 15 | - Added support for large icon 16 | 17 | ## 0.3.1 18 | 19 | - Fixed the notifications stream broadcast 20 | 21 | ## 0.0.3 22 | 23 | - Checked if you can reply to a notification 24 | - Added the possibility to reply to a notification 25 | 26 | ## 0.0.2 27 | 28 | - Usage improvements 29 | 30 | ## 0.0.1 31 | 32 | - Initial release 33 | -------------------------------------------------------------------------------- /example/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() 9 | rootProject.layout.buildDirectory.value(newBuildDir) 10 | 11 | subprojects { 12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 13 | project.layout.buildDirectory.value(newSubprojectBuildDir) 14 | } 15 | subprojects { 16 | project.evaluationDependsOn(":app") 17 | } 18 | 19 | tasks.register("clean") { 20 | delete(rootProject.layout.buildDirectory) 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 25 | /pubspec.lock 26 | **/doc/api/ 27 | .dart_tool/ 28 | .packages 29 | build/ 30 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # notification_listener_service_example 2 | 3 | Demonstrates how to use the notification_listener_service plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'notification.listener.service' 2 | version '1.0' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:7.4.1' 12 | } 13 | } 14 | 15 | rootProject.allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | 24 | android { 25 | namespace 'notification.listener.service' 26 | 27 | compileSdkVersion 34 28 | 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | 34 | defaultConfig { 35 | minSdkVersion 16 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example/android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = run { 3 | val properties = java.util.Properties() 4 | file("local.properties").inputStream().use { properties.load(it) } 5 | val flutterSdkPath = properties.getProperty("flutter.sdk") 6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 7 | flutterSdkPath 8 | } 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 21 | id("com.android.application") version "8.7.0" apply false 22 | id("org.jetbrains.kotlin.android") version "1.8.22" apply false 23 | } 24 | 25 | include(":app") -------------------------------------------------------------------------------- /android/src/main/java/notification/listener/service/NotificationConstants.java: -------------------------------------------------------------------------------- 1 | package notification.listener.service; 2 | 3 | public final class NotificationConstants { 4 | public static String ID = "notification_id"; 5 | public static String INTENT = "slayer.notification.listener.service.intent"; 6 | public static String PACKAGE_NAME = "package_name"; 7 | public static String NOTIFICATION_CONTENT = "message"; 8 | public static String NOTIFICATION_TITLE = "title"; 9 | public static String HAVE_EXTRA_PICTURE = "contain_image"; 10 | public static String EXTRAS_PICTURE = "extras_picture"; 11 | public static String NOTIFICATIONS_ICON = "notifications_icon"; 12 | public static String NOTIFICATIONS_LARGE_ICON = "notifications_large_icon"; 13 | public static String IS_REMOVED = "is_removed"; 14 | public static String CAN_REPLY = "can_reply_to_it"; 15 | public static String IS_ONGOING = "is_ongoing"; 16 | } 17 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:notification_listener_service_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => widget is Text && 22 | widget.data!.startsWith('Running on:'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Iheb Briki 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to deal 4 | in the Software without restriction, including without limitation the rights 5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 16 | SOFTWARE. -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id("dev.flutter.flutter-gradle-plugin") 6 | } 7 | 8 | android { 9 | namespace = "com.example.notification_listener_service_example" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = "27.0.12077973" 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_11 15 | targetCompatibility = JavaVersion.VERSION_11 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_11.toString() 20 | } 21 | 22 | defaultConfig { 23 | applicationId = "com.example.notification_listener_service_example" 24 | // You can update the following values to match your application needs. 25 | // For more information, see: https://flutter.dev/to/review-gradle-config. 26 | minSdk = flutter.minSdkVersion 27 | targetSdk = flutter.targetSdkVersion 28 | versionCode = flutter.versionCode 29 | versionName = flutter.versionName 30 | } 31 | 32 | buildTypes { 33 | release { 34 | isMinifyEnabled = true 35 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 36 | } 37 | } 38 | } 39 | 40 | flutter { 41 | source = "../.." 42 | } -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /lib/notification_listener_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | import 'dart:io'; 4 | 5 | import 'package:flutter/services.dart'; 6 | import 'package:notification_listener_service/notification_event.dart'; 7 | 8 | const MethodChannel methodeChannel = 9 | MethodChannel('x-slayer/notifications_channel'); 10 | const EventChannel _eventChannel = EventChannel('x-slayer/notifications_event'); 11 | Stream? _stream; 12 | 13 | class NotificationListenerService { 14 | NotificationListenerService._(); 15 | 16 | /// stream the incoming notifications events 17 | static Stream get notificationsStream { 18 | if (Platform.isAndroid) { 19 | _stream ??= 20 | _eventChannel.receiveBroadcastStream().map( 21 | (event) => ServiceNotificationEvent.fromMap(event), 22 | ); 23 | return _stream!; 24 | } 25 | throw Exception("Notifications API exclusively available on Android!"); 26 | } 27 | 28 | /// request notification permission 29 | /// it will open the notification settings page and return `true` once the permission granted. 30 | static Future requestPermission() async { 31 | try { 32 | return await methodeChannel.invokeMethod('requestPermission'); 33 | } on PlatformException catch (error) { 34 | log("$error"); 35 | return Future.value(false); 36 | } 37 | } 38 | 39 | /// check if notification permission is enebaled 40 | static Future isPermissionGranted() async { 41 | try { 42 | return await methodeChannel.invokeMethod('isPermissionGranted'); 43 | } on PlatformException catch (error) { 44 | log("$error"); 45 | return false; 46 | } 47 | } 48 | 49 | /// get currently active notifications 50 | static Future> getActiveNotifications() async { 51 | try { 52 | final List result = 53 | await methodeChannel.invokeMethod('getActiveNotifications'); 54 | return result 55 | .map((item) => ServiceNotificationEvent.fromMap(item)) 56 | .toList(); 57 | } on PlatformException catch (error) { 58 | log("getActiveNotifications error: $error"); 59 | return []; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /android/src/main/java/notification/listener/service/NotificationReceiver.java: -------------------------------------------------------------------------------- 1 | package notification.listener.service; 2 | 3 | import static notification.listener.service.NotificationConstants.*; 4 | 5 | import android.content.BroadcastReceiver; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.os.Build.VERSION_CODES; 9 | 10 | import androidx.annotation.RequiresApi; 11 | 12 | import io.flutter.plugin.common.EventChannel.EventSink; 13 | 14 | import java.util.HashMap; 15 | 16 | public class NotificationReceiver extends BroadcastReceiver { 17 | 18 | private EventSink eventSink; 19 | 20 | public NotificationReceiver(EventSink eventSink) { 21 | this.eventSink = eventSink; 22 | } 23 | 24 | @RequiresApi(api = VERSION_CODES.JELLY_BEAN_MR2) 25 | @Override 26 | public void onReceive(Context context, Intent intent) { 27 | String packageName = intent.getStringExtra(PACKAGE_NAME); 28 | String title = intent.getStringExtra(NOTIFICATION_TITLE); 29 | String content = intent.getStringExtra(NOTIFICATION_CONTENT); 30 | byte[] notificationIcon = intent.getByteArrayExtra(NOTIFICATIONS_ICON); 31 | byte[] notificationExtrasPicture = intent.getByteArrayExtra(EXTRAS_PICTURE); 32 | byte[] largeIcon = intent.getByteArrayExtra(NOTIFICATIONS_LARGE_ICON); 33 | boolean haveExtraPicture = intent.getBooleanExtra(HAVE_EXTRA_PICTURE, false); 34 | boolean hasRemoved = intent.getBooleanExtra(IS_REMOVED, false); 35 | boolean canReply = intent.getBooleanExtra(CAN_REPLY, false); 36 | boolean isOngoing = intent.getBooleanExtra(IS_ONGOING, false); 37 | int id = intent.getIntExtra(ID, -1); 38 | 39 | 40 | HashMap data = new HashMap<>(); 41 | data.put("id", id); 42 | data.put("packageName", packageName); 43 | data.put("title", title); 44 | data.put("content", content); 45 | data.put("notificationIcon", notificationIcon); 46 | data.put("notificationExtrasPicture", notificationExtrasPicture); 47 | data.put("haveExtraPicture", haveExtraPicture); 48 | data.put("largeIcon", largeIcon); 49 | data.put("hasRemoved", hasRemoved); 50 | data.put("canReply", canReply); 51 | data.put("onGoing", isOngoing); 52 | 53 | eventSink.success(data); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/notification_listener_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:notification_listener_service/notification_listener_service.dart'; 4 | 5 | void main() { 6 | TestWidgetsFlutterBinding.ensureInitialized(); 7 | const MethodChannel methodChannel = 8 | MethodChannel('x-slayer/notifications_channel'); 9 | 10 | setUp(() { 11 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger 12 | .setMockMethodCallHandler(methodChannel, (MethodCall methodCall) async { 13 | switch (methodCall.method) { 14 | case 'requestPermission': 15 | return true; 16 | case 'isPermissionGranted': 17 | return true; 18 | default: 19 | return null; 20 | } 21 | }); 22 | }); 23 | 24 | tearDown(() { 25 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger 26 | .setMockMethodCallHandler(methodChannel, null); 27 | }); 28 | 29 | group('NotificationListenerService', () { 30 | test('requestPermission returns true when permission is granted', () async { 31 | final result = await NotificationListenerService.requestPermission(); 32 | expect(result, isTrue); 33 | }); 34 | 35 | test('requestPermission returns false and logs on PlatformException', 36 | () async { 37 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger 38 | .setMockMethodCallHandler(methodChannel, 39 | (MethodCall methodCall) async { 40 | if (methodCall.method == 'requestPermission') { 41 | throw PlatformException(code: 'PERMISSION_DENIED'); 42 | } 43 | return null; 44 | }); 45 | 46 | final result = await NotificationListenerService.requestPermission(); 47 | expect(result, isFalse); 48 | }); 49 | 50 | test('isPermissionGranted returns true when permission is granted', 51 | () async { 52 | final result = await NotificationListenerService.isPermissionGranted(); 53 | expect(result, isTrue); 54 | }); 55 | 56 | test('isPermissionGranted returns false and logs on PlatformException', 57 | () async { 58 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger 59 | .setMockMethodCallHandler(methodChannel, 60 | (MethodCall methodCall) async { 61 | if (methodCall.method == 'isPermissionGranted') { 62 | throw PlatformException(code: 'PERMISSION_DENIED'); 63 | } 64 | return null; 65 | }); 66 | 67 | final result = await NotificationListenerService.isPermissionGranted(); 68 | expect(result, isFalse); 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: notification_listener_service 2 | description: Flutter Plugin to listen to all incoming notifications (posted or removed) with the possibility to reply to them 3 | version: 0.3.5 4 | homepage: https://github.com/X-SLAYER/notification_listener_service 5 | 6 | environment: 7 | sdk: ">=2.16.2 <4.0.0" 8 | flutter: ">=2.5.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | flutter_lints: ^1.0.0 18 | 19 | # For information on the generic Dart part of this file, see the 20 | # following page: https://dart.dev/tools/pub/pubspec 21 | 22 | # The following section is specific to Flutter. 23 | flutter: 24 | # This section identifies this Flutter project as a plugin project. 25 | # The 'pluginClass' and Android 'package' identifiers should not ordinarily 26 | # be modified. They are used by the tooling to maintain consistency when 27 | # adding or updating assets for this project. 28 | plugin: 29 | platforms: 30 | # This plugin project was generated without specifying any 31 | # platforms with the `--platform` argument. If you see the `some_platform` map below, remove it and 32 | # then add platforms following the instruction here: 33 | # https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms 34 | # ------------------- 35 | android: 36 | package: notification.listener.service 37 | pluginClass: NotificationListenerServicePlugin 38 | # ------------------- 39 | 40 | # To add assets to your plugin package, add an assets section, like this: 41 | # assets: 42 | # - images/a_dot_burr.jpeg 43 | # - images/a_dot_ham.jpeg 44 | # 45 | # For details regarding assets in packages, see 46 | # https://flutter.dev/assets-and-images/#from-packages 47 | # 48 | # An image asset can refer to one or more resolution-specific "variants", see 49 | # https://flutter.dev/assets-and-images/#resolution-aware. 50 | 51 | # To add custom fonts to your plugin package, add a fonts section here, 52 | # in this "flutter" section. Each entry in this list should have a 53 | # "family" key with the font family name, and a "fonts" key with a 54 | # list giving the asset and other descriptors for the font. For 55 | # example: 56 | # fonts: 57 | # - family: Schyler 58 | # fonts: 59 | # - asset: fonts/Schyler-Regular.ttf 60 | # - asset: fonts/Schyler-Italic.ttf 61 | # style: italic 62 | # - family: Trajan Pro 63 | # fonts: 64 | # - asset: fonts/TrajanPro.ttf 65 | # - asset: fonts/TrajanPro_Bold.ttf 66 | # weight: 700 67 | # 68 | # For details regarding fonts in packages, see 69 | # https://flutter.dev/custom-fonts/#from-packages 70 | -------------------------------------------------------------------------------- /android/src/main/java/notification/listener/service/models/RemoteInputParcel.java: -------------------------------------------------------------------------------- 1 | package notification.listener.service.models; 2 | 3 | import android.os.Bundle; 4 | import android.os.Parcel; 5 | import android.os.Parcelable; 6 | 7 | import androidx.core.app.RemoteInput; 8 | 9 | /** 10 | * Created by JJ on 05/08/15. 11 | */ 12 | public class RemoteInputParcel implements Parcelable { 13 | 14 | private String label; 15 | private String resultKey; 16 | private String[] choices = new String[0]; 17 | private boolean allowFreeFormInput; 18 | private Bundle extras; 19 | 20 | 21 | public RemoteInputParcel(RemoteInput input) { 22 | label = input.getLabel().toString(); 23 | resultKey = input.getResultKey(); 24 | charSequenceToStringArray(input.getChoices()); 25 | allowFreeFormInput = input.getAllowFreeFormInput(); 26 | extras = input.getExtras(); 27 | } 28 | 29 | public RemoteInputParcel(Parcel in) { 30 | label = in.readString(); 31 | resultKey = in.readString(); 32 | choices = in.createStringArray(); 33 | allowFreeFormInput = in.readByte() != 0; 34 | extras = in.readParcelable(Bundle.class.getClassLoader()); 35 | } 36 | 37 | public void charSequenceToStringArray(CharSequence[] charSequence) { 38 | if (charSequence != null) { 39 | int size = charSequence.length; 40 | choices = new String[charSequence.length]; 41 | for (int i = 0; i < size; i++) 42 | choices[i] = charSequence[i].toString(); 43 | } 44 | } 45 | 46 | public String getResultKey() { 47 | return resultKey; 48 | } 49 | 50 | public String getLabel() { 51 | return label; 52 | } 53 | 54 | public CharSequence[] getChoices() { 55 | return choices; 56 | } 57 | 58 | public boolean isAllowFreeFormInput() { 59 | return allowFreeFormInput; 60 | } 61 | 62 | public Bundle getExtras() { 63 | return extras; 64 | } 65 | 66 | @Override 67 | public void writeToParcel(Parcel dest, int flags) { 68 | dest.writeString(label); 69 | dest.writeString(resultKey); 70 | dest.writeStringArray(choices); 71 | dest.writeByte((byte) (allowFreeFormInput ? 1 : 0)); 72 | dest.writeParcelable(extras, flags); 73 | } 74 | 75 | @Override 76 | public int describeContents() { 77 | return 0; 78 | } 79 | 80 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 81 | public RemoteInputParcel createFromParcel(Parcel in) { 82 | return new RemoteInputParcel(in); 83 | } 84 | 85 | public RemoteInputParcel[] newArray(int size) { 86 | return new RemoteInputParcel[size]; 87 | } 88 | }; 89 | 90 | } -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /lib/notification_event.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'notification_listener_service.dart'; 4 | 5 | class ServiceNotificationEvent { 6 | /// the notification id 7 | int? id; 8 | 9 | /// check if we can reply the Notification 10 | bool? canReply; 11 | 12 | /// if the notification has an extras image 13 | bool? haveExtraPicture; 14 | 15 | /// if the notification has been removed 16 | bool? hasRemoved; 17 | 18 | /// notification extras image 19 | /// To display an image simply use the [Image.memory] widget. 20 | /// Example: 21 | /// 22 | /// ``` 23 | /// Image.memory(notif.extrasPicture) 24 | /// ``` 25 | Uint8List? extrasPicture; 26 | 27 | /// notification package name 28 | String? packageName; 29 | 30 | /// notification title 31 | String? title; 32 | 33 | /// the notification app icon 34 | /// To display an image simply use the [Image.memory] widget. 35 | /// Example: 36 | /// 37 | /// ``` 38 | /// Image.memory(notif.appIcon) 39 | /// ``` 40 | Uint8List? appIcon; 41 | 42 | /// the notification large icon (ex: album covers) 43 | /// To display an image simply use the [Image.memory] widget. 44 | /// Example: 45 | /// 46 | /// ``` 47 | /// Image.memory(notif.largeIcon) 48 | /// ``` 49 | Uint8List? largeIcon; 50 | 51 | /// the content of the notification 52 | String? content; 53 | 54 | /// if the notification is ongoing (cannot be dismissed and is in progress) 55 | bool? onGoing; 56 | 57 | ServiceNotificationEvent({ 58 | this.id, 59 | this.canReply, 60 | this.haveExtraPicture, 61 | this.hasRemoved, 62 | this.extrasPicture, 63 | this.packageName, 64 | this.title, 65 | this.appIcon, 66 | this.largeIcon, 67 | this.content, 68 | this.onGoing, 69 | }); 70 | 71 | ServiceNotificationEvent.fromMap(Map map) { 72 | id = map['id']; 73 | canReply = map['canReply']; 74 | haveExtraPicture = map['haveExtraPicture']; 75 | hasRemoved = map['hasRemoved']; 76 | extrasPicture = map['notificationExtrasPicture']; 77 | packageName = map['packageName']; 78 | title = map['title']; 79 | appIcon = map['appIcon']; 80 | largeIcon = map['largeIcon']; 81 | content = map['content']; 82 | onGoing = map['onGoing']; 83 | } 84 | 85 | /// send a direct message reply to the incoming notification 86 | Future sendReply(String message) async { 87 | if (!canReply!) throw Exception("The notification is not replyable"); 88 | try { 89 | return await methodeChannel.invokeMethod("sendReply", { 90 | 'message': message, 91 | 'notificationId': id, 92 | }) ?? 93 | false; 94 | } catch (e) { 95 | rethrow; 96 | } 97 | } 98 | 99 | @override 100 | String toString() { 101 | return '''ServiceNotificationEvent( 102 | id: $id 103 | can reply: $canReply 104 | packageName: $packageName 105 | title: $title 106 | content: $content 107 | hasRemoved: $hasRemoved 108 | haveExtraPicture: $haveExtraPicture 109 | onGoing: $onGoing 110 | '''; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # notification_listener_service 2 | 3 | A flutter plugin for interacting with Notification Service in Android. 4 | 5 | NotificationListenerService is a service that receives calls from the system when new notifications are posted or removed, 6 | 7 | for more info check [NotificationListenerService](https://developer.android.com/reference/android/service/notification/NotificationListenerService) 8 | 9 | ### Installation and usage 10 | 11 | Add package to your pubspec: 12 | 13 | ```yaml 14 | dependencies: 15 | notification_listener_service: any # or the latest version on Pub 16 | ``` 17 | 18 | Inside AndroidManifest add this to bind notification service with your application 19 | 20 | ``` 21 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ``` 31 | 32 | ### USAGE 33 | 34 | ```dart 35 | /// check if notification permession is enebaled 36 | final bool status = await NotificationListenerService.isPermissionGranted(); 37 | 38 | /// request notification permission 39 | /// it will open the notifications settings page and return `true` once the permission granted. 40 | final bool status = await NotificationListenerService.requestPermission(); 41 | 42 | /// stream the incoming notification events 43 | NotificationListenerService.notificationsStream.listen((event) { 44 | log("Current notification: $event"); 45 | }); 46 | ``` 47 | 48 | The `ServiceNotificationEvent` provides: 49 | 50 | ```dart 51 | /// the notification id 52 | int? id; 53 | 54 | /// check if we can reply the Notification 55 | bool? canReply; 56 | 57 | /// if the notification has an extras image 58 | bool? haveExtraPicture; 59 | 60 | /// if the notification has been removed 61 | bool? hasRemoved; 62 | 63 | /// notification extras image 64 | /// To display an image simply use the [Image.memory] widget. 65 | Uint8List? extrasPicture; 66 | 67 | /// notification large icon 68 | /// To display an image simply use the [Image.memory] widget. 69 | Uint8List? largeIcon; 70 | 71 | /// notification package name 72 | String? packageName; 73 | 74 | /// notification title 75 | String? title; 76 | 77 | /// the notification app icon 78 | /// To display an image simply use the [Image.memory] widget. 79 | Uint8List? appIcon; 80 | 81 | /// the content of the notification 82 | String? content; 83 | 84 | /// send a direct message reply to the incoming notification 85 | Future sendReply(String message) 86 | 87 | ``` 88 | 89 | To reply to a notification provides: 90 | 91 | ```dart 92 | try { 93 | await event.sendReply("This is an auto response"); 94 | } catch (e) { 95 | log(e.toString()); 96 | } 97 | 98 | ``` 99 | ## Example of the app on foreground 100 | 101 | Find the exemple app [Here](https://github.com/X-SLAYER/foreground_plugins_test) 102 | 103 | ## Screenshots 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: notification_listener_service_example 2 | description: Demonstrates how to use the notification_listener_service plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: ">=2.16.2 <3.0.0" 10 | 11 | # Dependencies specify other packages that your package needs in order to work. 12 | # To automatically upgrade your package dependencies to the latest versions 13 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 14 | # dependencies can be manually updated by changing the version numbers below to 15 | # the latest version available on pub.dev. To see which dependencies have newer 16 | # versions available, run `flutter pub outdated`. 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | 21 | notification_listener_service: 22 | # When depending on this package from a real application you should use: 23 | # notification_listener_service: ^x.y.z 24 | # See https://dart.dev/tools/pub/dependencies#version-constraints 25 | # The example app is bundled with the plugin so we use a path dependency on 26 | # the parent directory to use the current plugin's version. 27 | path: ../ 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^1.0.2 32 | 33 | dev_dependencies: 34 | flutter_test: 35 | sdk: flutter 36 | 37 | # The "flutter_lints" package below contains a set of recommended lints to 38 | # encourage good coding practices. The lint set provided by the package is 39 | # activated in the `analysis_options.yaml` file located at the root of your 40 | # package. See that file for information about deactivating specific lint 41 | # rules and activating additional ones. 42 | flutter_lints: ^1.0.0 43 | 44 | # For information on the generic Dart part of this file, see the 45 | # following page: https://dart.dev/tools/pub/pubspec 46 | 47 | # The following section is specific to Flutter. 48 | flutter: 49 | 50 | # The following line ensures that the Material Icons font is 51 | # included with your application, so that you can use the icons in 52 | # the material Icons class. 53 | uses-material-design: true 54 | 55 | # To add assets to your application, add an assets section, like this: 56 | # assets: 57 | # - images/a_dot_burr.jpeg 58 | # - images/a_dot_ham.jpeg 59 | 60 | # An image asset can refer to one or more resolution-specific "variants", see 61 | # https://flutter.dev/assets-and-images/#resolution-aware. 62 | 63 | # For details regarding adding assets from package dependencies, see 64 | # https://flutter.dev/assets-and-images/#from-packages 65 | 66 | # To add custom fonts to your application, add a fonts section here, 67 | # in this "flutter" section. Each entry in this list should have a 68 | # "family" key with the font family name, and a "fonts" key with a 69 | # list giving the asset and other descriptors for the font. For 70 | # example: 71 | # fonts: 72 | # - family: Schyler 73 | # fonts: 74 | # - asset: fonts/Schyler-Regular.ttf 75 | # - asset: fonts/Schyler-Italic.ttf 76 | # style: italic 77 | # - family: Trajan Pro 78 | # fonts: 79 | # - asset: fonts/TrajanPro.ttf 80 | # - asset: fonts/TrajanPro_Bold.ttf 81 | # weight: 700 82 | # 83 | # For details regarding fonts from package dependencies, 84 | # see https://flutter.dev/custom-fonts/#from-packages 85 | -------------------------------------------------------------------------------- /android/src/main/java/notification/listener/service/models/Action.java: -------------------------------------------------------------------------------- 1 | package notification.listener.service.models; 2 | 3 | import android.app.PendingIntent; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.os.Parcel; 8 | import android.os.Parcelable; 9 | import android.util.Log; 10 | 11 | import androidx.core.app.NotificationCompat; 12 | import androidx.core.app.RemoteInput; 13 | 14 | import java.util.ArrayList; 15 | 16 | public class Action implements Parcelable { 17 | 18 | private final String text; 19 | private final String packageName; 20 | private final PendingIntent p; 21 | private final boolean isQuickReply; 22 | private final ArrayList remoteInputs = new ArrayList<>(); 23 | 24 | public Action(Parcel in) { 25 | text = in.readString(); 26 | packageName = in.readString(); 27 | p = in.readParcelable(PendingIntent.class.getClassLoader()); 28 | isQuickReply = in.readByte() != 0; 29 | in.readTypedList(remoteInputs, RemoteInputParcel.CREATOR); 30 | } 31 | 32 | @Override 33 | public void writeToParcel(Parcel dest, int flags) { 34 | dest.writeString(text); 35 | dest.writeString(packageName); 36 | dest.writeParcelable(p, flags); 37 | dest.writeByte((byte) (isQuickReply ? 1 : 0)); 38 | dest.writeTypedList(remoteInputs); 39 | } 40 | 41 | public Action(String text, String packageName, PendingIntent p, RemoteInput remoteInput, boolean isQuickReply) { 42 | this.text = text; 43 | this.packageName = packageName; 44 | this.p = p; 45 | this.isQuickReply = isQuickReply; 46 | remoteInputs.add(new RemoteInputParcel(remoteInput)); 47 | } 48 | 49 | public Action(NotificationCompat.Action action, String packageName, boolean isQuickReply) { 50 | this.text = action.title.toString(); 51 | this.packageName = packageName; 52 | this.p = action.actionIntent; 53 | if (action.getRemoteInputs() != null) { 54 | int size = action.getRemoteInputs().length; 55 | for (int i = 0; i < size; i++) 56 | remoteInputs.add(new RemoteInputParcel(action.getRemoteInputs()[i])); 57 | } 58 | this.isQuickReply = isQuickReply; 59 | } 60 | 61 | public void sendReply(Context context, String msg) throws PendingIntent.CanceledException { 62 | Intent intent = new Intent(); 63 | Bundle bundle = new Bundle(); 64 | ArrayList actualInputs = new ArrayList<>(); 65 | 66 | for (RemoteInputParcel input : remoteInputs) { 67 | Log.i("", "RemoteInput: " + input.getLabel()); 68 | bundle.putCharSequence(input.getResultKey(), msg); 69 | RemoteInput.Builder builder = new RemoteInput.Builder(input.getResultKey()); 70 | builder.setLabel(input.getLabel()); 71 | builder.setChoices(input.getChoices()); 72 | builder.setAllowFreeFormInput(input.isAllowFreeFormInput()); 73 | builder.addExtras(input.getExtras()); 74 | actualInputs.add(builder.build()); 75 | } 76 | 77 | RemoteInput[] inputs = actualInputs.toArray(new RemoteInput[actualInputs.size()]); 78 | RemoteInput.addResultsToIntent(inputs, intent, bundle); 79 | p.send(context, 0, intent); 80 | } 81 | 82 | public ArrayList getRemoteInputs() { 83 | return remoteInputs; 84 | } 85 | 86 | public boolean isQuickReply() { 87 | return isQuickReply; 88 | } 89 | 90 | public String getText() { 91 | return text; 92 | } 93 | 94 | public PendingIntent getQuickReplyIntent() { 95 | return isQuickReply ? p : null; 96 | } 97 | 98 | public String getPackageName() { 99 | return packageName; 100 | } 101 | 102 | @Override 103 | public int describeContents() { 104 | return 0; 105 | } 106 | 107 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 108 | public Action createFromParcel(Parcel in) { 109 | return new Action(in); 110 | } 111 | 112 | public Action[] newArray(int size) { 113 | return new Action[size]; 114 | } 115 | }; 116 | 117 | } -------------------------------------------------------------------------------- /android/src/main/java/notification/listener/service/NotificationUtils.java: -------------------------------------------------------------------------------- 1 | package notification.listener.service; 2 | 3 | import android.app.Notification; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.graphics.Bitmap; 7 | import android.graphics.Canvas; 8 | import android.graphics.drawable.Drawable; 9 | import android.os.Build; 10 | import android.provider.Settings; 11 | import android.text.TextUtils; 12 | 13 | import androidx.annotation.RequiresApi; 14 | import androidx.core.app.NotificationCompat; 15 | import androidx.core.app.RemoteInput; 16 | 17 | import notification.listener.service.models.Action; 18 | 19 | public final class NotificationUtils { 20 | 21 | private static final String[] REPLY_KEYWORDS = {"reply", "android.intent.extra.text"}; 22 | private static final CharSequence INPUT_KEYWORD = "input"; 23 | 24 | public static Bitmap getBitmapFromDrawable(Drawable drawable) { 25 | final Bitmap bmp = Bitmap.createBitmap( 26 | drawable.getIntrinsicWidth(), 27 | drawable.getIntrinsicHeight(), 28 | Bitmap.Config.ARGB_8888); 29 | 30 | final Canvas canvas = new Canvas(bmp); 31 | drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 32 | drawable.draw(canvas); 33 | 34 | return bmp; 35 | } 36 | 37 | public static boolean isPermissionGranted(Context context) { 38 | String packageName = context.getPackageName(); 39 | String flat = Settings.Secure.getString(context.getContentResolver(), 40 | "enabled_notification_listeners"); 41 | if (!TextUtils.isEmpty(flat)) { 42 | String[] names = flat.split(":"); 43 | for (String name : names) { 44 | ComponentName componentName = ComponentName.unflattenFromString(name); 45 | boolean nameMatch = TextUtils.equals(packageName, componentName.getPackageName()); 46 | if (nameMatch) { 47 | return true; 48 | } 49 | } 50 | } 51 | return false; 52 | } 53 | 54 | @RequiresApi(api = Build.VERSION_CODES.KITKAT) 55 | public static Action getQuickReplyAction(Notification n, String packageName) { 56 | NotificationCompat.Action action = null; 57 | if (Build.VERSION.SDK_INT >= 24) 58 | action = getQuickReplyAction(n); 59 | if (action == null) 60 | action = getWearReplyAction(n); 61 | if (action == null) 62 | return null; 63 | return new Action(action, packageName, true); 64 | } 65 | 66 | private static NotificationCompat.Action getQuickReplyAction(Notification n) { 67 | for (int i = 0; i < NotificationCompat.getActionCount(n); i++) { 68 | NotificationCompat.Action action = NotificationCompat.getAction(n, i); 69 | if (action.getRemoteInputs() != null) { 70 | for (int x = 0; x < action.getRemoteInputs().length; x++) { 71 | RemoteInput remoteInput = action.getRemoteInputs()[x]; 72 | if (isKnownReplyKey(remoteInput.getResultKey())) 73 | return action; 74 | } 75 | } 76 | } 77 | return null; 78 | } 79 | 80 | private static NotificationCompat.Action getWearReplyAction(Notification n) { 81 | NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(n); 82 | for (NotificationCompat.Action action : wearableExtender.getActions()) { 83 | if (action.getRemoteInputs() != null) { 84 | for (int x = 0; x < action.getRemoteInputs().length; x++) { 85 | RemoteInput remoteInput = action.getRemoteInputs()[x]; 86 | if (isKnownReplyKey(remoteInput.getResultKey())) 87 | return action; 88 | else if (remoteInput.getResultKey().toLowerCase().contains(INPUT_KEYWORD)) 89 | return action; 90 | } 91 | } 92 | } 93 | return null; 94 | } 95 | 96 | private static boolean isKnownReplyKey(String resultKey) { 97 | if (TextUtils.isEmpty(resultKey)) 98 | return false; 99 | 100 | resultKey = resultKey.toLowerCase(); 101 | for (String keyword : REPLY_KEYWORDS) 102 | if (resultKey.contains(keyword)) 103 | return true; 104 | 105 | return false; 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:notification_listener_service/notification_event.dart'; 6 | import 'package:notification_listener_service/notification_listener_service.dart'; 7 | 8 | void main() { 9 | runApp(const MyApp()); 10 | } 11 | 12 | class MyApp extends StatefulWidget { 13 | const MyApp({Key? key}) : super(key: key); 14 | 15 | @override 16 | State createState() => _MyAppState(); 17 | } 18 | 19 | class _MyAppState extends State { 20 | StreamSubscription? _subscription; 21 | List events = []; 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return MaterialApp( 31 | debugShowCheckedModeBanner: false, 32 | home: Scaffold( 33 | appBar: AppBar( 34 | title: const Text('Plugin example app'), 35 | ), 36 | body: Center( 37 | child: Column( 38 | children: [ 39 | SingleChildScrollView( 40 | scrollDirection: Axis.horizontal, 41 | child: Row( 42 | crossAxisAlignment: CrossAxisAlignment.center, 43 | children: [ 44 | TextButton( 45 | onPressed: () async { 46 | final res = await NotificationListenerService 47 | .requestPermission(); 48 | log("Is enabled: $res"); 49 | }, 50 | child: const Text("Request Permission"), 51 | ), 52 | const SizedBox(height: 20.0), 53 | TextButton( 54 | onPressed: () async { 55 | final bool res = await NotificationListenerService 56 | .isPermissionGranted(); 57 | log("Is enabled: $res"); 58 | }, 59 | child: const Text("Check Permission"), 60 | ), 61 | const SizedBox(height: 20.0), 62 | TextButton( 63 | onPressed: () { 64 | _subscription = NotificationListenerService 65 | .notificationsStream 66 | .listen((event) { 67 | log("$event"); 68 | setState(() { 69 | events.add(event); 70 | }); 71 | }); 72 | }, 73 | child: const Text("Start Stream"), 74 | ), 75 | const SizedBox(height: 20.0), 76 | TextButton( 77 | onPressed: () { 78 | _subscription?.cancel(); 79 | }, 80 | child: const Text("Stop Stream"), 81 | ), 82 | ], 83 | ), 84 | ), 85 | Expanded( 86 | child: ListView.builder( 87 | shrinkWrap: true, 88 | itemCount: events.length, 89 | itemBuilder: (_, index) => Padding( 90 | padding: const EdgeInsets.only(bottom: 8.0), 91 | child: ListTile( 92 | onTap: () async { 93 | try { 94 | await events[index] 95 | .sendReply("This is an auto response"); 96 | } catch (e) { 97 | log(e.toString()); 98 | } 99 | }, 100 | trailing: events[index].hasRemoved! 101 | ? const Text( 102 | "Removed", 103 | style: TextStyle(color: Colors.red), 104 | ) 105 | : const SizedBox.shrink(), 106 | leading: events[index].appIcon == null 107 | ? const SizedBox.shrink() 108 | : Image.memory( 109 | events[index].appIcon!, 110 | width: 35.0, 111 | height: 35.0, 112 | ), 113 | title: Text(events[index].title ?? "No title"), 114 | subtitle: Column( 115 | crossAxisAlignment: CrossAxisAlignment.start, 116 | children: [ 117 | Text( 118 | events[index].content ?? "no content", 119 | style: const TextStyle(fontWeight: FontWeight.bold), 120 | ), 121 | const SizedBox(height: 8.0), 122 | if (events[index].onGoing == true) 123 | const Text( 124 | "Ongoing notification", 125 | style: TextStyle(color: Colors.orange), 126 | ), 127 | events[index].canReply! 128 | ? const Text( 129 | "Replied with: This is an auto reply", 130 | style: TextStyle(color: Colors.purple), 131 | ) 132 | : const SizedBox.shrink(), 133 | events[index].largeIcon != null 134 | ? Image.memory( 135 | events[index].largeIcon!, 136 | ) 137 | : const SizedBox.shrink(), 138 | ], 139 | ), 140 | isThreeLine: true, 141 | ), 142 | ), 143 | ), 144 | ) 145 | ], 146 | ), 147 | ), 148 | ), 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.13.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.2" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.4.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.2" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.19.1" 44 | cupertino_icons: 45 | dependency: "direct main" 46 | description: 47 | name: cupertino_icons 48 | sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.0.6" 52 | fake_async: 53 | dependency: transitive 54 | description: 55 | name: fake_async 56 | sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.3" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_lints: 66 | dependency: "direct dev" 67 | description: 68 | name: flutter_lints 69 | sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 70 | url: "https://pub.dev" 71 | source: hosted 72 | version: "1.0.4" 73 | flutter_test: 74 | dependency: "direct dev" 75 | description: flutter 76 | source: sdk 77 | version: "0.0.0" 78 | leak_tracker: 79 | dependency: transitive 80 | description: 81 | name: leak_tracker 82 | sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "11.0.2" 86 | leak_tracker_flutter_testing: 87 | dependency: transitive 88 | description: 89 | name: leak_tracker_flutter_testing 90 | sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "3.0.10" 94 | leak_tracker_testing: 95 | dependency: transitive 96 | description: 97 | name: leak_tracker_testing 98 | sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "3.0.2" 102 | lints: 103 | dependency: transitive 104 | description: 105 | name: lints 106 | sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "1.0.1" 110 | matcher: 111 | dependency: transitive 112 | description: 113 | name: matcher 114 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "0.12.17" 118 | material_color_utilities: 119 | dependency: transitive 120 | description: 121 | name: material_color_utilities 122 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 123 | url: "https://pub.dev" 124 | source: hosted 125 | version: "0.11.1" 126 | meta: 127 | dependency: transitive 128 | description: 129 | name: meta 130 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 131 | url: "https://pub.dev" 132 | source: hosted 133 | version: "1.16.0" 134 | notification_listener_service: 135 | dependency: "direct main" 136 | description: 137 | path: ".." 138 | relative: true 139 | source: path 140 | version: "0.3.4" 141 | path: 142 | dependency: transitive 143 | description: 144 | name: path 145 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 146 | url: "https://pub.dev" 147 | source: hosted 148 | version: "1.9.1" 149 | sky_engine: 150 | dependency: transitive 151 | description: flutter 152 | source: sdk 153 | version: "0.0.0" 154 | source_span: 155 | dependency: transitive 156 | description: 157 | name: source_span 158 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 159 | url: "https://pub.dev" 160 | source: hosted 161 | version: "1.10.1" 162 | stack_trace: 163 | dependency: transitive 164 | description: 165 | name: stack_trace 166 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 167 | url: "https://pub.dev" 168 | source: hosted 169 | version: "1.12.1" 170 | stream_channel: 171 | dependency: transitive 172 | description: 173 | name: stream_channel 174 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 175 | url: "https://pub.dev" 176 | source: hosted 177 | version: "2.1.4" 178 | string_scanner: 179 | dependency: transitive 180 | description: 181 | name: string_scanner 182 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 183 | url: "https://pub.dev" 184 | source: hosted 185 | version: "1.4.1" 186 | term_glyph: 187 | dependency: transitive 188 | description: 189 | name: term_glyph 190 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 191 | url: "https://pub.dev" 192 | source: hosted 193 | version: "1.2.2" 194 | test_api: 195 | dependency: transitive 196 | description: 197 | name: test_api 198 | sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" 199 | url: "https://pub.dev" 200 | source: hosted 201 | version: "0.7.6" 202 | vector_math: 203 | dependency: transitive 204 | description: 205 | name: vector_math 206 | sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b 207 | url: "https://pub.dev" 208 | source: hosted 209 | version: "2.2.0" 210 | vm_service: 211 | dependency: transitive 212 | description: 213 | name: vm_service 214 | sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 215 | url: "https://pub.dev" 216 | source: hosted 217 | version: "15.0.0" 218 | sdks: 219 | dart: ">=3.8.0-0 <4.0.0" 220 | flutter: ">=3.18.0-18.0.pre.54" 221 | -------------------------------------------------------------------------------- /android/src/main/java/notification/listener/service/NotificationListenerServicePlugin.java: -------------------------------------------------------------------------------- 1 | package notification.listener.service; 2 | 3 | import static notification.listener.service.NotificationUtils.isPermissionGranted; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.app.Activity; 7 | import android.app.PendingIntent; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.IntentFilter; 11 | import android.os.Build; 12 | import android.provider.Settings; 13 | import android.util.Log; 14 | import android.content.ActivityNotFoundException; 15 | import androidx.annotation.NonNull; 16 | import androidx.annotation.RequiresApi; 17 | 18 | import io.flutter.embedding.engine.plugins.FlutterPlugin; 19 | import io.flutter.embedding.engine.plugins.activity.ActivityAware; 20 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; 21 | import io.flutter.plugin.common.EventChannel; 22 | import io.flutter.plugin.common.MethodCall; 23 | import io.flutter.plugin.common.MethodChannel; 24 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 25 | import io.flutter.plugin.common.MethodChannel.Result; 26 | import io.flutter.plugin.common.PluginRegistry; 27 | import notification.listener.service.models.Action; 28 | import notification.listener.service.models.ActionCache; 29 | import android.annotation.SuppressLint; 30 | import android.os.Build; 31 | 32 | import java.util.List; 33 | import java.util.Map; 34 | 35 | public class NotificationListenerServicePlugin implements FlutterPlugin, ActivityAware, MethodCallHandler, PluginRegistry.ActivityResultListener, EventChannel.StreamHandler { 36 | 37 | private static final String CHANNEL_TAG = "x-slayer/notifications_channel"; 38 | private static final String EVENT_TAG = "x-slayer/notifications_event"; 39 | 40 | private MethodChannel channel; 41 | private EventChannel eventChannel; 42 | private NotificationReceiver notificationReceiver; 43 | private Context context; 44 | private Activity mActivity; 45 | 46 | private Result pendingResult; 47 | final int REQUEST_CODE_FOR_NOTIFICATIONS = 1199; 48 | 49 | @Override 50 | public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { 51 | context = flutterPluginBinding.getApplicationContext(); 52 | channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), CHANNEL_TAG); 53 | channel.setMethodCallHandler(this); 54 | eventChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), EVENT_TAG); 55 | eventChannel.setStreamHandler(this); 56 | } 57 | 58 | @Override 59 | public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { 60 | pendingResult = result; 61 | if (call.method.equals("isPermissionGranted")) { 62 | result.success(isPermissionGranted(context)); 63 | } else if (call.method.equals("requestPermission")) { 64 | Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS); 65 | try { 66 | mActivity.startActivityForResult(intent, REQUEST_CODE_FOR_NOTIFICATIONS); 67 | result.success(null); 68 | } catch (ActivityNotFoundException e) { 69 | Log.e("NotificationPlugin", "ActivityNotFoundException: " + e.getMessage()); 70 | result.error("ACTIVITY_NOT_FOUND", "No activity found to handle notification listener settings", null); 71 | } 72 | } else if (call.method.equals("sendReply")) { 73 | final String message = call.argument("message"); 74 | final int notificationId = call.argument("notificationId"); 75 | 76 | final Action action = ActionCache.cachedNotifications.get(notificationId); 77 | if (action == null) { 78 | result.error("Notification", "Can't find this cached notification", null); 79 | } 80 | try { 81 | action.sendReply(context, message); 82 | result.success(true); 83 | } catch (PendingIntent.CanceledException e) { 84 | result.success(false); 85 | e.printStackTrace(); 86 | } 87 | } else if (call.method.equals("getActiveNotifications")) { 88 | NotificationListener service = NotificationListener.getInstance(); 89 | if (service != null) { 90 | List> notifications = service.getActiveNotificationData(); 91 | result.success(notifications); 92 | } else { 93 | result.error("ServiceUnavailable", "NotificationService not running", null); 94 | } 95 | } 96 | else { 97 | result.notImplemented(); 98 | } 99 | } 100 | 101 | @Override 102 | public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { 103 | channel.setMethodCallHandler(null); 104 | eventChannel.setStreamHandler(null); 105 | } 106 | 107 | @Override 108 | public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { 109 | this.mActivity = binding.getActivity(); 110 | binding.addActivityResultListener(this); 111 | } 112 | 113 | @Override 114 | public void onDetachedFromActivityForConfigChanges() { 115 | onDetachedFromActivity(); 116 | } 117 | 118 | @Override 119 | public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { 120 | onAttachedToActivity(binding); 121 | } 122 | 123 | @Override 124 | public void onDetachedFromActivity() { 125 | this.mActivity = null; 126 | } 127 | @SuppressLint("WrongConstant") 128 | @Override 129 | public void onListen(Object arguments, EventChannel.EventSink events) { 130 | IntentFilter intentFilter = new IntentFilter(); 131 | intentFilter.addAction(NotificationConstants.INTENT); 132 | notificationReceiver = new NotificationReceiver(events); 133 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 134 | context.registerReceiver(notificationReceiver, intentFilter, Context.RECEIVER_EXPORTED); 135 | }else{ 136 | context.registerReceiver(notificationReceiver, intentFilter); 137 | } 138 | Intent listenerIntent = new Intent(context, NotificationReceiver.class); 139 | context.startService(listenerIntent); 140 | Log.i("NotificationPlugin", "Started the notifications tracking service."); 141 | } 142 | 143 | @Override 144 | public void onCancel(Object arguments) { 145 | context.unregisterReceiver(notificationReceiver); 146 | notificationReceiver = null; 147 | } 148 | 149 | @Override 150 | public boolean onActivityResult(int requestCode, int resultCode, Intent data) { 151 | if (requestCode == REQUEST_CODE_FOR_NOTIFICATIONS) { 152 | if (resultCode == Activity.RESULT_OK) { 153 | pendingResult.success(true); 154 | } else if (resultCode == Activity.RESULT_CANCELED) { 155 | pendingResult.success(isPermissionGranted(context)); 156 | } else { 157 | pendingResult.success(false); 158 | } 159 | return true; 160 | } 161 | return false; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /android/src/main/java/notification/listener/service/NotificationListener.java: -------------------------------------------------------------------------------- 1 | package notification.listener.service; 2 | 3 | import static notification.listener.service.NotificationUtils.getBitmapFromDrawable; 4 | import static notification.listener.service.models.ActionCache.cachedNotifications; 5 | 6 | import android.annotation.SuppressLint; 7 | import android.app.Notification; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.pm.PackageManager; 11 | import android.graphics.Bitmap; 12 | import android.graphics.drawable.BitmapDrawable; 13 | import android.graphics.drawable.Drawable; 14 | import android.graphics.drawable.Icon; 15 | import android.os.Build; 16 | import android.os.Build.VERSION_CODES; 17 | import android.os.Bundle; 18 | import android.service.notification.NotificationListenerService; 19 | import android.service.notification.StatusBarNotification; 20 | import android.util.Log; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.ArrayList; 24 | import java.util.HashMap; 25 | 26 | import androidx.annotation.RequiresApi; 27 | 28 | import java.io.ByteArrayOutputStream; 29 | 30 | import notification.listener.service.models.Action; 31 | 32 | 33 | @SuppressLint("OverrideAbstract") 34 | @RequiresApi(api = VERSION_CODES.JELLY_BEAN_MR2) 35 | public class NotificationListener extends NotificationListenerService { 36 | private static NotificationListener instance; 37 | 38 | public static NotificationListener getInstance() { 39 | return instance; 40 | } 41 | 42 | @Override 43 | public void onListenerConnected() { 44 | super.onListenerConnected(); 45 | instance = this; 46 | } 47 | 48 | @RequiresApi(api = VERSION_CODES.KITKAT) 49 | @Override 50 | public void onNotificationPosted(StatusBarNotification notification) { 51 | handleNotification(notification, false); 52 | } 53 | 54 | @RequiresApi(api = VERSION_CODES.KITKAT) 55 | @Override 56 | public void onNotificationRemoved(StatusBarNotification sbn) { 57 | handleNotification(sbn, true); 58 | } 59 | 60 | @RequiresApi(api = VERSION_CODES.KITKAT) 61 | private void handleNotification(StatusBarNotification notification, boolean isRemoved) { 62 | String packageName = notification.getPackageName(); 63 | Bundle extras = notification.getNotification().extras; 64 | boolean isOngoing = (notification.getNotification().flags & Notification.FLAG_ONGOING_EVENT) != 0; 65 | byte[] appIcon = getAppIcon(packageName); 66 | byte[] largeIcon = null; 67 | Action action = NotificationUtils.getQuickReplyAction(notification.getNotification(), packageName); 68 | 69 | if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { 70 | largeIcon = getNotificationLargeIcon(getApplicationContext(), notification.getNotification()); 71 | } 72 | 73 | Intent intent = new Intent(NotificationConstants.INTENT); 74 | intent.putExtra(NotificationConstants.PACKAGE_NAME, packageName); 75 | intent.putExtra(NotificationConstants.ID, notification.getId()); 76 | intent.putExtra(NotificationConstants.CAN_REPLY, action != null); 77 | intent.putExtra(NotificationConstants.IS_ONGOING, isOngoing); 78 | 79 | if (NotificationUtils.getQuickReplyAction(notification.getNotification(), packageName) != null) { 80 | cachedNotifications.put(notification.getId(), action); 81 | } 82 | 83 | intent.putExtra(NotificationConstants.NOTIFICATIONS_ICON, appIcon); 84 | intent.putExtra(NotificationConstants.NOTIFICATIONS_LARGE_ICON, largeIcon); 85 | 86 | if (extras != null) { 87 | CharSequence title = extras.getCharSequence(Notification.EXTRA_TITLE); 88 | CharSequence text = extras.getCharSequence(Notification.EXTRA_TEXT); 89 | 90 | intent.putExtra(NotificationConstants.NOTIFICATION_TITLE, title == null ? null : title.toString()); 91 | intent.putExtra(NotificationConstants.NOTIFICATION_CONTENT, text == null ? null : text.toString()); 92 | intent.putExtra(NotificationConstants.IS_REMOVED, isRemoved); 93 | intent.putExtra(NotificationConstants.HAVE_EXTRA_PICTURE, extras.containsKey(Notification.EXTRA_PICTURE)); 94 | 95 | if (extras.containsKey(Notification.EXTRA_PICTURE)) { 96 | Bitmap bmp = (Bitmap) extras.get(Notification.EXTRA_PICTURE); 97 | if (bmp != null) { 98 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 99 | bmp.compress(Bitmap.CompressFormat.PNG, 100, stream); 100 | intent.putExtra(NotificationConstants.EXTRAS_PICTURE, stream.toByteArray()); 101 | } else { 102 | Log.w("NotificationListener", "Notification.EXTRA_PICTURE exists but is null."); 103 | } 104 | } 105 | } 106 | sendBroadcast(intent); 107 | } 108 | 109 | 110 | public byte[] getAppIcon(String packageName) { 111 | try { 112 | PackageManager manager = getBaseContext().getPackageManager(); 113 | Drawable icon = manager.getApplicationIcon(packageName); 114 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 115 | getBitmapFromDrawable(icon).compress(Bitmap.CompressFormat.PNG, 100, stream); 116 | return stream.toByteArray(); 117 | } catch (PackageManager.NameNotFoundException e) { 118 | e.printStackTrace(); 119 | return null; 120 | } 121 | } 122 | 123 | @RequiresApi(api = VERSION_CODES.M) 124 | private byte[] getNotificationLargeIcon(Context context, Notification notification) { 125 | try { 126 | Icon largeIcon = notification.getLargeIcon(); 127 | if (largeIcon == null) { 128 | return null; 129 | } 130 | Drawable iconDrawable = largeIcon.loadDrawable(context); 131 | Bitmap iconBitmap = ((BitmapDrawable) iconDrawable).getBitmap(); 132 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 133 | iconBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); 134 | 135 | return outputStream.toByteArray(); 136 | } catch (Exception e) { 137 | e.printStackTrace(); 138 | Log.d("ERROR LARGE ICON", "getNotificationLargeIcon: " + e.getMessage()); 139 | return null; 140 | } 141 | } 142 | 143 | @RequiresApi(api = Build.VERSION_CODES.KITKAT) 144 | public List> getActiveNotificationData() { 145 | List> notificationList = new ArrayList<>(); 146 | StatusBarNotification[] activeNotifications = getActiveNotifications(); 147 | 148 | for (StatusBarNotification sbn : activeNotifications) { 149 | Map notifData = new HashMap<>(); 150 | Notification notification = sbn.getNotification(); 151 | Bundle extras = notification.extras; 152 | 153 | notifData.put("id", sbn.getId()); 154 | notifData.put("packageName", sbn.getPackageName()); 155 | notifData.put("title", extras.getCharSequence(Notification.EXTRA_TITLE) != null 156 | ? extras.getCharSequence(Notification.EXTRA_TITLE).toString() 157 | : null); 158 | notifData.put("content", extras.getCharSequence(Notification.EXTRA_TEXT) != null 159 | ? extras.getCharSequence(Notification.EXTRA_TEXT).toString() 160 | : null); 161 | boolean isOngoing = (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0; 162 | notifData.put("onGoing", isOngoing); 163 | 164 | notificationList.add(notifData); 165 | } 166 | return notificationList; 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | --------------------------------------------------------------------------------