diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java index 79bb32486..395392296 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java @@ -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 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) { diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppIconDecodeTask.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppIconDecodeTask.java deleted file mode 100644 index 5ad4e3472..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppIconDecodeTask.java +++ /dev/null @@ -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 { - 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 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); - } - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java index 6c6891653..fa16e58f3 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java @@ -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(); diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MethodResultWrapper.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MethodResultWrapper.java new file mode 100644 index 000000000..0033cdc5f --- /dev/null +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MethodResultWrapper.java @@ -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()); + } +} \ No newline at end of file diff --git a/lib/widgets/fullscreen/fullscreen_body.dart b/lib/widgets/fullscreen/fullscreen_body.dart index e9e337bce..355a89334 100644 --- a/lib/widgets/fullscreen/fullscreen_body.dart +++ b/lib/widgets/fullscreen/fullscreen_body.dart @@ -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 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(); + ], + ), + ), + ); }, ), ], diff --git a/lib/widgets/fullscreen/info/metadata_section.dart b/lib/widgets/fullscreen/info/metadata_section.dart index 85818c50a..66f10e177 100644 --- a/lib/widgets/fullscreen/info/metadata_section.dart +++ b/lib/widgets/fullscreen/info/metadata_section.dart @@ -67,7 +67,7 @@ class _MetadataSectionSliverState extends State 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]), diff --git a/lib/widgets/fullscreen/overlay/bottom.dart b/lib/widgets/fullscreen/overlay/bottom.dart index 12feb3f09..1b4ae6a72 100644 --- a/lib/widgets/fullscreen/overlay/bottom.dart +++ b/lib/widgets/fullscreen/overlay/bottom.dart @@ -54,7 +54,9 @@ class _FullscreenBottomOverlayState extends State { @override void didUpdateWidget(FullscreenBottomOverlay oldWidget) { super.didUpdateWidget(oldWidget); - _initDetailLoader(); + if (entry != _lastEntry) { + _initDetailLoader(); + } } void _initDetailLoader() { @@ -79,25 +81,26 @@ class _FullscreenBottomOverlayState extends State { return Container( color: Colors.black26, padding: viewInsets + viewPadding.copyWith(top: 0), - child: Padding( - padding: innerPadding, - child: FutureBuilder( - future: _detailLoader, - builder: (futureContext, AsyncSnapshot 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 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, - ); - }, - ), + ), + ); + }, ), ); },