Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[super_clipboard] App crashes on Android after copying an image and restart the app #435

Closed
EchoEllet opened this issue Sep 18, 2024 · 2 comments · Fixed by singerdmx/flutter-quill#2230 · May be fixed by #447
Closed

[super_clipboard] App crashes on Android after copying an image and restart the app #435

EchoEllet opened this issue Sep 18, 2024 · 2 comments · Fixed by singerdmx/flutter-quill#2230 · May be fixed by #447

Comments

@EchoEllet
Copy link
Contributor

EchoEllet commented Sep 18, 2024

This is an issue when using super_clipboard however the native platform code is in super_native_extensions.

Steps to reproduce:

  1. Run the app on Android, copy an image from another app while the app is running.
  2. Paste it into the app, it should succeed without any issues.
  3. Refrain from copying anything to the clipboard after that.
  4. Close the app completely and then start it again (using flutter run), you might get a crash.
  5. If you didn't get any crashes, try to paste the same image from step 1.
Crash Log
Shutting down VM
E/flutter ( 7331): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: PlatformException(super_native_extensions_error, "JNI: Java exception was thrown", otherError, null)
E/flutter ( 7331): #0      NativeMethodChannel.invokeMethod (package:irondash_message_channel/src/method_channel.dart:45:7)
E/flutter ( 7331): <asynchronous suspension>
E/flutter ( 7331): #1      ReaderManagerImpl.getItemInfo (package:super_native_extensions/src/native/reader_manager.dart:242:18)
E/flutter ( 7331): <asynchronous suspension>
E/flutter ( 7331): #2      SystemClipboard.read (package:super_clipboard/src/system_clipboard.dart:38:22)
E/flutter ( 7331): <asynchronous suspension>
E/flutter ( 7331): #3      SuperClipboardService.hasClipboardContent (package:flutter_quill_extensions/src/editor_toolbar_controller_shared/clipboard/super_clipboard_service.dart:137:20)
E/flutter ( 7331): <asynchronous suspension>
E/flutter ( 7331): #4      ClipboardMonitor._update (package:flutter_quill/src/toolbar/buttons/clipboard_button.dart:32:9)
E/flutter ( 7331): <asynchronous suspension>
E/flutter ( 7331): 
E/AndroidRuntime( 7331): FATAL EXCEPTION: main
E/AndroidRuntime( 7331): Process: com.example.example, PID: 7331
E/AndroidRuntime( 7331): java.lang.SecurityException: Permission Denial: opening provider org.chromium.chrome.browser.util.ChromeFileProvider from ProcessRecord{2832f48 7331:com.example.example/u0a191} (pid=7331, uid=10191) that is not exported from UID 10134
E/AndroidRuntime( 7331): 	at android.os.Parcel.createExceptionOrNull(Parcel.java:3057)
E/AndroidRuntime( 7331): 	at android.os.Parcel.createException(Parcel.java:3041)
E/AndroidRuntime( 7331): 	at android.os.Parcel.readException(Parcel.java:3024)
E/AndroidRuntime( 7331): 	at android.os.Parcel.readException(Parcel.java:2966)
E/AndroidRuntime( 7331): 	at android.app.IActivityManager$Stub$Proxy.getContentProvider(IActivityManager.java:5906)
E/AndroidRuntime( 7331): 	at android.app.ActivityThread.acquireProvider(ActivityThread.java:7310)
E/AndroidRuntime( 7331): 	at android.app.ContextImpl$ApplicationContentResolver.acquireProvider(ContextImpl.java:3649)
E/AndroidRuntime( 7331): 	at android.content.ContentResolver.acquireProvider(ContentResolver.java:2493)
E/AndroidRuntime( 7331): 	at android.content.ContentResolver.getStreamTypes(ContentResolver.java:1066)
E/AndroidRuntime( 7331): 	at com.superlist.super_native_extensions.ClipDataHelper.getFormats(ClipDataHelper.java:92)
E/AndroidRuntime( 7331): 	at com.superlist.super_native_extensions.ClipDataHelper.getFormats(ClipDataHelper.java:41)
E/AndroidRuntime( 7331): 	at android.os.MessageQueue.nativePollOnce(Native Method)
E/AndroidRuntime( 7331): 	at android.os.MessageQueue.next(MessageQueue.java:335)
E/AndroidRuntime( 7331): 	at android.os.Looper.loopOnce(Looper.java:162)
E/AndroidRuntime( 7331): 	at android.os.Looper.loop(Looper.java:294)
E/AndroidRuntime( 7331): 	at android.app.ActivityThread.main(ActivityThread.java:8177)
E/AndroidRuntime( 7331): 	at java.lang.reflect.Method.invoke(Native Method)
E/AndroidRuntime( 7331): 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
E/AndroidRuntime( 7331): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
E/AndroidRuntime( 7331): Caused by: android.os.RemoteException: Remote stack trace:
E/AndroidRuntime( 7331): 	at com.android.server.am.ContentProviderHelper.checkAssociationAndPermissionLocked(ContentProviderHelper.java:691)
E/AndroidRuntime( 7331): 	at com.android.server.am.ContentProviderHelper.getContentProviderImpl(ContentProviderHelper.java:287)
E/AndroidRuntime( 7331): 	at com.android.server.am.ContentProviderHelper.getContentProvider(ContentProviderHelper.java:144)
E/AndroidRuntime( 7331): 	at com.android.server.am.ActivityManagerService.getContentProvider(ActivityManagerService.java:6713)
E/AndroidRuntime( 7331): 	at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:2761)
Video

