fixed crash when relaunching destroyed activity + minor fixes

This commit is contained in:
Thibault Deckers 2020-10-19 15:33:01 +09:00
parent c1e27d643d
commit f18befe486
7 changed files with 37 additions and 21 deletions

View file

@ -1,7 +1,6 @@
package deckers.thibault.aves.channel.streams; package deckers.thibault.aves.channel.streams;
import android.app.Activity; import android.app.Activity;
import android.content.ContentResolver;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
@ -21,6 +20,7 @@ import java.util.Map;
import deckers.thibault.aves.decoder.VideoThumbnail; import deckers.thibault.aves.decoder.VideoThumbnail;
import deckers.thibault.aves.utils.BitmapUtils; import deckers.thibault.aves.utils.BitmapUtils;
import deckers.thibault.aves.utils.MimeTypes; import deckers.thibault.aves.utils.MimeTypes;
import deckers.thibault.aves.utils.StorageUtils;
import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.EventChannel;
public class ImageByteStreamHandler implements EventChannel.StreamHandler { public class ImageByteStreamHandler implements EventChannel.StreamHandler {
@ -137,8 +137,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
Glide.with(activity).clear(target); Glide.with(activity).clear(target);
} }
} else { } else {
ContentResolver cr = activity.getContentResolver(); try (InputStream is = StorageUtils.openInputStream(activity, uri)) {
try (InputStream is = cr.openInputStream(uri)) {
if (is != null) { if (is != null) {
streamBytes(is); streamBytes(is);
} else { } else {

View file

@ -4,7 +4,10 @@ import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink import io.flutter.plugin.common.EventChannel.EventSink
class IntentStreamHandler : EventChannel.StreamHandler { 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) { override fun onListen(arguments: Any?, eventSink: EventSink) {
this.eventSink = eventSink this.eventSink = eventSink
@ -13,6 +16,6 @@ class IntentStreamHandler : EventChannel.StreamHandler {
override fun onCancel(arguments: Any?) {} override fun onCancel(arguments: Any?) {}
fun notifyNewIntent() { fun notifyNewIntent() {
eventSink.success(true) eventSink?.success(true)
} }
} }

View file

@ -257,20 +257,25 @@ object StorageUtils {
@JvmStatic @JvmStatic
fun getDocumentFile(context: Context, anyPath: String, mediaUri: Uri): DocumentFileCompat? { fun getDocumentFile(context: Context, anyPath: String, mediaUri: Uri): DocumentFileCompat? {
if (requireAccessPermission(anyPath)) { try {
// need a document URI (not a media content URI) to open a `DocumentFile` output stream if (requireAccessPermission(anyPath)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // need a document URI (not a media content URI) to open a `DocumentFile` output stream
// cleanest API to get it if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isMediaStoreContentUri(mediaUri)) {
val docUri = MediaStore.getDocumentUri(context, mediaUri) // cleanest API to get it
if (docUri != null) { val docUri = MediaStore.getDocumentUri(context, mediaUri)
return DocumentFileCompat.fromSingleUri(context, docUri) 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 // good old `File`
return getVolumePath(context, anyPath)?.let { convertDirPathToTreeUri(context, it) }?.let { getDocumentFileFromVolumeTree(context, it, anyPath) } 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 null
return DocumentFileCompat.fromFile(File(anyPath))
} }
// returns the directory `DocumentFile` (from tree URI when scoped storage is required, `File` otherwise) // 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) return ContentResolver.SCHEME_CONTENT.equals(uri.scheme, ignoreCase = true) && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)
} }
@JvmStatic
fun openInputStream(context: Context, uri: Uri): InputStream? { fun openInputStream(context: Context, uri: Uri): InputStream? {
var effectiveUri = uri var effectiveUri = uri
// we get a permission denial if we require original from a provider other than the media store // 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) { } catch (e: FileNotFoundException) {
Log.w(LOG_TAG, "failed to find file at uri=$effectiveUri") Log.w(LOG_TAG, "failed to find file at uri=$effectiveUri")
null null
} catch (e: SecurityException) {
Log.w(LOG_TAG, "failed to open file at uri=$effectiveUri", e)
null
} }
} }

View file

@ -18,6 +18,8 @@ class Constants {
offset: Offset(0.5, 1.0), offset: Offset(0.5, 1.0),
); );
static const String unknown = 'unknown';
static const pointNemo = Tuple2(-48.876667, -123.393333); static const pointNemo = Tuple2(-48.876667, -123.393333);
static const int infoGroupMaxValueLength = 140; static const int infoGroupMaxValueLength = 140;

View file

@ -6,6 +6,7 @@ import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/mime_types.dart'; import 'package:aves/model/mime_types.dart';
import 'package:aves/model/source/collection_lens.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/utils/file_utils.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
@ -28,7 +29,7 @@ class BasicSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final date = entry.bestDate; 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 showMegaPixels = entry.isPhoto && entry.megaPixels != null && entry.megaPixels > 0;
final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}'; final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
@ -36,12 +37,12 @@ class BasicSection extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
InfoRowGroup({ InfoRowGroup({
'Title': entry.bestTitle ?? '?', 'Title': entry.bestTitle ?? Constants.unknown,
'Date': dateText, 'Date': dateText,
if (entry.isVideo) ..._buildVideoRows(), if (entry.isVideo) ..._buildVideoRows(),
if (!entry.isSvg) 'Resolution': resolutionText, if (!entry.isSvg) 'Resolution': resolutionText,
'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : '?', 'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : Constants.unknown,
'URI': entry.uri ?? '?', 'URI': entry.uri ?? Constants.unknown,
if (entry.path != null) 'Path': entry.path, if (entry.path != null) 'Path': entry.path,
}), }),
_buildChips(), _buildChips(),

View file

@ -65,6 +65,8 @@ class InfoPageState extends State<InfoPage> {
return ValueListenableBuilder<ImageEntry>( return ValueListenableBuilder<ImageEntry>(
valueListenable: widget.entryNotifier, valueListenable: widget.entryNotifier,
builder: (context, entry, child) { builder: (context, entry, child) {
if (entry == null) return SizedBox.shrink();
final locationAtTop = split && entry.hasGps; final locationAtTop = split && entry.hasGps;
final locationSection = LocationSection( final locationSection = LocationSection(
collection: collection, collection: collection,

View file

@ -228,7 +228,7 @@ class _DateRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final date = entry.bestDate; 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 ?? '?'}'; final resolution = '${entry.width ?? '?'} × ${entry.height ?? '?'}';
return Row( return Row(
children: [ children: [