get thumbnails from mediastore (faster, lower quality)

This commit is contained in:
Thibault Deckers 2019-12-26 14:34:38 +09:00
parent 38a89c5b19
commit dc14c354a8
6 changed files with 124 additions and 54 deletions

View file

@ -1,10 +1,15 @@
package deckers.thibault.aves.channelhandlers;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Build;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Size;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.Key;
@ -59,52 +64,100 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
@Override
protected Result doInBackground(Params... params) {
Params p = params[0];
ImageEntry entry = p.entry;
byte[] data = null;
Bitmap bitmap = null;
if (!this.isCancelled()) {
// add signature to ignore cache for images which got modified but kept the same URI
Key signature = new ObjectKey("" + entry.getDateModifiedSecs() + entry.getWidth() + entry.getOrientationDegrees());
RequestOptions options = new RequestOptions()
.signature(signature)
.override(p.width, p.height);
FutureTarget<Bitmap> target;
if (entry.isVideo()) {
options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE);
target = Glide.with(activity)
.asBitmap()
.apply(options)
.load(new VideoThumbnail(activity, entry.getUri()))
.signature(signature)
.submit(p.width, p.height);
} else {
target = Glide.with(activity)
.asBitmap()
.apply(options)
.load(entry.getUri())
.signature(signature)
.submit(p.width, p.height);
}
try {
Bitmap bmp = target.get();
if (bmp != null) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.JPEG, 90, stream);
data = stream.toByteArray();
}
} catch (InterruptedException e) {
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " interrupted");
} catch (Exception e) {
e.printStackTrace();
}
Glide.with(activity).clear(target);
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// bitmap = getBytesByResolverThumbnail(p);
// } else {
bitmap = getBytesByMediaStoreThumbnail(p);
// bitmap = getBytesByGlide(p);
// }
} else {
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " cancelled");
Log.d(LOG_TAG, "getImageBytes with uri=" + p.entry.getUri() + " cancelled");
}
byte[] data = null;
if (bitmap != null) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
// Bitmap.CompressFormat.PNG is slower than JPEG
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
data = stream.toByteArray();
}
return new Result(p, data);
}
@TargetApi(Build.VERSION_CODES.Q)
private Bitmap getBytesByResolverThumbnail(Params params) {
ImageEntry entry = params.entry;
int width = params.width;
int height = params.height;
ContentResolver resolver = activity.getContentResolver();
try {
return resolver.loadThumbnail(entry.getUri(), new Size(width, height), null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private Bitmap getBytesByMediaStoreThumbnail(Params params) {
ImageEntry entry = params.entry;
long contentId = entry.getContentId();
ContentResolver resolver = activity.getContentResolver();
try {
if (entry.isVideo()) {
return MediaStore.Video.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Video.Thumbnails.MINI_KIND, null);
} else {
return MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private Bitmap getBytesByGlide(Params params) {
ImageEntry entry = params.entry;
int width = params.width;
int height = params.height;
// add signature to ignore cache for images which got modified but kept the same URI
Key signature = new ObjectKey("" + entry.getDateModifiedSecs() + entry.getWidth() + entry.getOrientationDegrees());
RequestOptions options = new RequestOptions()
.signature(signature)
.override(width, height);
FutureTarget<Bitmap> target;
if (entry.isVideo()) {
options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE);
target = Glide.with(activity)
.asBitmap()
.apply(options)
.load(new VideoThumbnail(activity, entry.getUri()))
.signature(signature)
.submit(width, height);
} else {
target = Glide.with(activity)
.asBitmap()
.apply(options)
.load(entry.getUri())
.signature(signature)
.submit(width, height);
}
try {
return target.get();
} catch (InterruptedException e) {
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " interrupted");
} catch (Exception e) {
e.printStackTrace();
}
Glide.with(activity).clear(target);
return null;
}
@Override
protected void onPostExecute(Result result) {
MethodChannel.Result r = result.params.result;

View file

@ -109,9 +109,9 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
} catch (ImageProcessingException e) {
getAllVideoMetadataFallback(call, result);
} catch (FileNotFoundException e) {
result.error("getAllMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
result.error("getAllMetadata-filenotfound", "failed to get metadata for path=" + path, e.getMessage());
} catch (Exception e) {
result.error("getAllMetadata-exception", "failed to get metadata for path=" + path, e);
result.error("getAllMetadata-exception", "failed to get metadata for path=" + path, e.getMessage());
}
}
@ -144,7 +144,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
result.success(metadataMap);
} catch (Exception e) {
result.error("getAllVideoMetadataFallback-exception", "failed to get metadata for path=" + path, e);
result.error("getAllVideoMetadataFallback-exception", "failed to get metadata for path=" + path, e.getMessage());
}
}
@ -233,16 +233,16 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
}
}
} catch (Exception e) {
result.error("getCatalogMetadata-exception", "failed to get video metadata for path=" + path, e);
result.error("getCatalogMetadata-exception", "failed to get video metadata for path=" + path, e.getMessage());
}
}
result.success(metadataMap);
} catch (ImageProcessingException e) {
result.error("getCatalogMetadata-imageprocessing", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
result.error("getCatalogMetadata-imageprocessing", "failed to get metadata for path=" + path, e.getMessage());
} catch (FileNotFoundException e) {
result.error("getCatalogMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
result.error("getCatalogMetadata-filenotfound", "failed to get metadata for path=" + path, e.getMessage());
} catch (Exception e) {
result.error("getCatalogMetadata-exception", "failed to get metadata for path=" + path, e);
result.error("getCatalogMetadata-exception", "failed to get metadata for path=" + path, e.getMessage());
}
}
@ -274,11 +274,11 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
}
result.success(metadataMap);
} catch (ImageProcessingException e) {
result.error("getOverlayMetadata-imageprocessing", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
result.error("getOverlayMetadata-imageprocessing", "failed to get metadata for path=" + path, e.getMessage());
} catch (FileNotFoundException e) {
result.error("getOverlayMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
result.error("getOverlayMetadata-filenotfound", "failed to get metadata for path=" + path, e.getMessage());
} catch (Exception e) {
result.error("getOverlayMetadata-exception", "failed to get metadata for path=" + path, e);
result.error("getOverlayMetadata-exception", "failed to get metadata for path=" + path, e.getMessage());
}
}
}

