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;
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -65,6 +65,8 @@ class InfoPageState extends State<InfoPage> {
|
|||
return ValueListenableBuilder<ImageEntry>(
|
||||
valueListenable: widget.entryNotifier,
|
||||
builder: (context, entry, child) {
|
||||
if (entry == null) return SizedBox.shrink();
|
||||
|
||||
final locationAtTop = split && entry.hasGps;
|
||||
final locationSection = LocationSection(
|
||||
collection: collection,
|
||||
|
|
|
@ -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: [
|
||||
|
|
Loading…
Reference in a new issue