fixed crash when relaunching destroyed activity + minor fixes
This commit is contained in:
parent
c1e27d643d
commit
f18befe486
7 changed files with 37 additions and 21 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
Loading…
Reference in a new issue