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