diff --git a/CHANGELOG.md b/CHANGELOG.md index d07f39ae..93c29391 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 2.0.2 + +* Android: Restore support for the legacy v1 embedding via `registerWith` and + guard lifecycle cleanup to avoid crashes when activities change. +* Android: Remove the strict `FlutterActivity` cast so the plugin works with + `FlutterFragmentActivity` and other activity types. + +## 2.0.1 + +* Guard against unsupported platforms and provide clearer errors instead of + `MissingPluginException` when the plugin is invoked outside Android/iOS. +* Document platform support expectations in the README. + ## 2.0.0 * Android: Fixes #137 and #132 diff --git a/README.md b/README.md index b2c4e100..6189365c 100755 --- a/README.md +++ b/README.md @@ -6,6 +6,20 @@ A plugin for Flutter apps that adds barcode scanning support on both Android and ![Demo gif](https://github.com/AmolGangadhare/MyProfileRepo/blob/master/flutter_barcode_scanning_demo.gif "Demo") +## Platform support + +> **Note** +> `flutter_barcode_scanner` only provides native implementations for **Android** and +> **iOS**. Invoking the plugin on the web, macOS, Windows, Linux, or Fuchsia now +> throws a descriptive `PlatformException` instead of the previous +> `MissingPluginException`. If you see this message, run the sample on a real or +> emulated mobile device. + +> **Android embedding** +> Apps that still rely on the legacy (pre-1.12) Android embedding are fully +> supported again starting with **v2.0.2**. After upgrading, run `flutter clean` +> and rebuild so Gradle picks up the refreshed plugin registration code. + ## Try example Just clone or download the repository, open the project in `Android Studio/ VS Code`, open `pubspec.yaml` and click on `Packages get`. diff --git a/android/build.gradle b/android/build.gradle index 0408f69c..31cb6e6f 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,11 +4,11 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.3.2' + classpath 'com.android.tools.build:gradle:7.4.2' } } diff --git a/android/src/main/java/com/amolg/flutterbarcodescanner/FlutterBarcodeScannerPlugin.java b/android/src/main/java/com/amolg/flutterbarcodescanner/FlutterBarcodeScannerPlugin.java index 93580b61..368addd7 100755 --- a/android/src/main/java/com/amolg/flutterbarcodescanner/FlutterBarcodeScannerPlugin.java +++ b/android/src/main/java/com/amolg/flutterbarcodescanner/FlutterBarcodeScannerPlugin.java @@ -2,6 +2,7 @@ import android.app.Activity; import android.app.Application; + import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.util.Log; @@ -14,29 +15,31 @@ import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.vision.barcode.Barcode; + import java.lang.reflect.Method; import java.util.Map; - import io.flutter.embedding.android.FlutterActivity; 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.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; + import io.flutter.plugin.common.EventChannel.StreamHandler; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; + import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry.ActivityResultListener; - import io.flutter.plugin.common.EventChannel.StreamHandler; - import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; /** * FlutterBarcodeScannerPlugin */ public class FlutterBarcodeScannerPlugin implements MethodCallHandler, ActivityResultListener, StreamHandler, FlutterPlugin, ActivityAware { - private static final String CHANNEL = "flutter_barcode_scanner"; + private static final String CHANNEL = "flutter_barcode_scanner"; + private static final String LEGACY_REGISTRAR_KEY = "com.amolg.flutterbarcodescanner.FlutterBarcodeScannerPlugin"; - private static FlutterActivity activity; + private static Activity activity; private static Result pendingResult; private Map arguments; @@ -46,7 +49,7 @@ public class FlutterBarcodeScannerPlugin implements MethodCallHandler, ActivityR public static boolean isShowFlashIcon = false; public static boolean isContinuousScan = false; static EventChannel.EventSink barcodeStream; - private EventChannel eventChannel; + private EventChannel eventChannel; private MethodChannel channel; private FlutterPluginBinding pluginBinding; @@ -54,12 +57,49 @@ public class FlutterBarcodeScannerPlugin implements MethodCallHandler, ActivityR private Application applicationContext; private Lifecycle lifecycle; private LifeCycleObserver observer; + private boolean isLifecycleObserverAdded = false; + private boolean isApplicationObserverAdded = false; public FlutterBarcodeScannerPlugin() { } - private FlutterBarcodeScannerPlugin(Activity activity) { - FlutterBarcodeScannerPlugin.activity = (FlutterActivity) activity; + @SuppressWarnings("deprecation") + public static void registerWith(PluginRegistry registry) { + if (registry == null) { + Log.w(TAG, "FlutterBarcodeScannerPlugin registration skipped: registry is null."); + return; + } + + final Object registrar = getLegacyRegistrar(registry); + if (registrar == null) { + Log.w(TAG, "FlutterBarcodeScannerPlugin registration skipped: registrar unavailable."); + return; + } + + final BinaryMessenger messenger = invokeRegistrarMethod(registrar, "messenger", BinaryMessenger.class); + final Activity legacyActivity = invokeRegistrarMethod(registrar, "activity", Activity.class); + final Context legacyContext = invokeRegistrarMethod(registrar, "context", Context.class); + Application application = null; + if (legacyActivity != null) { + application = legacyActivity.getApplication(); + } + if (application == null && legacyContext instanceof Application) { + application = (Application) legacyContext; + } else if (application == null && legacyContext != null) { + final Context appContext = legacyContext.getApplicationContext(); + if (appContext instanceof Application) { + application = (Application) appContext; + } + } + + if (messenger == null || legacyActivity == null || application == null) { + Log.w(TAG, "FlutterBarcodeScannerPlugin registration skipped: missing legacy dependencies."); + return; + } + + FlutterBarcodeScannerPlugin plugin = new FlutterBarcodeScannerPlugin(); + plugin.createPluginSetup(messenger, application, legacyActivity, null); + plugin.registerLegacyActivityResultListener(registrar); } @Override @@ -98,6 +138,12 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { private void startBarcodeScannerActivityView(String buttonText, boolean isContinuousScan) { try { + if (activity == null) { + Log.e(TAG, "Activity reference is null. Cannot start scanner view."); + pendingResult.success("-1"); + return; + } + Intent intent = new Intent(activity, BarcodeCaptureActivity.class).putExtra("cancelButtonText", buttonText); if (isContinuousScan) { activity.startActivity(intent); @@ -152,7 +198,7 @@ public void onCancel(Object o) { public static void onBarcodeScanReceiver(final Barcode barcode) { try { - if (barcode != null && !barcode.displayValue.isEmpty()) { + if (barcode != null && !barcode.displayValue.isEmpty() && activity != null) { activity.runOnUiThread(new Runnable() { @Override public void run() { @@ -185,13 +231,18 @@ public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding onAttachedToActivity(binding); } - private void createPluginSetup( + private void createPluginSetup( final BinaryMessenger messenger, final Application applicationContext, final Activity activity, final ActivityPluginBinding activityBinding) { - this.activity = (FlutterActivity) activity; + if (activity == null) { + Log.e(TAG, "Unable to start flutter_barcode_scanner: activity is null"); + return; + } + + FlutterBarcodeScannerPlugin.activity = activity; eventChannel = new EventChannel(messenger, "flutter_barcode_scanner_receiver"); eventChannel.setStreamHandler(this); @@ -199,22 +250,26 @@ private void createPluginSetup( channel = new MethodChannel(messenger, CHANNEL); channel.setMethodCallHandler(this); + observer = new LifeCycleObserver(activity); if (activityBinding != null) { activityBinding.addActivityResultListener(this); lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding); - observer = new LifeCycleObserver(activity); lifecycle.addObserver(observer); + isLifecycleObserverAdded = true; + } else if (applicationContext != null) { + applicationContext.registerActivityLifecycleCallbacks(observer); + isApplicationObserverAdded = true; } } @Override public void onAttachedToActivity(ActivityPluginBinding binding) { - activityBinding = binding; - createPluginSetup( - pluginBinding.getBinaryMessenger(), - (Application) pluginBinding.getApplicationContext(), - activityBinding.getActivity(), - activityBinding); + activityBinding = binding; + createPluginSetup( + pluginBinding.getBinaryMessenger(), + (Application) pluginBinding.getApplicationContext(), + activityBinding.getActivity(), + activityBinding); } @Override @@ -224,14 +279,35 @@ public void onDetachedFromActivity() { private void clearPluginSetup() { activity = null; - activityBinding.removeActivityResultListener(this); - activityBinding = null; - lifecycle.removeObserver(observer); + + if (activityBinding != null) { + activityBinding.removeActivityResultListener(this); + activityBinding = null; + } + + if (isLifecycleObserverAdded && lifecycle != null && observer != null) { + lifecycle.removeObserver(observer); + isLifecycleObserverAdded = false; + } + + if (isApplicationObserverAdded && applicationContext != null && observer != null) { + applicationContext.unregisterActivityLifecycleCallbacks(observer); + isApplicationObserverAdded = false; + } + lifecycle = null; - channel.setMethodCallHandler(null); - eventChannel.setStreamHandler(null); - channel = null; - applicationContext.unregisterActivityLifecycleCallbacks(observer); + observer = null; + + if (channel != null) { + channel.setMethodCallHandler(null); + channel = null; + } + + if (eventChannel != null) { + eventChannel.setStreamHandler(null); + eventChannel = null; + } + applicationContext = null; } @@ -299,4 +375,46 @@ public void onActivityDestroyed(Activity activity) { public void onActivityStopped(Activity activity) { } } + + @SuppressWarnings("deprecation") + private static Object getLegacyRegistrar(PluginRegistry registry) { + if (registry == null) { + return null; + } + try { + Method method = registry.getClass().getMethod("registrarFor", String.class); + return method.invoke(registry, LEGACY_REGISTRAR_KEY); + } catch (Exception e) { + Log.e(TAG, "Unable to obtain legacy registrar", e); + return null; + } + } + + private static T invokeRegistrarMethod(Object registrar, String methodName, Class clazz) { + if (registrar == null) { + return null; + } + try { + Method method = registrar.getClass().getMethod(methodName); + Object result = method.invoke(registrar); + if (clazz.isInstance(result)) { + return clazz.cast(result); + } + } catch (Exception e) { + Log.e(TAG, "Failed to invoke legacy registrar method " + methodName, e); + } + return null; + } + + private void registerLegacyActivityResultListener(Object registrar) { + if (registrar == null) { + return; + } + try { + Method method = registrar.getClass().getMethod("addActivityResultListener", ActivityResultListener.class); + method.invoke(registrar, this); + } catch (Exception e) { + Log.e(TAG, "Failed to attach legacy activity result listener", e); + } + } } \ No newline at end of file diff --git a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 00000000..a88caf99 --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/example/ios/Flutter/ephemeral/flutter_lldbinit b/example/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 00000000..e3ba6fbe --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/lib/flutter_barcode_scanner.dart b/lib/flutter_barcode_scanner.dart index c750b9f0..b44843ad 100755 --- a/lib/flutter_barcode_scanner.dart +++ b/lib/flutter_barcode_scanner.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform, kIsWeb; import 'package:flutter/services.dart'; /// Scan mode which is either QR code or BARCODE @@ -18,6 +19,43 @@ class FlutterBarcodeScanner { static Stream? _onBarcodeReceiver; + static bool get _isSupportedPlatform { + if (kIsWeb) { + return false; + } + + return defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS; + } + + static void _ensureSupported(String operation) { + if (_isSupportedPlatform) { + return; + } + + final platformLabel = kIsWeb + ? 'web' + : defaultTargetPlatform.toString().split('.').last; + + throw PlatformException( + code: 'UNSUPPORTED_PLATFORM', + message: + 'flutter_barcode_scanner:$operation is only available on Android and iOS. ' + 'You\'re currently running on $platformLabel. ' + 'Please switch to a mobile device or guard the call with a platform check.', + ); + } + + static Never _throwMissingPlugin(String operation, MissingPluginException ex) { + throw PlatformException( + code: 'PLUGIN_NOT_REGISTERED', + message: + 'flutter_barcode_scanner:$operation could not find a native implementation. ' + 'Make sure the plugin is correctly added to pubspec.yaml and that you\'ve rebuilt the app.', + details: ex.message, + ); + } + /// Scan with the camera until a barcode is identified, then return. /// /// Shows a scan line with [lineColor] over a scan window. A flash icon is @@ -25,6 +63,8 @@ class FlutterBarcodeScanner { /// be customized with the [cancelButtonText] string. static Future scanBarcode(String lineColor, String cancelButtonText, bool isShowFlashIcon, ScanMode scanMode) async { + _ensureSupported('scanBarcode'); + if (cancelButtonText.isEmpty) { cancelButtonText = 'Cancel'; } @@ -39,9 +79,13 @@ class FlutterBarcodeScanner { }; /// Get barcode scan result - final barcodeResult = - await _channel.invokeMethod('scanBarcode', params) ?? ''; - return barcodeResult; + try { + final barcodeResult = + await _channel.invokeMethod('scanBarcode', params) ?? ''; + return barcodeResult; + } on MissingPluginException catch (ex) { + _throwMissingPlugin('scanBarcode', ex); + } } /// Returns a continuous stream of barcode scans until the user cancels the @@ -53,6 +97,8 @@ class FlutterBarcodeScanner { /// detected barcode strings. static Stream? getBarcodeStreamReceiver(String lineColor, String cancelButtonText, bool isShowFlashIcon, ScanMode scanMode) { + _ensureSupported('getBarcodeStreamReceiver'); + if (cancelButtonText.isEmpty) { cancelButtonText = 'Cancel'; } @@ -68,7 +114,11 @@ class FlutterBarcodeScanner { // Invoke method to open camera, and then create an event channel which will // return a stream - _channel.invokeMethod('scanBarcode', params); + try { + _channel.invokeMethod('scanBarcode', params); + } on MissingPluginException catch (ex) { + _throwMissingPlugin('getBarcodeStreamReceiver', ex); + } _onBarcodeReceiver ??= _eventChannel.receiveBroadcastStream(); return _onBarcodeReceiver; } diff --git a/pubspec.yaml b/pubspec.yaml index 8bf40764..419ed5db 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,6 @@ name: flutter_barcode_scanner description: A plugin for barcode scanning support on Android and iOS. Supports barcodes, QR codes, etc. -version: 2.0.0 -author: Amol Gangadhare +version: 2.0.2 homepage: https://github.com/AmolGangadhare/flutter_barcode_scanner environment: @@ -13,6 +12,8 @@ dependencies: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.24 dev_dependencies: + flutter_test: + sdk: flutter pedantic: ^1.11.1 test: ^1.25.15 diff --git a/test/flutter_barcode_scanner_test.dart b/test/flutter_barcode_scanner_test.dart new file mode 100644 index 00000000..2dc29b56 --- /dev/null +++ b/test/flutter_barcode_scanner_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const methodChannel = MethodChannel('flutter_barcode_scanner'); + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (MethodCall call) async { + if (call.method == 'scanBarcode') { + return 'mock-barcode'; + } + return null; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, null); + debugDefaultTargetPlatformOverride = null; + }); + + test('scanBarcode throws on unsupported platforms', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; + + expect( + FlutterBarcodeScanner.scanBarcode( + '#ffffff', + 'Cancel', + true, + ScanMode.QR, + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'UNSUPPORTED_PLATFORM', + ), + ), + ); + }); + + test('scanBarcode returns native value on supported platform', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final result = await FlutterBarcodeScanner.scanBarcode( + '#ffffff', + 'Cancel', + true, + ScanMode.QR, + ); + + expect(result, 'mock-barcode'); + }); +}