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;
|
package deckers.thibault.aves.channelhandlers;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.ResolveInfo;
|
import android.content.pm.ResolveInfo;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
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.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import deckers.thibault.aves.utils.Utils;
|
|
||||||
import io.flutter.plugin.common.MethodCall;
|
import io.flutter.plugin.common.MethodCall;
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
|
||||||
|
import static com.bumptech.glide.request.RequestOptions.centerCropTransform;
|
||||||
|
|
||||||
public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
|
public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
|
||||||
public static final String CHANNEL = "deckers.thibault/aves/app";
|
public static final String CHANNEL = "deckers.thibault/aves/app";
|
||||||
|
|
||||||
private static final String LOG_TAG = Utils.createLogTag(AppAdapterHandler.class);
|
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
public AppAdapterHandler(Context context) {
|
public AppAdapterHandler(Context context) {
|
||||||
|
@ -31,20 +38,13 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||||
Log.d(LOG_TAG, "onMethodCall method=" + call.method + ", arguments=" + call.arguments);
|
|
||||||
switch (call.method) {
|
switch (call.method) {
|
||||||
case "getAppNames": {
|
case "getAppIcon": {
|
||||||
result.success(getAppNames());
|
new Thread(() -> getAppIcon(call, new MethodResultWrapper(result))).start();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "getAppIcon": {
|
case "getAppNames": {
|
||||||
String packageName = call.argument("packageName");
|
result.success(getAppNames());
|
||||||
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);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "edit": {
|
case "edit": {
|
||||||
|
@ -109,8 +109,57 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
|
||||||
return nameMap;
|
return nameMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getAppIcon(String packageName, int size, MethodChannel.Result result) {
|
private void getAppIcon(MethodCall call, MethodChannel.Result result) {
|
||||||
new AppIconDecodeTask().execute(new AppIconDecodeTask.Params(context, packageName, size, 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) {
|
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) {
|
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||||
switch (call.method) {
|
switch (call.method) {
|
||||||
case "getAllMetadata":
|
case "getAllMetadata":
|
||||||
getAllMetadata(call, result);
|
new Thread(() -> getAllMetadata(call, new MethodResultWrapper(result))).start();
|
||||||
break;
|
break;
|
||||||
case "getCatalogMetadata":
|
case "getCatalogMetadata":
|
||||||
getCatalogMetadata(call, result);
|
new Thread(() -> getCatalogMetadata(call, new MethodResultWrapper(result))).start();
|
||||||
break;
|
break;
|
||||||
case "getOverlayMetadata":
|
case "getOverlayMetadata":
|
||||||
getOverlayMetadata(call, result);
|
new Thread(() -> getOverlayMetadata(call, new MethodResultWrapper(result))).start();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
result.notImplemented();
|
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/model/image_entry.dart';
|
||||||
import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart';
|
import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart';
|
||||||
import 'package:aves/widgets/fullscreen/image_page.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/info/info_page.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/bottom.dart';
|
import 'package:aves/widgets/fullscreen/overlay/bottom.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/top.dart';
|
import 'package:aves/widgets/fullscreen/overlay/top.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/video.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/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -169,9 +169,10 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
builder: (context, page, child) {
|
builder: (context, page, child) {
|
||||||
final showOverlay = _entry != null && page == imagePage;
|
final showOverlay = _entry != null && page == imagePage;
|
||||||
final videoController = showOverlay && _entry.isVideo ? _videoControllers.firstWhere((kv) => kv.item1 == _entry.uri, orElse: () => null)?.item2 : null;
|
final videoController = showOverlay && _entry.isVideo ? _videoControllers.firstWhere((kv) => kv.item1 == _entry.uri, orElse: () => null)?.item2 : null;
|
||||||
return showOverlay
|
return Positioned(
|
||||||
? Positioned(
|
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: showOverlay ? 1 : 0,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (videoController != null)
|
if (videoController != null)
|
||||||
|
@ -194,8 +195,8 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: const SizedBox.shrink();
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -67,7 +67,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
return SliverList(
|
return SliverList(
|
||||||
delegate: SliverChildListDelegate(
|
delegate: SliverChildListDelegate(
|
||||||
[
|
[
|
||||||
const SectionRow('Metadata'),
|
if (_metadata.isNotEmpty) const SectionRow('Metadata'),
|
||||||
...directoriesWithoutTitle.map((dir) => InfoRowGroup(dir.tags)),
|
...directoriesWithoutTitle.map((dir) => InfoRowGroup(dir.tags)),
|
||||||
Theme(
|
Theme(
|
||||||
data: Theme.of(context).copyWith(cardColor: Colors.grey[900]),
|
data: Theme.of(context).copyWith(cardColor: Colors.grey[900]),
|
||||||
|
|
|
@ -54,8 +54,10 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(FullscreenBottomOverlay oldWidget) {
|
void didUpdateWidget(FullscreenBottomOverlay oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (entry != _lastEntry) {
|
||||||
_initDetailLoader();
|
_initDetailLoader();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _initDetailLoader() {
|
void _initDetailLoader() {
|
||||||
_detailLoader = MetadataService.getOverlayMetadata(entry);
|
_detailLoader = MetadataService.getOverlayMetadata(entry);
|
||||||
|
@ -79,8 +81,6 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.black26,
|
color: Colors.black26,
|
||||||
padding: viewInsets + viewPadding.copyWith(top: 0),
|
padding: viewInsets + viewPadding.copyWith(top: 0),
|
||||||
child: Padding(
|
|
||||||
padding: innerPadding,
|
|
||||||
child: FutureBuilder(
|
child: FutureBuilder(
|
||||||
future: _detailLoader,
|
future: _detailLoader,
|
||||||
builder: (futureContext, AsyncSnapshot<OverlayMetadata> snapshot) {
|
builder: (futureContext, AsyncSnapshot<OverlayMetadata> snapshot) {
|
||||||
|
@ -90,15 +90,18 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
||||||
}
|
}
|
||||||
return _lastEntry == null
|
return _lastEntry == null
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: _FullscreenBottomOverlayContent(
|
: Padding(
|
||||||
|
// keep padding inside `FutureBuilder` so that overlay takes no space until data is ready
|
||||||
|
padding: innerPadding,
|
||||||
|
child: _FullscreenBottomOverlayContent(
|
||||||
entry: _lastEntry,
|
entry: _lastEntry,
|
||||||
details: _lastDetails,
|
details: _lastDetails,
|
||||||
position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null,
|
position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null,
|
||||||
maxWidth: overlayContentMaxWidth,
|
maxWidth: overlayContentMaxWidth,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue