Dev-hwang / flutter_foreground_task

This plugin is used to implement a foreground service on the Android platform.
https://pub.dev/packages/flutter_foreground_task
MIT License
140 stars 105 forks source link

Foreground service type missing in to the new version 6.4.0 #226

Closed anandStratBeans closed 1 month ago

anandStratBeans commented 3 months ago

The foreground service type is missing in the new version 6.4.0. Due to this the Android app crashes continuously. FATAL EXCEPTION: main Process: com.stratbeans.bytecastingevolve, PID: 13153 java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=333, result=-1, data=Intent { (has extras) }} to activity {com.stratbeans.bytecastingevolve/com.stratbeans.bytecastingevolve.MainActivity}: java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION at android.app.ActivityThread.deliverResults(ActivityThread.java:5527) at android.app.ActivityThread.handleSendResult(ActivityThread.java:5566) at android.app.servertransaction.ActivityResultItem.execute(ActivityResultItem.java:67) at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loopOnce(Looper.java:205) at android.os.Looper.loop(Looper.java:294) at android.app.ActivityThread.main(ActivityThread.java:8177) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971) Caused by: java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION at android.os.Parcel.createExceptionOrNull(Parcel.java:3057) at android.os.Parcel.createException(Parcel.java:3041) at android.os.Parcel.readException(Parcel.java:3024) at android.os.Parcel.readException(Parcel.java:2966) at android.media.projection.IMediaProjection$Stub$Proxy.start(IMediaProjection.java:313) at android.media.projection.MediaProjection.(MediaProjection.java:84) at android.media.projection.MediaProjection.(MediaProjection.java:75) at android.media.projection.MediaProjectionManager.getMediaProjection(MediaProjectionManager.java:236) at com.isvisoft.flutter_screen_recording.FlutterScreenRecordingPlugin.onActivityResult(FlutterScreenRecordingPlugin.kt:53) at io.flutter.embedding.engine.FlutterEngineConnectionRegistry$FlutterEngineActivityPluginBinding.onActivityResult(FlutterEngineConnectionRegistry.java:774) at io.flutter.embedding.engine.FlutterEngineConnectionRegistry.onActivityResult(FlutterEngineConnectionRegistry.java:422) at io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.onActivityResult(FlutterActivityAndFragmentDelegate.java:857) at io.flutter.embedding.android.FlutterActivity.onActivityResult(FlutterActivity.java:884) at android.app.Activity.dispatchActivityResult(Activity.java:8943) at android.app.ActivityThread.deliverResults(ActivityThread.java:5520) at android.app.ActivityThread.handleSendResult(ActivityThread.java:5566)  at android.app.servertransaction.ActivityResultItem.execute(ActivityResultItem.java:67)  at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)  at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)  at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)  at android.os.Handler.dispatchMessage(Handler.java:106)  at android.os.Looper.loopOnce(Looper.java:205)  at android.os.Looper.loop(Looper.java:294)  at android.app.ActivityThread.main(ActivityThread.java:8177)  at java.lang.reflect.Method.invoke(Native Method)  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)  Caused by: android.os.RemoteException: Remote stack trace: at com.android.server.media.projection.MediaProjectionManagerService$MediaProjection.start(MediaProjectionManagerService.java:940) at android.media.projection.IMediaProjection$Stub.onTransact(IMediaProjection.java:192) at android.os.Binder.execTransactInternal(Binder.java:1339) at android.os.Binder.execTransact(Binder.java:1275)

tatuan19 commented 3 months ago

I got this error when trying both version 6.3.0 and 6.4.0. Is it related to this package's error?

FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to create service com.pravera.flutter_foreground_task.service.ForegroundService: java.lang.SecurityException: Starting FGS with type microphone callerApp=ProcessRecord{64b48c9 18748:/u0a185} targetSDK=34 requires permissions: all of the permissions allOf=true [android.permission.FOREGROUND_SERVICE_MICROPHONE] any of the permissions allOf=false [android.permission.CAPTURE_AUDIO_HOTWORD, android.permission.CAPTURE_AUDIO_OUTPUT, android.permission.CAPTURE_MEDIA_OUTPUT, android.permission.CAPTURE_TUNER_AUDIO_INPUT, android.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT, android.permission.RECORD_AUDIO]  and the app must be in the eligible state/exemptions to access the foreground only permission
    at android.app.ActivityThread.handleCreateService(ActivityThread.java:4663)
    at android.app.ActivityThread.-$$Nest$mhandleCreateService(Unknown Source:0)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2264)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loopOnce(Looper.java:205)
    at android.os.Looper.loop(Looper.java:294)
    at android.app.ActivityThread.main(ActivityThread.java:8176)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Caused by: java.lang.SecurityException: Starting FGS with type microphone callerApp=ProcessRecord{64b48c9 18748:/u0a185} targetSDK=34 requires permissions: all of the permissions allOf=true [android.permission.FOREGROUND_SERVICE_MICROPHONE] any of the permissions allOf=false [android.permission.CAPTURE_AUDIO_HOTWORD, android.permission.CAPTURE_AUDIO_OUTPUT, android.permission.CAPTURE_MEDIA_OUTPUT, android.permission.CAPTURE_TUNER_AUDIO_INPUT, android.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT, android.permission.RECORD_AUDIO]  and the app must be in the eligible state/exemptions to access the foreground only permission
    at android.os.Parcel.createExceptionOrNull(Parcel.java:3057)
    at android.os.Parcel.createException(Parcel.java:3041)
    at android.os.Parcel.readException(Parcel.java:3024)
    at android.os.Parcel.readException(Parcel.java:2966)
    at android.app.IActivityManager$Stub$Proxy.setServiceForeground(IActivityManager.java:6745)
    at android.app.Service.startForeground(Service.java:862)
    at com.pravera.flutter_foreground_task.service.ForegroundService.startForegroundService(ForegroundService.kt:252)
    at com.pravera.flutter_foreground_task.service.ForegroundService.onCreate(ForegroundService.kt:85)
    at android.app.ActivityThread.handleCreateService(ActivityThread.java:4650)
    at android.app.ActivityThread.-$$Nest$mhandleCreateService(Unknown Source:0) 
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2264) 
    at android.os.Handler.dispatchMessage(Handler.java:106) 
    at android.os.Looper.loopOnce(Looper.java:205) 
    at android.os.Looper.loop(Looper.java:294) 
    at android.app.ActivityThread.main(ActivityThread.java:8176) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971) 
Caused by: android.os.RemoteException: Remote stack trace:
    at com.android.server.am.ActiveServices.validateForegroundServiceType(ActiveServices.java:2718)
    at com.android.server.am.ActiveServices.setServiceForegroundInnerLocked(ActiveServices.java:2418)
    at com.android.server.am.ActiveServices.setServiceForegroundLocked(ActiveServices.java:1666)
    at com.android.server.am.ActivityManagerService.setServiceForeground(ActivityManagerService.java:13253)
    at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:3378)

In AndroidManifest.xml:

 <uses-permission android:name="android.permission.RECORD_AUDIO"/>
 <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<service
            android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
            android:stopWithTask="true"
            android:foregroundServiceType="microphone"
            android:exported="false" />
Dev-hwang commented 3 months ago

Check runtime prerequisites. https://developer.android.com/about/versions/14/changes/fgs-types-required#microphone

image

You must grant RECORD_AUDIO permission before starting the service.

bhavinb98 commented 3 months ago

@Dev-hwang My issue is with the MEDIA_PROJECTION service. How exactly do I grant it before the service starts? It's already specified in the manifest.

Plugin version - 6.4.0

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<service
    android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
    android:foregroundServiceType="mediaPlayback|camera|microphone|mediaProjection"
    android:stopWithTask="true"/>

Error -

java.lang.RuntimeException: Unable to create service com.pravera.flutter_foreground_task.service.ForegroundService: java.lang.SecurityException: Starting FGS with type mediaProjection callerApp=ProcessRecord{91d52c4 15903:com.example.test/u0a206} targetSDK=34 requires permissions: all of the permissions allOf=true [android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION] any of the permissions allOf=false [android.permission.CAPTURE_VIDEO_OUTPUT, android:project_media] 
knma1992 commented 1 month ago

I am also experience issues on Android regarding the FOREGROUND_SERVICE_MICROPHONE, I downgraded to 6.3 to specify the foregroundServiceTypes. The ForgroundService starts as expected but when I try to start my audio recording with the record dependency I get an error from the record that I lack the permission. As @bhavinb98 and @tatuan19 are using the FOREGROUND_SERVICE_MICROPHONE I wonder how you manage to record audio in the FS.

bhavinb98 commented 1 month ago

@knma1992 Hmm not sure. My app asks for the access microphone permission during startup, after that there's no other configuration. My foreground service code is pretty much similar to the one in the readme.

knma1992 commented 1 month ago

@bhavinb98 Mine as well, recording works but not in the FS for some reason, is your app on github?

bhavinb98 commented 1 month ago

@knma1992 Yes, but can't share it since it's a company's app.

knma1992 commented 1 month ago

Let me just post my code here, maybe I am doing something obviously wrong.

