Browse Source

feat(android): 添加Android录音悬浮窗功能

实现Android平台的录音悬浮窗服务,支持显示录音时长、类型和暂停状态
添加悬浮窗操作事件通道,支持暂停/继续、打开会议记录和关闭悬浮窗功能
在会议记录控制器中集成悬浮窗逻辑
weicu
lxm 9 months ago
parent
commit
f2e03791b3
  1. 2
      lib/modules/meeting/bindings/meeting_record_binding.dart
  2. 127
      lib/modules/meeting/controllers/meeting_record_controller.dart
  3. 8
      local_plugins/native_plugin/android/src/main/AndroidManifest.xml
  4. 88
      local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java
  5. 292
      local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/RecordingFloatingService.java
  6. 31
      local_plugins/native_plugin/lib/native_plugin.dart
  7. 43
      local_plugins/native_plugin/lib/native_plugin_method_channel.dart
  8. 28
      local_plugins/native_plugin/lib/native_plugin_platform_interface.dart

2
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() {
if (!Get.isRegistered<MeetingRecordController>()) {
Get.put<MeetingRecordController>(MeetingRecordController(),
permanent: true);
}
}
}

127
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<String>? _recordingFloatingActionSubscription;
bool _floatingBarManuallyClosed = false;
Future<FlutterLocalNotificationsPlugin>
_ensureLocalNotificationsPlugin() async {
@ -60,6 +67,12 @@ class MeetingRecordController extends GetxController
}
Future<void> 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<void> 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<void> _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<void> _stopAndroidRecordingFloatingBar() async {
if (!Platform.isAndroid) return;
_recordingFloatingBarTimer?.cancel();
_recordingFloatingBarTimer = null;
await _nativePlugin.hideRecordingFloatingBar();
}
Future<void> 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<AsrService>();
final BleManager _bleManager = Get.find<BleManager>();
@ -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;

8
local_plugins/native_plugin/android/src/main/AndroidManifest.xml

@ -1,3 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.native_plugin">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application>
<service
android:name=".RecordingFloatingService"
android:exported="false" />
</application>
</manifest>

88
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;
}
}

292
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();
}
}

31
local_plugins/native_plugin/lib/native_plugin.dart

@ -16,4 +16,35 @@ class NativePlugin {
Future<void> disposePipContentView(int viewId) {
return NativePluginPlatform.instance.disposePipContentView(viewId);
}
Future<void> showRecordingFloatingBar({
required String duration,
String? recordingType,
bool isPaused = false,
}) {
return NativePluginPlatform.instance.showRecordingFloatingBar(
duration: duration,
recordingType: recordingType,
isPaused: isPaused,
);
}
Future<void> updateRecordingFloatingBar({
required String duration,
String? recordingType,
bool isPaused = false,
}) {
return NativePluginPlatform.instance.updateRecordingFloatingBar(
duration: duration,
recordingType: recordingType,
isPaused: isPaused,
);
}
Future<void> hideRecordingFloatingBar() {
return NativePluginPlatform.instance.hideRecordingFloatingBar();
}
Stream<String> get recordingFloatingActionStream =>
NativePluginPlatform.instance.recordingFloatingActionStream;
}

43
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<String> get recordingFloatingActionStream {
if (kIsWeb) return Stream<String>.empty();
return _recordingFloatingActionsChannel.receiveBroadcastStream().map(
(event) => event?.toString() ?? '',
);
}
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>(
@ -34,4 +46,35 @@ class MethodChannelNativePlugin extends NativePluginPlatform {
Future<void> disposePipContentView(int viewId) async {
await methodChannel.invokeMethod<void>('disposePipContentView', viewId);
}
@override
Future<void> showRecordingFloatingBar({
required String duration,
String? recordingType,
bool isPaused = false,
}) async {
await methodChannel.invokeMethod<void>('showRecordingFloatingBar', {
'duration': duration,
'recordingType': recordingType,
'isPaused': isPaused,
});
}
@override
Future<void> updateRecordingFloatingBar({
required String duration,
String? recordingType,
bool isPaused = false,
}) async {
await methodChannel.invokeMethod<void>('updateRecordingFloatingBar', {
'duration': duration,
'recordingType': recordingType,
'isPaused': isPaused,
});
}
@override
Future<void> hideRecordingFloatingBar() async {
await methodChannel.invokeMethod<void>('hideRecordingFloatingBar');
}
}

28
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<void> showRecordingFloatingBar({
required String duration,
String? recordingType,
bool isPaused = false,
}) {
throw UnimplementedError(
'showRecordingFloatingBar() has not been implemented.',
);
}
Future<void> updateRecordingFloatingBar({
required String duration,
String? recordingType,
bool isPaused = false,
}) {
throw UnimplementedError(
'updateRecordingFloatingBar() has not been implemented.',
);
}
Future<void> hideRecordingFloatingBar() {
throw UnimplementedError(
'hideRecordingFloatingBar() has not been implemented.',
);
}
Stream<String> get recordingFloatingActionStream => Stream<String>.empty();
}

Loading…
Cancel
Save