diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java index ce401086f..d36934dbc 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java @@ -1,7 +1,6 @@ package deckers.thibault.aves.channel.streams; import android.app.Activity; -import android.content.ContentResolver; import android.graphics.Bitmap; import android.net.Uri; import android.os.Handler; @@ -21,6 +20,7 @@ import java.util.Map; import deckers.thibault.aves.decoder.VideoThumbnail; import deckers.thibault.aves.utils.BitmapUtils; import deckers.thibault.aves.utils.MimeTypes; +import deckers.thibault.aves.utils.StorageUtils; import io.flutter.plugin.common.EventChannel; public class ImageByteStreamHandler implements EventChannel.StreamHandler { @@ -137,8 +137,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler { Glide.with(activity).clear(target); } } else { - ContentResolver cr = activity.getContentResolver(); - try (InputStream is = cr.openInputStream(uri)) { + try (InputStream is = StorageUtils.openInputStream(activity, uri)) { if (is != null) { streamBytes(is); } else { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt index caa69e352..b796e340e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt @@ -4,7 +4,10 @@ import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink class IntentStreamHandler : EventChannel.StreamHandler { - private lateinit var eventSink: EventSink + // cannot use `lateinit` because we cannot guarantee + // its initialization in `onListen` at the right time + // e.g. when resuming the app after the activity got destroyed + private var eventSink: EventSink? = null override fun onListen(arguments: Any?, eventSink: EventSink) { this.eventSink = eventSink @@ -13,6 +16,6 @@ class IntentStreamHandler : EventChannel.StreamHandler { override fun onCancel(arguments: Any?) {} fun notifyNewIntent() { - eventSink.success(true) + eventSink?.success(true) } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt index 250dd28c5..b10cbd564 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt @@ -257,20 +257,25 @@ object StorageUtils { @JvmStatic fun getDocumentFile(context: Context, anyPath: String, mediaUri: Uri): DocumentFileCompat? { - if (requireAccessPermission(anyPath)) { - // need a document URI (not a media content URI) to open a `DocumentFile` output stream - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // cleanest API to get it - val docUri = MediaStore.getDocumentUri(context, mediaUri) - if (docUri != null) { - return DocumentFileCompat.fromSingleUri(context, docUri) + try { + if (requireAccessPermission(anyPath)) { + // need a document URI (not a media content URI) to open a `DocumentFile` output stream + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isMediaStoreContentUri(mediaUri)) { + // cleanest API to get it + val docUri = MediaStore.getDocumentUri(context, mediaUri) + if (docUri != null) { + return DocumentFileCompat.fromSingleUri(context, docUri) + } } + // fallback for older APIs + return getVolumePath(context, anyPath)?.let { convertDirPathToTreeUri(context, it) }?.let { getDocumentFileFromVolumeTree(context, it, anyPath) } } - // fallback for older APIs - return getVolumePath(context, anyPath)?.let { convertDirPathToTreeUri(context, it) }?.let { getDocumentFileFromVolumeTree(context, it, anyPath) } + // good old `File` + return DocumentFileCompat.fromFile(File(anyPath)) + } catch (e: SecurityException) { + Log.w(LOG_TAG, "failed to get document file from mediaUri=$mediaUri", e) } - // good old `File` - return DocumentFileCompat.fromFile(File(anyPath)) + return null } // returns the directory `DocumentFile` (from tree URI when scoped storage is required, `File` otherwise) @@ -368,6 +373,7 @@ object StorageUtils { return ContentResolver.SCHEME_CONTENT.equals(uri.scheme, ignoreCase = true) && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true) } + @JvmStatic fun openInputStream(context: Context, uri: Uri): InputStream? { var effectiveUri = uri // we get a permission denial if we require original from a provider other than the media store @@ -380,6 +386,9 @@ object StorageUtils { } catch (e: FileNotFoundException) { Log.w(LOG_TAG, "failed to find file at uri=$effectiveUri") null + } catch (e: SecurityException) { + Log.w(LOG_TAG, "failed to open file at uri=$effectiveUri", e) + null } } diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 80ae5f9e9..8585dde8a 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -18,6 +18,8 @@ class Constants { offset: Offset(0.5, 1.0), ); + static const String unknown = 'unknown'; + static const pointNemo = Tuple2(-48.876667, -123.393333); static const int infoGroupMaxValueLength = 140; diff --git a/lib/widgets/fullscreen/info/basic_section.dart b/lib/widgets/fullscreen/info/basic_section.dart index 1d84d2151..498c41331 100644 --- a/lib/widgets/fullscreen/info/basic_section.dart +++ b/lib/widgets/fullscreen/info/basic_section.dart @@ -6,6 +6,7 @@ import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/mime_types.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; @@ -28,7 +29,7 @@ class BasicSection extends StatelessWidget { @override Widget build(BuildContext context) { final date = entry.bestDate; - final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : '?'; + final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.unknown; final showMegaPixels = entry.isPhoto && entry.megaPixels != null && entry.megaPixels > 0; final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}'; @@ -36,12 +37,12 @@ class BasicSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ InfoRowGroup({ - 'Title': entry.bestTitle ?? '?', + 'Title': entry.bestTitle ?? Constants.unknown, 'Date': dateText, if (entry.isVideo) ..._buildVideoRows(), if (!entry.isSvg) 'Resolution': resolutionText, - 'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : '?', - 'URI': entry.uri ?? '?', + 'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : Constants.unknown, + 'URI': entry.uri ?? Constants.unknown, if (entry.path != null) 'Path': entry.path, }), _buildChips(), diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index cc8450dfb..33556a12a 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -65,6 +65,8 @@ class InfoPageState extends State { return ValueListenableBuilder( valueListenable: widget.entryNotifier, builder: (context, entry, child) { + if (entry == null) return SizedBox.shrink(); + final locationAtTop = split && entry.hasGps; final locationSection = LocationSection( collection: collection, diff --git a/lib/widgets/fullscreen/overlay/bottom.dart b/lib/widgets/fullscreen/overlay/bottom.dart index f9edab547..fd1183db2 100644 --- a/lib/widgets/fullscreen/overlay/bottom.dart +++ b/lib/widgets/fullscreen/overlay/bottom.dart @@ -228,7 +228,7 @@ class _DateRow extends StatelessWidget { @override Widget build(BuildContext context) { final date = entry.bestDate; - final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : '?'; + final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.unknown; final resolution = '${entry.width ?? '?'} × ${entry.height ?? '?'}'; return Row( children: [