`

void initForegroundTask() {
  FlutterForegroundTask.init(
    androidNotificationOptions: AndroidNotificationOptions(
      foregroundServiceTypes: [AndroidForegroundServiceType.MICROPHONE],
      channelId: 'foreground_service',
      channelName: 'Foreground Service Notification',
      channelDescription: 'This notification appears when the foreground service is running.',
      channelImportance: NotificationChannelImportance.LOW,
      priority: NotificationPriority.LOW,
      iconData: const NotificationIconData(
        resType: ResourceType.mipmap,
        resPrefix: ResourcePrefix.ic,
        name: 'launcher',
      ),
      buttons: [
        const NotificationButton(id: 'sendButton', text: 'Send'),
        const NotificationButton(id: 'testButton', text: 'Test'),
      ],
    ),
    iosNotificationOptions: const IOSNotificationOptions(
      showNotification: true,
      playSound: false,
    ),
    foregroundTaskOptions: const ForegroundTaskOptions(
      interval: 1000,
      isOnceEvent: false,
      autoRunOnBoot: true,
      allowWakeLock: true,
      allowWifiLock: true,
    ),
  );
}

@override
void initState() {
  initForegroundTask();
}

// The callback function should always be a top-level function.
@pragma('vm:entry-point')
void startCallback() {
  // The setTaskHandler function must be called to handle the task in the background.
  FlutterForegroundTask.setTaskHandler(FirstTaskHandler());
}

enum TaskState { none, running, paused }

class FirstTaskHandler extends TaskHandler {
  SendPort? _sendPort;
  TaskState _taskState = TaskState.none;

  void _startController() async {

    final record = AudioRecorder();

    final inputDevices = await record.listInputDevices();
    final inputDevice = inputDevices[1];
    logger.i(inputDevice);

    if (await record.hasPermission()) {
      logger.i("Permission granted");
    } else {
      logger.e("Permission denied");  //<-This is where it throws permission denied even though it works perfectly fine outside the FS 
      return;
    }

    final recordConfig = RecordConfig(
        encoder: AudioEncoder.pcm16bits,
        sampleRate: 16000,
        numChannels: 1,
        device: inputDevice); //bitRate: 128000

    final stream = await record.startStream(recordConfig);

    stream.listen(
      (data) async {
        print(data);
      },

      onError: (error) {
        logger.e("onError error = ${error.toString()}");
      },

      onDone: () {
        logger.i("onDone");
      },
    ); 

  }

  @override
  void onStart(DateTime timestamp, SendPort? sendPort) async {
    print("onStart");
    _taskState = TaskState.running;
    _sendPort = sendPort;

    final customData = await FlutterForegroundTask.getData<String>(key: 'customData');
    print('customData: $customData');

    _startController();
  }

  @override
  void onRepeatEvent(DateTime timestamp, SendPort? sendPort) async {
    print("onRepeatEvent");
    // Send data to the main isolate.
    sendPort?.send("timestamp: ${timestamp.toString()}");
  }

  @override
  void onDestroy(DateTime timestamp, SendPort? sendPort) async {
    print("onDestroy");
  }

  @override
  void onNotificationButtonPressed(String id) {
    print('onNotificationButtonPressed >> $id');
  }

  @override
  void onNotificationPressed() {
    FlutterForegroundTask.launchApp("/resume-route");
    _sendPort?.send('onNotificationPressed');
  }
}

`

bhavinb98 commented 1 month ago

@knma1992 Have you tried asking for the record permission outside of the task handler function? or is it the first time it asks for it?

knma1992 commented 1 month ago

Currently, I grant permission on a separate screen using the permission handler and I only access the screen from which I start my FS when I have granted all my permissions. What is weird is the fact that if I check permission in my task handler with the permission handler it is granted.

final microphonePermission = await Permission.microphone.isGranted;

But record for some reason does not.

bhavinb98 commented 1 month ago

Hmm that is weird. I don't know any more :(

knma1992 commented 1 month ago

Looking into the ForegroundService was a mistake, I should have debugged the record plugin way earlier. When the record plugin checks for permission and when it sinks the audio buffer into the EventChannel it does so via an activity variable that won't be set when the record plugin is started from the second dart entry point. I forked the record plugin and changed it accordingly.

This is the old implementation. fun sendRecordChunkEvent(buffer: ByteArray) { activity?.runOnUiThread { eventSink?.success(buffer) } }

My implementation: fun sendRecordChunkEvent(buffer: ByteArray) { uiThreadHandler.post(Runnable { eventSink?.success(buffer) }) }

Currently, I don't change the permission check implementation inside record as I can check for permissions using the permission_handler plugin and I have no idea how to supply the context to the record plugin. Thanks @bhavinb98 for the help.