async metadata loading

This commit is contained in:
Thibault Deckers 2020-03-23 16:07:48 +09:00
parent 6c8441642c
commit 0c30bfd19e
7 changed files with 149 additions and 177 deletions

View file

@ -1,28 +1,35 @@
package deckers.thibault.aves.channelhandlers;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.request.FutureTarget;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.signature.ObjectKey;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import deckers.thibault.aves.utils.Utils;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import static com.bumptech.glide.request.RequestOptions.centerCropTransform;
public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
public static final String CHANNEL = "deckers.thibault/aves/app";
private static final String LOG_TAG = Utils.createLogTag(AppAdapterHandler.class);
private Context context;
public AppAdapterHandler(Context context) {
@ -31,20 +38,13 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Log.d(LOG_TAG, "onMethodCall method=" + call.method + ", arguments=" + call.arguments);
switch (call.method) {
case "getAppNames": {
result.success(getAppNames());
case "getAppIcon": {
new Thread(() -> getAppIcon(call, new MethodResultWrapper(result))).start();
break;
}
case "getAppIcon": {
String packageName = call.argument("packageName");
Integer size = call.argument("size");
if (packageName == null || size == null) {
result.error("getAppIcon-args", "failed because of missing arguments", null);
return;
}
getAppIcon(packageName, size, result);
case "getAppNames": {
result.success(getAppNames());
break;
}
case "edit": {
@ -109,8 +109,57 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
return nameMap;
}
private void getAppIcon(String packageName, int size, MethodChannel.Result result) {
new AppIconDecodeTask().execute(new AppIconDecodeTask.Params(context, packageName, size, result));
private void getAppIcon(MethodCall call, MethodChannel.Result result) {
String packageName = call.argument("packageName");
Integer size = call.argument("size");
if (packageName == null || size == null) {
result.error("getAppIcon-args", "failed because of missing arguments", null);
return;
}
byte[] data = null;
try {
int iconResourceId = context.getPackageManager().getApplicationInfo(packageName, 0).icon;
Uri uri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.authority(packageName)
.path(String.valueOf(iconResourceId))
.build();
// add signature to ignore cache for images which got modified but kept the same URI
Key signature = new ObjectKey(packageName + size);
RequestOptions options = new RequestOptions()
.signature(signature)
.override(size, size);
FutureTarget<Bitmap> target = Glide.with(context)
.asBitmap()
.apply(options)
.apply(centerCropTransform())
.load(uri)
.signature(signature)
.submit(size, size);
try {
Bitmap bmp = target.get();
if (bmp != null) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
data = stream.toByteArray();
}
} catch (Exception e) {
e.printStackTrace();
}
Glide.with(context).clear(target);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return;
}
if (data != null) {
result.success(data);
} else {
result.error("getAppIcon-null", "failed to get icon for packageName=" + packageName, null);
}
}
private void edit(String title, Uri uri, String mimeType) {

View file

@ -1,113 +0,0 @@
package deckers.thibault.aves.channelhandlers;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.request.FutureTarget;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.signature.ObjectKey;
import java.io.ByteArrayOutputStream;
import deckers.thibault.aves.utils.Utils;
import io.flutter.plugin.common.MethodChannel;
import static com.bumptech.glide.request.RequestOptions.centerCropTransform;
public class AppIconDecodeTask extends AsyncTask<AppIconDecodeTask.Params, Void, AppIconDecodeTask.Result> {
private static final String LOG_TAG = Utils.createLogTag(AppIconDecodeTask.class);
static class Params {
Context context;
String packageName;
int size;
MethodChannel.Result result;
Params(Context context, String packageName, int size, MethodChannel.Result result) {
this.context = context;
this.packageName = packageName;
this.size = size;
this.result = result;
}
}
static class Result {
Params params;
byte[] data;
Result(Params params, byte[] data) {
this.params = params;
this.data = data;
}
}
@Override
protected Result doInBackground(Params... params) {
Params p = params[0];
Context context = p.context;
String packageName = p.packageName;
int size = p.size;
byte[] data = null;
if (!this.isCancelled()) {
try {
int iconResourceId = context.getPackageManager().getApplicationInfo(packageName, 0).icon;
Uri uri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.authority(packageName)
.path(String.valueOf(iconResourceId))
.build();
// add signature to ignore cache for images which got modified but kept the same URI
Key signature = new ObjectKey(packageName + size);
RequestOptions options = new RequestOptions()
.signature(signature)
.override(size, size);
FutureTarget<Bitmap> target = Glide.with(context)
.asBitmap()
.apply(options)
.apply(centerCropTransform())
.load(uri)
.signature(signature)
.submit(size, size);
try {
Bitmap bmp = target.get();
if (bmp != null) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
data = stream.toByteArray();
}
} catch (InterruptedException e) {
Log.d(LOG_TAG, "getAppIcon with packageName=" + packageName + " interrupted");
} catch (Exception e) {
e.printStackTrace();
}
Glide.with(context).clear(target);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
} else {
Log.d(LOG_TAG, "getAppIcon with packageName=" + packageName + " cancelled");
}
return new Result(p, data);
}
@Override
protected void onPostExecute(Result result) {
MethodChannel.Result r = result.params.result;
if (result.data != null) {
r.success(result.data);
} else {
r.error("getAppIcon-null", "failed to get icon for packageName=" + result.params.packageName, null);
}
}
}

View file

@ -55,13 +55,13 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "getAllMetadata":
getAllMetadata(call, result);
new Thread(() -> getAllMetadata(call, new MethodResultWrapper(result))).start();
break;
case "getCatalogMetadata":
getCatalogMetadata(call, result);
new Thread(() -> getCatalogMetadata(call, new MethodResultWrapper(result))).start();
break;
case "getOverlayMetadata":
getOverlayMetadata(call, result);
new Thread(() -> getOverlayMetadata(call, new MethodResultWrapper(result))).start();
break;
default:
result.notImplemented();

View file

@ -0,0 +1,32 @@
package deckers.thibault.aves.channelhandlers;
import android.os.Handler;
import android.os.Looper;
import io.flutter.plugin.common.MethodChannel;
// ensure `result` methods are called on the main looper thread
public class MethodResultWrapper implements MethodChannel.Result {
private MethodChannel.Result methodResult;
private Handler handler;
MethodResultWrapper(MethodChannel.Result result) {
methodResult = result;
handler = new Handler(Looper.getMainLooper());
}
@Override
public void success(final Object result) {
handler.post(() -> methodResult.success(result));
}
@Override
public void error(final String errorCode, final String errorMessage, final Object errorDetails) {
handler.post(() -> methodResult.error(errorCode, errorMessage, errorDetails));
}
@Override
public void notImplemented() {
handler.post(() -> methodResult.notImplemented());
}
}

View file

@ -5,11 +5,11 @@ import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart';
import 'package:aves/widgets/fullscreen/image_page.dart';
import 'package:aves/widgets/fullscreen/uri_image_provider.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:aves/widgets/fullscreen/overlay/bottom.dart';
import 'package:aves/widgets/fullscreen/overlay/top.dart';
import 'package:aves/widgets/fullscreen/overlay/video.dart';
import 'package:aves/widgets/fullscreen/uri_image_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
@ -169,33 +169,34 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
builder: (context, page, child) {
final showOverlay = _entry != null && page == imagePage;
final videoController = showOverlay && _entry.isVideo ? _videoControllers.firstWhere((kv) => kv.item1 == _entry.uri, orElse: () => null)?.item2 : null;
return showOverlay
? Positioned(
bottom: 0,
child: Column(
children: [
if (videoController != null)
VideoControlOverlay(
entry: _entry,
controller: videoController,
scale: _bottomOverlayScale,
viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding,
),
SlideTransition(
position: _bottomOverlayOffset,
child: FullscreenBottomOverlay(
entries: entries,
index: _currentHorizontalPage,
showPosition: hasCollection,
viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding,
),
),
],
return Positioned(
bottom: 0,
child: Opacity(
opacity: showOverlay ? 1 : 0,
child: Column(
children: [
if (videoController != null)
VideoControlOverlay(
entry: _entry,
controller: videoController,
scale: _bottomOverlayScale,
viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding,
),
SlideTransition(
position: _bottomOverlayOffset,
child: FullscreenBottomOverlay(
entries: entries,
index: _currentHorizontalPage,
showPosition: hasCollection,
viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding,
),
),
)
: const SizedBox.shrink();
],
),
),
);
},
),
],

View file

@ -67,7 +67,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
return SliverList(
delegate: SliverChildListDelegate(
[
const SectionRow('Metadata'),
if (_metadata.isNotEmpty) const SectionRow('Metadata'),
...directoriesWithoutTitle.map((dir) => InfoRowGroup(dir.tags)),
Theme(
data: Theme.of(context).copyWith(cardColor: Colors.grey[900]),

View file

@ -54,7 +54,9 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
@override
void didUpdateWidget(FullscreenBottomOverlay oldWidget) {
super.didUpdateWidget(oldWidget);
_initDetailLoader();
if (entry != _lastEntry) {
_initDetailLoader();
}
}
void _initDetailLoader() {
@ -79,25 +81,26 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
return Container(
color: Colors.black26,
padding: viewInsets + viewPadding.copyWith(top: 0),
child: Padding(
padding: innerPadding,
child: FutureBuilder(
future: _detailLoader,
builder: (futureContext, AsyncSnapshot<OverlayMetadata> snapshot) {
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
_lastDetails = snapshot.data;
_lastEntry = entry;
}
return _lastEntry == null
? const SizedBox.shrink()
: _FullscreenBottomOverlayContent(
child: FutureBuilder(
future: _detailLoader,
builder: (futureContext, AsyncSnapshot<OverlayMetadata> snapshot) {
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
_lastDetails = snapshot.data;
_lastEntry = entry;
}
return _lastEntry == null
? const SizedBox.shrink()
: Padding(
// keep padding inside `FutureBuilder` so that overlay takes no space until data is ready
padding: innerPadding,
child: _FullscreenBottomOverlayContent(
entry: _lastEntry,
details: _lastDetails,
position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null,
maxWidth: overlayContentMaxWidth,
);
},
),
),
);
},
),
);
},