async metadata loading
This commit is contained in:
parent
6c8441642c
commit
0c30bfd19e
7 changed files with 149 additions and 177 deletions
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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]),
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue