├── android ├── .idea │ ├── .name │ ├── .gitignore │ ├── compiler.xml │ ├── vcs.xml │ ├── modules.xml │ ├── runConfigurations.xml │ ├── misc.xml │ ├── gradle.xml │ └── jarRepositories.xml ├── settings.gradle ├── gradle.properties ├── .gitignore ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── im │ │ └── zoe │ │ └── labs │ │ └── flutter_notification_listener │ │ ├── RebootBroadcastReceiver.kt │ │ ├── NotificationEvent.kt │ │ ├── FlutterNotificationListenerPlugin.kt │ │ ├── Utils.kt │ │ └── NotificationsHandlerService.kt └── build.gradle ├── .idea ├── .gitignore ├── misc.xml ├── kotlinc.xml ├── libraries │ ├── Flutter_Plugins.xml │ ├── KotlinJavaRuntime.xml │ └── Dart_SDK.xml ├── runConfigurations │ └── example_lib_main_dart.xml ├── runConfigurations.xml └── modules.xml ├── .gitignore ├── lib ├── flutter_notification_listener.dart └── src │ ├── event.dart │ └── plugin.dart ├── 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 │ │ │ │ ├── kotlin │ │ │ │ │ └── im │ │ │ │ │ │ └── zoe │ │ │ │ │ │ └── labs │ │ │ │ │ │ └── flutter_notification_listener_example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── .metadata ├── README.md ├── .gitignore ├── test │ └── widget_test.dart ├── pubspec.yaml ├── pubspec.lock └── lib │ └── main.dart ├── .metadata ├── LICENSE ├── .github └── FUNDING.yml ├── flutter_notification_listener.iml ├── CHANGELOG.md ├── pubspec.yaml ├── pubspec.lock └── README.md /android/.idea/.name: -------------------------------------------------------------------------------- 1 | flutter_notification_listener -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'flutter_notification_listener' 2 | -------------------------------------------------------------------------------- /android/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | -------------------------------------------------------------------------------- /lib/flutter_notification_listener.dart: -------------------------------------------------------------------------------- 1 | export './src/event.dart'; 2 | export './src/plugin.dart'; 3 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiusanzhou/flutter_notification_listener/HEAD/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/jiusanzhou/flutter_notification_listener/HEAD/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/jiusanzhou/flutter_notification_listener/HEAD/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/jiusanzhou/flutter_notification_listener/HEAD/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/jiusanzhou/flutter_notification_listener/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /android/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/im/zoe/labs/flutter_notification_listener_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package im.zoe.labs.flutter_notification_listener_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /.idea/libraries/Flutter_Plugins.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jun 05 09:48:27 CST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /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-6.7-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/runConfigurations/example_lib_main_dart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.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: 1d9032c7e1d867f071f2277eb1673e8f9b0274e3 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /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: 1d9032c7e1d867f071f2277eb1673e8f9b0274e3 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /android/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /android/.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /android/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 wellwell.work, LLC by Zoe 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flutter_notification_listener_example 2 | 3 | Demonstrates how to use the flutter_notification_listener 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 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.20' 3 | // ext.kotlin_version = '1.3.50' 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:4.1.0' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /android/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: zoeim # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | # custom: ['https://payone.wencai.app/s/zoe'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'im.zoe.labs.flutter_notification_listener' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.9.0' 6 | repositories { 7 | google() 8 | jcenter() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:7.3.0' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | 27 | android { 28 | compileSdkVersion 34 29 | 30 | sourceSets { 31 | main.java.srcDirs += 'src/main/kotlin' 32 | } 33 | defaultConfig { 34 | minSdkVersion 23 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 40 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/src/main/kotlin/im/zoe/labs/flutter_notification_listener/RebootBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package im.zoe.labs.flutter_notification_listener 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.util.Log 9 | 10 | 11 | class RebootBroadcastReceiver : BroadcastReceiver() { 12 | override fun onReceive(context: Context, intent: Intent) { 13 | when (intent.action) { 14 | Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> { 15 | Log.i("NotificationListener", "Registering notification listener, after reboot!") 16 | FlutterNotificationListenerPlugin.registerAfterReboot(context) 17 | } 18 | else -> { 19 | Log.i("NotificationListener", intent.action.toString()) 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /.idea/libraries/KotlinJavaRuntime.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /flutter_notification_listener.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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:flutter_notification_listener_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(MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => 22 | widget is Text && 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 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_SDK.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.4 2 | 3 | - chore: support gradle plugin version 4 | 5 | ## 1.3.3 6 | 7 | - bugfix: fix NullPointerException sometimes thrown by label field 8 | 9 | ## 1.3.2 10 | 11 | - bugfix: with some kotlin version build error (asserts-null) 12 | 13 | ## 1.3.1 14 | 15 | - chore: imporve code style 16 | - bugfix: fix foreground when first started, #16 17 | 18 | ## 1.3.0 19 | 20 | - feature: support full notification information 21 | - chore: add funding support 22 | 23 | ## 1.2.0 24 | 25 | - feature: add unique id for notification 26 | - feature: support interactive notification 27 | - chore: add docs for readme 28 | 29 | ## 1.1.3 30 | 31 | - fix: add try catch while invoke method 32 | 33 | ## 1.1.2 34 | 35 | - hotfix: fix exception of some app send bitmap as large icon 36 | 37 | ## 1.1.1 38 | 39 | - hotfix: fix utils class miss 40 | 41 | 42 | ## 1.1.0 43 | 44 | - feature: add notification id 45 | - feature: add notification large icon 46 | 47 | ## 1.0.8 48 | 49 | - hotfix: fix start service failed after reboot 50 | 51 | ## 1.0.7 52 | 53 | - feature: add foreground service 54 | 55 | ## 1.0.6 56 | 57 | - fix: charseq cause excatpion 58 | 59 | ## 1.0.5 60 | 61 | - fix: textLines cause panic 62 | - chore: format code 63 | 64 | ## 1.0.4 65 | 66 | - chore 67 | ## 1.0.3 68 | 69 | - remove extra 70 | 71 | ## 1.0.1 72 | 73 | - fix dart in backgroun run 74 | - change the api for register handle 75 | - add log in android 76 | 77 | ## 1.0.0 78 | 79 | First implement. 80 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 31 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "im.zoe.labs.flutter_notification_listener_example" 38 | minSdkVersion 19 39 | targetSdkVersion 30 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 59 | } 60 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_notification_listener 2 | description: Flutter plugin to listen for all incoming notifications for Android. 3 | version: 1.3.4 4 | homepage: https://github.com/jiusanzhou/flutter_notification_listener 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | flutter: ">=1.20.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | # For information on the generic Dart part of this file, see the 19 | # following page: https://dart.dev/tools/pub/pubspec 20 | 21 | # The following section is specific to Flutter. 22 | flutter: 23 | # This section identifies this Flutter project as a plugin project. 24 | # The 'pluginClass' and Android 'package' identifiers should not ordinarily 25 | # be modified. They are used by the tooling to maintain consistency when 26 | # adding or updating assets for this project. 27 | plugin: 28 | platforms: 29 | android: 30 | package: im.zoe.labs.flutter_notification_listener 31 | pluginClass: FlutterNotificationListenerPlugin 32 | 33 | # To add assets to your plugin package, add an assets section, like this: 34 | # assets: 35 | # - images/a_dot_burr.jpeg 36 | # - images/a_dot_ham.jpeg 37 | # 38 | # For details regarding assets in packages, see 39 | # https://flutter.dev/assets-and-images/#from-packages 40 | # 41 | # An image asset can refer to one or more resolution-specific "variants", see 42 | # https://flutter.dev/assets-and-images/#resolution-aware. 43 | 44 | # To add custom fonts to your plugin package, add a fonts section here, 45 | # in this "flutter" section. Each entry in this list should have a 46 | # "family" key with the font family name, and a "fonts" key with a 47 | # list giving the asset and other descriptors for the font. For 48 | # example: 49 | # fonts: 50 | # - family: Schyler 51 | # fonts: 52 | # - asset: fonts/Schyler-Regular.ttf 53 | # - asset: fonts/Schyler-Italic.ttf 54 | # style: italic 55 | # - family: Trajan Pro 56 | # fonts: 57 | # - asset: fonts/TrajanPro.ttf 58 | # - asset: fonts/TrajanPro_Bold.ttf 59 | # weight: 700 60 | # 61 | # For details regarding fonts in packages, see 62 | # https://flutter.dev/custom-fonts/#from-packages 63 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_notification_listener_example 2 | description: Demonstrates how to use the flutter_notification_listener plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `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.12.0 <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | flutter_notification_listener: 16 | # When depending on this package from a real application you should use: 17 | # flutter_notification_listener: ^x.y.z 18 | # See https://dart.dev/tools/pub/dependencies#version-constraints 19 | # The example app is bundled with the plugin so we use a path dependency on 20 | # the parent directory to use the current plugin's version. 21 | path: ../ 22 | 23 | # The following adds the Cupertino Icons font to your application. 24 | # Use with the CupertinoIcons class for iOS style icons. 25 | cupertino_icons: ^1.0.2 26 | 27 | dev_dependencies: 28 | flutter_test: 29 | sdk: flutter 30 | 31 | # For information on the generic Dart part of this file, see the 32 | # following page: https://dart.dev/tools/pub/pubspec 33 | 34 | # The following section is specific to Flutter. 35 | flutter: 36 | 37 | # The following line ensures that the Material Icons font is 38 | # included with your application, so that you can use the icons in 39 | # the material Icons class. 40 | uses-material-design: true 41 | 42 | # To add assets to your application, add an assets section, like this: 43 | # assets: 44 | # - images/a_dot_burr.jpeg 45 | # - images/a_dot_ham.jpeg 46 | 47 | # An image asset can refer to one or more resolution-specific "variants", see 48 | # https://flutter.dev/assets-and-images/#resolution-aware. 49 | 50 | # For details regarding adding assets from package dependencies, see 51 | # https://flutter.dev/assets-and-images/#from-packages 52 | 53 | # To add custom fonts to your application, add a fonts section here, 54 | # in this "flutter" section. Each entry in this list should have a 55 | # "family" key with the font family name, and a "fonts" key with a 56 | # list giving the asset and other descriptors for the font. For 57 | # example: 58 | # fonts: 59 | # - family: Schyler 60 | # fonts: 61 | # - asset: fonts/Schyler-Regular.ttf 62 | # - asset: fonts/Schyler-Italic.ttf 63 | # style: italic 64 | # - family: Trajan Pro 65 | # fonts: 66 | # - asset: fonts/TrajanPro.ttf 67 | # - asset: fonts/TrajanPro_Bold.ttf 68 | # weight: 700 69 | # 70 | # For details regarding fonts from package dependencies, 71 | # see https://flutter.dev/custom-fonts/#from-packages 72 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 19 | 23 | 27 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 46 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /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: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.17.2" 44 | fake_async: 45 | dependency: transitive 46 | description: 47 | name: fake_async 48 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.3.1" 52 | flutter: 53 | dependency: "direct main" 54 | description: flutter 55 | source: sdk 56 | version: "0.0.0" 57 | flutter_test: 58 | dependency: "direct dev" 59 | description: flutter 60 | source: sdk 61 | version: "0.0.0" 62 | matcher: 63 | dependency: transitive 64 | description: 65 | name: matcher 66 | sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" 67 | url: "https://pub.dev" 68 | source: hosted 69 | version: "0.12.16" 70 | material_color_utilities: 71 | dependency: transitive 72 | description: 73 | name: material_color_utilities 74 | sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" 75 | url: "https://pub.dev" 76 | source: hosted 77 | version: "0.5.0" 78 | meta: 79 | dependency: transitive 80 | description: 81 | name: meta 82 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "1.9.1" 86 | path: 87 | dependency: transitive 88 | description: 89 | name: path 90 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "1.8.3" 94 | sky_engine: 95 | dependency: transitive 96 | description: flutter 97 | source: sdk 98 | version: "0.0.99" 99 | source_span: 100 | dependency: transitive 101 | description: 102 | name: source_span 103 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 104 | url: "https://pub.dev" 105 | source: hosted 106 | version: "1.10.0" 107 | stack_trace: 108 | dependency: transitive 109 | description: 110 | name: stack_trace 111 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 112 | url: "https://pub.dev" 113 | source: hosted 114 | version: "1.11.0" 115 | stream_channel: 116 | dependency: transitive 117 | description: 118 | name: stream_channel 119 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 120 | url: "https://pub.dev" 121 | source: hosted 122 | version: "2.1.1" 123 | string_scanner: 124 | dependency: transitive 125 | description: 126 | name: string_scanner 127 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 128 | url: "https://pub.dev" 129 | source: hosted 130 | version: "1.2.0" 131 | term_glyph: 132 | dependency: transitive 133 | description: 134 | name: term_glyph 135 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "1.2.1" 139 | test_api: 140 | dependency: transitive 141 | description: 142 | name: test_api 143 | sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "0.6.0" 147 | vector_math: 148 | dependency: transitive 149 | description: 150 | name: vector_math 151 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "2.1.4" 155 | web: 156 | dependency: transitive 157 | description: 158 | name: web 159 | sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "0.1.4-beta" 163 | sdks: 164 | dart: ">=3.1.0-185.0.dev <4.0.0" 165 | flutter: ">=1.20.0" 166 | -------------------------------------------------------------------------------- /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: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.17.2" 44 | cupertino_icons: 45 | dependency: "direct main" 46 | description: 47 | name: cupertino_icons 48 | sha256: "486b7bc707424572cdf7bd7e812a0c146de3fd47ecadf070254cc60383f21dd8" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.0.3" 52 | fake_async: 53 | dependency: transitive 54 | description: 55 | name: fake_async 56 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.1" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_notification_listener: 66 | dependency: "direct main" 67 | description: 68 | path: ".." 69 | relative: true 70 | source: path 71 | version: "1.3.3" 72 | flutter_test: 73 | dependency: "direct dev" 74 | description: flutter 75 | source: sdk 76 | version: "0.0.0" 77 | matcher: 78 | dependency: transitive 79 | description: 80 | name: matcher 81 | sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" 82 | url: "https://pub.dev" 83 | source: hosted 84 | version: "0.12.16" 85 | material_color_utilities: 86 | dependency: transitive 87 | description: 88 | name: material_color_utilities 89 | sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" 90 | url: "https://pub.dev" 91 | source: hosted 92 | version: "0.5.0" 93 | meta: 94 | dependency: transitive 95 | description: 96 | name: meta 97 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 98 | url: "https://pub.dev" 99 | source: hosted 100 | version: "1.9.1" 101 | path: 102 | dependency: transitive 103 | description: 104 | name: path 105 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 106 | url: "https://pub.dev" 107 | source: hosted 108 | version: "1.8.3" 109 | sky_engine: 110 | dependency: transitive 111 | description: flutter 112 | source: sdk 113 | version: "0.0.99" 114 | source_span: 115 | dependency: transitive 116 | description: 117 | name: source_span 118 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 119 | url: "https://pub.dev" 120 | source: hosted 121 | version: "1.10.0" 122 | stack_trace: 123 | dependency: transitive 124 | description: 125 | name: stack_trace 126 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 127 | url: "https://pub.dev" 128 | source: hosted 129 | version: "1.11.0" 130 | stream_channel: 131 | dependency: transitive 132 | description: 133 | name: stream_channel 134 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 135 | url: "https://pub.dev" 136 | source: hosted 137 | version: "2.1.1" 138 | string_scanner: 139 | dependency: transitive 140 | description: 141 | name: string_scanner 142 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 143 | url: "https://pub.dev" 144 | source: hosted 145 | version: "1.2.0" 146 | term_glyph: 147 | dependency: transitive 148 | description: 149 | name: term_glyph 150 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 151 | url: "https://pub.dev" 152 | source: hosted 153 | version: "1.2.1" 154 | test_api: 155 | dependency: transitive 156 | description: 157 | name: test_api 158 | sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" 159 | url: "https://pub.dev" 160 | source: hosted 161 | version: "0.6.0" 162 | vector_math: 163 | dependency: transitive 164 | description: 165 | name: vector_math 166 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 167 | url: "https://pub.dev" 168 | source: hosted 169 | version: "2.1.4" 170 | web: 171 | dependency: transitive 172 | description: 173 | name: web 174 | sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 175 | url: "https://pub.dev" 176 | source: hosted 177 | version: "0.1.4-beta" 178 | sdks: 179 | dart: ">=3.1.0-185.0.dev <4.0.0" 180 | flutter: ">=1.20.0" 181 | -------------------------------------------------------------------------------- /lib/src/event.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter_notification_listener/flutter_notification_listener.dart'; 5 | 6 | /// ActionInput is the remote inputs for action 7 | class ActionInput { 8 | String? label; 9 | String? resultKey; 10 | 11 | ActionInput({ 12 | this.label, 13 | this.resultKey, 14 | }); 15 | 16 | factory ActionInput.fromMap(Map map) { 17 | return ActionInput( 18 | label: map["label"], 19 | resultKey: map["key"], 20 | ); 21 | } 22 | } 23 | 24 | /// Action is the action for notification 25 | class Action { 26 | String? title; 27 | int? id; 28 | int? semantic; 29 | List? inputs; 30 | 31 | /// store the notifaction event 32 | NotificationEvent? _evt; 33 | 34 | Action( 35 | this._evt, { 36 | this.title, 37 | this.id, 38 | this.inputs, 39 | this.semantic, 40 | }); 41 | 42 | /// create Action from map 43 | factory Action.fromMap(NotificationEvent? evt, Map map) { 44 | return Action( 45 | evt, 46 | title: map["title"], 47 | id: map["id"], 48 | semantic: map["semantic"], 49 | inputs: ((map["inputs"] ?? []) as List) 50 | .map((e) => ActionInput.fromMap(e)) 51 | .toList(), 52 | ); 53 | } 54 | 55 | Future tap() async { 56 | if (_evt == null) throw Exception("The notification is null"); 57 | return NotificationsListener.tapNotificationAction( 58 | _evt!.uniqueId!, 59 | id!, 60 | ); 61 | } 62 | 63 | Future postInputs(Map map) async { 64 | if (_evt == null) throw Exception("The notification is null"); 65 | if (inputs == null || inputs!.length == 0) 66 | throw Exception("No inputs were provided"); 67 | 68 | // check if we have set the data 69 | var hasData = false; 70 | for (var input in inputs!) { 71 | if (map[input.resultKey] != null) { 72 | hasData = true; 73 | break; 74 | } 75 | } 76 | if (!hasData) { 77 | throw Exception("You must offer data with resultKey from inputs"); 78 | } 79 | 80 | return NotificationsListener.postActionInputs( 81 | _evt!.uniqueId!, 82 | id!, 83 | map, 84 | ); 85 | } 86 | } 87 | 88 | /// NotificationEvent is the object converted from notification 89 | /// Notification anatomy: 90 | /// https://developer.android.com/guide/topics/ui/notifiers/notifications 91 | class NotificationEvent { 92 | /// generate the unique id for notification 93 | /// generated by plugin 94 | String? uniqueId; 95 | 96 | /// the key of status bar notification 97 | String? key; 98 | 99 | /// the uid of status bar notification 100 | int? uid; 101 | 102 | /// the notification id 103 | int? id; 104 | 105 | /// the channel id of the notification 106 | String? channelId; 107 | 108 | /// the notification create time in flutter side 109 | DateTime? createAt; 110 | 111 | /// the nofication create time in the android side 112 | int? timestamp; 113 | 114 | /// the package name of the notification 115 | String? packageName; 116 | 117 | /// the title of the notification 118 | String? title; 119 | 120 | /// the content of the notification 121 | String? text; 122 | 123 | /// DEPRECATE 124 | String? message; 125 | 126 | /// icon of the notification which setted by setSmallIcon, 127 | /// at most time this is icon of the application package. 128 | /// So no need to set this, use a method to take from android. 129 | /// To display as a image use the Image.memory widget. 130 | /// Example: 131 | /// 132 | /// ``` 133 | /// Image.memory(evt.icon) 134 | /// ``` 135 | // Uint8List? icon; 136 | 137 | /// if we have the large icon 138 | bool? hasLargeIcon; 139 | 140 | /// large icon of the notification which setted by setLargeIcon. 141 | /// To display as a image use the Image.memory widget. 142 | /// Example: 143 | /// 144 | /// ``` 145 | /// Image.memory(evt.largeIcon) 146 | /// ``` 147 | Uint8List? largeIcon; 148 | 149 | /// if this notification can be tapped 150 | bool? canTap; 151 | 152 | /// actions of notification 153 | List? actions; 154 | 155 | int? flags; 156 | 157 | bool? isGroup; 158 | 159 | /// the raw notifaction data from android 160 | dynamic _data; 161 | 162 | NotificationEvent({ 163 | this.uniqueId, 164 | this.key, 165 | this.id, 166 | this.uid, 167 | this.channelId, 168 | this.createAt, 169 | this.packageName, 170 | this.title, 171 | this.text, 172 | this.message, 173 | this.timestamp, 174 | // this.icon, 175 | this.hasLargeIcon, 176 | this.largeIcon, 177 | this.canTap, 178 | this.flags, 179 | this.isGroup, 180 | }); 181 | 182 | Map? get raw => _data; 183 | 184 | /// Create the event from a map 185 | factory NotificationEvent.fromMap(Map map) { 186 | map['hasLargeIcon'] = 187 | map['largeIcon'] != null && (map['largeIcon'] as Uint8List).isNotEmpty; 188 | var evt = NotificationEvent( 189 | createAt: DateTime.now(), 190 | uniqueId: map["_id"], 191 | key: map["key"], 192 | uid: map['uid'], 193 | channelId: map["channelId"], 194 | id: map['id'], 195 | packageName: map['package_name'], 196 | title: map['title'], 197 | text: map['text'], 198 | message: map["message"], 199 | timestamp: map["timestamp"], 200 | // icon: map['icon'], 201 | hasLargeIcon: map['hasLargeIcon'], 202 | largeIcon: map['largeIcon'], 203 | canTap: map["canTap"], 204 | flags: map["flags"], 205 | isGroup: map["isGroup"], 206 | ); 207 | 208 | // set the raw data 209 | evt._data = map; 210 | 211 | // create the actions from map 212 | evt.actions = ((map["actions"] ?? []) as List) 213 | .map((e) => Action.fromMap(evt, e)) 214 | .toList(); 215 | 216 | return evt; 217 | } 218 | 219 | @override 220 | String toString() { 221 | var tmp = Map.from(this._data) 222 | ..remove('icon') 223 | ..remove('largeIcon'); 224 | return json.encode(tmp).toString(); 225 | } 226 | 227 | /// tap the notification return false if not exits 228 | Future tap() { 229 | if (canTap == null || canTap == false) 230 | throw Exception("The notification can not be tapped"); 231 | return NotificationsListener.tapNotification(uniqueId!); 232 | } 233 | 234 | /// get full notification 235 | Future getFull() { 236 | return NotificationsListener.getFullNotification(uniqueId!); 237 | } 238 | } 239 | 240 | /// newEvent package level function create event from map 241 | NotificationEvent newEvent(Map data) { 242 | return NotificationEvent.fromMap(data); 243 | } 244 | -------------------------------------------------------------------------------- /lib/src/plugin.dart: -------------------------------------------------------------------------------- 1 | import 'dart:isolate'; 2 | import 'dart:ui'; 3 | import 'dart:async'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | 8 | import './event.dart'; 9 | 10 | typedef EventCallbackFunc = void Function(NotificationEvent evt); 11 | 12 | /// NotificationsListener 13 | class NotificationsListener { 14 | static const CHANNELID = "flutter_notification_listener"; 15 | static const SEND_PORT_NAME = "notifications_send_port"; 16 | 17 | static const MethodChannel _methodChannel = 18 | const MethodChannel('$CHANNELID/method'); 19 | 20 | static const MethodChannel _bgMethodChannel = 21 | const MethodChannel('$CHANNELID/bg_method'); 22 | 23 | static MethodChannel get bgMethodChannel => _bgMethodChannel; 24 | 25 | static ReceivePort? _receivePort; 26 | 27 | /// Get a defualt receivePort 28 | static ReceivePort? get receivePort { 29 | if (_receivePort == null) { 30 | _receivePort = ReceivePort(); 31 | // remove the old one at first. 32 | IsolateNameServer.removePortNameMapping(SEND_PORT_NAME); 33 | IsolateNameServer.registerPortWithName( 34 | _receivePort!.sendPort, SEND_PORT_NAME); 35 | } 36 | return _receivePort; 37 | } 38 | 39 | /// Check have permission or not 40 | static Future get hasPermission async { 41 | return await _methodChannel.invokeMethod('plugin.hasPermission'); 42 | } 43 | 44 | /// Open the settings activity 45 | static Future openPermissionSettings() async { 46 | return await _methodChannel.invokeMethod('plugin.openPermissionSettings'); 47 | } 48 | 49 | /// Initialize the plugin and request relevant permissions from the user. 50 | static Future initialize({ 51 | EventCallbackFunc callbackHandle = _defaultCallbackHandle, 52 | }) async { 53 | final CallbackHandle _callbackDispatch = 54 | PluginUtilities.getCallbackHandle(callbackDispatcher)!; 55 | await _methodChannel.invokeMethod( 56 | 'plugin.initialize', _callbackDispatch.toRawHandle()); 57 | 58 | // call this call back in the current engine 59 | // this is important to use ui flutter engine access `service.channel` 60 | callbackDispatcher(inited: false); 61 | 62 | // register event handler 63 | // register the default event handler 64 | await registerEventHandle(callbackHandle); 65 | } 66 | 67 | /// Register a new event handler 68 | static Future registerEventHandle(EventCallbackFunc callback) async { 69 | final CallbackHandle _callback = 70 | PluginUtilities.getCallbackHandle(callback)!; 71 | await _methodChannel.invokeMethod( 72 | 'plugin.registerEventHandle', _callback.toRawHandle()); 73 | } 74 | 75 | /// check the service running or not 76 | static Future get isRunning async { 77 | return await _methodChannel.invokeMethod('plugin.isServiceRunning'); 78 | } 79 | 80 | /// start the service 81 | static Future startService({ 82 | bool foreground = true, 83 | String subTitle = "", 84 | bool showWhen = false, 85 | String title = "Notification Listener", 86 | String description = "Service is running", 87 | }) async { 88 | var data = {}; 89 | data["foreground"] = foreground; 90 | data["subTitle"] = subTitle; 91 | data["showWhen"] = showWhen; 92 | data["title"] = title; 93 | data["description"] = description; 94 | 95 | var res = await _methodChannel.invokeMethod('plugin.startService', data); 96 | 97 | return res; 98 | } 99 | 100 | /// stop the service 101 | static Future stopService() async { 102 | return await _methodChannel.invokeMethod('plugin.stopService'); 103 | } 104 | 105 | /// promote the service to foreground 106 | static Future promoteToForeground( 107 | String title, { 108 | String subTitle = "", 109 | bool showWhen = false, 110 | String description = "Service is running", 111 | }) async { 112 | var data = {}; 113 | data["foreground"] = true; 114 | data["subTitle"] = subTitle; 115 | data["showWhen"] = showWhen; 116 | data["title"] = title; 117 | data["description"] = description; 118 | 119 | return await _bgMethodChannel.invokeMethod( 120 | 'service.promoteToForeground', data); 121 | } 122 | 123 | /// demote the service to background 124 | static Future demoteToBackground() async => 125 | await _bgMethodChannel.invokeMethod('service.demoteToBackground'); 126 | 127 | /// tap the notification 128 | static Future tapNotification(String uid) async { 129 | return await _bgMethodChannel.invokeMethod('service.tap', [uid]) ?? 130 | false; 131 | } 132 | 133 | /// tap the notification action 134 | /// use the index to locate the action 135 | static Future tapNotificationAction(String uid, int actionId) async { 136 | return await _bgMethodChannel 137 | .invokeMethod('service.tap_action', [uid, actionId]) ?? 138 | false; 139 | } 140 | 141 | /// set content for action's input 142 | /// this is useful while auto reply by notification 143 | static Future postActionInputs( 144 | String uid, int actionId, Map map) async { 145 | return await _bgMethodChannel 146 | .invokeMethod("service.send_input", [uid, actionId, map]) ?? 147 | false; 148 | } 149 | 150 | /// get the full notification from android 151 | /// with the unqiue id 152 | static Future getFullNotification(String uid) async { 153 | return await _bgMethodChannel 154 | .invokeMethod("service.get_full_notification", [uid]); 155 | } 156 | 157 | static void _defaultCallbackHandle(NotificationEvent evt) { 158 | final SendPort? _send = IsolateNameServer.lookupPortByName(SEND_PORT_NAME); 159 | print("[default callback handler] [send isolate nameserver]"); 160 | if (_send == null) 161 | print("IsolateNameServer: can not find send $SEND_PORT_NAME"); 162 | _send?.send(evt); 163 | } 164 | } 165 | 166 | /// callbackDispatcher use to install background channel 167 | void callbackDispatcher({inited = true}) { 168 | WidgetsFlutterBinding.ensureInitialized(); 169 | 170 | NotificationsListener._bgMethodChannel 171 | .setMethodCallHandler((MethodCall call) async { 172 | try { 173 | switch (call.method) { 174 | case "sink_event": 175 | { 176 | final List args = call.arguments; 177 | final evt = NotificationEvent.fromMap(args[1]); 178 | 179 | final Function? callback = PluginUtilities.getCallbackFromHandle( 180 | CallbackHandle.fromRawHandle(args[0])); 181 | 182 | if (callback == null) { 183 | print("callback is not register: ${args[0]}"); 184 | return; 185 | } 186 | 187 | callback(evt); 188 | } 189 | break; 190 | default: 191 | { 192 | print("unknown bg_method: ${call.method}"); 193 | } 194 | } 195 | } catch (e) { 196 | print(e); 197 | } 198 | }); 199 | 200 | // if start the ui first, this will cause method not found error 201 | if (inited) 202 | NotificationsListener._bgMethodChannel.invokeMethod('service.initialized'); 203 | } 204 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:isolate'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'dart:async'; 6 | 7 | import 'package:flutter_notification_listener/flutter_notification_listener.dart'; 8 | 9 | void main() { 10 | runApp(MyApp()); 11 | } 12 | 13 | class MyApp extends StatefulWidget { 14 | @override 15 | _MyAppState createState() => _MyAppState(); 16 | } 17 | 18 | class _MyAppState extends State { 19 | @override 20 | void initState() { 21 | super.initState(); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return new MaterialApp( 27 | home: NotificationsLog(), 28 | ); 29 | } 30 | } 31 | 32 | class NotificationsLog extends StatefulWidget { 33 | @override 34 | _NotificationsLogState createState() => _NotificationsLogState(); 35 | } 36 | 37 | class _NotificationsLogState extends State { 38 | List _log = []; 39 | bool started = false; 40 | bool _loading = false; 41 | 42 | ReceivePort port = ReceivePort(); 43 | 44 | @override 45 | void initState() { 46 | initPlatformState(); 47 | super.initState(); 48 | } 49 | 50 | // we must use static method, to handle in background 51 | @pragma('vm:entry-point') // prevent dart from stripping out this function on release build in Flutter 3.x 52 | static void _callback(NotificationEvent evt) { 53 | print("send evt to ui: $evt"); 54 | final SendPort? send = IsolateNameServer.lookupPortByName("_listener_"); 55 | if (send == null) print("can't find the sender"); 56 | send?.send(evt); 57 | } 58 | 59 | // Platform messages are asynchronous, so we initialize in an async method. 60 | Future initPlatformState() async { 61 | NotificationsListener.initialize(callbackHandle: _callback); 62 | 63 | // this can fix restart can't handle error 64 | IsolateNameServer.removePortNameMapping("_listener_"); 65 | IsolateNameServer.registerPortWithName(port.sendPort, "_listener_"); 66 | port.listen((message) => onData(message)); 67 | 68 | // don't use the default receivePort 69 | // NotificationsListener.receivePort.listen((evt) => onData(evt)); 70 | 71 | var isRunning = (await NotificationsListener.isRunning) ?? false; 72 | print("""Service is ${!isRunning ? "not " : ""}already running"""); 73 | 74 | setState(() { 75 | started = isRunning; 76 | }); 77 | } 78 | 79 | void onData(NotificationEvent event) { 80 | setState(() { 81 | _log.add(event); 82 | }); 83 | 84 | print(event.toString()); 85 | } 86 | 87 | void startListening() async { 88 | print("start listening"); 89 | setState(() { 90 | _loading = true; 91 | }); 92 | var hasPermission = (await NotificationsListener.hasPermission) ?? false; 93 | if (!hasPermission) { 94 | print("no permission, so open settings"); 95 | NotificationsListener.openPermissionSettings(); 96 | return; 97 | } 98 | 99 | var isRunning = (await NotificationsListener.isRunning) ?? false; 100 | 101 | if (!isRunning) { 102 | await NotificationsListener.startService( 103 | foreground: false, 104 | title: "Listener Running", 105 | description: "Welcome to having me"); 106 | } 107 | 108 | setState(() { 109 | started = true; 110 | _loading = false; 111 | }); 112 | } 113 | 114 | void stopListening() async { 115 | print("stop listening"); 116 | 117 | setState(() { 118 | _loading = true; 119 | }); 120 | 121 | await NotificationsListener.stopService(); 122 | 123 | setState(() { 124 | started = false; 125 | _loading = false; 126 | }); 127 | } 128 | 129 | @override 130 | Widget build(BuildContext context) { 131 | return Scaffold( 132 | appBar: AppBar( 133 | title: Text('Listener Example'), 134 | actions: [ 135 | IconButton( 136 | onPressed: () { 137 | print("TODO:"); 138 | }, 139 | icon: Icon(Icons.settings)) 140 | ], 141 | ), 142 | body: Center( 143 | child: ListView.builder( 144 | itemCount: _log.length, 145 | reverse: true, 146 | itemBuilder: (BuildContext context, int idx) { 147 | final entry = _log[idx]; 148 | return ListTile( 149 | onTap: () { 150 | entry.tap(); 151 | }, 152 | // trailing: 153 | // entry.hasLargeIcon ? Image.memory(entry.largeIcon, width: 80, height: 80) : 154 | // Text(entry.packageName.toString().split('.').last), 155 | title: Container( 156 | child: Column( 157 | crossAxisAlignment: CrossAxisAlignment.start, 158 | children: [ 159 | Text(entry.title ?? "<>"), 160 | Text(entry.text ?? "<>"), 161 | Row( 162 | children: (entry.actions ?? []).map((act) { 163 | return TextButton( 164 | onPressed: () { 165 | // semantic is 1 means reply quick 166 | if (act.semantic == 1) { 167 | Map map = {}; 168 | (act.inputs ?? []).forEach((e) { 169 | print( 170 | "set inputs: ${e.label}<${e.resultKey}>"); 171 | map[e.resultKey ?? 'null'] = 172 | "Auto reply from me"; 173 | }); 174 | act.postInputs(map); 175 | } else { 176 | // just tap 177 | act.tap(); 178 | } 179 | }, 180 | child: Text(act.title ?? '')); 181 | }).toList() 182 | ..add(TextButton( 183 | child: Text("Full"), 184 | onPressed: () async { 185 | try { 186 | var data = await entry.getFull(); 187 | print("full notifaction: $data"); 188 | } catch (e) { 189 | print(e); 190 | } 191 | })), 192 | ), 193 | Text(entry.createAt.toString().substring(0, 19)), 194 | ], 195 | ), 196 | )); 197 | })), 198 | floatingActionButton: FloatingActionButton( 199 | onPressed: started ? stopListening : startListening, 200 | tooltip: 'Start/Stop sensing', 201 | child: _loading 202 | ? Icon(Icons.close) 203 | : (started ? Icon(Icons.stop) : Icon(Icons.play_arrow)), 204 | ), 205 | ); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /android/src/main/kotlin/im/zoe/labs/flutter_notification_listener/NotificationEvent.kt: -------------------------------------------------------------------------------- 1 | package im.zoe.labs.flutter_notification_listener 2 | 3 | import android.app.Notification 4 | import android.content.Context 5 | import android.graphics.Bitmap 6 | import android.graphics.drawable.Icon 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.service.notification.StatusBarNotification 10 | import androidx.annotation.RequiresApi 11 | import im.zoe.labs.flutter_notification_listener.Utils.Companion.toBitmap 12 | import java.io.ByteArrayOutputStream 13 | 14 | class NotificationEvent(context: Context, sbn: StatusBarNotification) { 15 | 16 | var mSbn = sbn 17 | 18 | var data: Map = fromSbn(context, sbn) 19 | 20 | val uid: String 21 | get() = data[NOTIFICATION_UNIQUE_ID] as String 22 | 23 | companion object { 24 | private const val NOTIFICATION_PACKAGE_NAME = "package_name" 25 | private const val NOTIFICATION_TIMESTAMP = "timestamp" 26 | private const val NOTIFICATION_ID = "id" 27 | private const val NOTIFICATION_UID = "uid" 28 | private const val NOTIFICATION_CHANNEL_ID = "channelId" 29 | private const val NOTIFICATION_ACTIONS = "actions" 30 | private const val NOTIFICATION_CAN_TAP = "canTap" 31 | private const val NOTIFICATION_KEY = "key" 32 | private const val NOTIFICATION_UNIQUE_ID = "_id" 33 | private const val NOTIFICATION_FLAGS = "flags" 34 | private const val NOTIFICATION_IS_GROUP = "isGroup" 35 | 36 | fun genKey(vararg items: Any?): String { 37 | return Utils.md5(items.joinToString(separator="-"){ "$it" }).slice(IntRange(0, 12)) 38 | } 39 | 40 | // https://developer.android.com/guide/topics/ui/notifiers/notifications 41 | // extra more fields from docs 42 | fun fromSbn(context: Context, sbn: StatusBarNotification): Map { 43 | // val map = HashMap() 44 | 45 | // Retrieve extra object from notification to extract payload. 46 | val notify = sbn.notification 47 | 48 | val map = turnExtraToMap(context, notify?.extras) 49 | 50 | // add 3 sbn fields 51 | map[NOTIFICATION_TIMESTAMP] = sbn.postTime 52 | map[NOTIFICATION_PACKAGE_NAME] = sbn.packageName 53 | map[NOTIFICATION_ID] = sbn.id 54 | 55 | 56 | map[NOTIFICATION_FLAGS] = notify.flags 57 | map[NOTIFICATION_IS_GROUP] = (notify.flags and Notification.FLAG_GROUP_SUMMARY) != 0 58 | 59 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 60 | map[NOTIFICATION_UID] = sbn.uid 61 | } 62 | 63 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 64 | map[NOTIFICATION_CHANNEL_ID] = notify.channelId 65 | } 66 | 67 | // generate the unique id 68 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { 69 | map[NOTIFICATION_KEY] = sbn.key 70 | map[NOTIFICATION_UNIQUE_ID] = genKey(sbn.key) 71 | } else { 72 | map[NOTIFICATION_UNIQUE_ID] = genKey( 73 | map[NOTIFICATION_PACKAGE_NAME], 74 | map[NOTIFICATION_CHANNEL_ID], 75 | map[NOTIFICATION_ID] 76 | ) 77 | } 78 | 79 | map[NOTIFICATION_CAN_TAP] = notify.contentIntent != null 80 | 81 | map[NOTIFICATION_ACTIONS] = getActions(context, notify) 82 | 83 | return map 84 | } 85 | 86 | private val EXTRA_KEYS_WHITE_LIST = arrayOf( 87 | Notification.EXTRA_TITLE, 88 | Notification.EXTRA_TEXT, 89 | Notification.EXTRA_SUB_TEXT, 90 | Notification.EXTRA_SUMMARY_TEXT, 91 | Notification.EXTRA_TEXT_LINES, 92 | Notification.EXTRA_BIG_TEXT, 93 | Notification.EXTRA_INFO_TEXT, 94 | Notification.EXTRA_SHOW_WHEN, 95 | Notification.EXTRA_LARGE_ICON 96 | // Notification.EXTRA_LARGE_ICON_BIG 97 | ) 98 | 99 | private fun turnExtraToMap(context: Context, extras: Bundle?): HashMap { 100 | val map = HashMap() 101 | if (extras == null) return map 102 | val ks: Set = extras.keySet() 103 | val iterator = ks.iterator() 104 | while (iterator.hasNext()) { 105 | val key = iterator.next() 106 | if (!EXTRA_KEYS_WHITE_LIST.contains(key)) continue 107 | 108 | val bits = key.split(".") 109 | val nKey = bits[bits.size - 1] 110 | 111 | map[nKey] = marshalled(context, extras.get(key)) 112 | } 113 | return map 114 | } 115 | 116 | private fun marshalled(context: Context, v: Any?): Any? { 117 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 118 | when (v) { 119 | is Icon -> { 120 | convertIconToByteArray(context, v) 121 | } 122 | else -> internalMarshalled(context, v) 123 | } 124 | } else { 125 | internalMarshalled(context, v) 126 | } 127 | } 128 | 129 | private fun internalMarshalled(context: Context, v: Any?): Any? { 130 | return when (v) { 131 | is CharSequence -> v.toString() 132 | is Array<*> -> v.map { marshalled(context, it) } 133 | is Bitmap -> convertBitmapToByteArray(v) 134 | // TODO: turn other types which cause exception 135 | else -> v 136 | } 137 | } 138 | 139 | @RequiresApi(Build.VERSION_CODES.M) 140 | private fun convertIconToByteArray(context: Context, icon: Icon): ByteArray { 141 | return convertBitmapToByteArray(icon.loadDrawable(context)!!.toBitmap()) 142 | } 143 | 144 | private fun convertBitmapToByteArray(bitmap: Bitmap): ByteArray { 145 | val stream = ByteArrayOutputStream() 146 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) 147 | return stream.toByteArray() 148 | } 149 | 150 | private fun getActions(context: Context, n: Notification?): List<*>? { 151 | if (n?.actions == null) return null 152 | var items: List?> = mutableListOf() 153 | n.actions.forEachIndexed { idx, act -> 154 | val map = HashMap() 155 | map["id"] = idx 156 | map["title"] = act.title.toString() 157 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 158 | map["semantic"] = act.semanticAction 159 | } 160 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { 161 | var ins: List?> = mutableListOf() 162 | if (act.remoteInputs != null) { 163 | act.remoteInputs.forEach { 164 | val input = HashMap() 165 | input["label"] = it.label?.toString() ?: "" 166 | input["key"] = it.resultKey 167 | // input["choices"] = it.choices 168 | ins = ins + input 169 | } 170 | } 171 | map["inputs"] = ins 172 | 173 | // val iterator = act.extras.keySet().iterator() 174 | // while (iterator.hasNext()) { 175 | // val key = iterator.next() 176 | // Log.d("=====>", "action extra key: $key, value: ${act.extras.get(key)}") 177 | // } 178 | } 179 | items = items.plus(map) 180 | } 181 | return items 182 | } 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /android/src/main/kotlin/im/zoe/labs/flutter_notification_listener/FlutterNotificationListenerPlugin.kt: -------------------------------------------------------------------------------- 1 | package im.zoe.labs.flutter_notification_listener 2 | 3 | import android.app.ActivityManager 4 | import android.content.* 5 | import android.content.Context.RECEIVER_EXPORTED 6 | import android.os.Build 7 | import android.util.Log 8 | import androidx.annotation.NonNull 9 | import androidx.annotation.RequiresApi 10 | import io.flutter.embedding.engine.FlutterEngineCache 11 | import io.flutter.embedding.engine.FlutterJNI 12 | import io.flutter.embedding.engine.plugins.FlutterPlugin 13 | import io.flutter.plugin.common.EventChannel 14 | import io.flutter.plugin.common.JSONMessageCodec 15 | import io.flutter.plugin.common.MethodCall 16 | import io.flutter.plugin.common.MethodChannel 17 | import org.json.JSONObject 18 | import java.nio.ByteBuffer 19 | import java.util.* 20 | 21 | 22 | class FlutterNotificationListenerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, EventChannel.StreamHandler { 23 | private var eventSink: EventChannel.EventSink? = null 24 | 25 | private var methodChannel: MethodChannel? = null 26 | private var eventChannel: EventChannel? = null 27 | private val flutterJNI: FlutterJNI = FlutterJNI() 28 | 29 | private lateinit var mContext: Context 30 | 31 | override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 32 | Log.i(TAG, "on attached to engine") 33 | 34 | mContext = flutterPluginBinding.applicationContext 35 | 36 | val binaryMessenger = flutterPluginBinding.binaryMessenger 37 | 38 | // method channel 39 | val method = MethodChannel(binaryMessenger, METHOD_CHANNEL_NAME) 40 | if(method != null) { 41 | methodChannel = method 42 | method.setMethodCallHandler(this) 43 | } 44 | // event stream channel 45 | val event = EventChannel(binaryMessenger, EVENT_CHANNEL_NAME) 46 | if(event != null) { 47 | eventChannel = event 48 | event.setStreamHandler(this) 49 | } 50 | Log.i(TAG, "Attaching FlutterJNI to native") 51 | flutterJNI.attachToNative() 52 | 53 | // store the flutter engine 54 | val engine = flutterPluginBinding.flutterEngine 55 | FlutterEngineCache.getInstance().put(FLUTTER_ENGINE_CACHE_KEY, engine) 56 | 57 | // TODO: remove those code 58 | val receiver = NotificationReceiver() 59 | val intentFilter = IntentFilter() 60 | intentFilter.addAction(NotificationsHandlerService.NOTIFICATION_INTENT) 61 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 62 | mContext.registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED) 63 | }else { 64 | mContext.registerReceiver(receiver, intentFilter) 65 | } 66 | Log.i(TAG, "attached engine finished") 67 | } 68 | 69 | override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { 70 | val method = methodChannel 71 | if (method != null) { 72 | method.setMethodCallHandler(null) 73 | methodChannel = null 74 | } 75 | 76 | val event = eventChannel 77 | if (event != null) { 78 | event.setStreamHandler(null) 79 | eventChannel = null 80 | } 81 | 82 | Log.i(TAG, "Detaching FlutterJNI from native") 83 | flutterJNI.detachFromNativeAndReleaseResources() 84 | } 85 | 86 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) 87 | override fun onListen(o: Any?, eventSink: EventChannel.EventSink?) { 88 | this.eventSink = eventSink 89 | } 90 | 91 | override fun onCancel(o: Any?) { 92 | eventSink = null 93 | } 94 | 95 | internal inner class NotificationReceiver : BroadcastReceiver() { 96 | override fun onReceive(context: Context, intent: Intent) { 97 | eventSink?.success(intent.getStringExtra(NotificationsHandlerService.NOTIFICATION_INTENT_KEY)?:"{}") 98 | } 99 | } 100 | 101 | companion object { 102 | const val TAG = "ListenerPlugin" 103 | 104 | private const val EVENT_CHANNEL_NAME = "flutter_notification_listener/events" 105 | private const val METHOD_CHANNEL_NAME = "flutter_notification_listener/method" 106 | 107 | const val SHARED_PREFERENCES_KEY = "flutter_notification_cache" 108 | 109 | const val CALLBACK_DISPATCHER_HANDLE_KEY = "callback_dispatch_handler" 110 | const val PROMOTE_SERVICE_ARGS_KEY = "promote_service_args" 111 | const val CALLBACK_HANDLE_KEY = "callback_handler" 112 | 113 | const val FLUTTER_ENGINE_CACHE_KEY = "flutter_engine_main" 114 | 115 | private val sNotificationCacheLock = Object() 116 | 117 | fun registerAfterReboot(context: Context) { 118 | synchronized(sNotificationCacheLock) { 119 | Log.i(TAG, "try to start service after reboot") 120 | internalStartService(context, null) 121 | } 122 | } 123 | 124 | private fun initialize(context: Context, cbId: Long) { 125 | Log.d(TAG, "plugin init: install callback and notify the service flutter engine changed") 126 | context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) 127 | .edit() 128 | .putLong(CALLBACK_DISPATCHER_HANDLE_KEY, cbId) 129 | .apply() 130 | 131 | // TODO: update the flutter engine 132 | // call the service to update the flutter engine 133 | NotificationsHandlerService.updateFlutterEngine(context) 134 | } 135 | 136 | fun internalStartService(context: Context, cfg: Utils.PromoteServiceConfig?): Boolean { 137 | if (!NotificationsHandlerService.permissionGiven(context)) { 138 | Log.e(TAG, "can't get permission to start service.") 139 | return false 140 | } 141 | 142 | Log.d(TAG, "start service with args: $cfg") 143 | 144 | val cfg = cfg ?: Utils.PromoteServiceConfig.load(context) 145 | 146 | // and try to toggle the service to trigger rebind 147 | with(NotificationsHandlerService) { 148 | 149 | /* Start the notification service once permission has been given. */ 150 | val intent = Intent(context, NotificationsHandlerService::class.java) 151 | 152 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && cfg.foreground == true) { 153 | Log.i(TAG, "start service foreground") 154 | context.startForegroundService(intent) 155 | } else { 156 | Log.i(TAG, "start service normal") 157 | context.startService(intent) 158 | } 159 | 160 | // and try to toggle the service to trigger rebind 161 | disableServiceSettings(context) 162 | enableServiceSettings(context) 163 | } 164 | 165 | return true 166 | } 167 | 168 | fun startService(context: Context, cfg: Utils.PromoteServiceConfig): Boolean { 169 | // store the config 170 | cfg.save(context) 171 | return internalStartService(context, cfg) 172 | } 173 | 174 | fun stopService(context: Context): Boolean { 175 | if (!isServiceRunning(context, NotificationsHandlerService::class.java)) return true 176 | 177 | val intent = Intent(context, NotificationsHandlerService::class.java) 178 | intent.action = NotificationsHandlerService.ACTION_SHUTDOWN 179 | context.startService(intent) 180 | return true 181 | } 182 | 183 | fun isServiceRunning(context: Context, serviceClass: Class<*>): Boolean { 184 | return null != getRunningService(context, serviceClass) 185 | } 186 | 187 | private fun getRunningService(context: Context, serviceClass: Class<*>): ActivityManager.RunningServiceInfo? { 188 | val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? 189 | for (service in manager!!.getRunningServices(Int.MAX_VALUE)) { 190 | if (serviceClass.name == service.service.className) { 191 | return service 192 | } 193 | } 194 | 195 | return null 196 | } 197 | 198 | fun registerEventHandle(context: Context, cbId: Long): Boolean { 199 | context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) 200 | .edit() 201 | .putLong(CALLBACK_HANDLE_KEY, cbId) 202 | .apply() 203 | return true 204 | } 205 | } 206 | 207 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 208 | when (call.method) { 209 | "plugin.initialize" -> { 210 | val cbId = call.arguments()!! 211 | initialize(mContext, cbId) 212 | return result.success(true) 213 | } 214 | "plugin.startService" -> { 215 | val cfg = Utils.PromoteServiceConfig.fromMap(call.arguments as Map<*, *>) 216 | return result.success(startService(mContext, cfg)) 217 | } 218 | "plugin.stopService" -> { 219 | return result.success(stopService(mContext)) 220 | } 221 | "plugin.hasPermission" -> { 222 | return result.success(NotificationsHandlerService.permissionGiven(mContext)) 223 | } 224 | "plugin.openPermissionSettings" -> { 225 | return result.success(NotificationsHandlerService.openPermissionSettings(mContext)) 226 | } 227 | "plugin.isServiceRunning" -> { 228 | return result.success(isServiceRunning(mContext, NotificationsHandlerService::class.java)) 229 | } 230 | "plugin.registerEventHandle" -> { 231 | val cbId = call.arguments()!! 232 | registerEventHandle(mContext, cbId) 233 | return result.success(true) 234 | } 235 | // TODO: register handle with filter 236 | "setFilter" -> { 237 | // TODO 238 | } 239 | else -> result.notImplemented() 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # flutter_notification_listener 4 | 5 | [![Version](https://img.shields.io/pub/v/flutter_notification_listener.svg)](https://pub.dartlang.org/packages/flutter_notification_listener) 6 | [![pub points](https://badges.bar/flutter_notification_listener/pub%20points)](https://pub.dev/packages/flutter_notification_listener/score) 7 | [![popularity](https://badges.bar/flutter_notification_listener/popularity)](https://pub.dev/packages/flutter_notification_listener/score) 8 | [![likes](https://badges.bar/flutter_notification_listener/likes)](https://pub.dev/packages/flutter_notification_listener/score) 9 | [![License](https://img.shields.io/badge/license-AL2-blue.svg)](https://github.com/jiusanzhou/flutter_notification_listener/blob/master/LICENSE) 10 | 11 | Flutter plugin to listen for all incoming notifications for Android. 12 | 13 |
14 | 15 | --- 16 | 17 | ## Features 18 | 19 | - **Service**: start a service to listen the notifications. 20 | - **Simple**: it's simple to access notification's fields. 21 | - **Backgrounded**: execute the dart code in the background and auto start the service after reboot. 22 | - **Interactive**: the notification is interactive in flutter. 23 | 24 | ## Installtion 25 | 26 | Open the `pubspec.yaml` file located inside the app folder, and add `flutter_notification_listener`: under `dependencies`. 27 | ```yaml 28 | dependencies: 29 | flutter_notification_listener: 30 | ``` 31 | 32 | The latest version is 33 | [![Version](https://img.shields.io/pub/v/flutter_notification_listener.svg)](https://pub.dartlang.org/packages/flutter_notification_listener) 34 | 35 | Then you should install it, 36 | - From the terminal: Run `flutter pub get`. 37 | - From Android Studio/IntelliJ: Click Packages get in the action ribbon at the top of `pubspec.yaml`. 38 | - From VS Code: Click Get Packages located in right side of the action ribbon at the top of `pubspec.yaml`. 39 | 40 | ## Quick Start 41 | 42 | **1. Register the service in the manifest** 43 | 44 | The plugin uses an Android system service to track notifications. To allow this service to run on your application, the following code should be put inside the Android manifest, between the `application` tags. 45 | 46 | ```xml 47 | 50 | 51 | 52 | 53 | 54 | ``` 55 | 56 | And don't forget to add the permissions to the manifest, 57 | ```xml 58 | 59 | 60 | ``` 61 | 62 | **2. Init the plugin and add listen handler** 63 | 64 | We have a default static event handler which send event with a channel. 65 | So if you can listen the event in the ui logic simply. 66 | 67 | ```dart 68 | // define the handler for ui 69 | void onData(NotificationEvent event) { 70 | print(event.toString()); 71 | } 72 | 73 | Future initPlatformState() async { 74 | NotificationsListener.initialize(); 75 | // register you event handler in the ui logic. 76 | NotificationsListener.receivePort.listen((evt) => onData(evt)); 77 | } 78 | ``` 79 | 80 | **3. Check permission and start the service** 81 | 82 | ```dart 83 | void startListening() async { 84 | print("start listening"); 85 | var hasPermission = await NotificationsListener.hasPermission; 86 | if (!hasPermission) { 87 | print("no permission, so open settings"); 88 | NotificationsListener.openPermissionSettings(); 89 | return; 90 | } 91 | 92 | var isR = await NotificationsListener.isRunning; 93 | 94 | if (!isR) { 95 | await NotificationsListener.startService(); 96 | } 97 | 98 | setState(() => started = true); 99 | } 100 | ``` 101 | 102 | --- 103 | 104 | Please check the [./example/lib/main.dart](./example/lib/main.dart) for more detail. 105 | 106 | ## Usage 107 | 108 | ### Start the service after reboot 109 | 110 | It's every useful while you want to start listening notifications automatically after reboot. 111 | 112 | Register a broadcast receiver in the `AndroidManifest.xml`, 113 | ```xml 114 | 116 | 117 | 118 | 119 | 120 | ``` 121 | 122 | Then the listening service will start automatically when the system fired the `BOOT_COMPLETED` intent. 123 | 124 | 125 | And don't forget to add the permissions to the manifest, 126 | ```xml 127 | 128 | 129 | 130 | 131 | ``` 132 | 133 | ### Execute task without UI thread 134 | 135 | > You should know that the function `(evt) => onData(evt)` would **not be called** if the ui thread is not running. 136 | 137 | **:warning: It's recommended that you should register your own static function `callbackHandle` to handle the event which make sure events consumed.** 138 | 139 | That means the `callbackHandle` static function is guaranteed, while the channel handle function is not. This is every useful when you should persist the events to the database. 140 | 141 | > For Flutter 3.x: 142 | Annotate the _callback function with `@pragma('vm:entry-point')` to prevent Flutter from stripping out this function on services. 143 | 144 | We want to run some code in background without UI thread, like persist the notifications to database or storage. 145 | 146 | 1. Define your own callback to handle the incoming notifications. 147 | ```dart 148 | @pragma('vm:entry-point') 149 | static void _callback(NotificationEvent evt) { 150 | // persist data immediately 151 | db.save(evt) 152 | 153 | // send data to ui thread if necessary. 154 | // try to send the event to ui 155 | print("send evt to ui: $evt"); 156 | final SendPort send = IsolateNameServer.lookupPortByName("_listener_"); 157 | if (send == null) print("can't find the sender"); 158 | send?.send(evt); 159 | } 160 | ``` 161 | 162 | 2. Register the handler when invoke the `initialize`. 163 | ```dart 164 | Future initPlatformState() async { 165 | // register the static to handle the events 166 | NotificationsListener.initialize(callbackHandle: _callback); 167 | } 168 | ``` 169 | 170 | 3. Listen events in the UI thread if necessary. 171 | ```dart 172 | // define the handler for ui 173 | void onData(NotificationEvent event) { 174 | print(event.toString()); 175 | } 176 | 177 | Future initPlatformState() async { 178 | // ... 179 | // register you event handler in the ui logic. 180 | NotificationsListener.receivePort.listen((evt) => onData(evt)); 181 | } 182 | ``` 183 | 184 | ### Change notification of listening service 185 | 186 | Before you start the listening service, you can offer some parameters. 187 | ```dart 188 | await NotificationsListener.startService({ 189 | bool foreground = true, // use false will not promote to foreground and without a notification 190 | String title = "Change the title", 191 | String description = "Change the text", 192 | }); 193 | ``` 194 | 195 | ### Tap the notification 196 | 197 | We can tap the notification if it can be triggered in the flutter side. 198 | 199 | 200 | For example, tap the notification automatically when the notification arrived. 201 | 202 | ```dart 203 | // define the handler for ui 204 | void onData(NotificationEvent event) { 205 | print(event.toString()); 206 | // tap the notification automatically 207 | // usually remove the notification 208 | if (event.canTap) event.tap(); 209 | } 210 | ``` 211 | 212 | ### Tap action of the notification 213 | 214 | The notifications from some applications will setted the actions. 215 | We can interact with the notificaions in the flutter side. 216 | 217 | For example, make the notification as readed automatically when the notification arrived. 218 | 219 | ```dart 220 | // define the handler for ui 221 | void onData(NotificationEvent event) { 222 | print(event.toString()); 223 | 224 | events.actions.forEach(act => { 225 | // semantic code is 2 means this is an ignore action 226 | if (act.semantic == 2) { 227 | act.tap(); 228 | } 229 | }) 230 | } 231 | ``` 232 | 233 | ### Reply to conversation of the notification 234 | 235 | Android provider a quick replying method in the notification. 236 | So we can use this to implement a reply logic in the flutter. 237 | 238 | For example, reply to the conversation automatically when the notification arrived. 239 | 240 | ```dart 241 | // define the handler for ui 242 | void onData(NotificationEvent event) { 243 | print(event.toString()); 244 | 245 | events.actions.forEach(act => { 246 | // semantic is 1 means reply quick 247 | if (act.semantic == 1) { 248 | Map map = {}; 249 | act.inputs.forEach((e) { 250 | print("set inputs: ${e.label}<${e.resultKey}>"); 251 | map[e.resultKey] = "Auto reply from flutter"; 252 | }); 253 | 254 | // send to the data 255 | act.postInputs(map); 256 | } 257 | }) 258 | } 259 | ``` 260 | 261 | ## API Reference 262 | 263 | ### Object `NotificationEvent` 264 | 265 | Fields of `NotificationEvent`: 266 | - `uniqueId`: `String`, unique id of the notification which generated from `key`. 267 | - `key`: `String`, key of the status bar notification, required android sdk >= 20. 268 | - `packageName`: `String`, package name of the application which notification posted by. 269 | - `uid`: `int`, uid of the notification, required android sdk >= 29. 270 | - `channelId`: `String` channel if of the notification, required android sdk >= 26. 271 | - `id`: `int`, id of the notification. 272 | - `createAt`: `DateTime`, created time of the notfication in the flutter side. 273 | - `timestamp`: `int`, post time of the notfication. 274 | - `title`: `title`, title of the notification. 275 | - `text`: `String`, text of the notification. 276 | - `hasLargeIcon`: `bool`, if this notification has a large icon. 277 | - `largeIcon`: `Uint8List`, large icon of the notification which setted by setLargeIcon. To display as a image use the Image.memory widget. 278 | - `canTap`: `bool`, if this notification has content pending intent. 279 | - `raw`: `Map`, the original map of this notification, you can get all fields. 280 | 281 | Other original fields in `raw` which not assgin to the class: 282 | - `subText`: `String`, subText of the notification. 283 | - `summaryText`: `String`, summaryText of the notification. 284 | - `textLines`: `List`, multi text lines of the notification. 285 | - `showWhen`: `bool`, if show the time of the notification. 286 | 287 | Methods for notification: 288 | - `Future tap()`: tap the notification if it can be triggered, you should check `canTap` first. Normally will clean up the notification. 289 | - `Future getFull()`: get the full notification object from android. 290 | 291 | ### Object `Action` 292 | 293 | Fields of `Action`: 294 | - `id`: `int`, the index of the action in the actions array 295 | - `title`: `String`, title of the action 296 | - `semantic`: `int`, semantic type of the action, check below for details 297 | - `inputs`: `ActionInput`, emote inputs list of the action 298 | 299 | Action's semantic types: 300 | ``` 301 | SEMANTIC_ACTION_ARCHIVE = 5; 302 | SEMANTIC_ACTION_CALL = 10; 303 | SEMANTIC_ACTION_DELETE = 4; 304 | SEMANTIC_ACTION_MARK_AS_READ = 2; 305 | SEMANTIC_ACTION_MARK_AS_UNREAD = 3; 306 | SEMANTIC_ACTION_MUTE = 6; 307 | SEMANTIC_ACTION_NONE = 0; 308 | SEMANTIC_ACTION_REPLY = 1; 309 | SEMANTIC_ACTION_THUMBS_DOWN = 9; 310 | SEMANTIC_ACTION_THUMBS_UP = 8; 311 | SEMANTIC_ACTION_UNMUTE = 7; 312 | ``` 313 | 314 | For more details, please see [Notification.Action Constants](https://developer.android.com/reference/android/app/Notification.Action#constants_1). 315 | 316 | 317 | Methods of `Action`: 318 | - `Future tap()`: tap the action of the notification. If action's semantic code is `1`, it can't be tapped. 319 | - `Future postInputs(Map map)`: post inputs to the notification, useful for replying automaticly. Only works when semantic code is `1`. 320 | 321 | ### Object `ActionInput` 322 | 323 | Fields of `ActionInput`: 324 | - `label`: `String`, label for input. 325 | - `resultKey`: `String`, result key for input. Must use correct to post data to inputs. 326 | 327 | 328 | ### Class `NotificationsListener` 329 | 330 | Fields of `NotificationsListener`: 331 | - `isRunning`: `bool`, check if the listener service is running. 332 | - `hasPermission`: `bool`, check if grant the permission to start the listener service. 333 | - `receivePort`: `ReceivePort`, default receive port for listening events. 334 | 335 | Static methods of `NotificationsListener`: 336 | - `Future initialize()`: initialize the plugin, must be called at first. 337 | - `Future registerEventHandle(EventCallbackFunc callback)`: register the event handler which will be called from android service, **shoube be static function**. 338 | - `Future openPermissionSettings()`: open the system listen notifactoin permission setting page. 339 | - `Future startService({...})`: start the listening service. arguments, 340 | - `foreground`: `bool`, optional, promote the service to foreground. 341 | - `subTitle`: `String`, optional, sub title of the service's notification. 342 | - `title`: `String`, optional, title of the service's notification. 343 | - `description`: `String`, optional, text contenet of the service's notification. 344 | - `showWhen`: `bool`, optional 345 | - `Future stopService()`: stop the listening service. 346 | - `Future promoteToForeground({...})` proomte the service to the foreground. *Arguments are same `startService`*. 347 | - `Future demoteToBackground()`: demote the service to background. 348 | 349 | ## Known Issues 350 | 351 | - If the service is not foreground, service will start failed after reboot. 352 | 353 | ## Support 354 | 355 | Did you find this plugin useful? Please consider to make a donation to help improve it! 356 | 357 | ## Contributing 358 | 359 | Contributions are always welcome! 360 | -------------------------------------------------------------------------------- /android/src/main/kotlin/im/zoe/labs/flutter_notification_listener/Utils.kt: -------------------------------------------------------------------------------- 1 | package im.zoe.labs.flutter_notification_listener 2 | 3 | import android.app.Notification 4 | import android.app.PendingIntent 5 | import android.app.Person 6 | import android.app.RemoteInput 7 | import android.content.Context 8 | import android.content.IntentSender 9 | import android.content.pm.ApplicationInfo 10 | import android.graphics.Bitmap 11 | import android.graphics.Canvas 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.Bundle 17 | import android.os.UserHandle 18 | import android.service.notification.StatusBarNotification 19 | import android.util.Log 20 | import io.flutter.plugin.common.JSONMessageCodec 21 | import org.json.JSONObject 22 | import java.io.ByteArrayOutputStream 23 | import java.math.BigInteger 24 | import java.nio.ByteBuffer 25 | import java.security.MessageDigest 26 | 27 | class Utils { 28 | companion object { 29 | fun Drawable.toBitmap(): Bitmap { 30 | if (this is BitmapDrawable) { 31 | return this.bitmap 32 | } 33 | 34 | val bitmap = Bitmap.createBitmap(this.intrinsicWidth, this.intrinsicHeight, Bitmap.Config.ARGB_8888) 35 | val canvas = Canvas(bitmap) 36 | this.setBounds(0, 0, canvas.width, canvas.height) 37 | this.draw(canvas) 38 | 39 | return bitmap 40 | } 41 | 42 | fun md5(input:String): String { 43 | val md = MessageDigest.getInstance("MD5") 44 | return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') 45 | } 46 | 47 | 48 | fun convertBitmapToByteArray(bitmap: Bitmap): ByteArray { 49 | val stream = ByteArrayOutputStream() 50 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) 51 | return stream.toByteArray() 52 | } 53 | } 54 | 55 | class Marshaller { 56 | 57 | val convertorFactory = HashMap, Convertor>() 58 | 59 | init { 60 | // improve performance 61 | register { passConvertor(it) } 62 | register { passConvertor(it) } 63 | register { passConvertor(it) } 64 | register { passConvertor(it) } 65 | register { passConvertor(it) } 66 | 67 | // basic types 68 | register { it?.toString() } 69 | 70 | // collections type 71 | register> { arrayConvertor(it as List<*>) } 72 | // register> { arrayConvertor(it as Array<*>) } 73 | register> { obj -> 74 | val items = mutableListOf() 75 | (obj as Array<*>).forEach { 76 | items.add(marshal(it)) 77 | } 78 | items 79 | } 80 | /* 81 | register> { obj -> 82 | val items = mutableSetOf() 83 | (obj as LinkedHashSet<*>).forEach { 84 | items.add(marshal(it)) 85 | } 86 | items 87 | }*/ 88 | 89 | // extends type 90 | register { 91 | val v = it as StatusBarNotification 92 | val map = HashMap() 93 | map["id"] = v.id 94 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 95 | map["groupKey"] = v.groupKey 96 | } 97 | map["isClearable"] = v.isClearable 98 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 99 | map["isGroup"] = v.isGroup 100 | } 101 | map["isOngoing"] = v.isOngoing 102 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { 103 | map["key"] = v.key 104 | } 105 | map["packageName"] = v.packageName 106 | map["postTime"] = v.postTime 107 | map["tag"] = v.tag 108 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 109 | map["uid"] = v.uid 110 | } 111 | map["notification"] = marshal(v.notification) 112 | map 113 | } 114 | register { 115 | val v = it as Notification 116 | val map = HashMap() 117 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 118 | map["channelId"] = v.channelId 119 | } 120 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 121 | map["category"] = v.category 122 | } 123 | map["extras"] = marshal(v.extras) 124 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 125 | map["color"] = v.color 126 | } 127 | map["contentIntent"] = marshal(v.contentIntent) 128 | map["deleteIntent"] = marshal(v.deleteIntent) 129 | map["fullScreenIntent"] = marshal(v.fullScreenIntent) 130 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { 131 | map["group"] = v.group 132 | } 133 | map["actions"] = marshal(v.actions) 134 | map["when"] = v.`when` 135 | map["tickerText"] = marshal(v.tickerText) 136 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 137 | map["tickerText"] = marshal(v.settingsText) 138 | } 139 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 140 | map["timeoutAfter"] = v.timeoutAfter 141 | } 142 | map["number"] = v.number 143 | map["sound"] = v.sound?.toString() 144 | map 145 | } 146 | register { 147 | val v = it as PendingIntent 148 | val map = HashMap() 149 | map["creatorPackage"] = v.creatorPackage 150 | map["creatorUid"] = v.creatorUid 151 | map 152 | } 153 | register { 154 | val v = it as ApplicationInfo 155 | val map = HashMap() 156 | map["name"] = v.name 157 | map["processName"] = v.processName 158 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 159 | map["category"] = v.category 160 | } 161 | map["dataDir"] = v.dataDir 162 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 163 | map["deviceProtectedDataDir"] = v.deviceProtectedDataDir 164 | } 165 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 166 | map["appComponentFactory"] = v.appComponentFactory 167 | } 168 | map["manageSpaceActivityName"] = v.manageSpaceActivityName 169 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 170 | map["deviceProtectedDataDir"] = v.deviceProtectedDataDir 171 | } 172 | map["publicSourceDir"] = v.publicSourceDir 173 | map["sourceDir"] = v.sourceDir 174 | map 175 | } 176 | 177 | register { convertBitmapToByteArray(it as Bitmap) } 178 | register { obj -> 179 | val v = obj as Bundle 180 | val map = HashMap() 181 | val keys = obj.keySet() 182 | keys.forEach { map[it] = marshal(v.get(it)) } 183 | map 184 | } 185 | register { obj -> 186 | val v = obj as Notification.Action 187 | val map = HashMap() 188 | map["title"] = v.title 189 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 190 | map["semantic"] = v.semanticAction 191 | } 192 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { 193 | map["inputs"] = marshal(v.remoteInputs) 194 | map["extras"] = marshal(v.extras) 195 | } 196 | map 197 | } 198 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { 199 | register { 200 | val v = it as RemoteInput 201 | val map = HashMap() 202 | map["label"] = v.label 203 | map["resultKey"] = v.resultKey 204 | map["choices"] = marshal(v.choices) 205 | map["extras"] = marshal(v.extras) 206 | map 207 | } 208 | } 209 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 210 | register { ignoreConvertor(it) } 211 | } 212 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 213 | register { 214 | val v = it as Notification.MessagingStyle 215 | val map = HashMap() 216 | map["conversationTitle"] = marshal(v.conversationTitle) 217 | map["messages"] = marshal(v.messages) 218 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 219 | map["user"] = marshal(v.user) 220 | } 221 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 222 | map["historicMessages"] = marshal(v.historicMessages) 223 | } 224 | map 225 | } 226 | } 227 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 228 | register { 229 | val v = it as Person 230 | val map = HashMap() 231 | map["isBot"] = v.isBot 232 | map["isImportant"] = v.isImportant 233 | map["key"] = v.key 234 | map["name"] = v.name 235 | map["uri"] = v.uri 236 | map 237 | } 238 | } 239 | } 240 | 241 | private fun arrayConvertor(obj: T): List 242 | where T: List<*> { 243 | val items = mutableListOf() 244 | (obj as List<*>).forEach { items.add(marshal(it)) } 245 | return items 246 | } 247 | 248 | private fun ignoreConvertor(obj: Any?): Any? { 249 | return null 250 | } 251 | 252 | private fun passConvertor(obj: Any?): Any? { 253 | return obj 254 | } 255 | 256 | inline fun register(noinline fn: Convertor) { 257 | convertorFactory[T::class.java] = fn 258 | } 259 | 260 | fun marshal(obj: Any?): Any? { 261 | if (obj == null) return null 262 | // get the type of obj? and return 263 | // can we use get directly? 264 | for (et in convertorFactory) { 265 | if (et.key.isAssignableFrom(obj.javaClass)) { 266 | return et.value.invoke(obj) 267 | } 268 | } 269 | return obj 270 | } 271 | 272 | companion object { 273 | val instance = Marshaller() 274 | 275 | fun marshal(obj: Any?): Any? { 276 | return instance.marshal(obj) 277 | } 278 | 279 | inline fun register(noinline fn: Convertor) { 280 | return instance.register(fn) 281 | } 282 | } 283 | } 284 | 285 | class PromoteServiceConfig { 286 | var foreground: Boolean? = false 287 | var title: String? = "Flutter Notification Listener" 288 | var subTitle: String? = null 289 | var description: String? = "Let's scraping the notifications ..." 290 | var showWhen: Boolean? = false 291 | 292 | fun toMap(): Map { 293 | val map = HashMap() 294 | map["foreground"] = foreground 295 | map["title"] = title 296 | map["subTitle"] = subTitle 297 | map["description"] = description 298 | map["showWhen"] = showWhen 299 | return map 300 | } 301 | 302 | override fun toString(): String { 303 | return JSONObject(toMap()).toString() 304 | } 305 | 306 | fun save(context: Context) { 307 | val str = toString() 308 | Log.d(TAG, "save the promote config: $str") 309 | context.getSharedPreferences(FlutterNotificationListenerPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) 310 | .edit() 311 | .putString(FlutterNotificationListenerPlugin.PROMOTE_SERVICE_ARGS_KEY, str) 312 | .apply() 313 | } 314 | 315 | companion object { 316 | val TAG = "PromoteConfig" 317 | 318 | fun fromMap(map: Map<*, *>?): PromoteServiceConfig { 319 | val cfg = PromoteServiceConfig() 320 | map?.let { m -> 321 | m["foreground"]?.let { cfg.foreground = it as Boolean? } 322 | m["title"]?.let { cfg.title = it as String? } 323 | m["subTitle"]?.let { cfg.subTitle = it as String? } 324 | m["description"]?.let { cfg.description = it as String? } 325 | m["showWhen"]?.let { cfg.showWhen = it as Boolean? } 326 | } 327 | return cfg 328 | } 329 | 330 | fun fromString(str: String = "{}"): PromoteServiceConfig { 331 | val cfg = PromoteServiceConfig() 332 | val map = JSONObject(str) 333 | map.let { m -> 334 | try { 335 | m["foreground"].let { cfg.foreground = it as Boolean? } 336 | m["title"].let { cfg.title = it as String? } 337 | m["subTitle"].let { cfg.subTitle = it as String? } 338 | m["description"].let { cfg.description = it as String? } 339 | m["showWhen"].let { cfg.showWhen = it as Boolean? } 340 | } catch (e: Exception) { 341 | e.printStackTrace() 342 | } 343 | } 344 | return cfg 345 | } 346 | 347 | fun load(context: Context): PromoteServiceConfig { 348 | val str = context.getSharedPreferences(FlutterNotificationListenerPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) 349 | .getString(FlutterNotificationListenerPlugin.PROMOTE_SERVICE_ARGS_KEY, "{}") 350 | Log.d(TAG, "load the promote config: ${str.toString()}") 351 | return fromString(str?:"{}") 352 | } 353 | } 354 | } 355 | } 356 | 357 | typealias Convertor = (Any) -> Any? -------------------------------------------------------------------------------- /android/src/main/kotlin/im/zoe/labs/flutter_notification_listener/NotificationsHandlerService.kt: -------------------------------------------------------------------------------- 1 | package im.zoe.labs.flutter_notification_listener 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.RemoteInput 7 | import android.content.ComponentName 8 | import android.content.Context 9 | import android.content.Intent 10 | import android.content.pm.PackageManager 11 | import android.os.Build 12 | import android.os.Bundle 13 | import android.os.Handler 14 | import android.os.PowerManager 15 | import android.provider.Settings 16 | import android.service.notification.NotificationListenerService 17 | import android.service.notification.StatusBarNotification 18 | import android.text.TextUtils 19 | import android.util.Log 20 | import androidx.annotation.NonNull 21 | import androidx.annotation.RequiresApi 22 | import androidx.core.app.NotificationCompat 23 | import io.flutter.FlutterInjector 24 | import io.flutter.embedding.engine.FlutterEngine 25 | import io.flutter.embedding.engine.FlutterEngineCache 26 | import io.flutter.embedding.engine.dart.DartExecutor 27 | import io.flutter.plugin.common.MethodCall 28 | import io.flutter.plugin.common.MethodChannel 29 | import io.flutter.view.FlutterCallbackInformation 30 | import org.json.JSONObject 31 | import java.util.* 32 | import java.util.concurrent.atomic.AtomicBoolean 33 | import kotlin.collections.HashMap 34 | 35 | class NotificationsHandlerService: MethodChannel.MethodCallHandler, NotificationListenerService() { 36 | private val queue = ArrayDeque() 37 | private lateinit var mBackgroundChannel: MethodChannel 38 | private lateinit var mContext: Context 39 | 40 | // notification event cache: packageName_id -> event 41 | private val eventsCache = HashMap() 42 | 43 | override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { 44 | when (call.method) { 45 | "service.initialized" -> { 46 | initFinish() 47 | return result.success(true) 48 | } 49 | // this should move to plugin 50 | "service.promoteToForeground" -> { 51 | // add data 52 | val cfg = Utils.PromoteServiceConfig.fromMap(call.arguments as Map<*, *>).apply { 53 | foreground = true 54 | } 55 | return result.success(promoteToForeground(cfg)) 56 | } 57 | "service.demoteToBackground" -> { 58 | return result.success(demoteToBackground()) 59 | } 60 | "service.tap" -> { 61 | // tap the notification 62 | Log.d(TAG, "tap the notification") 63 | val args = call.arguments?>() 64 | val uid = args!![0]!! as String 65 | return result.success(tapNotification(uid)) 66 | } 67 | "service.tap_action" -> { 68 | // tap the action 69 | Log.d(TAG, "tap action of notification") 70 | val args = call.arguments?>() 71 | val uid = args!![0]!! as String 72 | val idx = args[1]!! as Int 73 | return result.success(tapNotificationAction(uid, idx)) 74 | } 75 | "service.send_input" -> { 76 | // send the input data 77 | Log.d(TAG, "set the content for input and the send action") 78 | val args = call.arguments?>() 79 | val uid = args!![0]!! as String 80 | val idx = args[1]!! as Int 81 | val data = args[2]!! as Map<*, *> 82 | return result.success(sendNotificationInput(uid, idx, data)) 83 | } 84 | "service.get_full_notification" -> { 85 | val args = call.arguments?>() 86 | val uid = args!![0]!! as String 87 | if (!eventsCache.contains(uid)) { 88 | return result.error("notFound", "can't found this notification $uid", "") 89 | } 90 | return result.success(Utils.Marshaller.marshal(eventsCache[uid]?.mSbn)) 91 | } 92 | else -> { 93 | Log.d(TAG, "unknown method ${call.method}") 94 | result.notImplemented() 95 | } 96 | } 97 | } 98 | 99 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 100 | // if get shutdown release the wake lock 101 | when (intent?.action) { 102 | ACTION_SHUTDOWN -> { 103 | (getSystemService(Context.POWER_SERVICE) as PowerManager).run { 104 | newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply { 105 | if (isHeld) release() 106 | } 107 | } 108 | Log.i(TAG, "stop notification handler service!") 109 | disableServiceSettings(mContext) 110 | stopForeground(true) 111 | stopSelf() 112 | } 113 | else -> { 114 | 115 | } 116 | } 117 | return START_STICKY 118 | } 119 | 120 | override fun onCreate() { 121 | super.onCreate() 122 | 123 | mContext = this 124 | 125 | // store the service instance 126 | instance = this 127 | 128 | Log.i(TAG, "notification listener service onCreate") 129 | startListenerService(this) 130 | } 131 | 132 | override fun onDestroy() { 133 | super.onDestroy() 134 | Log.i(TAG, "notification listener service onDestroy") 135 | val bdi = Intent(mContext, RebootBroadcastReceiver::class.java) 136 | // remove notification 137 | sendBroadcast(bdi) 138 | } 139 | 140 | override fun onTaskRemoved(rootIntent: Intent?) { 141 | super.onTaskRemoved(rootIntent) 142 | Log.i(TAG, "notification listener service onTaskRemoved") 143 | } 144 | 145 | override fun onNotificationPosted(sbn: StatusBarNotification) { 146 | super.onNotificationPosted(sbn) 147 | 148 | FlutterInjector.instance().flutterLoader().startInitialization(mContext) 149 | FlutterInjector.instance().flutterLoader().ensureInitializationComplete(mContext, null) 150 | 151 | val evt = NotificationEvent(mContext, sbn) 152 | 153 | // store the evt to cache 154 | eventsCache[evt.uid] = evt 155 | 156 | synchronized(sServiceStarted) { 157 | if (!sServiceStarted.get()) { 158 | Log.d(TAG, "service is not start try to queue the event") 159 | queue.add(evt) 160 | } else { 161 | Log.d(TAG, "send event to flutter side immediately!") 162 | Handler(mContext.mainLooper).post { sendEvent(evt) } 163 | } 164 | } 165 | } 166 | 167 | override fun onNotificationRemoved(sbn: StatusBarNotification?) { 168 | super.onNotificationRemoved(sbn) 169 | if (sbn == null) return 170 | val evt = NotificationEvent(mContext, sbn) 171 | // remove the event from cache 172 | eventsCache.remove(evt.uid) 173 | Log.d(TAG, "notification removed: ${evt.uid}") 174 | } 175 | 176 | private fun initFinish() { 177 | Log.d(TAG, "service's flutter engine initialize finished") 178 | synchronized(sServiceStarted) { 179 | while (!queue.isEmpty()) sendEvent(queue.remove()) 180 | sServiceStarted.set(true) 181 | } 182 | } 183 | 184 | private fun promoteToForeground(cfg: Utils.PromoteServiceConfig? = null): Boolean { 185 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 186 | Log.e(TAG, "promoteToForeground need sdk >= 26") 187 | return false 188 | } 189 | 190 | if (cfg?.foreground != true) { 191 | Log.i(TAG, "no need to start foreground: ${cfg?.foreground}") 192 | return false 193 | } 194 | 195 | // first is not running already, start at first 196 | if (!FlutterNotificationListenerPlugin.isServiceRunning(mContext, this.javaClass)) { 197 | Log.e(TAG, "service is not running") 198 | return false 199 | } 200 | 201 | // get args from store or args 202 | val cfg = cfg ?: Utils.PromoteServiceConfig.load(this) 203 | // make the service to foreground 204 | 205 | // take a wake lock 206 | (getSystemService(Context.POWER_SERVICE) as PowerManager).run { 207 | newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply { 208 | setReferenceCounted(false) 209 | acquire() 210 | } 211 | } 212 | 213 | // create a channel for notification 214 | val channel = NotificationChannel(CHANNEL_ID, "Flutter Notifications Listener Plugin", NotificationManager.IMPORTANCE_HIGH) 215 | val imageId = resources.getIdentifier("ic_launcher", "mipmap", packageName) 216 | (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel) 217 | 218 | val notification = NotificationCompat.Builder(this, CHANNEL_ID) 219 | .setContentTitle(cfg.title) 220 | .setContentText(cfg.description) 221 | .setShowWhen(cfg.showWhen ?: false) 222 | .setSubText(cfg.subTitle) 223 | .setSmallIcon(imageId) 224 | .setPriority(NotificationCompat.PRIORITY_HIGH) 225 | .setCategory(NotificationCompat.CATEGORY_SERVICE) 226 | .build() 227 | 228 | Log.d(TAG, "promote the service to foreground") 229 | startForeground(ONGOING_NOTIFICATION_ID, notification) 230 | 231 | return true 232 | } 233 | 234 | private fun demoteToBackground(): Boolean { 235 | Log.d(TAG, "demote the service to background") 236 | (getSystemService(Context.POWER_SERVICE) as PowerManager).run { 237 | newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply { 238 | if (isHeld) release() 239 | } 240 | } 241 | stopForeground(true) 242 | return true 243 | } 244 | 245 | private fun tapNotification(uid: String): Boolean { 246 | Log.d(TAG, "tap the notification: $uid") 247 | if (!eventsCache.containsKey(uid)) { 248 | Log.d(TAG, "notification is not exits: $uid") 249 | return false 250 | } 251 | val n = eventsCache[uid] ?: return false 252 | n.mSbn.notification.contentIntent.send() 253 | return true 254 | } 255 | 256 | private fun tapNotificationAction(uid: String, idx: Int): Boolean { 257 | Log.d(TAG, "tap the notification action: $uid @$idx") 258 | if (!eventsCache.containsKey(uid)) { 259 | Log.d(TAG, "notification is not exits: $uid") 260 | return false 261 | } 262 | val n = eventsCache[uid] 263 | if (n == null) { 264 | Log.e(TAG, "notification is null: $uid") 265 | return false 266 | } 267 | if (n.mSbn.notification.actions.size <= idx) { 268 | Log.e(TAG, "tap action out of range: size ${n.mSbn.notification.actions.size} index $idx") 269 | return false 270 | } 271 | 272 | val act = n.mSbn.notification.actions[idx] 273 | if (act == null) { 274 | Log.e(TAG, "notification $uid action $idx not exits") 275 | return false 276 | } 277 | act.actionIntent.send() 278 | return true 279 | } 280 | 281 | private fun sendNotificationInput(uid: String, idx: Int, data: Map<*, *>): Boolean { 282 | Log.d(TAG, "tap the notification action: $uid @$idx") 283 | if (!eventsCache.containsKey(uid)) { 284 | Log.d(TAG, "notification is not exits: $uid") 285 | return false 286 | } 287 | val n = eventsCache[uid] 288 | if (n == null) { 289 | Log.e(TAG, "notification is null: $uid") 290 | return false 291 | } 292 | if (n.mSbn.notification.actions.size <= idx) { 293 | Log.e(TAG, "send inputs out of range: size ${n.mSbn.notification.actions.size} index $idx") 294 | return false 295 | } 296 | 297 | val act = n.mSbn.notification.actions[idx] 298 | if (act == null) { 299 | Log.e(TAG, "notification $uid action $idx not exits") 300 | return false 301 | } 302 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { 303 | if (act.remoteInputs == null) { 304 | Log.e(TAG, "notification $uid action $idx remote inputs not exits") 305 | return false 306 | } 307 | 308 | val intent = Intent() 309 | val bundle = Bundle() 310 | act.remoteInputs.forEach { 311 | if (data.containsKey(it.resultKey as String)) { 312 | Log.d(TAG, "add input content: ${it.resultKey} => ${data[it.resultKey]}") 313 | bundle.putCharSequence(it.resultKey, data[it.resultKey] as String) 314 | } 315 | } 316 | RemoteInput.addResultsToIntent(act.remoteInputs, intent, bundle) 317 | act.actionIntent.send(mContext, 0, intent) 318 | Log.d(TAG, "send the input action success") 319 | return true 320 | } else { 321 | Log.e(TAG, "not implement :sdk < KITKAT_WATCH") 322 | return false 323 | } 324 | } 325 | 326 | companion object { 327 | 328 | var callbackHandle = 0L 329 | 330 | @SuppressLint("StaticFieldLeak") 331 | @JvmStatic 332 | var instance: NotificationsHandlerService? = null 333 | 334 | @JvmStatic 335 | private val TAG = "NotificationsListenerService" 336 | 337 | private const val ONGOING_NOTIFICATION_ID = 100 338 | @JvmStatic 339 | private val WAKELOCK_TAG = "IsolateHolderService::WAKE_LOCK" 340 | @JvmStatic 341 | val ACTION_SHUTDOWN = "SHUTDOWN" 342 | 343 | private const val CHANNEL_ID = "flutter_notifications_listener_channel" 344 | 345 | @JvmStatic 346 | private var sBackgroundFlutterEngine: FlutterEngine? = null 347 | @JvmStatic 348 | private val sServiceStarted = AtomicBoolean(false) 349 | 350 | private const val BG_METHOD_CHANNEL_NAME = "flutter_notification_listener/bg_method" 351 | 352 | private const val ENABLED_NOTIFICATION_LISTENERS = "enabled_notification_listeners" 353 | private const val ACTION_NOTIFICATION_LISTENER_SETTINGS = "android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS" 354 | 355 | const val NOTIFICATION_INTENT_KEY = "object" 356 | const val NOTIFICATION_INTENT = "notification_event" 357 | 358 | fun permissionGiven(context: Context): Boolean { 359 | val packageName = context.packageName 360 | val flat = Settings.Secure.getString(context.contentResolver, ENABLED_NOTIFICATION_LISTENERS) 361 | if (!TextUtils.isEmpty(flat)) { 362 | val names = flat.split(":").toTypedArray() 363 | for (name in names) { 364 | val componentName = ComponentName.unflattenFromString(name) 365 | val nameMatch = TextUtils.equals(packageName, componentName?.packageName) 366 | if (nameMatch) { 367 | return true 368 | } 369 | } 370 | } 371 | 372 | return false 373 | } 374 | 375 | fun openPermissionSettings(context: Context): Boolean { 376 | context.startActivity(Intent(ACTION_NOTIFICATION_LISTENER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) 377 | return true 378 | } 379 | 380 | fun enableServiceSettings(context: Context) { 381 | toggleServiceSettings(context, PackageManager.COMPONENT_ENABLED_STATE_ENABLED) 382 | } 383 | 384 | fun disableServiceSettings(context: Context) { 385 | toggleServiceSettings(context, PackageManager.COMPONENT_ENABLED_STATE_DISABLED) 386 | } 387 | 388 | private fun toggleServiceSettings(context: Context, state: Int) { 389 | val receiver = ComponentName(context, NotificationsHandlerService::class.java) 390 | val pm = context.packageManager 391 | pm.setComponentEnabledSetting(receiver, state, PackageManager.DONT_KILL_APP) 392 | } 393 | 394 | fun updateFlutterEngine(context: Context) { 395 | Log.d(TAG, "call instance update flutter engine from plugin init") 396 | instance?.updateFlutterEngine(context) 397 | // we need to `finish init` manually 398 | instance?.initFinish() 399 | } 400 | } 401 | 402 | private fun getFlutterEngine(context: Context): FlutterEngine { 403 | var eng = FlutterEngineCache.getInstance().get(FlutterNotificationListenerPlugin.FLUTTER_ENGINE_CACHE_KEY) 404 | if (eng != null) return eng 405 | 406 | Log.i(TAG, "flutter engine cache is null, create a new one") 407 | eng = FlutterEngine(context) 408 | 409 | // ensure initialization 410 | FlutterInjector.instance().flutterLoader().startInitialization(context) 411 | FlutterInjector.instance().flutterLoader().ensureInitializationComplete(context, arrayOf()) 412 | 413 | // call the flutter side init 414 | // get the call back handle information 415 | val cb = context.getSharedPreferences(FlutterNotificationListenerPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) 416 | .getLong(FlutterNotificationListenerPlugin.CALLBACK_DISPATCHER_HANDLE_KEY, 0) 417 | 418 | if (cb != 0L) { 419 | Log.d(TAG, "try to find callback: $cb") 420 | val info = FlutterCallbackInformation.lookupCallbackInformation(cb) 421 | val args = DartExecutor.DartCallback(context.assets, 422 | FlutterInjector.instance().flutterLoader().findAppBundlePath(), info) 423 | // call the callback 424 | eng.dartExecutor.executeDartCallback(args) 425 | } else { 426 | Log.e(TAG, "Fatal: no callback register") 427 | } 428 | 429 | FlutterEngineCache.getInstance().put(FlutterNotificationListenerPlugin.FLUTTER_ENGINE_CACHE_KEY, eng) 430 | return eng 431 | } 432 | 433 | private fun updateFlutterEngine(context: Context) { 434 | Log.d(TAG, "update the flutter engine of service") 435 | // take the engine 436 | val eng = getFlutterEngine(context) 437 | sBackgroundFlutterEngine = eng 438 | 439 | // set the method call 440 | mBackgroundChannel = MethodChannel(eng.dartExecutor.binaryMessenger, BG_METHOD_CHANNEL_NAME) 441 | mBackgroundChannel.setMethodCallHandler(this) 442 | } 443 | 444 | private fun startListenerService(context: Context) { 445 | Log.d(TAG, "start listener service") 446 | synchronized(sServiceStarted) { 447 | // promote to foreground 448 | // TODO: take from intent, currently just load form store 449 | promoteToForeground(Utils.PromoteServiceConfig.load(context)) 450 | 451 | // we should to update 452 | Log.d(TAG, "service's flutter engine is null, should update one") 453 | updateFlutterEngine(context) 454 | 455 | sServiceStarted.set(true) 456 | } 457 | Log.d(TAG, "service start finished") 458 | } 459 | 460 | private fun sendEvent(evt: NotificationEvent) { 461 | Log.d(TAG, "send notification event: ${evt.data}") 462 | if (callbackHandle == 0L) { 463 | callbackHandle = mContext.getSharedPreferences(FlutterNotificationListenerPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) 464 | .getLong(FlutterNotificationListenerPlugin.CALLBACK_HANDLE_KEY, 0) 465 | } 466 | 467 | // why mBackgroundChannel can be null? 468 | 469 | try { 470 | // don't care about the method name 471 | mBackgroundChannel.invokeMethod("sink_event", listOf(callbackHandle, evt.data)) 472 | } catch (e: Exception) { 473 | e.printStackTrace() 474 | } 475 | } 476 | 477 | } 478 | 479 | --------------------------------------------------------------------------------