├── .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