fullscreen: work with uri/path & flutter image widget

This commit is contained in:
Thibault Deckers 2019-07-21 12:22:01 +09:00
parent 1313d0c845
commit 10be8b1f2e
5 changed files with 282 additions and 69 deletions

View file

@ -93,7 +93,8 @@ public class MainActivity extends FlutterActivity {
case "cancelGetImageBytes": { case "cancelGetImageBytes": {
String uri = call.argument("uri"); String uri = call.argument("uri");
thumbnailFetcher.cancel(uri); thumbnailFetcher.cancel(uri);
result.success(null); // do not send `null`, as it closes the channel
result.success("");
break; break;
} }
default: default:
@ -214,12 +215,14 @@ class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, Bi
bmp.compress(Bitmap.CompressFormat.JPEG, 90, stream); bmp.compress(Bitmap.CompressFormat.JPEG, 90, stream);
data = stream.toByteArray(); data = stream.toByteArray();
} }
} catch (InterruptedException e) {
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " interrupted");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
Glide.with(activity).clear(target); Glide.with(activity).clear(target);
} else { } else {
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " (cancelled)"); Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " cancelled");
} }
return new MyTaskResult(p, data); return new MyTaskResult(p, data);
} }

View file

@ -1,24 +1,16 @@
package deckers.thibault.aves.model; package deckers.thibault.aves.model;
import android.content.ContentUris;
import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import deckers.thibault.aves.utils.Constants; import deckers.thibault.aves.utils.Constants;
public class ImageEntry { public class ImageEntry {
private static final Uri mediaStoreContentUri = MediaStore.Files.getContentUri("external");
// from source // from source
private String path; // best effort to get local path from content providers private String path; // best effort to get local path from content providers
private long contentId; // should be defined for mediastore, use full URI otherwise private long contentId; // should be defined for mediastore, use full URI otherwise
@ -39,11 +31,13 @@ public class ImageEntry {
init(); init();
} }
public ImageEntry(String path, long id, String mimeType, int width, int height, int orientationDegrees, long sizeBytes, // uri: content provider uri
// path: FileUtils.getPathFromUri(activity, itemUri) is useful (for Download, File, etc.) but is slower than directly using `MediaStore.MediaColumns.DATA` from the MediaStore query
public ImageEntry(Uri uri, String path, long id, String mimeType, int width, int height, int orientationDegrees, long sizeBytes,
String title, long dateModifiedSecs, long dateTakenMillis, String bucketDisplayName, long durationMillis) { String title, long dateModifiedSecs, long dateTakenMillis, String bucketDisplayName, long durationMillis) {
this.uri = uri;
this.path = path; this.path = path;
this.contentId = id; this.contentId = id;
this.uri = null;
this.mimeType = mimeType; this.mimeType = mimeType;
this.width = width; this.width = width;
this.height = height; this.height = height;
@ -59,6 +53,7 @@ public class ImageEntry {
public ImageEntry(Map map) { public ImageEntry(Map map) {
this( this(
Uri.parse((String) map.get("uri")),
(String) map.get("path"), (String) map.get("path"),
toLong(map.get("contentId")), toLong(map.get("contentId")),
(String) map.get("mimeType"), (String) map.get("mimeType"),
@ -76,6 +71,7 @@ public class ImageEntry {
public static Map toMap(ImageEntry entry) { public static Map toMap(ImageEntry entry) {
return new HashMap<String, Object>() {{ return new HashMap<String, Object>() {{
put("uri", entry.uri.toString());
put("path", entry.path); put("path", entry.path);
put("contentId", entry.contentId); put("contentId", entry.contentId);
put("mimeType", entry.mimeType); put("mimeType", entry.mimeType);
@ -88,8 +84,6 @@ public class ImageEntry {
put("sourceDateTakenMillis", entry.sourceDateTakenMillis); put("sourceDateTakenMillis", entry.sourceDateTakenMillis);
put("bucketDisplayName", entry.bucketDisplayName); put("bucketDisplayName", entry.bucketDisplayName);
put("durationMillis", entry.durationMillis); put("durationMillis", entry.durationMillis);
//
put("uri", entry.getUri().toString());
}}; }};
} }
@ -97,6 +91,10 @@ public class ImageEntry {
isVideo = mimeType.startsWith(Constants.MIME_VIDEO); isVideo = mimeType.startsWith(Constants.MIME_VIDEO);
} }
public Uri getUri() {
return uri;
}
@Nullable @Nullable
public String getPath() { public String getPath() {
return path; return path;
@ -106,18 +104,6 @@ public class ImageEntry {
return path == null ? null : new File(path).getName(); return path == null ? null : new File(path).getName();
} }
public InputStream getInputStream(Context context) throws FileNotFoundException {
// FileInputStream is faster than input stream from ContentResolver
return path != null ? new FileInputStream(path) : context.getContentResolver().openInputStream(getUri());
}
public Uri getUri() {
if (uri != null) {
return uri;
}
return ContentUris.withAppendedId(mediaStoreContentUri, contentId);
}
public boolean isVideo() { public boolean isVideo() {
return isVideo; return isVideo;
} }

View file

@ -1,6 +1,7 @@
package deckers.thibault.aves.model.provider; package deckers.thibault.aves.model.provider;
import android.app.Activity; import android.app.Activity;
import android.content.ContentUris;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore; import android.provider.MediaStore;
@ -39,22 +40,24 @@ public class MediaStoreImageProvider {
public List<ImageEntry> fetchAll(Activity activity) { public List<ImageEntry> fetchAll(Activity activity) {
return fetch(activity, FILES_URI, null); return fetch(activity, FILES_URI);
} }
public List<ImageEntry> fetch(final Activity activity, final Uri uri, String mimeType) { private List<ImageEntry> fetch(final Activity activity, final Uri queryUri) {
ArrayList<ImageEntry> entries = new ArrayList<>(); ArrayList<ImageEntry> entries = new ArrayList<>();
// URI should refer to the "files" table, not to the "images" or "videos" one, // URI should refer to the "files" table, not to the "images" or "videos" one,
// as our projection includes a mix of columns from both // as our projection includes a mix of columns from both
Uri filesUri = uri; Uri filesUri = queryUri;
if (!FILES_URI.equals(uri)) { if (!FILES_URI.equals(queryUri)) {
String id = uri.getLastPathSegment(); String id = queryUri.getLastPathSegment();
filesUri = Uri.withAppendedPath(FILES_URI, id); filesUri = Uri.withAppendedPath(FILES_URI, id);
} }
String orderBy = MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC";
try { try {
Cursor cursor = activity.getContentResolver().query(filesUri, PROJECTION, SELECTION, null, null); Cursor cursor = activity.getContentResolver().query(filesUri, PROJECTION, SELECTION, null, orderBy);
if (cursor != null) { if (cursor != null) {
Log.d(LOG_TAG, "fetch query returned " + cursor.getCount() + " entries"); Log.d(LOG_TAG, "fetch query returned " + cursor.getCount() + " entries");
@ -72,9 +75,12 @@ public class MediaStoreImageProvider {
int durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION); int durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
long contentId = cursor.getLong(idColumn);
Uri itemUri = ContentUris.withAppendedId(FILES_URI, contentId);
ImageEntry imageEntry = new ImageEntry( ImageEntry imageEntry = new ImageEntry(
itemUri,
cursor.getString(pathColumn), cursor.getString(pathColumn),
cursor.getLong(idColumn), contentId,
cursor.getString(mimeTypeColumn), cursor.getString(mimeTypeColumn),
cursor.getInt(widthColumn), cursor.getInt(widthColumn),
cursor.getInt(heightColumn), cursor.getInt(heightColumn),

View file

@ -0,0 +1,211 @@
/*
* Copyright (C) 2007-2008 OpenIntents.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file was modified by the Flutter authors from the following original file:
* https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java
*/
// TLAD: copied from https://raw.githubusercontent.com/flutter/plugins/master/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java
package deckers.thibault.aves.utils;
import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class FileUtils {
public String getPathFromUri(final Context context, final Uri uri) {
String path = getPathFromLocalUri(context, uri);
if (path == null) {
path = getPathFromRemoteUri(context, uri);
}
return path;
}
@SuppressLint("NewApi")
private String getPathFromLocalUri(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
} else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
if (!TextUtils.isEmpty(id)) {
try {
final Uri contentUri =
ContentUris.withAppendedId(
Uri.parse(Environment.DIRECTORY_DOWNLOADS), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
} catch (NumberFormatException e) {
return null;
}
}
} else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] {split[1]};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
// Return the remote address
if (isGooglePhotosUri(uri)) {
return null;
}
return getDataColumn(context, uri, null, null);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
private static String getDataColumn(
Context context, Uri uri, String selection, String[] selectionArgs) {
final String column = "_data";
final String[] projection = {column};
try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) {
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndex(column);
//yandex.disk and dropbox do not have _data column
if (column_index == -1) {
return null;
}
return cursor.getString(column_index);
}
}
return null;
}
private static String getPathFromRemoteUri(final Context context, final Uri uri) {
// The code below is why Java now has try-with-resources and the Files utility.
File file = null;
InputStream inputStream = null;
OutputStream outputStream = null;
boolean success = false;
try {
String extension = getImageExtension(context, uri);
inputStream = context.getContentResolver().openInputStream(uri);
file = File.createTempFile("image_picker", extension, context.getCacheDir());
outputStream = new FileOutputStream(file);
if (inputStream != null) {
copy(inputStream, outputStream);
success = true;
}
} catch (IOException ignored) {
} finally {
try {
if (inputStream != null) inputStream.close();
} catch (IOException ignored) {
}
try {
if (outputStream != null) outputStream.close();
} catch (IOException ignored) {
// If closing the output stream fails, we cannot be sure that the
// target file was written in full. Flushing the stream merely moves
// the bytes into the OS, not necessarily to the file.
success = false;
}
}
return success ? file.getPath() : null;
}
/** @return extension of image with dot, or default .jpg if it none. */
private static String getImageExtension(Context context, Uri uriImage) {
String extension = null;
try (Cursor cursor = context
.getContentResolver()
.query(uriImage, new String[]{MediaStore.MediaColumns.MIME_TYPE}, null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
String mimeType = cursor.getString(0);
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
}
}
if (extension == null) {
//default extension for matches the previous behavior of the plugin
extension = "jpg";
}
return "." + extension;
}
private static void copy(InputStream in, OutputStream out) throws IOException {
final byte[] buffer = new byte[4 * 1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.flush();
}
private static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
private static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
private static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
private static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority());
}
}

View file

@ -1,8 +1,9 @@
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:aves/model/image_fetcher.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:transparent_image/transparent_image.dart';
class ImageFullscreenPage extends StatefulWidget { class ImageFullscreenPage extends StatefulWidget {
final Map entry; final Map entry;
@ -15,14 +16,16 @@ class ImageFullscreenPage extends StatefulWidget {
} }
class ImageFullscreenPageState extends State<ImageFullscreenPage> { class ImageFullscreenPageState extends State<ImageFullscreenPage> {
Future<Uint8List> loader;
int get imageWidth => widget.entry['width']; int get imageWidth => widget.entry['width'];
int get imageHeight => widget.entry['height']; int get imageHeight => widget.entry['height'];
String get uri => widget.entry['uri']; String get uri => widget.entry['uri'];
String get path => widget.entry['path'];
double requestWidth, requestHeight;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -30,53 +33,57 @@ class ImageFullscreenPageState extends State<ImageFullscreenPage> {
@override @override
void dispose() { void dispose() {
ImageFetcher.cancelGetImageBytes(uri);
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (loader == null) { if (requestWidth == null || requestHeight == null) {
var mediaQuery = MediaQuery.of(context); var mediaQuery = MediaQuery.of(context);
var screenSize = mediaQuery.size; var screenSize = mediaQuery.size;
var dpr = mediaQuery.devicePixelRatio; var dpr = mediaQuery.devicePixelRatio;
var requestWidth = imageWidth * dpr; requestWidth = imageWidth * dpr;
var requestHeight = imageHeight * dpr; requestHeight = imageHeight * dpr;
if (imageWidth > screenSize.width || imageHeight > screenSize.height) { if (imageWidth > screenSize.width || imageHeight > screenSize.height) {
var ratio = max(imageWidth / screenSize.width, imageHeight / screenSize.height); var ratio = max(imageWidth / screenSize.width, imageHeight / screenSize.height);
requestWidth /= ratio; requestWidth /= ratio;
requestHeight /= ratio; requestHeight /= ratio;
} }
loader = ImageFetcher.getImageBytes(widget.entry, requestWidth.round(), requestHeight.round());
} }
return FutureBuilder( return MediaQuery.removeViewInsets(
future: loader, context: context,
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) { // remove bottom view insets to paint underneath the translucent navigation bar
var ready = snapshot.connectionState == ConnectionState.done && !snapshot.hasError; removeBottom: true,
return Hero( child: Scaffold(
tag: uri, body: Hero(
child: Stack( tag: uri,
children: [ child: Stack(
Center( children: [
child: widget.thumbnail == null Center(
? CircularProgressIndicator() child: widget.thumbnail == null
: Image.memory( ? CircularProgressIndicator()
widget.thumbnail, : Image.memory(
width: imageWidth.toDouble(), widget.thumbnail,
height: imageHeight.toDouble(), width: requestWidth,
fit: BoxFit.contain, height: requestHeight,
), fit: BoxFit.contain,
),
),
Center(
child: FadeInImage(
placeholder: MemoryImage(kTransparentImage),
image: FileImage(File(path)),
fadeOutDuration: Duration(milliseconds: 1),
fadeInDuration: Duration(milliseconds: 200),
width: requestWidth,
height: requestHeight,
fit: BoxFit.contain,
), ),
if (ready) ),
Image.memory( ],
snapshot.data, ),
width: imageWidth.toDouble(), ),
height: imageHeight.toDouble(), ),
fit: BoxFit.contain, );
),
],
),
);
});
} }
} }