View file

@ -87,6 +87,10 @@ public class ImageEntry {
return uri;
}
public long getContentId() {
return contentId;
}
@Nullable
public String getPath() {
return path;

View file

@ -194,6 +194,11 @@ public abstract class ImageProvider {
}
Bitmap originalImage = BitmapFactory.decodeFile(path);
if (originalImage == null) {
Log.e(LOG_TAG, "failed to decode image at path=" + path);
callback.onFailure();
return;
}
Matrix matrix = new Matrix();
int originalWidth = originalImage.getWidth();
int originalHeight = originalImage.getHeight();
@ -207,14 +212,14 @@ public abstract class ImageProvider {
ParcelFileDescriptor pfd = activity.getContentResolver().openFileDescriptor(uri, "rw");
if (pfd != null) fd = pfd.getFileDescriptor();
} catch (FileNotFoundException e) {
Log.w(LOG_TAG, "failed to get file descriptor for document at uri=" + path, e);
Log.e(LOG_TAG, "failed to get file descriptor for document at uri=" + path, e);
}
if (fd != null) {
try (FileOutputStream fos = new FileOutputStream(fd)) {
rotatedImage.compress(Bitmap.CompressFormat.PNG, 100, fos);
rotated = true;
} catch (IOException e) {
Log.w(LOG_TAG, "failed to save rotated image to document at uri=" + path, e);
Log.e(LOG_TAG, "failed to save rotated image to document at uri=" + path, e);
}
}
} else {
@ -222,7 +227,7 @@ public abstract class ImageProvider {
rotatedImage.compress(Bitmap.CompressFormat.PNG, 100, fos);
rotated = true;
} catch (IOException e) {
Log.w(LOG_TAG, "failed to save rotated image to path=" + path, e);
Log.e(LOG_TAG, "failed to save rotated image to path=" + path, e);
}
}
if (!rotated) {

View file

@ -68,6 +68,7 @@ class _HomePageState extends State<HomePage> {
Future<void> setup() async {
debugPrint('$runtimeType setup start, elapsed=${_stopwatch.elapsed}');
// TODO reduce permission check time
// TODO TLAD ask android.permission.ACCESS_MEDIA_LOCATION (unredacted EXIF with scoped storage)
final permissions = await PermissionHandler().requestPermissions([
PermissionGroup.storage
]); // 350ms

View file

@ -76,7 +76,14 @@ class ImagePreviewState extends State<ImagePreview> with AfterInitMixin {
future: _byteLoader,
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
final bytes = (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) ? snapshot.data : kTransparentImage;
return bytes.isNotEmpty ? widget.builder(bytes) : Icon(Icons.error);
return bytes.isNotEmpty
? widget.builder(bytes)
: Center(
child: Icon(
Icons.error,
color: Colors.blueGrey,
),
);
});
}
}