Переглянути джерело

[new]增加录音通知栏操作

zk 1 рік тому
батько
коміт
fb8db6b838
33 змінених файлів з 1211 додано та 68 видалено
  1. 4 17
      lib/module/record/controller.dart
  2. 118 35
      lib/module/record/record_handler.dart
  3. 21 0
      lib/utils/notification_util.dart
  4. 29 0
      plugin/custom_notification/.gitignore
  5. 30 0
      plugin/custom_notification/.metadata
  6. 3 0
      plugin/custom_notification/CHANGELOG.md
  7. 1 0
      plugin/custom_notification/LICENSE
  8. 15 0
      plugin/custom_notification/README.md
  9. 4 0
      plugin/custom_notification/analysis_options.yaml
  10. 9 0
      plugin/custom_notification/android/.gitignore
  11. 72 0
      plugin/custom_notification/android/build.gradle
  12. BIN
      plugin/custom_notification/android/gradle/wrapper/gradle-wrapper.jar
  13. 7 0
      plugin/custom_notification/android/gradle/wrapper/gradle-wrapper.properties
  14. 249 0
      plugin/custom_notification/android/gradlew
  15. 92 0
      plugin/custom_notification/android/gradlew.bat
  16. 1 0
      plugin/custom_notification/android/settings.gradle
  17. 7 0
      plugin/custom_notification/android/src/main/AndroidManifest.xml
  18. 80 0
      plugin/custom_notification/android/src/main/java/com/atmob/custom_notification/CustomNotificationPlugin.java
  19. 34 0
      plugin/custom_notification/android/src/main/java/com/atmob/custom_notification/MethodCallDistribute.java
  20. 140 0
      plugin/custom_notification/android/src/main/java/com/atmob/custom_notification/NotificationUtil.java
  21. BIN
      plugin/custom_notification/android/src/main/res/drawable-xxhdpi/icon_notification_logo.webp
  22. BIN
      plugin/custom_notification/android/src/main/res/drawable-xxhdpi/icon_record_done.webp
  23. BIN
      plugin/custom_notification/android/src/main/res/drawable-xxhdpi/icon_record_pause.webp
  24. BIN
      plugin/custom_notification/android/src/main/res/drawable-xxhdpi/icon_record_playing.webp
  25. BIN
      plugin/custom_notification/android/src/main/res/drawable-xxhdpi/logo.png
  26. 61 0
      plugin/custom_notification/android/src/main/res/layout/custom_notification_layout.xml
  27. 29 0
      plugin/custom_notification/android/src/test/java/com/atmob/custom_notification/CustomNotificationPluginTest.java
  28. 17 0
      plugin/custom_notification/lib/custom_notification.dart
  29. 54 0
      plugin/custom_notification/lib/custom_notification_method_channel.dart
  30. 37 0
      plugin/custom_notification/lib/custom_notification_platform_interface.dart
  31. 70 0
      plugin/custom_notification/pubspec.yaml
  32. 23 16
      pubspec.lock
  33. 4 0
      pubspec.yaml

+ 4 - 17
lib/module/record/controller.dart

@@ -9,7 +9,6 @@ import 'package:electronic_assistant/module/talk/view.dart';
 import 'package:electronic_assistant/utils/desktop_shortcut_utils.dart';
 import 'package:electronic_assistant/utils/toast_util.dart';
 import 'package:get/get.dart';
-import 'package:path_provider/path_provider.dart';
 
 import '../../data/consts/error_code.dart';
 import '../../data/repositories/talk_repository.dart';
