diff --git a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/ActivityCompatHostApiImpl.java b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/ActivityCompatHostApiImpl.java
new file mode 100644
index 000000000..71eadb655
--- /dev/null
+++ b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/ActivityCompatHostApiImpl.java
@@ -0,0 +1,56 @@
+package com.baseflow.permissionhandler;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+
+import com.baseflow.instancemanager.InstanceManager;
+import com.baseflow.permissionhandler.PermissionHandlerPigeon.*;
+
+import java.util.UUID;
+
+import io.flutter.plugin.common.BinaryMessenger;
+
+/**
+ * Host API implementation for `ActivityCompat`.
+ *
+ *
This class may handle instantiating and adding native object instances that are attached to a
+ * Dart instance or handle method calls on the associated native class or an instance of the class.
+ */
+public class ActivityCompatHostApiImpl implements ActivityCompatHostApi {
+ // To ease adding additional methods, this value is added prematurely.
+ @SuppressWarnings({"unused", "FieldCanBeLocal"})
+ private final BinaryMessenger binaryMessenger;
+
+ private final InstanceManager instanceManager;
+
+ /**
+ * Constructs an {@link ActivityCompatHostApiImpl}.
+ *
+ * @param binaryMessenger used to communicate with Dart over asynchronous messages
+ * @param instanceManager maintains instances stored to communicate with attached Dart objects
+ */
+ public ActivityCompatHostApiImpl(
+ @NonNull BinaryMessenger binaryMessenger,
+ @NonNull InstanceManager instanceManager
+ ) {
+ this.binaryMessenger = binaryMessenger;
+ this.instanceManager = instanceManager;
+ }
+
+ @NonNull
+ @Override
+ public Boolean shouldShowRequestPermissionRationale(
+ @NonNull String activityInstanceId,
+ @NonNull String permission
+ ) {
+ final UUID activityInstanceUuid = UUID.fromString(activityInstanceId);
+ final Activity activity = instanceManager.getInstance(activityInstanceUuid);
+ if (activity == null) {
+ throw new ActivityNotFoundException();
+ }
+ return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
+ }
+}
diff --git a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/ActivityFlutterApiImpl.java b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/ActivityFlutterApiImpl.java
new file mode 100644
index 000000000..76eefe6d5
--- /dev/null
+++ b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/ActivityFlutterApiImpl.java
@@ -0,0 +1,64 @@
+package com.baseflow.permissionhandler;
+
+import android.app.Activity;
+
+import androidx.annotation.NonNull;
+
+import com.baseflow.instancemanager.InstanceManager;
+import com.baseflow.permissionhandler.PermissionHandlerPigeon.ActivityFlutterApi;
+
+import io.flutter.plugin.common.BinaryMessenger;
+
+import java.util.UUID;
+
+/**
+ * Flutter API implementation for `Activity`.
+ *
+ *
This class may handle adding native instances that are attached to a Dart instance or passing
+ * arguments of callbacks methods to a Dart instance.
+ */
+public class ActivityFlutterApiImpl {
+ // To ease adding additional methods, this value is added prematurely.
+ @SuppressWarnings({"unused", "FieldCanBeLocal"})
+ private final BinaryMessenger binaryMessenger;
+
+ private final InstanceManager instanceManager;
+
+ private final ActivityFlutterApi api;
+
+ /**
+ * Constructs a {@link ActivityFlutterApiImpl}.
+ *
+ * @param binaryMessenger used to communicate with Dart over asynchronous messages
+ * @param instanceManager maintains instances stored to communicate with attached Dart objects
+ */
+ public ActivityFlutterApiImpl(
+ @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) {
+ this.binaryMessenger = binaryMessenger;
+ this.instanceManager = instanceManager;
+ api = new ActivityFlutterApi(binaryMessenger);
+ }
+
+ /**
+ * Stores the `Activity` instance and notifies Dart to create and store a new `Activity`
+ * instance that is attached to this one. If `instance` has already been added, this method does
+ * nothing.
+ */
+ public void create(@NonNull Activity instance) {
+ if (!instanceManager.containsInstance(instance)) {
+ final UUID activityInstanceUuid = instanceManager.addHostCreatedInstance(instance);
+ api.create(activityInstanceUuid.toString(), reply -> {});
+ }
+ }
+
+ /**
+ * Disposes of the `Activity` instance in the instance manager and notifies Dart to do the same.
+ * If `instance` was already disposed, this method does nothing.
+ */
+ public void dispose(Activity instance) {
+ final UUID activityInstanceUuid = instanceManager.getIdentifierForStrongReference(instance);
+ if (activityInstanceUuid != null) {
+ api.dispose(activityInstanceUuid.toString(), reply -> {});
+ }
+ }
+}
diff --git a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionHandlerPigeon.java b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionHandlerPigeon.java
new file mode 100644
index 000000000..eb718aa01
--- /dev/null
+++ b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionHandlerPigeon.java
@@ -0,0 +1,154 @@
+// Autogenerated from Pigeon (v11.0.1), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package com.baseflow.permissionhandler;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import io.flutter.plugin.common.BasicMessageChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MessageCodec;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Generated class from Pigeon. */
+@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})
+public class PermissionHandlerPigeon {
+
+ /** Error class for passing custom error details to Flutter via a thrown PlatformException. */
+ public static class FlutterError extends RuntimeException {
+
+ /** The error code. */
+ public final String code;
+
+ /** The error details. Must be a datatype supported by the api codec. */
+ public final Object details;
+
+ public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details)
+ {
+ super(message);
+ this.code = code;
+ this.details = details;
+ }
+ }
+
+ @NonNull
+ protected static ArrayList wrapError(@NonNull Throwable exception) {
+ ArrayList errorList = new ArrayList(3);
+ if (exception instanceof FlutterError) {
+ FlutterError error = (FlutterError) exception;
+ errorList.add(error.code);
+ errorList.add(error.getMessage());
+ errorList.add(error.details);
+ } else {
+ errorList.add(exception.toString());
+ errorList.add(exception.getClass().getSimpleName());
+ errorList.add(
+ "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
+ }
+ return errorList;
+ }
+ /**
+ * Host API for `ActivityCompat`.
+ *
+ * This class may handle instantiating and adding native object instances that
+ * are attached to a Dart instance or handle method calls on the associated
+ * native class or an instance of the class.
+ *
+ * See https://developer.android.com/reference/androidx/core/app/ActivityCompat.
+ *
+ * Generated interface from Pigeon that represents a handler of messages from Flutter.
+ */
+ public interface ActivityCompatHostApi {
+ /** Gets whether you should show UI with rationale before requesting a permission. */
+ @NonNull
+ Boolean shouldShowRequestPermissionRationale(@NonNull String activityInstanceId, @NonNull String permission);
+
+ /** The codec used by ActivityCompatHostApi. */
+ static @NonNull MessageCodec getCodec() {
+ return new StandardMessageCodec();
+ }
+ /**Sets up an instance of `ActivityCompatHostApi` to handle messages through the `binaryMessenger`. */
+ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ActivityCompatHostApi api) {
+ {
+ BasicMessageChannel channel =
+ new BasicMessageChannel<>(
+ binaryMessenger, "dev.flutter.pigeon.permission_handler_android.ActivityCompatHostApi.shouldShowRequestPermissionRationale", getCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ ArrayList wrapped = new ArrayList();
+ ArrayList args = (ArrayList) message;
+ String activityInstanceIdArg = (String) args.get(0);
+ String permissionArg = (String) args.get(1);
+ try {
+ Boolean output = api.shouldShowRequestPermissionRationale(activityInstanceIdArg, permissionArg);
+ wrapped.add(0, output);
+ }
+ catch (Throwable exception) {
+ ArrayList wrappedError = wrapError(exception);
+ wrapped = wrappedError;
+ }
+ reply.reply(wrapped);
+ });
+ } else {
+ channel.setMessageHandler(null);
+ }
+ }
+ }
+ }
+ /**
+ * Flutter API for `Activity`.
+ *
+ * This class may handle instantiating and adding Dart instances that are
+ * attached to a native instance or receiving callback methods from an
+ * overridden native class.
+ *
+ * See https://developer.android.com/reference/android/app/Activity.
+ *
+ * Generated class from Pigeon that represents Flutter messages that can be called from Java.
+ */
+ public static class ActivityFlutterApi {
+ private final @NonNull BinaryMessenger binaryMessenger;
+
+ public ActivityFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) {
+ this.binaryMessenger = argBinaryMessenger;
+ }
+
+ /** Public interface for sending reply. */
+ @SuppressWarnings("UnknownNullness")
+ public interface Reply {
+ void reply(T reply);
+ }
+ /** The codec used by ActivityFlutterApi. */
+ static @NonNull MessageCodec getCodec() {
+ return new StandardMessageCodec();
+ }
+ /** Create a new Dart instance and add it to the `InstanceManager`. */
+ public void create(@NonNull String instanceIdArg, @NonNull Reply callback) {
+ BasicMessageChannel channel =
+ new BasicMessageChannel<>(
+ binaryMessenger, "dev.flutter.pigeon.permission_handler_android.ActivityFlutterApi.create", getCodec());
+ channel.send(
+ new ArrayList(Collections.singletonList(instanceIdArg)),
+ channelReply -> callback.reply(null));
+ }
+ /** Dispose of the Dart instance and remove it from the `InstanceManager`. */
+ public void dispose(@NonNull String instanceIdArg, @NonNull Reply callback) {
+ BasicMessageChannel channel =
+ new BasicMessageChannel<>(
+ binaryMessenger, "dev.flutter.pigeon.permission_handler_android.ActivityFlutterApi.dispose", getCodec());
+ channel.send(
+ new ArrayList(Collections.singletonList(instanceIdArg)),
+ channelReply -> callback.reply(null));
+ }
+ }
+}
diff --git a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionHandlerPlugin.java b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionHandlerPlugin.java
index d76f37185..411643e14 100644
--- a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionHandlerPlugin.java
+++ b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionHandlerPlugin.java
@@ -1,82 +1,61 @@
package com.baseflow.permissionhandler;
import android.app.Activity;
-import android.content.Context;
+
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import com.baseflow.instancemanager.InstanceManager;
+import com.baseflow.instancemanager.InstanceManagerPigeon.JavaObjectHostApi;
+import com.baseflow.instancemanager.InstanceManagerPigeon.InstanceManagerHostApi;
+import com.baseflow.instancemanager.JavaObjectHostApiImpl;
+import com.baseflow.permissionhandler.PermissionHandlerPigeon.ActivityCompatHostApi;
+
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.BinaryMessenger;
-import io.flutter.plugin.common.MethodChannel;
/**
* Platform implementation of the permission_handler Flutter plugin.
*
* Instantiate this in an add-to-app scenario to gracefully handle activity and context changes.
- * See {@code com.example.permissionhandlerexample.MainActivity} for an example.
+ * See {@code com.example.example.MainActivity} for an example.
*/
public final class PermissionHandlerPlugin implements FlutterPlugin, ActivityAware {
+ private InstanceManager instanceManager;
- private PermissionManager permissionManager;
-
- private MethodChannel methodChannel;
-
- @SuppressWarnings("deprecation")
- @Nullable private io.flutter.plugin.common.PluginRegistry.Registrar pluginRegistrar;
+ private ActivityFlutterApiImpl activityFlutterApi;
- @Nullable private ActivityPluginBinding pluginBinding;
+ private Activity activity;
- @Nullable
- private MethodCallHandlerImpl methodCallHandler;
-
- /**
- * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common}
- * package.
- *
- *
Calling this automatically initializes the plugin. However plugins initialized this way
- * won't react to changes in activity or context, unlike {@link PermissionHandlerPlugin}.
- */
- @SuppressWarnings("deprecation")
- public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) {
- final PermissionHandlerPlugin plugin = new PermissionHandlerPlugin();
-
- plugin.pluginRegistrar = registrar;
- plugin.permissionManager = new PermissionManager(registrar.context());
- plugin.registerListeners();
+ @Override
+ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
+ final BinaryMessenger binaryMessenger = binding.getBinaryMessenger();
- plugin.startListening(registrar.context(), registrar.messenger());
+ instanceManager = InstanceManager.create(identifier -> {});
+ InstanceManagerHostApi.setup(binaryMessenger, () -> {});
- if (registrar.activeContext() instanceof Activity) {
- plugin.startListeningToActivity(
- registrar.activity()
- );
- }
- }
+ final JavaObjectHostApi javaObjectHostApi = new JavaObjectHostApiImpl(instanceManager);
+ JavaObjectHostApi.setup(binaryMessenger, javaObjectHostApi);
- @Override
- public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
- this.permissionManager = new PermissionManager(binding.getApplicationContext());
+ activityFlutterApi = new ActivityFlutterApiImpl(binaryMessenger, instanceManager);
- startListening(
- binding.getApplicationContext(),
- binding.getBinaryMessenger()
- );
+ final ActivityCompatHostApi activityCompatHostApi = new ActivityCompatHostApiImpl(binaryMessenger, instanceManager);
+ ActivityCompatHostApi.setup(binaryMessenger, activityCompatHostApi);
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
- stopListening();
+ if (instanceManager != null) {
+ instanceManager.stopFinalizationListener();
+ instanceManager = null;
+ }
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
- startListeningToActivity(
- binding.getActivity()
- );
-
- this.pluginBinding = binding;
- registerListeners();
+ this.activity = binding.getActivity();
+ activityFlutterApi.create(this.activity);
}
@Override
@@ -86,66 +65,12 @@ public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBindin
@Override
public void onDetachedFromActivity() {
- stopListeningToActivity();
-
- deregisterListeners();
+ activityFlutterApi.dispose(this.activity);
+ this.activity = null;
}
@Override
public void onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity();
}
-
-
- private void startListening(Context applicationContext, BinaryMessenger messenger) {
- methodChannel = new MethodChannel(
- messenger,
- "flutter.baseflow.com/permissions/methods");
-
- methodCallHandler = new MethodCallHandlerImpl(
- applicationContext,
- new AppSettingsManager(),
- this.permissionManager,
- new ServiceManager()
- );
-
- methodChannel.setMethodCallHandler(methodCallHandler);
- }
-
- private void stopListening() {
- methodChannel.setMethodCallHandler(null);
- methodChannel = null;
- methodCallHandler = null;
- }
-
- private void startListeningToActivity(
- Activity activity
- ) {
- if (permissionManager != null) {
- permissionManager.setActivity(activity);
- }
- }
-
- private void stopListeningToActivity() {
- if (permissionManager != null) {
- permissionManager.setActivity(null);
- }
- }
-
- private void registerListeners() {
- if (this.pluginRegistrar != null) {
- this.pluginRegistrar.addActivityResultListener(this.permissionManager);
- this.pluginRegistrar.addRequestPermissionsResultListener(this.permissionManager);
- } else if (pluginBinding != null) {
- this.pluginBinding.addActivityResultListener(this.permissionManager);
- this.pluginBinding.addRequestPermissionsResultListener(this.permissionManager);
- }
- }
-
- private void deregisterListeners() {
- if (this.pluginBinding != null) {
- this.pluginBinding.removeActivityResultListener(this.permissionManager);
- this.pluginBinding.removeRequestPermissionsResultListener(this.permissionManager);
- }
- }
}
diff --git a/permission_handler_android/example/android/app/build.gradle b/permission_handler_android/example/android/app/build.gradle
index 2c1692d79..cc67568bd 100644
--- a/permission_handler_android/example/android/app/build.gradle
+++ b/permission_handler_android/example/android/app/build.gradle
@@ -31,7 +31,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.baseflow.permissionhandler.example"
- minSdkVersion 16
+ minSdkVersion 19
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/permission_handler_android/example/pubspec.yaml b/permission_handler_android/example/pubspec.yaml
index fa732cb58..9e6100450 100644
--- a/permission_handler_android/example/pubspec.yaml
+++ b/permission_handler_android/example/pubspec.yaml
@@ -4,15 +4,14 @@ description: Demonstrates how to use the permission_handler_android plugin.
environment:
sdk: ">=2.15.0 <3.0.0"
+dependency_overrides:
+ permission_handler_platform_interface:
+ path: ../../permission_handler_platform_interface
+
dependencies:
baseflow_plugin_template: ^2.1.2
flutter:
sdk: flutter
-
-dev_dependencies:
- flutter_test:
- sdk: flutter
-
permission_handler_android:
# When depending on this package from a real application you should use:
# permission_handler_android: ^x.y.z
@@ -20,9 +19,12 @@ dev_dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
-
url_launcher: ^6.0.12
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
flutter:
uses-material-design: true
diff --git a/permission_handler_android/lib/permission_handler_android.dart b/permission_handler_android/lib/permission_handler_android.dart
new file mode 100644
index 000000000..dab374aef
--- /dev/null
+++ b/permission_handler_android/lib/permission_handler_android.dart
@@ -0,0 +1,5 @@
+export 'src/android_object_mirrors/activity.dart';
+export 'src/android_object_mirrors/activity_compat.dart';
+
+export 'src/android.dart';
+export 'src/permission_handler_android.dart';
diff --git a/permission_handler_android/lib/src/android.dart b/permission_handler_android/lib/src/android.dart
new file mode 100644
index 000000000..0c3a866a1
--- /dev/null
+++ b/permission_handler_android/lib/src/android.dart
@@ -0,0 +1,100 @@
+import 'package:flutter/material.dart';
+
+import 'android_permission_handler_api_impls.dart';
+import 'android_object_mirrors/activity.dart';
+import 'permission_handler.pigeon.dart';
+
+/// Provides access to the attached Android Activity.
+///
+/// Usage:
+/// ```dart
+/// void main() {
+/// Android.register(
+/// onAttachedToActivityCallback((Activity activity) => this.activity = activity),
+/// onDetachedFromActivityCallback(() => this.activity = null),
+/// );
+/// }
+///
+/// void someMethod() {
+/// if (activity != null) {
+/// activity.getSystemService(Context.LOCATION_SERVICE);
+///
+/// ActivityCompat.shouldShowRequestPermissionRationale(
+/// activity,
+/// 'permission_name',
+/// );
+/// }
+/// }
+/// ```
+class Android {
+ /// Private constructor for creating a new instance of [Android].
+ const Android._({
+ required ActivityFlutterApiImpl activityFlutterApi,
+ }) : _activityFlutterApi = activityFlutterApi;
+
+ static Android? _instance;
+
+ /// Resets the [Android] singleton instance, by setting it to `null`.
+ ///
+ /// For testing purposes only.
+ @visibleForTesting
+ static void reset() {
+ _instance = null;
+ }
+
+ /// The [ActivityFlutterApiImpl] instance used to receive callbacks from the
+ /// Android side.
+ final ActivityFlutterApiImpl? _activityFlutterApi;
+
+ /// Register callbacks for [Activity] changes from Android.
+ ///
+ /// The [Activity] can be used to access Android APIs.
+ /// If [onDetachedFromActivityCallback] is called, the [Activity] received in
+ /// [onAttachedToActivityCallback] is no longer valid and should not be used.
+ ///
+ /// **Note:** If an activity is already attached before registration,
+ /// [onAttachedToActivityCallback] is called immediately.
+ factory Android.register({
+ required void Function(Activity activity) onAttachedToActivityCallback,
+ required void Function() onDetachedFromActivityCallback,
+ @visibleForTesting ActivityFlutterApiImpl? activityFlutterApi,
+ }) {
+ if (_instance == null) {
+ final ActivityFlutterApiImpl activityFlutterApiImpl =
+ activityFlutterApi ?? ActivityFlutterApiImpl();
+ ActivityFlutterApi.setup(activityFlutterApiImpl);
+ _instance = Android._(activityFlutterApi: activityFlutterApiImpl);
+ }
+
+ _instance!._activityFlutterApi!
+ .addOnAttachedToActivityCallback(onAttachedToActivityCallback);
+ _instance!._activityFlutterApi!
+ .addOnDetachedFromActivityCallback(onDetachedFromActivityCallback);
+
+ return _instance!;
+ }
+
+ /// Unregister callbacks for [Activity] changes from Android.
+ ///
+ /// Unregister callbacks so garbage collection can occur. For example, when
+ /// calling `Android.register(...)` directly from a [Widget], the widget
+ /// might not be garbage collected when it is disposed. This can happen when
+ /// one of the callbacks alters the state of the widget. This causes memory
+ /// leaks and should be avoided.
+ static void unregister({
+ void Function(Activity activity)? onAttachedToActivityCallback,
+ void Function()? onDetachedFromActivityCallback,
+ }) {
+ if (_instance == null) return;
+
+ if (onAttachedToActivityCallback != null) {
+ _instance!._activityFlutterApi!
+ .removeOnAttachedToActivityCallback(onAttachedToActivityCallback);
+ }
+
+ if (onDetachedFromActivityCallback != null) {
+ _instance!._activityFlutterApi!
+ .removeOnDetachedFromActivityCallback(onDetachedFromActivityCallback);
+ }
+ }
+}
diff --git a/permission_handler_android/lib/src/android_object_mirrors/activity.dart b/permission_handler_android/lib/src/android_object_mirrors/activity.dart
new file mode 100644
index 000000000..601d4c263
--- /dev/null
+++ b/permission_handler_android/lib/src/android_object_mirrors/activity.dart
@@ -0,0 +1,17 @@
+import 'package:flutter/services.dart';
+import 'package:flutter_instance_manager/flutter_instance_manager.dart';
+
+/// An activity is a single, focused thing that the user can do.
+///
+/// See https://developer.android.com/reference/android/app/Activity.
+class Activity extends JavaObject {
+ /// Instantiates an [Activity] without creating and attaching to an instance
+ /// of the associated native class.
+ Activity.detached({
+ InstanceManager? instanceManager,
+ BinaryMessenger? binaryMessenger,
+ }) : super.detached(
+ instanceManager: instanceManager,
+ binaryMessenger: binaryMessenger,
+ );
+}
diff --git a/permission_handler_android/lib/src/android_object_mirrors/activity_compat.dart b/permission_handler_android/lib/src/android_object_mirrors/activity_compat.dart
new file mode 100644
index 000000000..86adcb582
--- /dev/null
+++ b/permission_handler_android/lib/src/android_object_mirrors/activity_compat.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/foundation.dart';
+
+import '../android_permission_handler_api_impls.dart';
+import 'activity.dart';
+
+/// Helper for accessing features in android.app.Activity.
+///
+/// See https://developer.android.com/reference/androidx/core/app/ActivityCompat.
+class ActivityCompat {
+ static ActivityCompatHostApiImpl _api = ActivityCompatHostApiImpl();
+
+ @visibleForTesting
+ static set api(ActivityCompatHostApiImpl api) => _api = api;
+
+ /// Gets whether you should show UI with rationale before requesting a permission.
+ static Future shouldShowRequestPermissionRationale(
+ Activity activity,
+ String permission,
+ ) {
+ return _api.shouldShowRequestPermissionRationaleFromInstance(
+ activity,
+ permission,
+ );
+ }
+}
diff --git a/permission_handler_android/lib/src/android_permission_handler_api_impls.dart b/permission_handler_android/lib/src/android_permission_handler_api_impls.dart
new file mode 100644
index 000000000..b192f92a0
--- /dev/null
+++ b/permission_handler_android/lib/src/android_permission_handler_api_impls.dart
@@ -0,0 +1,137 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_instance_manager/flutter_instance_manager.dart';
+
+import 'android_object_mirrors/activity.dart';
+import 'permission_handler.pigeon.dart';
+
+/// Host API implementation of ActivityCompat.
+class ActivityCompatHostApiImpl extends ActivityCompatHostApi {
+ /// Creates a new instance of [ActivityCompatHostApiImpl].
+ ActivityCompatHostApiImpl({
+ this.binaryMessenger,
+ InstanceManager? instanceManager,
+ }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager,
+ super(binaryMessenger: binaryMessenger);
+
+ /// Sends binary data across the Flutter platform barrier.
+ ///
+ /// If it is null, the default BinaryMessenger will be used which routes to
+ /// the host platform.
+ final BinaryMessenger? binaryMessenger;
+
+ /// Maintains instances stored to communicate with native language objects.
+ final InstanceManager instanceManager;
+
+ /// Gets whether you should show UI with rationale before requesting a permission.
+ Future shouldShowRequestPermissionRationaleFromInstance(
+ Activity activity,
+ String permission,
+ ) async {
+ final String activityInstanceId = instanceManager.getIdentifier(activity)!;
+
+ return shouldShowRequestPermissionRationale(
+ activityInstanceId,
+ permission,
+ );
+ }
+}
+
+/// Flutter API implementation of Activity.
+class ActivityFlutterApiImpl extends ActivityFlutterApi {
+ /// Constructs a new instance of [ActivityFlutterApiImpl].
+ ActivityFlutterApiImpl({
+ InstanceManager? instanceManager,
+ }) : _instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
+
+ /// Maintains instances stored to communicate with native language objects.
+ final InstanceManager _instanceManager;
+
+ /// The activity currently attached to the Flutter engine.
+ ///
+ /// This is null when no activity is attached.
+ Activity? _activity;
+
+ /// Registered callbacks for activity attach events.
+ final List _onAttachedToActivityCallbacks =
+ [];
+
+ /// Registered callbacks for activity detach events.
+ final List _onDetachedFromActivityCallbacks = [];
+
+ /// Adds a callback to be called when an activity is attached.
+ ///
+ /// If an activity is attached when this method is called, the callback is
+ /// called immediately.
+ void addOnAttachedToActivityCallback(
+ void Function(Activity attachedActivity) onActivityAttached,
+ ) {
+ if (_activity != null) {
+ onActivityAttached(_activity!);
+ }
+ _onAttachedToActivityCallbacks.add(onActivityAttached);
+ }
+
+ /// Removes a callback to be called when an activity is attached.
+ void removeOnAttachedToActivityCallback(
+ void Function(Activity attachedActivity) onActivityAttached,
+ ) {
+ _onAttachedToActivityCallbacks.remove(onActivityAttached);
+ }
+
+ /// Adds a callback to be called when an activity is detached.
+ void addOnDetachedFromActivityCallback(
+ void Function() onActivityDetached,
+ ) {
+ _onDetachedFromActivityCallbacks.add(onActivityDetached);
+ }
+
+ /// Removes a callback to be called when an activity is detached.
+ void removeOnDetachedFromActivityCallback(
+ void Function() onActivityDetached,
+ ) {
+ _onDetachedFromActivityCallbacks.remove(onActivityDetached);
+ }
+
+ /// Pretend an activity attaches.
+ ///
+ /// For testing purposes only.
+ @visibleForTesting
+ void attachToActivity(Activity activity) {
+ _activity = activity;
+ for (final callback in _onAttachedToActivityCallbacks) {
+ callback(activity);
+ }
+ }
+
+ /// Pretend the attached activity detaches.
+ ///
+ /// For testing purposes only.
+ @visibleForTesting
+ void detachFromActivity() {
+ _activity = null;
+ for (final callback in _onDetachedFromActivityCallbacks) {
+ callback();
+ }
+ }
+
+ @override
+ void create(String instanceId) {
+ _activity = Activity.detached();
+ _instanceManager.addHostCreatedInstance(_activity!, instanceId);
+
+ for (final callback in _onAttachedToActivityCallbacks) {
+ callback(_activity!);
+ }
+ }
+
+ @override
+ void dispose(String instanceId) {
+ _instanceManager.remove(instanceId);
+ _activity = null;
+
+ for (final callback in _onDetachedFromActivityCallbacks) {
+ callback();
+ }
+ }
+}
diff --git a/permission_handler_android/lib/src/missing_android_activity_exception.dart b/permission_handler_android/lib/src/missing_android_activity_exception.dart
new file mode 100644
index 000000000..7f7697a9f
--- /dev/null
+++ b/permission_handler_android/lib/src/missing_android_activity_exception.dart
@@ -0,0 +1,9 @@
+/// An exception thrown when the Android activity is missing.
+class MissingAndroidActivityException implements Exception {
+ /// Creates a new instance of [MissingAndroidActivityException].
+ const MissingAndroidActivityException();
+
+ @override
+ String toString() =>
+ 'MissingAndroidActivityException: There is no attached activity';
+}
diff --git a/permission_handler_android/lib/src/permission_handler.pigeon.dart b/permission_handler_android/lib/src/permission_handler.pigeon.dart
new file mode 100644
index 000000000..7a8c99b8f
--- /dev/null
+++ b/permission_handler_android/lib/src/permission_handler.pigeon.dart
@@ -0,0 +1,119 @@
+// Autogenerated from Pigeon (v11.0.1), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
+import 'dart:async';
+import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
+
+import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
+import 'package:flutter/services.dart';
+
+/// Host API for `ActivityCompat`.
+///
+/// This class may handle instantiating and adding native object instances that
+/// are attached to a Dart instance or handle method calls on the associated
+/// native class or an instance of the class.
+///
+/// See https://developer.android.com/reference/androidx/core/app/ActivityCompat.
+class ActivityCompatHostApi {
+ /// Constructor for [ActivityCompatHostApi]. The [binaryMessenger] named argument is
+ /// available for dependency injection. If it is left null, the default
+ /// BinaryMessenger will be used which routes to the host platform.
+ ActivityCompatHostApi({BinaryMessenger? binaryMessenger})
+ : _binaryMessenger = binaryMessenger;
+ final BinaryMessenger? _binaryMessenger;
+
+ static const MessageCodec codec = StandardMessageCodec();
+
+ /// Gets whether you should show UI with rationale before requesting a permission.
+ Future shouldShowRequestPermissionRationale(
+ String arg_activityInstanceId, String arg_permission) async {
+ final BasicMessageChannel channel = BasicMessageChannel(
+ 'dev.flutter.pigeon.permission_handler_android.ActivityCompatHostApi.shouldShowRequestPermissionRationale',
+ codec,
+ binaryMessenger: _binaryMessenger);
+ final List? replyList =
+ await channel.send([arg_activityInstanceId, arg_permission])
+ as List?;
+ if (replyList == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ );
+ } else if (replyList.length > 1) {
+ throw PlatformException(
+ code: replyList[0]! as String,
+ message: replyList[1] as String?,
+ details: replyList[2],
+ );
+ } else if (replyList[0] == null) {
+ throw PlatformException(
+ code: 'null-error',
+ message: 'Host platform returned null value for non-null return value.',
+ );
+ } else {
+ return (replyList[0] as bool?)!;
+ }
+ }
+}
+
+/// Flutter API for `Activity`.
+///
+/// This class may handle instantiating and adding Dart instances that are
+/// attached to a native instance or receiving callback methods from an
+/// overridden native class.
+///
+/// See https://developer.android.com/reference/android/app/Activity.
+abstract class ActivityFlutterApi {
+ static const MessageCodec codec = StandardMessageCodec();
+
+ /// Create a new Dart instance and add it to the `InstanceManager`.
+ void create(String instanceId);
+
+ /// Dispose of the Dart instance and remove it from the `InstanceManager`.
+ void dispose(String instanceId);
+
+ static void setup(ActivityFlutterApi? api,
+ {BinaryMessenger? binaryMessenger}) {
+ {
+ final BasicMessageChannel channel = BasicMessageChannel(
+ 'dev.flutter.pigeon.permission_handler_android.ActivityFlutterApi.create',
+ codec,
+ binaryMessenger: binaryMessenger);
+ if (api == null) {
+ channel.setMessageHandler(null);
+ } else {
+ channel.setMessageHandler((Object? message) async {
+ assert(message != null,
+ 'Argument for dev.flutter.pigeon.permission_handler_android.ActivityFlutterApi.create was null.');
+ final List args = (message as List?)!;
+ final String? arg_instanceId = (args[0] as String?);
+ assert(arg_instanceId != null,
+ 'Argument for dev.flutter.pigeon.permission_handler_android.ActivityFlutterApi.create was null, expected non-null String.');
+ api.create(arg_instanceId!);
+ return;
+ });
+ }
+ }
+ {
+ final BasicMessageChannel channel = BasicMessageChannel(
+ 'dev.flutter.pigeon.permission_handler_android.ActivityFlutterApi.dispose',
+ codec,
+ binaryMessenger: binaryMessenger);
+ if (api == null) {
+ channel.setMessageHandler(null);
+ } else {
+ channel.setMessageHandler((Object? message) async {
+ assert(message != null,
+ 'Argument for dev.flutter.pigeon.permission_handler_android.ActivityFlutterApi.dispose was null.');
+ final List args = (message as List?)!;
+ final String? arg_instanceId = (args[0] as String?);
+ assert(arg_instanceId != null,
+ 'Argument for dev.flutter.pigeon.permission_handler_android.ActivityFlutterApi.dispose was null, expected non-null String.');
+ api.dispose(arg_instanceId!);
+ return;
+ });
+ }
+ }
+ }
+}
diff --git a/permission_handler_android/lib/src/permission_handler_android.dart b/permission_handler_android/lib/src/permission_handler_android.dart
new file mode 100644
index 000000000..54a8d8369
--- /dev/null
+++ b/permission_handler_android/lib/src/permission_handler_android.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/foundation.dart';
+import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
+
+import 'android_object_mirrors/activity.dart';
+import 'android_object_mirrors/activity_compat.dart';
+import 'android.dart';
+import 'missing_android_activity_exception.dart';
+
+/// An implementation of [PermissionHandlerPlatform] for Android.
+class PermissionHandlerAndroid extends PermissionHandlerPlatform {
+ /// The activity that Flutter is attached to.
+ ///
+ /// Used for method invocation that require an activity or context.
+ Activity? _activity;
+
+ /// Allow overriding the attached activity for testing purposes.
+ @visibleForTesting
+ set activity(Activity? activity) {
+ _activity = activity;
+ }
+
+ /// Private constructor for creating a new instance of [PermissionHandlerAndroid].
+ PermissionHandlerAndroid._();
+
+ /// Creates and initializes an instance of [PermissionHandlerAndroid].
+ factory PermissionHandlerAndroid() {
+ final instance = PermissionHandlerAndroid._();
+ Android.register(
+ onAttachedToActivityCallback: (Activity activity) =>
+ instance._activity = activity,
+ onDetachedFromActivityCallback: () => instance._activity = null,
+ );
+ return instance;
+ }
+
+ /// Registers this class as the default instance of [PermissionHandlerPlatform].
+ static void registerWith() {
+ PermissionHandlerPlatform.setInstanceBuilder(
+ () => PermissionHandlerAndroid(),
+ );
+ }
+
+ /// TODO(jweener): implement this method.
+ @override
+ Future checkPermissionStatus(Permission permission) {
+ return Future(() => PermissionStatus.denied);
+ }
+
+ /// TODO(jweener): implement this method.
+ @override
+ Future> requestPermissions(
+ List permissions) {
+ return Future(() => {});
+ }
+
+ @override
+ Future shouldShowRequestPermissionRationale(Permission permission) {
+ if (_activity == null) {
+ throw const MissingAndroidActivityException();
+ }
+
+ return ActivityCompat.shouldShowRequestPermissionRationale(
+ _activity!,
+ // TODO(jweener): replace with Android manifest name for permission once
+ // they have been ported over.
+ 'android.permission.READ_CONTACTS',
+ );
+ }
+}
diff --git a/permission_handler_android/pigeons/android_permission_handler.dart b/permission_handler_android/pigeons/android_permission_handler.dart
new file mode 100644
index 000000000..c1dd63f44
--- /dev/null
+++ b/permission_handler_android/pigeons/android_permission_handler.dart
@@ -0,0 +1,50 @@
+import 'package:pigeon/pigeon.dart';
+
+/// Pigeon configuration file for the communication with the Android platform.
+///
+/// To regenerate these files, run
+/// `dart run pigeon --input pigeons/android_permission_handler.dart`.
+@ConfigurePigeon(
+ PigeonOptions(
+ dartOut: 'lib/src/permission_handler.pigeon.dart',
+ dartTestOut: 'test/test_permission_handler.pigeon.dart',
+ javaOut:
+ 'android/src/main/java/com/baseflow/permissionhandler/PermissionHandlerPigeon.java',
+ javaOptions: JavaOptions(
+ package: 'com.baseflow.permissionhandler',
+ className: 'PermissionHandlerPigeon',
+ ),
+ ),
+)
+
+/// Host API for `ActivityCompat`.
+///
+/// This class may handle instantiating and adding native object instances that
+/// are attached to a Dart instance or handle method calls on the associated
+/// native class or an instance of the class.
+///
+/// See https://developer.android.com/reference/androidx/core/app/ActivityCompat.
+@HostApi(dartHostTestHandler: 'ActivityCompatTestHostApi')
+abstract class ActivityCompatHostApi {
+ /// Gets whether you should show UI with rationale before requesting a permission.
+ bool shouldShowRequestPermissionRationale(
+ String activityInstanceId,
+ String permission,
+ );
+}
+
+/// Flutter API for `Activity`.
+///
+/// This class may handle instantiating and adding Dart instances that are
+/// attached to a native instance or receiving callback methods from an
+/// overridden native class.
+///
+/// See https://developer.android.com/reference/android/app/Activity.
+@FlutterApi()
+abstract class ActivityFlutterApi {
+ /// Create a new Dart instance and add it to the `InstanceManager`.
+ void create(String instanceId);
+
+ /// Dispose of the Dart instance and remove it from the `InstanceManager`.
+ void dispose(String instanceId);
+}
diff --git a/permission_handler_android/pubspec.yaml b/permission_handler_android/pubspec.yaml
index 84580dbe2..63632e7ad 100644
--- a/permission_handler_android/pubspec.yaml
+++ b/permission_handler_android/pubspec.yaml
@@ -4,7 +4,7 @@ homepage: https://github.com/baseflow/flutter-permission-handler
version: 12.0.0
environment:
- sdk: ">=2.15.0 <4.0.0"
+ sdk: ">=2.17.0 <4.0.0"
flutter: ">=2.8.0"
flutter:
@@ -14,12 +14,23 @@ flutter:
android:
package: com.baseflow.permissionhandler
pluginClass: PermissionHandlerPlugin
+ dartPluginClass: PermissionHandlerAndroid
+
+dependency_overrides:
+ permission_handler_platform_interface:
+ path: ../permission_handler_platform_interface
dependencies:
flutter:
sdk: flutter
+ flutter_instance_manager:
+ path: ../../flutter_instance_manager
permission_handler_platform_interface: ^4.0.0
dev_dependencies:
flutter_lints: ^1.0.4
+ flutter_test:
+ sdk: flutter
+ pigeon: ^11.0.1
plugin_platform_interface: ^2.0.0
+ mockito: ^5.4.2
diff --git a/permission_handler_android/test/android_test.dart b/permission_handler_android/test/android_test.dart
new file mode 100644
index 000000000..4412432a6
--- /dev/null
+++ b/permission_handler_android/test/android_test.dart
@@ -0,0 +1,175 @@
+import 'dart:async';
+
+import 'package:flutter_instance_manager/flutter_instance_manager.dart';
+import 'package:flutter_instance_manager/test/test_instance_manager.pigeon.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:permission_handler_android/permission_handler_android.dart';
+import 'package:permission_handler_android/src/android_permission_handler_api_impls.dart';
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+ late final MockTestInstanceManagerHostApi mockInstanceManagerHostApi;
+
+ setUpAll(() {
+ mockInstanceManagerHostApi = MockTestInstanceManagerHostApi();
+ TestInstanceManagerHostApi.setup(mockInstanceManagerHostApi);
+ });
+
+ tearDown(() {
+ Android.reset();
+ });
+
+ tearDown(() {
+ TestInstanceManagerHostApi.setup(null);
+ });
+
+ group('Android', () {
+ group('`register`', () {
+ group('`onActivityAttached`', () {
+ test('does not emit initially', () async {
+ // > Arrange
+ final Completer callbackCompleter = Completer();
+
+ // > Act
+ Android.register(
+ onAttachedToActivityCallback: callbackCompleter.complete,
+ onDetachedFromActivityCallback: () {},
+ );
+
+ // > Assert
+ expect(callbackCompleter.isCompleted, isFalse);
+ });
+
+ test('emits activity if it was already attached', () async {
+ // > Arrange
+ final Completer activityCompleter = Completer();
+ final Activity activity = Activity.detached();
+ final ActivityFlutterApiImpl activityFlutterApi =
+ ActivityFlutterApiImpl();
+ activityFlutterApi.attachToActivity(activity);
+
+ // > Act
+ Android.register(
+ onAttachedToActivityCallback: activityCompleter.complete,
+ onDetachedFromActivityCallback: () {},
+ activityFlutterApi: activityFlutterApi,
+ );
+
+ // > Assert
+ expect(activityCompleter.isCompleted, isTrue);
+ expect(activityCompleter.future, completion(activity));
+ });
+
+ test('emits a new activity if it attaches', () async {
+ // > Arrange
+ final Completer activityCompleter = Completer();
+
+ final Activity activity = Activity.detached();
+
+ final ActivityFlutterApiImpl activityFlutterApiImpl =
+ ActivityFlutterApiImpl();
+
+ Android.register(
+ onAttachedToActivityCallback: activityCompleter.complete,
+ onDetachedFromActivityCallback: () {},
+ activityFlutterApi: activityFlutterApiImpl,
+ );
+
+ // > Act
+ activityFlutterApiImpl.attachToActivity(activity);
+
+ // > Assert
+ expect(activityCompleter.isCompleted, isTrue);
+ expect(
+ activityCompleter.future,
+ completion(activity),
+ );
+ });
+ });
+
+ group('`onActivityDetached`', () {
+ test('does not emit initially', () async {
+ // > Arrange
+ final Completer callbackCompleter = Completer();
+
+ // > Act
+ Android.register(
+ onAttachedToActivityCallback: (Activity activity) {},
+ onDetachedFromActivityCallback: callbackCompleter.complete,
+ );
+
+ // > Assert
+ expect(callbackCompleter.isCompleted, isFalse);
+ });
+
+ test('does not emit when activity was already attached', () async {
+ // > Arrange
+ final Completer callbackCompleter = Completer();
+ final ActivityFlutterApiImpl activityFlutterApiImpl =
+ ActivityFlutterApiImpl();
+ final Activity activity = Activity.detached();
+ activityFlutterApiImpl.attachToActivity(activity);
+
+ // > Act
+ Android.register(
+ onAttachedToActivityCallback: (Activity activity) {},
+ onDetachedFromActivityCallback: callbackCompleter.complete,
+ activityFlutterApi: activityFlutterApiImpl,
+ );
+
+ // > Assert
+ expect(callbackCompleter.isCompleted, isFalse);
+ });
+
+ test('does not emit when an activity attaches', () async {
+ // > Arrange
+ final Completer callbackCompleter = Completer();
+ final ActivityFlutterApiImpl activityFlutterApiImpl =
+ ActivityFlutterApiImpl();
+ final Activity activity = Activity.detached();
+ activityFlutterApiImpl.attachToActivity(activity);
+ Android.register(
+ onAttachedToActivityCallback: (Activity activity) {},
+ onDetachedFromActivityCallback: callbackCompleter.complete,
+ activityFlutterApi: activityFlutterApiImpl,
+ );
+
+ // > Act
+ activityFlutterApiImpl.attachToActivity(activity);
+
+ // > Assert
+ expect(callbackCompleter.isCompleted, isFalse);
+ });
+ });
+ });
+
+ group('`unregister`', () {
+ test('Successfully unregisters callbacks', () async {
+ // > Arrange
+ final ActivityFlutterApiImpl activityFlutterApiImpl =
+ ActivityFlutterApiImpl();
+ final Completer attachCompleter = Completer();
+ final Completer detachCompleter = Completer();
+ onAttachCallback(Activity activity) => attachCompleter.complete();
+ onDetachCallback() => detachCompleter.complete();
+ Android.register(
+ onAttachedToActivityCallback: onAttachCallback,
+ onDetachedFromActivityCallback: onDetachCallback,
+ activityFlutterApi: activityFlutterApiImpl,
+ );
+ Android.unregister(
+ onAttachedToActivityCallback: onAttachCallback,
+ onDetachedFromActivityCallback: onDetachCallback,
+ );
+
+ // > Act
+ activityFlutterApiImpl.attachToActivity(Activity.detached());
+ activityFlutterApiImpl.detachFromActivity();
+
+ // > Assert
+ expect(attachCompleter.isCompleted, isFalse);
+ expect(detachCompleter.isCompleted, isFalse);
+ });
+ });
+ });
+}
diff --git a/permission_handler_android/test/permission_handler_test.dart b/permission_handler_android/test/permission_handler_test.dart
new file mode 100644
index 000000000..d16ab17e8
--- /dev/null
+++ b/permission_handler_android/test/permission_handler_test.dart
@@ -0,0 +1,153 @@
+import 'dart:async';
+
+import 'package:flutter/services.dart';
+import 'package:flutter_instance_manager/flutter_instance_manager.dart';
+import 'package:flutter_instance_manager/test/test_instance_manager.pigeon.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:permission_handler_android/permission_handler_android.dart';
+import 'package:permission_handler_android/src/android_permission_handler_api_impls.dart';
+import 'package:permission_handler_android/src/missing_android_activity_exception.dart';
+import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ late List> requestLog;
+ late final MockTestInstanceManagerHostApi mockInstanceManagerHostApi;
+
+ setUpAll(() {
+ mockInstanceManagerHostApi = MockTestInstanceManagerHostApi();
+ TestInstanceManagerHostApi.setup(mockInstanceManagerHostApi);
+ });
+
+ setUp(() {
+ requestLog = >[];
+ });
+
+ tearDown(() {
+ TestInstanceManagerHostApi.setup(null);
+ });
+
+ group('ActivityFlutterApiImpl', () {
+ test('`create` calls attached callback', () async {
+ // > Arrange
+ final Completer completer = Completer();
+ final ActivityFlutterApiImpl activityFlutterApiImpl =
+ ActivityFlutterApiImpl(
+ instanceManager: InstanceManager(onWeakReferenceRemoved: (_) {}),
+ );
+ activityFlutterApiImpl.addOnAttachedToActivityCallback(
+ (activity) => completer.complete(activity),
+ );
+
+ // > Act
+ activityFlutterApiImpl.create('activity_instance_id');
+
+ // > Assert
+ expect(completer.isCompleted, isTrue);
+ expect(await completer.future, isA());
+ });
+
+ test('`dispose` calls detached callback', () async {
+ // > Arrange
+ final Completer completer = Completer();
+ final ActivityFlutterApiImpl activityFlutterApiImpl =
+ ActivityFlutterApiImpl(
+ instanceManager: InstanceManager(onWeakReferenceRemoved: (_) {}),
+ );
+ activityFlutterApiImpl.create('activity_instance_id');
+ activityFlutterApiImpl
+ .addOnDetachedFromActivityCallback(() => completer.complete());
+
+ // > Act
+ activityFlutterApiImpl.dispose('activity_instance_id');
+
+ // > Assert
+ expect(completer.isCompleted, isTrue);
+ });
+ });
+
+ group('PermissionHandlerAndroid', () {
+ group('shouldShowRequestPermissionRationale', () {
+ setUpAll(() {
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
+ .setMockMessageHandler(
+ 'dev.flutter.pigeon.permission_handler_android.ActivityCompatHostApi.shouldShowRequestPermissionRationale',
+ (ByteData? message) async {
+ const MessageCodec codec = StandardMessageCodec();
+
+ final List request = codec.decodeMessage(message);
+ requestLog.add(request);
+
+ final response = [true];
+ return codec.encodeMessage(response);
+ },
+ );
+ });
+
+ test(
+ 'throws `MissingAndroidActivityException` if no activity is attached',
+ () async {
+ // > Arrange
+ final instanceManager = InstanceManager(
+ onWeakReferenceRemoved: (_) {},
+ );
+ ActivityCompat.api = ActivityCompatHostApiImpl(
+ instanceManager: instanceManager,
+ );
+
+ final activity = Activity.detached();
+ instanceManager.addHostCreatedInstance(
+ activity,
+ 'activity_instance_id',
+ );
+
+ final permissionHandler = PermissionHandlerAndroid();
+
+ // > Act
+ shouldShowRequestPermissionRationale() async => await permissionHandler
+ .shouldShowRequestPermissionRationale(Permission.contacts);
+
+ // > Assert
+ expect(
+ shouldShowRequestPermissionRationale(),
+ throwsA(isA()),
+ );
+ });
+
+ test('returns properly', () async {
+ // > Arrange
+ final instanceManager = InstanceManager(
+ onWeakReferenceRemoved: (_) {},
+ );
+ ActivityCompat.api = ActivityCompatHostApiImpl(
+ instanceManager: instanceManager,
+ );
+ final activity = Activity.detached();
+ instanceManager.addHostCreatedInstance(
+ activity,
+ 'activity_instance_id',
+ );
+
+ final permissionHandler = PermissionHandlerAndroid();
+ permissionHandler.activity = activity;
+
+ // > Act
+ final shouldShowRequestPermissionRationale = await permissionHandler
+ .shouldShowRequestPermissionRationale(Permission.contacts);
+
+ // > Assert
+ expect(
+ requestLog,
+ [
+ [
+ 'activity_instance_id',
+ 'android.permission.READ_CONTACTS',
+ ],
+ ],
+ );
+ expect(shouldShowRequestPermissionRationale, isTrue);
+ });
+ });
+ });
+}
diff --git a/permission_handler_android/test/test_permission_handler.pigeon.dart b/permission_handler_android/test/test_permission_handler.pigeon.dart
new file mode 100644
index 000000000..9a0c3f02a
--- /dev/null
+++ b/permission_handler_android/test/test_permission_handler.pigeon.dart
@@ -0,0 +1,59 @@
+// Autogenerated from Pigeon (v11.0.1), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import
+// ignore_for_file: avoid_relative_lib_imports
+import 'dart:async';
+import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
+import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:permission_handler_android/src/permission_handler.pigeon.dart';
+
+/// Host API for `ActivityCompat`.
+///
+/// This class may handle instantiating and adding native object instances that
+/// are attached to a Dart instance or handle method calls on the associated
+/// native class or an instance of the class.
+///
+/// See https://developer.android.com/reference/androidx/core/app/ActivityCompat.
+abstract class ActivityCompatTestHostApi {
+ static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding =>
+ TestDefaultBinaryMessengerBinding.instance;
+ static const MessageCodec codec = StandardMessageCodec();
+
+ /// Gets whether you should show UI with rationale before requesting a permission.
+ bool shouldShowRequestPermissionRationale(
+ String activityInstanceId, String permission);
+
+ static void setup(ActivityCompatTestHostApi? api,
+ {BinaryMessenger? binaryMessenger}) {
+ {
+ final BasicMessageChannel channel = BasicMessageChannel(
+ 'dev.flutter.pigeon.permission_handler_android.ActivityCompatHostApi.shouldShowRequestPermissionRationale',
+ codec,
+ binaryMessenger: binaryMessenger);
+ if (api == null) {
+ _testBinaryMessengerBinding!.defaultBinaryMessenger
+ .setMockDecodedMessageHandler(channel, null);
+ } else {
+ _testBinaryMessengerBinding!.defaultBinaryMessenger
+ .setMockDecodedMessageHandler(channel,
+ (Object? message) async {
+ assert(message != null,
+ 'Argument for dev.flutter.pigeon.permission_handler_android.ActivityCompatHostApi.shouldShowRequestPermissionRationale was null.');
+ final List args = (message as List?)!;
+ final String? arg_activityInstanceId = (args[0] as String?);
+ assert(arg_activityInstanceId != null,
+ 'Argument for dev.flutter.pigeon.permission_handler_android.ActivityCompatHostApi.shouldShowRequestPermissionRationale was null, expected non-null String.');
+ final String? arg_permission = (args[1] as String?);
+ assert(arg_permission != null,
+ 'Argument for dev.flutter.pigeon.permission_handler_android.ActivityCompatHostApi.shouldShowRequestPermissionRationale was null, expected non-null String.');
+ final bool output = api.shouldShowRequestPermissionRationale(
+ arg_activityInstanceId!, arg_permission!);
+ return [output];
+ });
+ }
+ }
+ }
+}
diff --git a/permission_handler_platform_interface/CHANGELOG.md b/permission_handler_platform_interface/CHANGELOG.md
index 0b68e034f..d47167992 100644
--- a/permission_handler_platform_interface/CHANGELOG.md
+++ b/permission_handler_platform_interface/CHANGELOG.md
@@ -1,3 +1,8 @@
+## NEXT
+
+* Adds `setInstanceBuilder` to `PermissionHandlerPlatform`.
+* Deprecates `set instance` in `PermissionHandlerPlatform`. Use `setInstanceBuilder` instead.
+
## 4.0.1
* Updates Android documentation on how to use `permission.photo` on Android 12 (API 32) and below and Android 13 (API 33) and above.
diff --git a/permission_handler_platform_interface/lib/permission_handler_platform_interface.dart b/permission_handler_platform_interface/lib/permission_handler_platform_interface.dart
index 4f44f6143..8804e6d78 100644
--- a/permission_handler_platform_interface/lib/permission_handler_platform_interface.dart
+++ b/permission_handler_platform_interface/lib/permission_handler_platform_interface.dart
@@ -3,7 +3,6 @@ library permission_handler_platform_interface;
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
-import 'src/method_channel/method_channel_permission_handler.dart';
part 'src/permission_handler_platform_interface.dart';
part 'src/permission_status.dart';
diff --git a/permission_handler_platform_interface/lib/src/method_channel/method_channel_permission_handler.dart b/permission_handler_platform_interface/lib/src/method_channel/method_channel_permission_handler.dart
deleted file mode 100644
index 2f9cc0beb..000000000
--- a/permission_handler_platform_interface/lib/src/method_channel/method_channel_permission_handler.dart
+++ /dev/null
@@ -1,96 +0,0 @@
-import 'dart:async';
-
-import 'package:flutter/services.dart';
-
-import '../../permission_handler_platform_interface.dart';
-import 'utils/codec.dart';
-
-const MethodChannel _methodChannel =
- MethodChannel('flutter.baseflow.com/permissions/methods');
-
-/// An implementation of [PermissionHandlerPlatform] that uses [MethodChannel]s.
-class MethodChannelPermissionHandler extends PermissionHandlerPlatform {
- /// Checks the current status of the given [Permission].
- @override
- Future checkPermissionStatus(Permission permission) async {
- final status = await _methodChannel.invokeMethod(
- 'checkPermissionStatus', permission.value);
-
- return decodePermissionStatus(status);
- }
-
- /// Checks the current status of the service associated with the given
- /// [Permission].
- ///
- /// Notes about specific permissions:
- /// - **[Permission.phone]**
- /// - Android:
- /// - The method will return [ServiceStatus.notApplicable] when:
- /// - the device lacks the TELEPHONY feature
- /// - TelephonyManager.getPhoneType() returns PHONE_TYPE_NONE
- /// - when no Intents can be resolved to handle the `tel:` scheme
- /// - The method will return [ServiceStatus.disabled] when:
- /// - the SIM card is missing
- /// - iOS:
- /// - The method will return [ServiceStatus.notApplicable] when:
- /// - the native code can not find a handler for the `tel:` scheme
- /// - The method will return [ServiceStatus.disabled] when:
- /// - the mobile network code (MNC) is either 0 or 65535. See
- /// https://stackoverflow.com/a/11595365 for details
- /// - **PLEASE NOTE that this is still not a perfect indication** of the
- /// device's capability to place & connect phone calls as it also depends
- /// on the network condition.
- /// - **[Permission.bluetooth]**
- /// - iOS:
- /// - The method will **always** return [ServiceStatus.disabled] when the
- /// Bluetooth permission was denied by the user. It is not possible
- /// obtain the actual Bluetooth service status without having the
- /// Bluetooth permission granted.
- /// - The method will prompt the user for Bluetooth permission if the
- /// permission was not requested before.
- @override
- Future checkServiceStatus(Permission permission) async {
- final status = await _methodChannel.invokeMethod(
- 'checkServiceStatus', permission.value);
-
- return decodeServiceStatus(status);
- }
-
- /// Opens the app settings page.
- ///
- /// Returns [true] if the app settings page could be opened, otherwise
- /// [false].
- @override
- Future openAppSettings() async {
- final wasOpened = await _methodChannel.invokeMethod('openAppSettings');
-
- return wasOpened ?? false;
- }
-
- /// Requests the user for access to the supplied list of [Permission]s, if
- /// they have not already been granted before.
- ///
- /// Returns a [Map] containing the status per requested [Permission].
- @override
- Future> requestPermissions(
- List permissions) async {
- final data = encodePermissions(permissions);
- final status =
- await _methodChannel.invokeMethod('requestPermissions', data);
-
- return decodePermissionRequestResult(Map.from(status));
- }
-
- /// Checks if you should show a rationale for requesting permission.
- ///
- /// This method is only implemented on Android, calling this on iOS always
- /// returns [false].
- @override
- Future shouldShowRequestPermissionRationale(
- Permission permission) async {
- final shouldShowRationale = await _methodChannel.invokeMethod(
- 'shouldShowRequestPermissionRationale', permission.value);
-
- return shouldShowRationale ?? false;
- }
-}
diff --git a/permission_handler_platform_interface/lib/src/method_channel/utils/codec.dart b/permission_handler_platform_interface/lib/src/method_channel/utils/codec.dart
deleted file mode 100644
index dc5db075c..000000000
--- a/permission_handler_platform_interface/lib/src/method_channel/utils/codec.dart
+++ /dev/null
@@ -1,25 +0,0 @@
-import '../../../permission_handler_platform_interface.dart';
-
-/// Converts the given [value] into a [PermissionStatus] instance.
-PermissionStatus decodePermissionStatus(int value) {
- return PermissionStatusValue.statusByValue(value);
-}
-
-/// Converts the given [value] into a [ServiceStatus] instance.
-ServiceStatus decodeServiceStatus(int value) {
- return ServiceStatusValue.statusByValue(value);
-}
-
-/// Converts the given [Map] of [int]s into a [Map] with [Permission]s as
-/// keys and their respective [PermissionStatus] as value.
-Map decodePermissionRequestResult(
- Map value) {
- return value.map((key, value) => MapEntry(
- Permission.byValue(key), PermissionStatusValue.statusByValue(value)));
-}
-
-/// Converts the given [List] of [Permission]s into a [List] of [int]s which
-/// can be sent on the Flutter method channel.
-List encodePermissions(List permissions) {
- return permissions.map((it) => it.value).toList();
-}
diff --git a/permission_handler_platform_interface/lib/src/permission_handler_platform_interface.dart b/permission_handler_platform_interface/lib/src/permission_handler_platform_interface.dart
index 65d965bf6..bc64ed5af 100644
--- a/permission_handler_platform_interface/lib/src/permission_handler_platform_interface.dart
+++ b/permission_handler_platform_interface/lib/src/permission_handler_platform_interface.dart
@@ -14,21 +14,52 @@ abstract class PermissionHandlerPlatform extends PlatformInterface {
static final Object _token = Object();
- static PermissionHandlerPlatform _instance = MethodChannelPermissionHandler();
+ static PermissionHandlerPlatform? _instance;
- /// The default instance of [PermissionHandlerPlatform] to use.
+ /// The instance of [PermissionHandlerPlatform] to use.
///
- /// Defaults to [MethodChannelPermissionHandler].
- static PermissionHandlerPlatform get instance => _instance;
+ /// Returns the instance, if it has been created, or a newly created instance
+ /// through the builder provided in [setInstanceBuilder]. Throws an
+ /// [Exception] if there is no instance, and [setInstanceBuilder] has not been
+ /// called.
+ static PermissionHandlerPlatform get instance {
+ if (_instance == null) {
+ if (_instanceBuilder == null) {
+ throw Exception(
+ 'No instance builder was provided. Did you call `setInstanceBuilder`?');
+ }
- /// Platform-specific plugins should set this with their own
- /// platform-specific class that extends
- /// [PermissionHandlerPlatform] when they register themselves.
+ _instance = _instanceBuilder!();
+ PlatformInterface.verifyToken(_instance!, _token);
+ }
+
+ return _instance!;
+ }
+
+ /// Platform-specific plugins should set this with their own platform-specific
+ /// class that extends [PermissionHandlerPlatform] when they register
+ /// themselves.
+ @Deprecated('Use [setPlatformInstanceBuilder] instead.')
static set instance(PermissionHandlerPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
+ static PermissionHandlerPlatform Function()? _instanceBuilder;
+
+ /// Sets the builder function that creates a new instance of the
+ /// platform-specific implementation of [PermissionHandlerPlatform].
+ ///
+ /// This function allows for delayed initialisation of the handler. This is
+ /// especially useful in the plugin environment, where the handler is
+ /// registered early during start-up. As platform channels are not established
+ /// at that point, the implementation cannot directly be created.
+ static void setInstanceBuilder(
+ PermissionHandlerPlatform Function() builder,
+ ) {
+ _instanceBuilder = builder;
+ }
+
/// Checks the current status of the given [Permission].
Future checkPermissionStatus(Permission permission) {
throw UnimplementedError(
diff --git a/permission_handler_platform_interface/test/src/method_channel/method_channel_mock.dart b/permission_handler_platform_interface/test/src/method_channel/method_channel_mock.dart
deleted file mode 100644
index 2b5da964a..000000000
--- a/permission_handler_platform_interface/test/src/method_channel/method_channel_mock.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-import 'package:flutter/services.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-class MethodChannelMock {
- final MethodChannel methodChannel;
- final String method;
- final dynamic result;
- final Duration delay;
-
- MethodChannelMock({
- required String channelName,
- required this.method,
- this.result,
- this.delay = Duration.zero,
- }) : methodChannel = MethodChannel(channelName) {
- TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
- .setMockMethodCallHandler(methodChannel, _handler);
- }
-
- Future _handler(MethodCall methodCall) async {
- if (methodCall.method != method) {
- throw MissingPluginException('No implementation found for method '
- '$method on channel ${methodChannel.name}');
- }
-
- return Future.delayed(delay, () {
- if (result is Exception) {
- throw result;
- }
-
- return Future.value(result);
- });
- }
-}
diff --git a/permission_handler_platform_interface/test/src/method_channel/method_channel_permission_handler_test.dart b/permission_handler_platform_interface/test/src/method_channel/method_channel_permission_handler_test.dart
deleted file mode 100644
index c03c279a1..000000000
--- a/permission_handler_platform_interface/test/src/method_channel/method_channel_permission_handler_test.dart
+++ /dev/null
@@ -1,205 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
-import 'package:permission_handler_platform_interface/src/method_channel/method_channel_permission_handler.dart';
-import 'method_channel_mock.dart';
-
-List get mockPermissions => List.of({
- Permission.contacts,
- Permission.camera,
- Permission.calendarWriteOnly,
- });
-
-Map get mockPermissionMap => {};
-
-void main() {
- TestWidgetsFlutterBinding.ensureInitialized();
-
- group('checkPermissionStatus: When checking for permission', () {
- test('Should receive granted if user wants access to the requested feature',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'checkPermissionStatus',
- result: PermissionStatus.denied.value,
- );
-
- final permissionStatus = await MethodChannelPermissionHandler()
- .checkPermissionStatus(Permission.contacts);
-
- expect(permissionStatus, PermissionStatus.denied);
- });
-
- test('Should receive denied if user denied access to the requested feature',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'checkPermissionStatus',
- result: PermissionStatus.denied.value,
- );
-
- final permissionStatus = await MethodChannelPermissionHandler()
- .checkPermissionStatus(Permission.contacts);
-
- expect(permissionStatus, PermissionStatus.denied);
- });
-
- test(
- // ignore: lines_longer_than_80_chars
- 'Should receive restricted if OS denied rights for to the requested feature',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'checkPermissionStatus',
- result: PermissionStatus.restricted.value,
- );
-
- final permissionStatus = await MethodChannelPermissionHandler()
- .checkPermissionStatus(Permission.contacts);
-
- expect(permissionStatus, PermissionStatus.restricted);
- });
-
- test(
- // ignore: lines_longer_than_80_chars
- 'Should receive limited if user has authorized this application for limited access',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'checkPermissionStatus',
- result: PermissionStatus.limited.value,
- );
-
- final permissionStatus = await MethodChannelPermissionHandler()
- .checkPermissionStatus(Permission.contacts);
-
- expect(permissionStatus, PermissionStatus.limited);
- });
-
- test(
- // ignore: lines_longer_than_80_chars
- 'Should receive permanentlyDenied if user denied access and selected to never show a request for this permission again',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'checkPermissionStatus',
- result: PermissionStatus.permanentlyDenied.value,
- );
-
- final permissionStatus = await MethodChannelPermissionHandler()
- .checkPermissionStatus(Permission.contacts);
-
- expect(permissionStatus, PermissionStatus.permanentlyDenied);
- });
- });
-
- group('checkServiceStatus: When checking for service', () {
- // ignore: lines_longer_than_80_chars
- test(
- 'Should receive disabled if the service for the permission is disabled',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'checkServiceStatus',
- result: ServiceStatus.disabled.value,
- );
-
- final serviceStatus = await MethodChannelPermissionHandler()
- .checkServiceStatus(Permission.contacts);
-
- expect(serviceStatus, ServiceStatus.disabled);
- });
-
- test('Should receive enabled if the service for the permission is enabled',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'checkServiceStatus',
- result: ServiceStatus.enabled.value,
- );
-
- final serviceStatus = await MethodChannelPermissionHandler()
- .checkServiceStatus(Permission.contacts);
-
- expect(serviceStatus, ServiceStatus.enabled);
- });
-
- test(
- // ignore: lines_longer_than_80_chars
- 'Should receive notApplicable if the permission does not have an associated service on the current platform',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'checkServiceStatus',
- result: ServiceStatus.notApplicable.value,
- );
-
- final serviceStatus = await MethodChannelPermissionHandler()
- .checkServiceStatus(Permission.contacts);
-
- expect(serviceStatus, ServiceStatus.notApplicable);
- });
- });
-
- group('openAppSettings: When opening the App settings', () {
- test('Should receive true if the page can be opened', () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'openAppSettings',
- result: true,
- );
-
- final hasOpenedAppSettings =
- await MethodChannelPermissionHandler().openAppSettings();
-
- expect(hasOpenedAppSettings, true);
- });
-
- test('Should receive false if an error occurred', () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'openAppSettings',
- result: false,
- );
-
- final hasOpenedAppSettings =
- await MethodChannelPermissionHandler().openAppSettings();
-
- expect(hasOpenedAppSettings, false);
- });
- });
-
- group('requestPermissions: When requesting for permission', () {
- // ignore: lines_longer_than_80_chars
- test('returns a Map with all the PermissionStatus of the given permissions',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'requestPermissions',
- result: mockPermissionMap,
- );
-
- final result = await MethodChannelPermissionHandler()
- .requestPermissions(mockPermissions);
-
- expect(result, isA>());
- });
- });
-
- group('shouldShowRequestPermissionRationale:', () {
- test(
- // ignore: lines_longer_than_80_chars
- 'should return true when you should show a rationale for requesting permission.',
- () async {
- MethodChannelMock(
- channelName: 'flutter.baseflow.com/permissions/methods',
- method: 'shouldShowRequestPermissionRationale',
- result: true,
- );
-
- final shouldShowRationale = await MethodChannelPermissionHandler()
- .shouldShowRequestPermissionRationale(mockPermissions.first);
-
- expect(shouldShowRationale, true);
- });
- });
-}
diff --git a/permission_handler_platform_interface/test/src/method_channel/utils/coded_test.dart b/permission_handler_platform_interface/test/src/method_channel/utils/coded_test.dart
deleted file mode 100644
index 1742b9fc6..000000000
--- a/permission_handler_platform_interface/test/src/method_channel/utils/coded_test.dart
+++ /dev/null
@@ -1,36 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
-import 'package:permission_handler_platform_interface/src/method_channel/utils/codec.dart';
-
-void main() {
- group('Codec', () {
- test('decodePermissionStatus should return a PermissionStatus', () {
- expect(decodePermissionStatus(0), PermissionStatus.denied);
- });
-
- test('decodeServiceStatus should a corresponding ServiceStatus', () {
- expect(decodeServiceStatus(0), ServiceStatus.disabled);
- });
-
- test(
- 'decodePermissionRequestResult should convert a map'
- 'to map', () {
- var value = {
- 1: 1,
- };
-
- var permissionMap = decodePermissionRequestResult(value);
-
- expect(permissionMap.keys.first, isA());
- expect(permissionMap.values.first, isA());
- });
-
- test('encodePermissions should return a list of integers', () {
- var permissions = [Permission.accessMediaLocation];
-
- var integers = encodePermissions(permissions);
-
- expect(integers.first, isA());
- });
- });
-}
diff --git a/permission_handler_platform_interface/test/src/permission_handler_platform_interface_test.dart b/permission_handler_platform_interface/test/src/permission_handler_platform_interface_test.dart
index c965e72ed..a5491cff0 100644
--- a/permission_handler_platform_interface/test/src/permission_handler_platform_interface_test.dart
+++ b/permission_handler_platform_interface/test/src/permission_handler_platform_interface_test.dart
@@ -1,32 +1,43 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
-import 'package:permission_handler_platform_interface/src/method_channel/method_channel_permission_handler.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('$PermissionHandlerPlatform', () {
- test('$MethodChannelPermissionHandler is the default instance', () {
- expect(PermissionHandlerPlatform.instance,
- isA());
+ test('Throws an `Exception` if no instance builder is provided', () {
+ expect(
+ () => PermissionHandlerPlatform.instance,
+ throwsException,
+ );
});
test('Cannot be implemented with `implements`', () {
- expect(() {
- PermissionHandlerPlatform.instance =
- ImplementsPermissionHandlerPlatform();
- }, throwsA(anything));
+ PermissionHandlerPlatform.setInstanceBuilder(
+ () => ImplementsPermissionHandlerPlatform(),
+ );
+
+ expect(
+ () => PermissionHandlerPlatform.instance,
+ throwsA(anything),
+ );
});
test('Can be extended with `extend`', () {
- PermissionHandlerPlatform.instance = ExtendsPermissionHandlerPlatform();
+ PermissionHandlerPlatform.setInstanceBuilder(
+ () => ExtendsPermissionHandlerPlatform(),
+ );
+
+ PermissionHandlerPlatform.instance;
});
test('Can be mocked with `implements`', () {
final mock = MockPermissionHandlerPlatform();
- PermissionHandlerPlatform.instance = mock;
+ PermissionHandlerPlatform.setInstanceBuilder(() => mock);
+
+ PermissionHandlerPlatform.instance;
});
test(
diff --git a/permission_handler_web/pubspec.yaml b/permission_handler_web/pubspec.yaml
index 40159b01c..3868599f8 100644
--- a/permission_handler_web/pubspec.yaml
+++ b/permission_handler_web/pubspec.yaml
@@ -20,7 +20,7 @@ dev_dependencies:
flutter_lints: ^2.0.0
mockito: ^5.4.2
build_runner: ^2.1.2
- test: ^1.24.4
+ test: ^1.24.3
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec