flutter-permission-handler icon indicating copy to clipboard operation
flutter-permission-handler copied to clipboard

Notification callback when permission has changed

Open fvisticot opened this issue 4 years ago • 8 comments

🚀 Feature Requests

Notification callback when permission has changed

Contextualize the feature

User modify permission (by example location) in the device settings. The app is notified that the permission has changed

Describe the feature

Platforms affected (mark all that apply)

  • [x] :iphone: iOS
  • [x] :robot: Android

fvisticot avatar Dec 25 '19 10:12 fvisticot

We have to research if this is possible on Android / iOS. If someone knows about callbacks on the native Android / iOS side we could use please let me know.

mvanbeusekom avatar Jan 06 '20 08:01 mvanbeusekom

Just an idea - add status checks when the app is resumed, eg when it comes back from being backgrounded.

The developer adds something like:

StreamSubscription<PermissionStatus> _sub;

initState() {
  final stream = PermissionHandler().getChangesStream(PermissionGroup.notification);
  _sub = stream.listen((PermissionStatus status) {
    // make use of the new `status` *when* it changes
  });
}

dispose() {
  _sub.cancel();
}

Then this plugin knows that it needs to check certain statuses, when the app comes back from the background and publish to the stream.

I did it with pure Flutter, but it depends on Flutter's WidgetBindingObserver, while the plugin can use its native channel to add hooks to the app lifecycle.

class RegisterScreen extends StatefulWidget {
  @override
  State createState() => _RegisterScreenState();
}

class _RegisterScreenState extends State<RegisterScreen>
    with WidgetsBindingObserver {
  StreamSubscription<PermissionStatus> _sub;

  @override
  void initState() {
    super.initState();

    // Add a lifecycle observer
    WidgetsBinding.instance.addObserver(this);

    final stream = getChangesStream(PermissionGroup.notification);
    _sub = stream.listen((PermissionStatus status) {
      print("status: $status");
    });
  }

  // The following should be part of the plugin itself
  Map<PermissionGroup, StreamController<PermissionStatus>> _controllers = {};
  Map<PermissionGroup, PermissionStatus> _statuses = {};

  // The following should be part of the plugin itself
  Stream<PermissionStatus> getChangesStream(PermissionGroup group) {
    StreamController<PermissionStatus> controller = _controllers[group];
    if (controller == null) {
      controller = StreamController.broadcast(onListen: () {
        if (_statuses[group] == null) {
          // Do an initial status check
          PermissionHandler().checkPermissionStatus(group).then((status) {
            _statuses[group] = status;
          });
        }
      });
      _controllers[group] = controller;
    }
    return controller.stream;
  }

  @override
  void dispose() {
    _sub.cancel();

    // Remove the lifecycle observer
    WidgetsBinding.instance.removeObserver(this);

    super.dispose();
  }

  // The following should be part of the plugin itself, and not rely on the observer, but probably some native channel
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      for (final group in _controllers.keys) {
        final controller = _controllers[group];
        if (controller.hasListener) {
          PermissionHandler().checkPermissionStatus(group).then((status) {
            if (!controller.isPaused && !controller.isClosed && controller.hasListener && status != _statuses[group]) {
              controller.add(status);
            }
            _statuses[group] = status;
          });
        }
      }
    }
  }
}

avioli avatar Jan 14 '20 03:01 avioli

this is a good idea, I had to implement similar code with WidgetBindingObserver Its not a perfect solution since it only checks on lifecycle changes and doesnt help when the app is running in the background, but it is better than nothing.

gkrawiec avatar May 09 '20 18:05 gkrawiec

Just sharing this in case it's useful I accomplished this by creating a two builder widgets

  1. Observes app lifecycle changes in a more contained way
  2. Uses the lifecycle observer widget in combination with the permissions to get callbacks on change
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';

class AppLifecycleObserving extends StatefulWidget {
  final Widget Function(
    BuildContext context,
    Stream<AppLifecycleState> stateStream,
  ) builder;

  AppLifecycleObserving({
    Key key,
    @required this.builder,
  }) : super(key: key);

  @override
  _AppLifecycleObservingState createState() => _AppLifecycleObservingState();
}

class _AppLifecycleObservingState extends State<AppLifecycleObserving>
    with WidgetsBindingObserver {
  final _subject = BehaviorSubject<AppLifecycleState>();

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);

    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);

    _subject.add(state);
  }

  @override
  Widget build(BuildContext context) {
    return widget.builder(context, _subject.stream);
  }
}

And

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:wink/screens/achievements/widgets/lifecycle_observing.dart';

typedef _PermisisonChangeBuilder = Widget Function(
  BuildContext context,
  PermissionStatus status,
);

/// Combines permission checking with background notifiers to maintain the
/// latest state of permissions whether the app is foregrounded or not
class PermisisonChangeBuilder extends StatelessWidget {
  final Permission permission;
  final _PermisisonChangeBuilder builder;

  const PermisisonChangeBuilder({
    Key key,
    @required this.permission,
    @required this.builder,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AppLifecycleObserving(
      builder: (context, stateStream) => StreamBuilder<AppLifecycleState>(
        stream: stateStream,
        builder: (context, snapshot) => FutureBuilder<PermissionStatus>(
          future: Permission.contacts.status,
          builder: (context, snapshot) => builder(context, snapshot.data),
        ),
      ),
    );
  }
}

Then can be used via


    return PermisisonChangeBuilder(
      permission: Permission.contacts,
      builder: (context, status) {
        if (status == null) {
          return const LoadingPage();
        }
        return _renderPermissionStatus(status);
      },
    );

KieranLafferty avatar Sep 21 '20 08:09 KieranLafferty

The solutions help for the cases when user change a permission through app settings only. For the case the case of a user allowing a permission from within the app will not fire the stream above. There are cases where a plugin like camera asks for permissions by itself and this would not fire the stream mentioned above. I don't know if a native solution is possible. If someone with that knowledge can help out, it would be great

aytunch avatar Nov 16 '20 12:11 aytunch

@aytunch , @KieranLafferty , @gkrawiec @avioli @mvanbeusekom @fvisticot Hi, Guys, I am able to request remote notification, but wondering how we can set action for notification tap ?

ghost avatar Jul 06 '21 07:07 ghost

there are no native platform APIs for listening but current workarounds just flatmap an applifecycle state observable and asyncmap that into a new event being the (changed or not) permission status - which is pretty straightforward with flutter:services API and does not need any native implementation

nik-v-hax avatar Sep 10 '22 09:09 nik-v-hax

Similar issue: #784.

JeroenWeener avatar Nov 16 '23 11:11 JeroenWeener