From f2e03791b38f0ced46ec9dd65a9e4180fe15e3fc Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 10 Jan 2026 10:09:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(android):=20=E6=B7=BB=E5=8A=A0Android?= =?UTF-8?q?=E5=BD=95=E9=9F=B3=E6=82=AC=E6=B5=AE=E7=AA=97=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现Android平台的录音悬浮窗服务,支持显示录音时长、类型和暂停状态 添加悬浮窗操作事件通道,支持暂停/继续、打开会议记录和关闭悬浮窗功能 在会议记录控制器中集成悬浮窗逻辑 --- .../bindings/meeting_record_binding.dart | 6 +- .../meeting_record_controller.dart | 127 ++++++++ .../android/src/main/AndroidManifest.xml | 8 + .../example/native_plugin/NativePlugin.java | 88 +++++- .../RecordingFloatingService.java | 292 ++++++++++++++++++ .../native_plugin/lib/native_plugin.dart | 31 ++ .../lib/native_plugin_method_channel.dart | 43 +++ .../lib/native_plugin_platform_interface.dart | 28 ++ 8 files changed, 620 insertions(+), 3 deletions(-) create mode 100644 local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/RecordingFloatingService.java diff --git a/lib/modules/meeting/bindings/meeting_record_binding.dart b/lib/modules/meeting/bindings/meeting_record_binding.dart index e6f60e6ca..1a7a13aed 100644 --- a/lib/modules/meeting/bindings/meeting_record_binding.dart +++ b/lib/modules/meeting/bindings/meeting_record_binding.dart @@ -5,7 +5,9 @@ import '../controllers/meeting_record_controller.dart'; class MeetingRecordBinding extends Bindings { @override void dependencies() { - Get.put(MeetingRecordController(), - permanent: true); + if (!Get.isRegistered()) { + Get.put(MeetingRecordController(), + permanent: true); + } } } diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 70a998faf..635d39d41 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -4,14 +4,17 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import '/core/utils/logger.dart'; +import '/core/utils/overlay_permission_util.dart'; import '/core/utils/permission_util.dart'; import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; +import 'package:native_plugin/native_plugin.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import '../../../routes/app_routes.dart'; import '../../../data/services/asr_service.dart'; import '../../../data/services/ble_manager.dart'; @@ -29,6 +32,10 @@ class MeetingRecordController extends GetxController static const int _backgroundRecordingNotificationId = 91001; static FlutterLocalNotificationsPlugin? _localNotificationsPlugin; + final NativePlugin _nativePlugin = NativePlugin(); + Timer? _recordingFloatingBarTimer; + StreamSubscription? _recordingFloatingActionSubscription; + bool _floatingBarManuallyClosed = false; Future _ensureLocalNotificationsPlugin() async { @@ -60,6 +67,12 @@ class MeetingRecordController extends GetxController } Future showBackgroundRecordingIndicator() async { + if (Platform.isAndroid) { + if (_floatingBarManuallyClosed) return; + await _startAndroidRecordingFloatingBar(); + return; + } + await _requestNotificationPermissionIfNeeded(); if (!Platform.isIOS) return; @@ -81,11 +94,121 @@ class MeetingRecordController extends GetxController } Future clearBackgroundRecordingIndicator() async { + if (Platform.isAndroid) { + _floatingBarManuallyClosed = false; + await _stopAndroidRecordingFloatingBar(); + return; + } if (!Platform.isIOS) return; final plugin = await _ensureLocalNotificationsPlugin(); await plugin.cancel(_backgroundRecordingNotificationId); } + String _formatTimeForFloatingBar(int ms) { + final totalSeconds = ms ~/ 1000; + final hours = totalSeconds ~/ 3600; + final minutes = (totalSeconds % 3600) ~/ 60; + final secs = totalSeconds % 60; + if (hours > 0) { + return '$hours:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; + } + return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; + } + + String _getRecordingTypeText() { + switch (audioType.value) { + case 0: + return 'liveRecording'.tr; + case 1: + return 'audioVideoRecording'.tr; + case 2: + return 'callRecording'.tr; + default: + return 'liveRecording'.tr; + } + } + + Future _startAndroidRecordingFloatingBar() async { + if (!Platform.isAndroid) return; + if (!isRecording.value) return; + if (_floatingBarManuallyClosed) return; + + final hasPermission = await OverlayPermissionUtil.hasOverlayPermission(); + if (!hasPermission) { + final granted = await OverlayPermissionUtil.requestOverlayPermission(); + if (!granted) return; + } + + final typeText = _getRecordingTypeText(); + final durationText = _formatTimeForFloatingBar(milliseconds.value); + + await _nativePlugin.showRecordingFloatingBar( + duration: durationText, + recordingType: typeText, + isPaused: timerStatus.value == TimerStatus.paused, + ); + + _recordingFloatingBarTimer?.cancel(); + _recordingFloatingBarTimer = + Timer.periodic(const Duration(seconds: 1), (_) { + if (!isRecording.value) { + _stopAndroidRecordingFloatingBar(); + return; + } + _nativePlugin.updateRecordingFloatingBar( + duration: _formatTimeForFloatingBar(milliseconds.value), + recordingType: _getRecordingTypeText(), + isPaused: timerStatus.value == TimerStatus.paused, + ); + }); + } + + Future _stopAndroidRecordingFloatingBar() async { + if (!Platform.isAndroid) return; + _recordingFloatingBarTimer?.cancel(); + _recordingFloatingBarTimer = null; + await _nativePlugin.hideRecordingFloatingBar(); + } + + Future togglePauseResumeFromFloatingBar() async { + if (!isRecording.value) return; + if (timerStatus.value == TimerStatus.running) { + await _pauseRecording(); + return; + } + if (timerStatus.value == TimerStatus.paused) { + await _resumeRecording(); + } + } + + void _setupRecordingFloatingBarActionListener() { + _recordingFloatingActionSubscription?.cancel(); + if (!Platform.isAndroid) return; + _recordingFloatingActionSubscription = + _nativePlugin.recordingFloatingActionStream.listen((action) async { + if (action.isEmpty) return; + switch (action) { + case 'togglePause': + await togglePauseResumeFromFloatingBar(); + return; + case 'openMeetingRecord': + _floatingBarManuallyClosed = false; + if (Get.currentRoute == Routes.meetingRecord) return; + Get.toNamed( + Routes.meetingRecord, + arguments: {'audioTypes': audioType.value}, + ); + return; + case 'closeFloatingBar': + _floatingBarManuallyClosed = true; + await _stopAndroidRecordingFloatingBar(); + return; + default: + return; + } + }); + } + // 服务 final AsrService _asrService = Get.find(); final BleManager _bleManager = Get.find(); @@ -136,6 +259,7 @@ class MeetingRecordController extends GetxController super.onInit(); _initializeComponents(); _setupFromArguments(); + _setupRecordingFloatingBarActionListener(); //generateWaveData(); } @@ -206,6 +330,9 @@ class MeetingRecordController extends GetxController /// 控制器销毁时清理资源 void _cleanupResources() { _timer?.cancel(); + _stopAndroidRecordingFloatingBar(); + _recordingFloatingActionSubscription?.cancel(); + _recordingFloatingActionSubscription = null; _stopWaveformTimer(); _recognitionSubscription?.cancel(); _recognitionSubscription = null; diff --git a/local_plugins/native_plugin/android/src/main/AndroidManifest.xml b/local_plugins/native_plugin/android/src/main/AndroidManifest.xml index 5153b5dc6..1acdbb46b 100644 --- a/local_plugins/native_plugin/android/src/main/AndroidManifest.xml +++ b/local_plugins/native_plugin/android/src/main/AndroidManifest.xml @@ -1,3 +1,11 @@ + + + + + + diff --git a/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java index 265796e9d..d6ebf3a68 100644 --- a/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java +++ b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java @@ -2,37 +2,123 @@ package com.example.native_plugin; import androidx.annotation.NonNull; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; + import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.EventChannel; 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; /** NativePlugin */ -public class NativePlugin implements FlutterPlugin, MethodCallHandler { +public class NativePlugin implements FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler { /// 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 MethodChannel channel; + private EventChannel recordingFloatingActionsChannel; + private Context applicationContext; + + private static final Handler mainHandler = new Handler(Looper.getMainLooper()); + private static volatile EventChannel.EventSink recordingFloatingActionSink; @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + applicationContext = flutterPluginBinding.getApplicationContext(); channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "native_plugin"); channel.setMethodCallHandler(this); + + recordingFloatingActionsChannel = + new EventChannel(flutterPluginBinding.getBinaryMessenger(), "native_plugin/recording_floating_actions"); + recordingFloatingActionsChannel.setStreamHandler(this); } @Override public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { if (call.method.equals("getPlatformVersion")) { result.success("Android " + android.os.Build.VERSION.RELEASE); + } else if (call.method.equals("showRecordingFloatingBar")) { + String duration = call.argument("duration"); + String recordingType = call.argument("recordingType"); + Boolean isPaused = call.argument("isPaused"); + startRecordingFloatingService( + RecordingFloatingService.ACTION_SHOW, + duration, + recordingType, + isPaused != null && isPaused + ); + result.success(true); + } else if (call.method.equals("updateRecordingFloatingBar")) { + String duration = call.argument("duration"); + String recordingType = call.argument("recordingType"); + Boolean isPaused = call.argument("isPaused"); + startRecordingFloatingService( + RecordingFloatingService.ACTION_UPDATE, + duration, + recordingType, + isPaused != null && isPaused + ); + result.success(true); + } else if (call.method.equals("hideRecordingFloatingBar")) { + startRecordingFloatingService(RecordingFloatingService.ACTION_HIDE, null, null, false); + result.success(true); } else { result.notImplemented(); } } + private void startRecordingFloatingService( + String action, + String duration, + String recordingType, + boolean isPaused + ) { + if (applicationContext == null) return; + Intent intent = new Intent(applicationContext, RecordingFloatingService.class); + intent.setAction(action); + if (duration != null) { + intent.putExtra(RecordingFloatingService.EXTRA_DURATION, duration); + } + if (recordingType != null) { + intent.putExtra(RecordingFloatingService.EXTRA_RECORDING_TYPE, recordingType); + } + intent.putExtra(RecordingFloatingService.EXTRA_IS_PAUSED, isPaused); + applicationContext.startService(intent); + } + + static void emitRecordingFloatingAction(String action) { + final EventChannel.EventSink sink = recordingFloatingActionSink; + if (sink == null) return; + mainHandler.post(() -> { + try { + sink.success(action); + } catch (Exception ignored) { + } + }); + } + + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + recordingFloatingActionSink = events; + } + + @Override + public void onCancel(Object arguments) { + recordingFloatingActionSink = null; + } + @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { channel.setMethodCallHandler(null); + if (recordingFloatingActionsChannel != null) { + recordingFloatingActionsChannel.setStreamHandler(null); + } + recordingFloatingActionsChannel = null; + applicationContext = null; } } diff --git a/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/RecordingFloatingService.java b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/RecordingFloatingService.java new file mode 100644 index 000000000..435786212 --- /dev/null +++ b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/RecordingFloatingService.java @@ -0,0 +1,292 @@ +package com.example.native_plugin; + +import android.app.Service; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.drawable.GradientDrawable; +import android.os.Build; +import android.os.IBinder; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowManager; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +public class RecordingFloatingService extends Service { + static final String ACTION_SHOW = "com.example.native_plugin.action.SHOW_RECORDING_FLOATING_BAR"; + static final String ACTION_UPDATE = "com.example.native_plugin.action.UPDATE_RECORDING_FLOATING_BAR"; + static final String ACTION_HIDE = "com.example.native_plugin.action.HIDE_RECORDING_FLOATING_BAR"; + + static final String EXTRA_DURATION = "duration"; + static final String EXTRA_RECORDING_TYPE = "recordingType"; + static final String EXTRA_IS_PAUSED = "isPaused"; + + private WindowManager windowManager; + private View floatingView; + private WindowManager.LayoutParams layoutParams; + private TextView durationTextView; + private TextView typeTextView; + private TextView playPauseTextView; + private boolean isPaused = false; + + @Override + public void onCreate() { + super.onCreate(); + windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent == null) { + return START_NOT_STICKY; + } + + String action = intent.getAction(); + if (ACTION_HIDE.equals(action)) { + removeFloatingView(); + stopSelf(); + return START_NOT_STICKY; + } + + if (ACTION_SHOW.equals(action) || ACTION_UPDATE.equals(action)) { + String duration = intent.getStringExtra(EXTRA_DURATION); + String recordingType = intent.getStringExtra(EXTRA_RECORDING_TYPE); + boolean paused = intent.getBooleanExtra(EXTRA_IS_PAUSED, false); + showOrUpdate(duration, recordingType, paused); + } + + return START_STICKY; + } + + private void showOrUpdate(String duration, String recordingType, boolean paused) { + if (floatingView == null) { + createFloatingView(); + addFloatingView(); + } + updateContent(duration, recordingType, paused); + } + + private void updateContent(String duration, String recordingType, boolean paused) { + isPaused = paused; + updatePlayPauseIcon(); + if (durationTextView != null) { + durationTextView.setText(duration == null ? "" : duration); + } + if (typeTextView != null) { + String type = recordingType == null ? "" : recordingType.trim(); + if (type.isEmpty()) { + typeTextView.setVisibility(View.GONE); + } else { + typeTextView.setVisibility(View.VISIBLE); + typeTextView.setText(type); + } + } + } + + private void updatePlayPauseIcon() { + if (playPauseTextView == null) return; + playPauseTextView.setText(isPaused ? "▶" : "⏸"); + } + + private void createFloatingView() { + float density = getResources().getDisplayMetrics().density; + int touchSlop = ViewConfiguration.get(this).getScaledTouchSlop(); + + LinearLayout container = new LinearLayout(this); + container.setOrientation(LinearLayout.HORIZONTAL); + container.setGravity(Gravity.CENTER_VERTICAL); + int paddingH = (int) (10 * density); + int paddingV = (int) (6 * density); + container.setPadding(paddingH, paddingV, paddingH, paddingV); + + GradientDrawable bg = new GradientDrawable(); + bg.setColor(Color.parseColor("#1A1A1A")); + bg.setCornerRadius(999 * density); + container.setBackground(bg); + + playPauseTextView = new TextView(this); + int playSize = (int) (26 * density); + LinearLayout.LayoutParams playLp = new LinearLayout.LayoutParams(playSize, playSize); + playLp.rightMargin = (int) (10 * density); + playPauseTextView.setLayoutParams(playLp); + playPauseTextView.setTextColor(Color.WHITE); + playPauseTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + playPauseTextView.setGravity(Gravity.CENTER); + GradientDrawable playBg = new GradientDrawable(); + playBg.setColor(Color.parseColor("#FF3B30")); + playBg.setCornerRadius(999 * density); + playPauseTextView.setBackground(playBg); + playPauseTextView.setOnClickListener(v -> { + isPaused = !isPaused; + updatePlayPauseIcon(); + NativePlugin.emitRecordingFloatingAction("togglePause"); + }); + updatePlayPauseIcon(); + container.addView(playPauseTextView); + + LinearLayout middleArea = new LinearLayout(this); + middleArea.setOrientation(LinearLayout.HORIZONTAL); + middleArea.setGravity(Gravity.CENTER_VERTICAL); + LinearLayout.LayoutParams middleLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + middleLp.rightMargin = (int) (10 * density); + middleArea.setLayoutParams(middleLp); + middleArea.setClickable(true); + middleArea.setFocusable(true); + + durationTextView = new TextView(this); + durationTextView.setTextColor(Color.WHITE); + durationTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + LinearLayout.LayoutParams durationLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + durationLp.rightMargin = (int) (8 * density); + durationTextView.setLayoutParams(durationLp); + durationTextView.setText("00:00"); + middleArea.addView(durationTextView); + + typeTextView = new TextView(this); + typeTextView.setTextColor(Color.parseColor("#E6E6E6")); + typeTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + LinearLayout.LayoutParams typeLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + typeTextView.setLayoutParams(typeLp); + typeTextView.setVisibility(View.GONE); + middleArea.addView(typeTextView); + + middleArea.setOnClickListener(v -> { + bringAppToFront(); + NativePlugin.emitRecordingFloatingAction("openMeetingRecord"); + }); + + middleArea.setOnTouchListener(new View.OnTouchListener() { + private int initialY; + private float initialTouchY; + private boolean dragging; + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (layoutParams == null || windowManager == null) return false; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + initialY = layoutParams.y; + initialTouchY = event.getRawY(); + dragging = false; + return true; + case MotionEvent.ACTION_MOVE: + int dy = (int) (event.getRawY() - initialTouchY); + if (!dragging && Math.abs(dy) > touchSlop) { + dragging = true; + } + if (!dragging) return true; + + int newY = initialY + dy; + int screenHeight = getResources().getDisplayMetrics().heightPixels; + int viewHeight = floatingView == null ? 0 : floatingView.getHeight(); + int maxY = Math.max(0, screenHeight - viewHeight - (int) (24 * density)); + if (newY < 0) newY = 0; + if (newY > maxY) newY = maxY; + layoutParams.y = newY; + windowManager.updateViewLayout(floatingView, layoutParams); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (!dragging) { + v.performClick(); + } + return true; + default: + return false; + } + } + }); + + container.addView(middleArea); + + TextView close = new TextView(this); + int closeSize = (int) (22 * density); + LinearLayout.LayoutParams closeLp = new LinearLayout.LayoutParams(closeSize, closeSize); + close.setLayoutParams(closeLp); + close.setText("×"); + close.setTextColor(Color.WHITE); + close.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + close.setGravity(Gravity.CENTER); + close.setOnClickListener(v -> { + removeFloatingView(); + stopSelf(); + NativePlugin.emitRecordingFloatingAction("closeFloatingBar"); + }); + container.addView(close); + + floatingView = container; + } + + private void addFloatingView() { + if (windowManager == null || floatingView == null) return; + layoutParams = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + : WindowManager.LayoutParams.TYPE_PHONE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + PixelFormat.TRANSLUCENT + ); + + layoutParams.gravity = Gravity.TOP | Gravity.END; + float density = getResources().getDisplayMetrics().density; + layoutParams.x = (int) (6 * density); + layoutParams.y = (int) (48 * density); + + windowManager.addView(floatingView, layoutParams); + } + + private void removeFloatingView() { + if (windowManager != null && floatingView != null) { + try { + windowManager.removeView(floatingView); + } catch (Exception ignored) { + } + floatingView = null; + layoutParams = null; + durationTextView = null; + typeTextView = null; + playPauseTextView = null; + } + } + + private void bringAppToFront() { + try { + Intent launchIntent = getPackageManager().getLaunchIntentForPackage(getPackageName()); + if (launchIntent == null) return; + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(launchIntent); + } catch (Exception ignored) { + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + removeFloatingView(); + super.onDestroy(); + } +} diff --git a/local_plugins/native_plugin/lib/native_plugin.dart b/local_plugins/native_plugin/lib/native_plugin.dart index c54721f1f..9219477a4 100644 --- a/local_plugins/native_plugin/lib/native_plugin.dart +++ b/local_plugins/native_plugin/lib/native_plugin.dart @@ -16,4 +16,35 @@ class NativePlugin { Future disposePipContentView(int viewId) { return NativePluginPlatform.instance.disposePipContentView(viewId); } + + Future showRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) { + return NativePluginPlatform.instance.showRecordingFloatingBar( + duration: duration, + recordingType: recordingType, + isPaused: isPaused, + ); + } + + Future updateRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) { + return NativePluginPlatform.instance.updateRecordingFloatingBar( + duration: duration, + recordingType: recordingType, + isPaused: isPaused, + ); + } + + Future hideRecordingFloatingBar() { + return NativePluginPlatform.instance.hideRecordingFloatingBar(); + } + + Stream get recordingFloatingActionStream => + NativePluginPlatform.instance.recordingFloatingActionStream; } diff --git a/local_plugins/native_plugin/lib/native_plugin_method_channel.dart b/local_plugins/native_plugin/lib/native_plugin_method_channel.dart index ec8e18ac5..c58206c2a 100644 --- a/local_plugins/native_plugin/lib/native_plugin_method_channel.dart +++ b/local_plugins/native_plugin/lib/native_plugin_method_channel.dart @@ -9,6 +9,18 @@ class MethodChannelNativePlugin extends NativePluginPlatform { @visibleForTesting final methodChannel = const MethodChannel('native_plugin'); + final _recordingFloatingActionsChannel = const EventChannel( + 'native_plugin/recording_floating_actions', + ); + + @override + Stream get recordingFloatingActionStream { + if (kIsWeb) return Stream.empty(); + return _recordingFloatingActionsChannel.receiveBroadcastStream().map( + (event) => event?.toString() ?? '', + ); + } + @override Future getPlatformVersion() async { final version = await methodChannel.invokeMethod( @@ -34,4 +46,35 @@ class MethodChannelNativePlugin extends NativePluginPlatform { Future disposePipContentView(int viewId) async { await methodChannel.invokeMethod('disposePipContentView', viewId); } + + @override + Future showRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) async { + await methodChannel.invokeMethod('showRecordingFloatingBar', { + 'duration': duration, + 'recordingType': recordingType, + 'isPaused': isPaused, + }); + } + + @override + Future updateRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) async { + await methodChannel.invokeMethod('updateRecordingFloatingBar', { + 'duration': duration, + 'recordingType': recordingType, + 'isPaused': isPaused, + }); + } + + @override + Future hideRecordingFloatingBar() async { + await methodChannel.invokeMethod('hideRecordingFloatingBar'); + } } diff --git a/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart b/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart index c26495c2f..014548fe3 100644 --- a/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart +++ b/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart @@ -44,4 +44,32 @@ abstract class NativePluginPlatform extends PlatformInterface { 'disposePipContentView() has not been implemented.', ); } + + Future showRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) { + throw UnimplementedError( + 'showRecordingFloatingBar() has not been implemented.', + ); + } + + Future updateRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) { + throw UnimplementedError( + 'updateRecordingFloatingBar() has not been implemented.', + ); + } + + Future hideRecordingFloatingBar() { + throw UnimplementedError( + 'hideRecordingFloatingBar() has not been implemented.', + ); + } + + Stream get recordingFloatingActionStream => Stream.empty(); }