@@ -18,8 +17,6 @@ import '../../router/app_pages.dart';
 import '../../utils/http_handler.dart';
 
 class RecordController extends BaseController {
-  static const int minRecordDuration = 3;
-
   Rx<RecordStatus> currentStatus = recordHandler.currentStatus;
   RxDouble currentDuration = recordHandler.currentDuration;
 
@@ -112,18 +109,10 @@ class RecordController extends BaseController {
   }
 
   Future<void> _saveCurrentRecord() async {
-    final currentDurationValue = currentDuration.value;
-    if (currentDurationValue < minRecordDuration) {
-      ToastUtil.showToast("录音时长不足$minRecordDuration秒");
-      return;
-    }
-    await recordHandler.stopRecord();
-    talkRepository
-        .talkCreate(recordHandler.lastRecordId, currentDuration.value.toInt())
-        .then((talkInfo) async {
-      await recordHandler.getConvertWavFile(talkInfo.id);
+    try {
+      TalkBean talkInfo = await recordHandler.saveCurrentRecord();
       _dealSuccessNextStep(talkInfo);
-    }).catchError((error) {
+    } catch (error) {
       if (error is ServerErrorException) {
         if (error.code == ErrorCode.errorCodeNoLogin) {
           ToastUtil.showToast("录音已保存,请登录");
@@ -136,7 +125,7 @@ class RecordController extends BaseController {
       } else {
         ToastUtil.showToast("录音已保存,请检查网络并重试");
       }
-    });
+    }
   }
 
   void _dealSuccessNextStep(TalkBean talkInfo) {
@@ -150,6 +139,4 @@ class RecordController extends BaseController {
     Get.back();
     TalkPage.start(talkInfo, eventTag: EventId.id_001);
   }
-
-
 }

+ 118 - 35
lib/module/record/record_handler.dart

@@ -1,6 +1,9 @@
+import 'dart:async';
 import 'dart:io';
 import 'dart:typed_data';
+import 'package:custom_notification/custom_notification.dart';
 import 'package:electronic_assistant/module/record/record_task.dart';
+import 'package:electronic_assistant/router/app_pages.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:get/get.dart';
 import 'package:path_provider/path_provider.dart';
@@ -9,20 +12,32 @@ import 'package:get/get_rx/src/rx_types/rx_types.dart';
 import 'package:record/record.dart';
 import 'package:uuid/uuid.dart';
 import 'package:wakelock_plus/wakelock_plus.dart';
+import '../../data/bean/talks.dart';
+import '../../data/consts/error_code.dart';
+import '../../data/consts/event_report_id.dart';
+import '../../data/repositories/talk_repository.dart';
 import '../../resource/string.gen.dart';
+import '../../utils/http_handler.dart';
 import '../../utils/mmkv_util.dart';
+import '../../utils/notification_util.dart';
 import '../../utils/pcm_wav_converter.dart';
 import '../../utils/toast_util.dart';
+import '../talk/view.dart';
 import 'constants.dart';
 
 class RecordHandler {
   RecordHandler._();
 
+  static const int minRecordDuration = 3;
+  static const String recordDone = 'done';
+  static const String recordPause = 'pause';
+  static const String recordRecording = 'recording';
+
   static const String keyLastRecordId = "last_record_id";
 
   final Rx<RecordStatus> currentStatus = RecordStatus.pending.obs;
   final RxDouble currentDuration = 0.0.obs;
-  AudioRecorder? _record;
+  final AudioRecorder _record = AudioRecorder();
   final RecordConfig _recordConfig = RecordConfig(
     encoder: AudioEncoder.pcm16bits,
     bitRate: 16000,
@@ -32,17 +47,66 @@ class RecordHandler {
 
   String? _lastRecordId;
 
+  final int _serviceId = 256;
+  final String _channelId = StringName.recordNotificationChannelId.tr;
+  final String _channelName = StringName.recordNotificationChannelName.tr;
+
   String get lastRecordId => _lastRecordId ?? '';
+  StreamSubscription? _currentDurationListener;
+  StreamSubscription? _recordActionListener;
 
   void init() {
     if (currentStatus.value != RecordStatus.recording) {
-      _record = AudioRecorder();
       _initLastRecordId();
       _initLastRecordStatus();
       _initForegroundService();
+      _initRecordDurationStream();
     }
   }
 
+  void _initRecordDurationStream() {
+    _currentDurationListener?.cancel();
+    _currentDurationListener = currentDuration.listen((event) {
+      debugPrint('currentDuration: $event');
+      if (currentStatus.value == RecordStatus.pending) {
+        return;
+      }
+      NotificationUtil.showRecordNotification(
+          _serviceId, currentStatus.value == RecordStatus.recording, event,
+          channelId: _channelId, channelName: _channelName);
+    });
+    _recordActionListener?.cancel();
+    _recordActionListener =
+        CustomNotification.recordActionStream().listen((action) {
+      if (action == recordDone) {
+        _recordNotificationDone();
+      } else if (action == recordPause) {
+        stopRecord();
+      } else if (action == recordRecording) {
+        startOrContinueRecord();
+      }
+    });
+  }
+
+  void _recordNotificationDone() {
+    saveCurrentRecord().then((talkInfo) {
+      if (Get.currentRoute == RoutePath.record) {
+        Get.back();
+      }
+      TalkPage.start(talkInfo, eventTag: EventId.id_001);
+    }).catchError((error) {
+      if (error is ServerErrorException) {
+        if (error.code == ErrorCode.errorCodeNoLogin) {
+          ToastUtil.showToast("录音已保存,请登录");
+        } else {
+          ToastUtil.showToast("${error.message}");
+        }
+      } else {
+        ToastUtil.showToast("录音已保存,请检查网络并重试");
+      }
+    });
+  }
+
   void _initLastRecordId() {
     String? lastRecordId = KVUtil.getString(keyLastRecordId, null);
     if (lastRecordId == null || lastRecordId.isEmpty) {
@@ -58,8 +122,9 @@ class RecordHandler {
     var fileLength = currentRecordFile.lengthSync();
     if (currentRecordFile.existsSync() && fileLength > 0) {
       _changeRecordStatus(RecordStatus.paused);
-      currentDuration.value = _getPcmDuration(
+      double time = _getPcmDuration(
           fileLength, _recordConfig.sampleRate, 16, _recordConfig.numChannels);
+      currentDuration.value = time;
     } else {
       currentDuration.value = 0;
     }
@@ -81,17 +146,25 @@ class RecordHandler {
     return file;
   }
 
-  Future<void> stopRecord() async {
+  Future<void> stopRecord({bool? isStopService}) async {
     _releaseWakeLock();
-    _record
-        ?.stop()
-        .then((_) => _changeRecordStatus(RecordStatus.paused))
-        .then((_) => FlutterForegroundTask.stopService());
-    ;
+    if (await _record.isRecording()) {
+      await _record.stop();
+    }
+    _changeRecordStatus(RecordStatus.paused);
+    if (isStopService == true) {
+      await FlutterForegroundTask.stopService();
+    }
   }
 
   void _changeRecordStatus(RecordStatus status) {
     currentStatus.value = status;
+    if (status == RecordStatus.pending) {
+      return;
+    }
+    NotificationUtil.showRecordNotification(
+        _serviceId, status == RecordStatus.recording, currentDuration.value,
+        channelId: _channelId, channelName: _channelName);
   }
 
   void _setWakeLock() {
@@ -106,8 +179,8 @@ class RecordHandler {
     WidgetsBinding.instance
         .addPostFrameCallback((_) => FlutterForegroundTask.init(
               androidNotificationOptions: AndroidNotificationOptions(
-                channelId: StringName.recordNotificationChannelId.tr,
-                channelName: StringName.recordNotificationChannelName.tr,
+                channelId: _channelId,
+                channelName: _channelName,
                 channelDescription:
                     StringName.recordNotificationChannelDescription.tr,
                 channelImportance: NotificationChannelImportance.LOW,
@@ -132,10 +205,7 @@ class RecordHandler {
   }
 
   Future<void> startOrContinueRecord() async {
-    if (_record == null) {
-      return;
-    }
-    bool hasPermission = await _record!.hasPermission();
+    bool hasPermission = await _record.hasPermission();
     if (!hasPermission) {
       _onRecordPermissionDenied();
       return;
@@ -146,7 +216,7 @@ class RecordHandler {
     });
 
     File targetFile = await _getCurrentRecordFile();
-    Stream<Uint8List> recordStream = await _record!.startStream(_recordConfig);
+    Stream<Uint8List> recordStream = await _record.startStream(_recordConfig);
     _setWakeLock();
     _startForegroundService();
     if (currentStatus.value != RecordStatus.recording) {
@@ -183,29 +253,32 @@ class RecordHandler {
   }
 
   Future<void> deleteCurrentRecord() async {
-    await stopRecord();
+    await stopRecord(isStopService: true);
     File file = await _getCurrentRecordFile();
     if (file.existsSync()) {
       file.deleteSync();
     }
     KVUtil.putString(keyLastRecordId, "");
-    currentDuration.value = 0;
     _changeRecordStatus(RecordStatus.pending);
+    currentDuration.value = 0;
   }
 
-  Future<ServiceRequestResult> _startForegroundService() async {
-    if (await FlutterForegroundTask.isRunningService) {
-      return FlutterForegroundTask.restartService();
-    } else {
-      return FlutterForegroundTask.startService(
-        serviceId: 256,
-        notificationTitle: StringName.appName.tr,
-        notificationText: StringName.recordStatusRecording.tr,
-        notificationIcon: null,
-        notificationButtons: [],
-        callback: setRecordCallback,
-      );
+  Future<void> _startForegroundService() async {
+    final isRunningService = await FlutterForegroundTask.isRunningService;
+    if (isRunningService) {
+      return;
     }
+    await FlutterForegroundTask.startService(
+      serviceId: _serviceId,
+      notificationTitle: StringName.appName.tr,
+      notificationText: StringName.recordStatusRecording.tr,
+      notificationIcon: null,
+      notificationButtons: [],
+      callback: setRecordCallback,
+    );
+    // NotificationUtil.showRecordNotification(
+    //     _serviceId, true, currentDuration.value,
+    //     channelId: _channelId, channelName: _channelName);
   }
 
   /// 判断是否有未上传的录音
@@ -219,11 +292,7 @@ class RecordHandler {
     return await file.exists() && await file.length() > 0;
   }
 
-  void onClose() async {
-    if (currentStatus.value != RecordStatus.recording) {
-      _record?.dispose();
-    }
-  }
+  void onClose() async {}
 
   Future<void> getConvertWavFile(String talkId) async {
     File pcmFile = await _getCurrentRecordFile();
@@ -243,6 +312,20 @@ class RecordHandler {
     Directory documentDir = await getApplicationDocumentsDirectory();
     return File("${documentDir.path}/.atmob/record/$talkId.wav");
   }
+
+  Future<TalkBean> saveCurrentRecord() async {
+    final currentDurationValue = currentDuration.value;
+    if (currentDurationValue < minRecordDuration) {
+      throw ServerErrorException(-1, "录音时长不足$minRecordDuration秒");
+    }
+    await recordHandler.stopRecord(isStopService: true);
+    return talkRepository
+        .talkCreate(recordHandler.lastRecordId, currentDuration.value.toInt())
+        .then((talkInfo) async {
+      await recordHandler.getConvertWavFile(talkInfo.id);
+      return talkInfo;
+    });
+  }
 }
 
 final recordHandler = RecordHandler._();

+ 21 - 0
lib/utils/notification_util.dart

@@ -0,0 +1,21 @@
+import 'package:custom_notification/custom_notification.dart';
+
+class NotificationUtil {
+  NotificationUtil._();
+
+  static String formatDuration(double seconds) {
+    final duration = Duration(seconds: seconds.toInt());
+    final hours = duration.inHours.toString().padLeft(2, '0');
+    final minutes = (duration.inMinutes % 60).toString().padLeft(2, '0');
+    final secs = (duration.inSeconds % 60).toString().padLeft(2, '0');
+    return '$hours:$minutes:$secs';
+  }
+
+  static void showRecordNotification(
+      int notificationId, bool isRecording, double recordDuration,
+      {required String channelId, required String channelName}) {
+    CustomNotification.showRecordNotification(
+        notificationId, isRecording, formatDuration(recordDuration),
+        channelId: channelId, channelName: channelName);
+  }
+}

+ 29 - 0
plugin/custom_notification/.gitignore

@@ -0,0 +1,29 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+build/

+ 30 - 0
plugin/custom_notification/.metadata

@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: "80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819"
+  channel: "stable"
+
+project_type: plugin
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
+      base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
+    - platform: android
+      create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
+      base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
+
+  # User provided section
+
+  # List of Local paths (relative to this file) that should be
+  # ignored by the migrate tool.
+  #
+  # Files that are not part of the templates will be ignored by default.
+  unmanaged_files:
+    - 'lib/main.dart'
+    - 'ios/Runner.xcodeproj/project.pbxproj'

+ 3 - 0
plugin/custom_notification/CHANGELOG.md

@@ -0,0 +1,3 @@
+## 0.0.1
+
+* TODO: Describe initial release.

+ 1 - 0
plugin/custom_notification/LICENSE

@@ -0,0 +1 @@
+TODO: Add your license here.

+ 15 - 0
plugin/custom_notification/README.md

@@ -0,0 +1,15 @@
+# custom_notification
+
+自定义显示通知
+
+## Getting Started
+
+This project is a starting point for a Flutter
+[plug-in package](https://flutter.dev/to/develop-plugins),
+a specialized package that includes platform-specific implementation code for
+Android and/or iOS.
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
+

+ 4 - 0
plugin/custom_notification/analysis_options.yaml

@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options

+ 9 - 0
plugin/custom_notification/android/.gitignore

@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.cxx

+ 72 - 0
plugin/custom_notification/android/build.gradle

@@ -0,0 +1,72 @@
+group = "com.atmob.custom_notification"
+version = "1.0"
+
+buildscript {
+    repositories {
+        google()
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath("com.android.tools.build:gradle:7.3.0")
+    }
+}
+
+rootProject.allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+// 加载 local.properties 文件
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withInputStream { stream ->
+        localProperties.load(stream)
+    }
+}
+
+// 读取变量
+def flutterSdk = localProperties.getProperty('flutter.sdk')
+
+apply plugin: "com.android.library"
+
+android {
+    if (project.android.hasProperty("namespace")) {
+        namespace = "com.atmob.custom_notification"
+    }
+
+    compileSdk = 34
+
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_1_8
+        targetCompatibility = JavaVersion.VERSION_1_8
+    }
+
+    defaultConfig {
+        minSdk = 21
+    }
+
+    dependencies {
+        //flutter
+        compileOnly files("$flutterSdk/bin/cache/artifacts/engine/android-arm/flutter.jar")
+
+        //AndroidX
+        compileOnly "androidx.annotation:annotation:1.1.0"
+
+        //AndroidX
+        compileOnly "androidx.core:core:1.13.1"
+    }
+
+    testOptions {
+        unitTests.all {
+            testLogging {
+                events "passed", "skipped", "failed", "standardOut", "standardError"
+                outputs.upToDateWhen { false }
+                showStandardStreams = true
+            }
+        }
+    }
+}

BIN
plugin/custom_notification/android/gradle/wrapper/gradle-wrapper.jar


+ 7 - 0
plugin/custom_notification/android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 249 - 0
plugin/custom_notification/android/gradlew

@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"

+ 92 - 0
plugin/custom_notification/android/gradlew.bat

@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 1 - 0
plugin/custom_notification/android/settings.gradle

@@ -0,0 +1 @@
+rootProject.name = 'custom_notification'

+ 7 - 0
plugin/custom_notification/android/src/main/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.atmob.custom_notification">
+
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
+
+</manifest>

+ 80 - 0
plugin/custom_notification/android/src/main/java/com/atmob/custom_notification/CustomNotificationPlugin.java

@@ -0,0 +1,80 @@
+package com.atmob.custom_notification;
+
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.embedding.engine.plugins.activity.ActivityAware;
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
+import io.flutter.plugin.common.MethodChannel.Result;
+import io.flutter.plugin.common.PluginRegistry;
+
+/**
+ * CustomNotificationPlugin
+ */
+public class CustomNotificationPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.NewIntentListener {
+    /// The MethodChannel that will the communication between Flutter and native Android
+    ///
+    /// This local reference serves to register the plugin with the Flutter Engine and unregister it
+    /// when the Flutter Engine is detached from the Activity
+
+    private static final String TAG = "NotificationPlugin";
+
+    private MethodChannel channel;
+    private Context applicationContext;
+
+    @Override
+    public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
+        applicationContext = flutterPluginBinding.getApplicationContext();
+        channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "custom_notification");
+        channel.setMethodCallHandler(this);
+    }
+
+    @Override
+    public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
+        MethodCallDistribute.handleMethodCall(applicationContext, call, result);
+    }
+
+    @Override
+    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
+        channel.setMethodCallHandler(null);
+        applicationContext = null;
+    }
+
+
+    @Override
+    public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
+        binding.addOnNewIntentListener(this);
+    }
+
+    @Override
+    public void onDetachedFromActivityForConfigChanges() {
+
+    }
+
+    @Override
+    public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding activityPluginBinding) {
+
+    }
+
+    @Override
+    public void onDetachedFromActivity() {
+
+    }
+
+
+    @Override
+    public boolean onNewIntent(@NonNull Intent intent) {
+        String action = NotificationUtil.extraAction(intent);
+//        Log.d(TAG, "onNewIntent: " + action + " " + channel);
+        if (action != null) {
+            if (channel != null) channel.invokeMethod("recordAction", action);
+            return true;
+        }
+        return false;
+    }
+}

+ 34 - 0
plugin/custom_notification/android/src/main/java/com/atmob/custom_notification/MethodCallDistribute.java

@@ -0,0 +1,34 @@
+package com.atmob.custom_notification;
+
+import android.content.Context;
+import android.util.Log;
+
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+
+public class MethodCallDistribute {
+    public static void handleMethodCall(Context context,MethodCall call, MethodChannel.Result result) {
+        switch (call.method){
+            case "showRecordNotification":
+                showRecordNotification(context,call,result);
+                break;
+        }
+    }
+
+    private static void showRecordNotification(Context context, MethodCall call, MethodChannel.Result result){
+        try {
+            Boolean isRecording = call.argument("isRecording");
+            Integer notificationId =  call.argument("notificationId");
+            String timeDesc =  call.argument("timeDesc");
+            String channelId =  call.argument("channelId");
+            String channelName =  call.argument("channelName");
+            if (isRecording == null || notificationId == null || timeDesc == null || channelId == null || channelName == null) {
+                result.error("-1", "Arguments are invalid", null);
+            }
+//            Log.d("showRecordNotification", "isRecording: " + isRecording + " notificationId: " + notificationId + " timeDesc: " + timeDesc + " channelId: " + channelId + " channelName: " + channelName);
+            NotificationUtil.showRecordNotification(context,notificationId,channelId,channelName,timeDesc,isRecording);
+        } catch (Exception e) {
+            result.error("-1", e.getMessage(), null);
+        }
+    }
+}

+ 140 - 0
plugin/custom_notification/android/src/main/java/com/atmob/custom_notification/NotificationUtil.java

@@ -0,0 +1,140 @@
+package com.atmob.custom_notification;
+
+import android.Manifest;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Build;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.core.graphics.drawable.IconCompat;
+
+
+import java.io.IOException;
+import java.util.Objects;
+
+public class NotificationUtil {
+
+    private static final String TAG = "NotificationUtil";
+
+    private static final String RECORD_DONE = "done";
+    private static final String RECORD_RECORDING = "recording";
+    private static final String RECORD_PAUSE = "pause";
+
+    private static int NOTIFICATION_INTENT_REQUEST_CODE = 7777;
+    public static final String KEY_RECORD_OPERATION = "key_record_operation";
+
+    private static boolean isRecording;
+    private static String timeDesc;
+
+    private NotificationUtil() {
+
+    }
+
+    private static int getRequestCode() {
+        return NOTIFICATION_INTENT_REQUEST_CODE++;
+    }
+
+    public static void showRecordNotification(@NonNull Context context, int notificationId, @NonNull String channelId, String channelName, String timeDesc, Boolean isRecording) {
+        if(NotificationUtil.isRecording == isRecording && Objects.equals(NotificationUtil.timeDesc, timeDesc)) {
+            return;
+        }
+        NotificationUtil.isRecording = isRecording;
+        NotificationUtil.timeDesc = timeDesc;
+//        Log.d(TAG, "showRecordNotification: isRecording: " + isRecording + " notificationId: " + notificationId + " timeDesc: " + timeDesc + " channelId: " + channelId + " channelName: " + channelName);
+        RemoteViews contentView = new RemoteViews(context.getPackageName(), R.layout.custom_notification_layout);
+        contentView.setImageViewResource(R.id.icon_notification_operation, isRecording ? R.drawable.icon_record_playing : R.drawable.icon_record_pause);
+        contentView.setTextViewText(R.id.tv_title, isRecording ? "电子秘书 · 正在录音中" : "电子秘书 · 已暂停");
+        contentView.setTextViewText(R.id.tv_record_time_desc, timeDesc);
+
+
+        contentView.setOnClickPendingIntent(R.id.icon_notification_done, getActionIntent(context, RECORD_DONE));
+        contentView.setOnClickPendingIntent(R.id.icon_notification_operation, getActionIntent(context, isRecording ? RECORD_PAUSE : RECORD_RECORDING));
+
+        showNotification(context, contentView, notificationId, channelId, channelName, 3, true);
+    }
+
+    private static PendingIntent getActionIntent(Context context, String action) {
+        Intent intent = getIntentToOpenMainActivity(context, action);
+        return PendingIntent.getActivity(context, getRequestCode(),
+                intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+    }
+
+    private static Intent getIntentToOpenMainActivity(Context context, String action) {
+        final String packageName = context.getPackageName();
+        return context
+                .getPackageManager()
+                .getLaunchIntentForPackage(packageName)
+                .setAction(Intent.ACTION_VIEW)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                .putExtra(KEY_RECORD_OPERATION, action);
+    }
+
+
+    private static void showNotification(@NonNull Context context, RemoteViews contentView, int notificationId, @NonNull String channelId, String channelName, int importance, boolean ongoing) {
+
+        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
+
+        if (!notificationManager.areNotificationsEnabled()) {
+            return;
+        }
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            NotificationChannel notificationChannel = new NotificationChannel(channelId,
+                    channelName, importance);
+            notificationManager.createNotificationChannel(notificationChannel);
+        }
+
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);
+        builder
+                .setSmallIcon(R.drawable.logo)
+                .setOngoing(ongoing)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setCustomContentView(contentView)
+                .setCustomBigContentView(contentView)
+                .setPriority(Notification.PRIORITY_HIGH);
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE);
+        }
+        Notification notification = builder.build();
+
+        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
+            notificationManager.notify(notificationId, notification);
+        }
+    }
+
+
+    public static IconCompat getIconFromAsset(Context context, String imagePath) {
+        if (imagePath != null) {
+            AssetManager assetManager = context.getAssets();
+            try (AssetFileDescriptor fd = assetManager.openFd(imagePath)) {
+                Bitmap image = BitmapFactory.decodeStream(fd.createInputStream());
+                return IconCompat.createWithBitmap(image);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+    public static String extraAction(Intent intent) {
+        String action;
+        if (intent != null && intent.getExtras() != null && (action = intent.getStringExtra(KEY_RECORD_OPERATION)) != null) {
+            return action;
+        }
+        return null;
+    }
+}

BIN
plugin/custom_notification/android/src/main/res/drawable-xxhdpi/icon_notification_logo.webp


BIN
plugin/custom_notification/android/src/main/res/drawable-xxhdpi/icon_record_done.webp


BIN
plugin/custom_notification/android/src/main/res/drawable-xxhdpi/icon_record_pause.webp


BIN
plugin/custom_notification/android/src/main/res/drawable-xxhdpi/icon_record_playing.webp


BIN
plugin/custom_notification/android/src/main/res/drawable-xxhdpi/logo.png


+ 61 - 0
plugin/custom_notification/android/src/main/res/layout/custom_notification_layout.xml

@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    android:orientation="horizontal"
+    android:paddingVertical="13dp">
+
+    <ImageView
+        android:id="@+id/ic_logo"
+        android:layout_width="22dp"
+        android:layout_height="22dp"
+        android:layout_marginStart="14dp"
+        android:src="@drawable/icon_notification_logo" />
+
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="11dp"
+        android:layout_marginEnd="30dp">
+
+        <TextView
+            android:id="@+id/tv_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="#5F5F61"
+            android:textSize="12sp"
+            tools:text="电子秘书 · 正在录音中" />
+
+        <TextView
+            android:id="@+id/tv_record_time_desc"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@+id/tv_title"
+            android:text="00:00"
+            android:textColor="#25262A"
+            android:textSize="20sp"
+            android:textStyle="bold" />
+
+        <ImageView
+            android:id="@+id/icon_notification_done"
+            android:layout_width="28dp"
+            android:layout_height="28dp"
+            android:layout_alignParentEnd="true"
+            android:layout_centerVertical="true"
+            android:src="@drawable/icon_record_done" />
+
+        <ImageView
+            android:id="@+id/icon_notification_operation"
+            android:layout_width="28dp"
+            android:layout_height="28dp"
+            android:layout_centerVertical="true"
+            android:layout_marginEnd="14dp"
+            android:layout_toStartOf="@+id/icon_notification_done"
+            tools:src="@drawable/icon_record_playing" />
+
+    </RelativeLayout>
+
+</LinearLayout>

+ 29 - 0
plugin/custom_notification/android/src/test/java/com/atmob/custom_notification/CustomNotificationPluginTest.java

@@ -0,0 +1,29 @@
+package com.atmob.custom_notification;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import org.junit.Test;
+
+/**
+ * This demonstrates a simple unit test of the Java portion of this plugin's implementation.
+ *
+ * Once you have built the plugin's example app, you can run these tests from the command
+ * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
+ * you can run them directly from IDEs that support JUnit such as Android Studio.
+ */
+
+public class CustomNotificationPluginTest {
+  @Test
+  public void onMethodCall_getPlatformVersion_returnsExpectedValue() {
+    CustomNotificationPlugin plugin = new CustomNotificationPlugin();
+
+    final MethodCall call = new MethodCall("getPlatformVersion", null);
+    MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+    plugin.onMethodCall(call, mockResult);
+
+    verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE);
+  }
+}

+ 17 - 0
plugin/custom_notification/lib/custom_notification.dart

@@ -0,0 +1,17 @@
+import 'custom_notification_platform_interface.dart';
+
+class CustomNotification {
+  CustomNotification._();
+
+  static Future<void> showRecordNotification(
+      int notificationId, bool isRecording, String timeDesc,
+      {required String channelId, required String channelName}) {
+    return CustomNotificationPlatform.instance.showRecordNotification(
+        notificationId, isRecording, timeDesc,
+        channelId: channelId, channelName: channelName);
+  }
+
+  static Stream<String> recordActionStream() {
+    return CustomNotificationPlatform.instance.recordActionStream();
+  }
+}

+ 54 - 0
plugin/custom_notification/lib/custom_notification_method_channel.dart

@@ -0,0 +1,54 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+
+import 'custom_notification_platform_interface.dart';
+
+/// An implementation of [CustomNotificationPlatform] that uses method channels.
+class MethodChannelCustomNotification extends CustomNotificationPlatform {
+  /// The method channel used to interact with the native platform.
+  @visibleForTesting
+  final methodChannel = const MethodChannel('custom_notification');
+
+  MethodChannelCustomNotification() {
+    methodChannel.setMethodCallHandler(_handleMethod);
+  }
+
+  final StreamController<String> _recordActionStreamController =
+      StreamController<String>.broadcast();
+
+  Future<dynamic> _handleMethod(MethodCall call) async {
+    debugPrint(
+        'MethodChannelCustomNotification  _handleMethod  ${call.method}');
+    switch (call.method) {
+      case "recordAction":
+        _recordAction(call.arguments);
+        break;
+    }
+  }
+
+  void _recordAction(dynamic action) {
+    if (action is String) {
+      _recordActionStreamController.add(action);
+    }
+  }
+
+  @override
+  Stream<String> recordActionStream() {
+    return _recordActionStreamController.stream;
+  }
+
+  @override
+  Future<void> showRecordNotification(
+      int notificationId, bool isRecording, String timeDesc,
+      {required String channelId, required String channelName}) async {
+    return await methodChannel.invokeMethod("showRecordNotification", {
+      "notificationId": notificationId,
+      "isRecording": isRecording,
+      "timeDesc": timeDesc,
+      "channelId": channelId,
+      "channelName": channelName,
+    });
+  }
+}

+ 37 - 0
plugin/custom_notification/lib/custom_notification_platform_interface.dart

@@ -0,0 +1,37 @@
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+import 'custom_notification_method_channel.dart';
+
+abstract class CustomNotificationPlatform extends PlatformInterface {
+  /// Constructs a CustomNotificationPlatform.
+  CustomNotificationPlatform() : super(token: _token);
+
+  static final Object _token = Object();
+
+  static CustomNotificationPlatform _instance =
+      MethodChannelCustomNotification();
+
+  /// The default instance of [CustomNotificationPlatform] to use.
+  ///
+  /// Defaults to [MethodChannelCustomNotification].
+  static CustomNotificationPlatform get instance => _instance;
+
+  /// Platform-specific implementations should set this with their own
+  /// platform-specific class that extends [CustomNotificationPlatform] when
+  /// they register themselves.
+  static set instance(CustomNotificationPlatform instance) {
+    PlatformInterface.verifyToken(instance, _token);
+    _instance = instance;
+  }
+
+  Future<void> showRecordNotification(
+      int notificationId, bool isRecording, String timeDesc,
+      {required String channelId, required String channelName}) {
+    throw UnimplementedError(
+        'showRecordNotification() has not been implemented.');
+  }
+
+  Stream<String> recordActionStream() {
+    throw UnimplementedError('recordActionStream() has not been implemented.');
+  }
+}

+ 70 - 0
plugin/custom_notification/pubspec.yaml

@@ -0,0 +1,70 @@
+name: custom_notification
+description: "自定义显示通知"
+version: 0.0.1
+homepage:
+
+environment:
+  sdk: ^3.5.0
+  flutter: '>=3.3.0'
+
+dependencies:
+  flutter:
+    sdk: flutter
+  plugin_platform_interface: ^2.0.2
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  flutter_lints: ^4.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+  # This section identifies this Flutter project as a plugin project.
+  # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
+  # which should be registered in the plugin registry. This is required for
+  # using method channels.
+  # The Android 'package' specifies package in which the registered class is.
+  # This is required for using method channels on Android.
+  # The 'ffiPlugin' specifies that native code should be built and bundled.
+  # This is required for using `dart:ffi`.
+  # All these are used by the tooling to maintain consistency when
+  # adding or updating assets for this project.
+  plugin:
+    platforms:
+      android:
+        package: com.atmob.custom_notification
+        pluginClass: CustomNotificationPlugin
+
+  # To add assets to your plugin package, add an assets section, like this:
+  # assets:
+  #   - images/a_dot_burr.jpeg
+  #   - images/a_dot_ham.jpeg
+  #
+  # For details regarding assets in packages, see
+  # https://flutter.dev/to/asset-from-package
+  #
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/to/resolution-aware-images
+
+  # To add custom fonts to your plugin package, add a fonts section here,
+  # in this "flutter" section. Each entry in this list should have a
+  # "family" key with the font family name, and a "fonts" key with a
+  # list giving the asset and other descriptors for the font. For
+  # example:
+  # fonts:
+  #   - family: Schyler
+  #     fonts:
+  #       - asset: fonts/Schyler-Regular.ttf
+  #       - asset: fonts/Schyler-Italic.ttf
+  #         style: italic
+  #   - family: Trajan Pro
+  #     fonts:
+  #       - asset: fonts/TrajanPro.ttf
+  #       - asset: fonts/TrajanPro_Bold.ttf
+  #         weight: 700
+  #
+  # For details regarding fonts in packages, see
+  # https://flutter.dev/to/font-from-package

+ 23 - 16
pubspec.lock

@@ -341,6 +341,13 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.8"
+  custom_notification:
+    dependency: "direct main"
+    description:
+      path: "plugin/custom_notification"
+      relative: true
+    source: path
+    version: "0.0.1"
   dart_style:
     dependency: transitive
     description:
@@ -425,10 +432,10 @@ packages:
     dependency: "direct main"
     description:
       name: file_picker
-      sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12"
+      sha256: aac85f20436608e01a6ffd1fdd4e746a7f33c93a2c83752e626bdfaea139b877
       url: "https://pub.dev"
     source: hosted
-    version: "8.1.2"
+    version: "8.1.3"
   fixnum:
     dependency: transitive
     description:
@@ -734,10 +741,10 @@ packages:
     dependency: transitive
     description:
       name: in_app_purchase_storekit
-      sha256: "667e1ef92fcd9333103012b309b3493d07b478d48b1c7b6786355ebd1f9e5ff5"
+      sha256: "96cfa71121f4294079586a2fab7363b01bdb4c3381d9b397012c2580b391281d"
       url: "https://pub.dev"
     source: hosted
-    version: "0.3.18+2"
+    version: "0.3.18+3"
   io:
     dependency: transitive
     description:
@@ -902,34 +909,34 @@ packages:
     dependency: transitive
     description:
       name: mmkv_android
-      sha256: d54fed971a8918c6084012bb99be4262fdf0081a4bcbc9b44362598b01087a0b
+      sha256: ab0921f7d766d85c031b584e48ec89f94019f34b99786dd755f2430ce1a4fdc2
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.5"
+    version: "1.0.6"
   mmkv_ios:
     dependency: transitive
     description:
       name: mmkv_ios
-      sha256: "26cba4ca68cfe9c1480481518865c973110b3331698c0ccbce1bc6ba1eb08e02"
+      sha256: f28a4f80703d294365d41367502ff43d895e8aa23d4cf882692dd0096c146e32
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.5"
+    version: "1.0.7"
   mmkv_ohos:
     dependency: transitive
     description:
       name: mmkv_ohos
-      sha256: "33854328ffb2daf6b286eb8e08fc2d93fcb7efba46aa30ecd5906f6cc4ef79f3"
+      sha256: b6c82843fbec967b2dacc985fbdd407df375251f1941a34541201ad38761b26f
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.1"
+    version: "1.0.2"
   mmkv_platform_interface:
     dependency: transitive
     description:
       name: mmkv_platform_interface
-      sha256: "86d862638e75e6a1696066798b2672339e22bd42077d7dc18a7f1aa50c545446"
+      sha256: b5e060f1cfe590b9b22960e6580861e6f1e0798ccc8c5a5891358e3dea814eed
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.1"
+    version: "1.0.2"
   nested:
     dependency: transitive
     description:
@@ -1054,10 +1061,10 @@ packages:
     dependency: "direct main"
     description:
       name: photo_manager
-      sha256: "70159eee32203e8162d49d588232f0299ed3f383c63eef1e899cb6b83dee6b26"
+      sha256: "4e156239896c619c9f092ba6d200e4292b1d0084e3ca38c559818053ee483e84"
       url: "https://pub.dev"
     source: hosted
-    version: "3.5.1"
+    version: "3.5.2"
   platform:
     dependency: transitive
     description:
@@ -1713,10 +1720,10 @@ packages:
     dependency: transitive
     description:
       name: win32
-      sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd
+      sha256: e1d0cc62e65dc2561f5071fcbccecf58ff20c344f8f3dc7d4922df372a11df1f
       url: "https://pub.dev"
     source: hosted
-    version: "5.6.0"
+    version: "5.7.1"
   win32_registry:
     dependency: transitive
     description:

+ 4 - 0
pubspec.yaml

@@ -135,6 +135,10 @@ dependencies:
   flutter_umeng:
     path: plugin/flutter_umeng
 
+  #自定义通知
+  custom_notification:
+    path: plugin/custom_notification
+
 dev_dependencies:
   flutter_test:
     sdk: flutter