Android Emulator API 34.

SuperClipboardAndroidCrash.mov
Similar but not related issue

This issue is not related and doesn't use super_clipbaord however a similar issue will occur with the same steps to reproduce.

It uses our custom implementation in Flutter Quill #2230 using Kotlin (for Android) directly using the Flutter method channel without Rust.

Screen.Recording.2024-09-18.at.4.53.08.PM.mov

The difference is that the app doesn't crash:

Unhandled Exception: PlatformException(COULD_NOT_DECODE_IMAGE, Could not decode bitmap from Uri: Permission Denial: opening provider org.chromium.chrome.browser.util.ChromeFileProvider from ProcessRecord{b3d4d8e 14685:dev.flutterquill.quill_native_bridge_example/u0a191} (pid=14685, uid=10191) that is not exported from UID 10146, java.lang.SecurityException: Permission Denial: opening provider org.chromium.chrome.browser.util.ChromeFileProvider from ProcessRecord{b3d4d8e 14685:dev.flutterquill.quill_native_bridge_example/u0a191} (pid=14685, uid=10191) that is not exported from UID 10146, null)
E/flutter (14685): #0      StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:648:7)
E/flutter (14685): #1      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:334:18)
E/flutter (14685): <asynchronous suspension>
E/flutter (14685): #2      MethodChannelQuillNativeBridge.getClipboardImage (package:quill_native_bridge/src/quill_native_bridge_method_channel.dart:87:24)
E/flutter (14685): <asynchronous suspension>
E/flutter (14685): #3      Buttons.build.<anonymous closure> (package:quill_native_bridge_example/main.dart:150:32)
E/flutter (14685): <asynchronous suspension>
E/flutter (14685): 

It's handled in a way so it returns null and throws an exception instead though I'm not entirely sure about the exact issue with super_clipbaord implementation.

You might noticed that both quill_native_bridge (QuillNativeBridgePlugin) and super_clipbaord have java.lang.SecurityException: Permission Denial in the console, which confirms that it's a security issue related to the lifecycle of the app.

Sometimes it can be:

java.io.FileNotFoundException: No content provider: content://com.android.chrome.FileProvider/images/screenshot/17266705501256549663602733807552.gif
W/System.err(17433): 	at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:2029)
W/System.err(17433): 	at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1858)
W/System.err(17433): 	at android.content.ContentResolver.openInputStream(ContentResolver.java:1528)

Or:

java.lang.SecurityException: Permission Denial: opening provider org.chromium.chrome.browser.util.ChromeFileProvider from ProcessRecord{d1d20d7 17742:dev.flutterquill.quill_native_bridge_example/u0a191} (pid=17742, uid=10191) that is not exported from UID 10146
W/System.err(17742): 	at android.os.Parcel.createExceptionOrNull(Parcel.java:3057)
W/System.err(17742): 	at android.os.Parcel.createException(Parcel.java:3041)
W/System.err(17742): 	at android.os.Parcel.readException(Parcel.java:3024)
W/System.err(17742): 	at android.os.Parcel.readException(Parcel.java:2966)
W/System.err(17742): 	at android.app.IActivityManager$Stub$Proxy.getContentProvider(IActivityManager.java:5906)
W/System.err(17742): 	at android.app.ActivityThread.acquireProvider(ActivityThread.java:7310)
W/System.err(17742): 	at android.app.ContextImpl$ApplicationContentResolver.acquireUnstableProvider(ContextImpl.java:3668)
W/System.err(17742): 	at android.content.ContentResolver.acquireUnstableProvider(ContentResolver.java:2542)
W/System.err(17742): 	at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:2027)
W/System.err(17742): 	at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1858)
W/System.err(17742): 	at android.content.ContentResolver.openInputStream(ContentResolver.java:1528)
W/System.err(17742): 	at dev.flutterquill.quill_native_bridge.clipboard.ClipboardImageHandler.canRead(ClipboardImageHandler.kt:86)
W/System.err(17742): 	at dev.flutterquill.quill_native_bridge.clipboard.ClipboardImageHandler.getClipboardImage(ClipboardImageHandler.kt:114)
W/System.err(17742): 	at dev.flutterquill.quill_native_bridge.QuillNativeBridgePlugin.onMethodCall(QuillNativeBridgePlugin.kt:169)
W/System.err(17742): 	at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:267)
W/System.err(17742): 	at io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:292)
W/System.err(17742): 	at io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$io-flutter-embedding-engine-dart-DartMessenger(DartMessenger.java:319)
W/System.err(17742): 	at io.flutter.embedding.engine.dart.DartMessenger$$ExternalSyntheticLambda0.run(Unknown Source:12)
W/System.err(17742): 	at android.os.Handler.handleCallback(Handler.java:958)
W/System.err(17742): 	at android.os.Handler.dispatchMessage(Handler.java:99)
W/System.err(17742): 	at android.os.Looper.loopOnce(Looper.java:205)
W/System.err(17742): 	at android.os.Looper.loop(Looper.java:294)
W/System.err(17742): 	at android.app.ActivityThread.main(ActivityThread.java:8177)
W/System.err(17742): 	at java.lang.reflect.Method.invoke(Native Method)
W/System.err(17742): 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
W/System.err(17742): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
W/System.err(17742): Caused by: android.os.RemoteException: Remote stack trace:
W/System.err(17742): 	at com.android.server.am.ContentProviderHelper.checkAssociationAndPermissionLocked(ContentProviderHelper.java:691)
W/System.err(17742): 	at com.android.server.am.ContentProviderHelper.getContentProviderImpl(ContentProviderHelper.java:287)
W/System.err(17742): 	at com.android.server.am.ContentProviderHelper.getContentProvider(ContentProviderHelper.java:144)
W/System.err(17742): 	at com.android.server.am.ActivityManagerService.getContentProvider(ActivityManagerService.java:6713)
W/System.err(17742): 	at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:2761)

The only option I had was to write this image to a temporary location or internal storage, then once paste it again on app restart (when not having access to it), then get access to the already saved image, which seems like a workaround to me, there are similar issues on Android that need to solved manually and can be error prone such as process death. So I tried super_clipboard to see how it's handled, and the impl we have doesn't have the exact same exception details as super_clipboard, it seems like a similar unrelated issue.

Hint: The image URI that is given by the android.content.ClipboardManager gives the app limited access, and can be restricted to its lifecycle, once the lifecycle is destroyed, the app no longer has access to the android.net.Uri.

The super_clipboard plugin could check if it has access before processing further, avoid resulting in a crash, and return null (false for the canProvide or throw a dart platform exception) or save the image to somewhere and get access to the image later on app restart and return the image.

let me know If more details are needed or unable to reproduce the issue.

@EchoEllet
Copy link
Contributor Author

A quick workaround

Same solution in Kotlin/JVM can be used in Java.

// Check if you have access to the URI and the file exists before processing the URI:

/**
     * A method to see if any exceptions can occur
     * before start reading the file.
     *
     * The app can lose access to the [Uri] due to lifecycle changes.
     *
     * @throws SecurityException When the app loses access to the [Uri] due to app lifecycle changes
     * or app restart.
     * @throws FileNotFoundException Could be thrown when the [Uri] is no longer on the clipboard.
     * */
    @Throws(Exception::class)
    private fun Uri.readOrThrow(
        context: Context,
    ) = try {
        context.contentResolver.openInputStream(this)?.close()
    } catch (e: Exception) {
        throw e
    }

try {
            imageUri.readOrThrow(context)
        } catch (e: Exception) {
            when (e) {
                is SecurityException -> result.error(
                    "FILE_READ_PERMISSION_DENIED",
                    "An image exists on the clipboard, but the app no longer " +
                            "has permission to access it. This may be due to the app's " +
                            "lifecycle or a recent app restart: ${e.message}",
                    e.toString(),
                )

                is FileNotFoundException -> result.error(
                    "FILE_NOT_FOUND",
                    "The image file can't be found, it might not be on the system clipboard anymore: ${e.message}",
                    e.toString()
                )

                else -> result.error(
                    "UNKNOWN_ERROR_READING_FILE",
                    "An unknown occurred while reading the image file URI: ${e.message}",
                    e.toString()
                )
            }
            return
        }

// Process further with the URI...

Which doesn't fix the issue completely but at least no longer app crashes, since if you try to open the app and you still have that image on your clipboard without copying something else, the app will continue to crash and the system will recommend the user to uninstall the app or report it to the developer, the user will probably delete the app before reaching this state.

So it's probably better to return false to canProvide the image file return it null, or have unhandled exception from dart side instead of the Android side as a quick solution.

Saving an image to a temporary location is not guaranteed as it can be erased, somewhere in the app document directory is not ideal if we have to save all images on the clipboard to retain access to them on app restart, the plugin might not have full access to the app lifecycle to delete them later or the user don't want to save such images on the user device.

@EchoEllet EchoEllet changed the title App crashes on Android after copying an image and restart the app App crashes on Android after copying an image and restart the app for super_clipbaord Sep 18, 2024
@EchoEllet EchoEllet changed the title App crashes on Android after copying an image and restart the app for super_clipbaord App crashes on Android after copying an image and restart the app for super_clipboard Sep 18, 2024
@EchoEllet
Copy link
Contributor Author

EchoEllet commented Sep 18, 2024

I'm still uncertain why sometimes I get FileNotFoundException instead of SecurityException by slightly changing the steps to reproduce each time.

What I'm sure about this issue doesn't happen when not pasting that image into the app (step 2 removed)

@EchoEllet EchoEllet changed the title App crashes on Android after copying an image and restart the app for super_clipboard [super_clipboard] App crashes on Android after copying an image and restart the app Sep 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment