app icon decoding: use raw image descriptor in Flutter on decoded bytes from Android

This commit is contained in:
Thibault Deckers 2025-03-02 18:48:48 +01:00
parent d4791df333
commit c1a99d9be5
8 changed files with 49 additions and 48 deletions

View file

@ -38,7 +38,7 @@ import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getEncodedBytes
import deckers.thibault.aves.utils.BitmapUtils.getDecodedBytes
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.anyCauseIs
import deckers.thibault.aves.utils.getApplicationInfoCompat
@ -154,7 +154,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter
val density = context.resources.displayMetrics.density
val size = (sizeDip * density).roundToInt()
var data: ByteArray? = null
var bytes: ByteArray? = null
try {
val iconResourceId = context.packageManager.getApplicationInfoCompat(packageName, 0).icon
if (iconResourceId != Resources.ID_NULL) {
@ -175,7 +175,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
try {
val bitmap = withContext(Dispatchers.IO) { target.get() }
data = bitmap?.getEncodedBytes(canHaveAlpha = true, recycle = false)
bytes = bitmap?.getDecodedBytes(recycle = false)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e)
}
@ -185,8 +185,8 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to get app info for packageName=$packageName", e)
return
}
if (data != null) {
result.success(data)
if (bytes != null) {
result.success(bytes)
} else {
result.error("getAppIcon-null", "failed to get icon for packageName=$packageName", null)
}

View file

@ -1,10 +1,10 @@
import 'dart:ui' as ui;
import 'package:aves/services/common/services.dart';
import 'package:aves_report/aves_report.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:transparent_image/transparent_image.dart';
class AppIconImage extends ImageProvider<AppIconImageKey> {
const AppIconImage({
@ -39,12 +39,14 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
Future<ui.Codec> _loadAsync(AppIconImageKey key, ImageDecoderCallback decode) async {
try {
final bytes = await appService.getAppIcon(key.packageName, key.size);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes);
return await decode(buffer);
final descriptor = await appService.getAppIcon(key.packageName, key.size);
if (descriptor == null) {
throw UnreportedStateError('$packageName app icon decoding failed');
}
return descriptor.instantiateCodec();
} catch (error) {
debugPrint('$runtimeType _loadAsync failed with packageName=$packageName, error=$error');
throw StateError('$packageName app icon decoding failed');
throw UnreportedStateError('$packageName app icon decoding failed');
}
}
}

View file

@ -369,11 +369,6 @@ class Dependencies {
license: bsd3,
sourceUrl: 'https://github.com/dart-lang/stack_trace',
),
Dependency(
name: 'Transparent Image',
license: mit,
sourceUrl: 'https://github.com/brianegan/transparent_image',
),
Dependency(
name: 'Vector Math',
license: '$zlib, $bsd3',

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'dart:ui';
import 'package:aves/geo/uri.dart';
@ -6,6 +7,7 @@ import 'package:aves/model/app_inventory.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/decoding.dart';
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
@ -15,7 +17,7 @@ import 'package:streams_channel/streams_channel.dart';
abstract class AppService {
Future<Set<Package>> getPackages();
Future<Uint8List> getAppIcon(String packageName, double size);
Future<ui.ImageDescriptor?> getAppIcon(String packageName, double size);
Future<bool> copyToClipboard(String uri, String? label);
@ -75,17 +77,20 @@ class PlatformAppService implements AppService {
}
@override
Future<Uint8List> getAppIcon(String packageName, double size) async {
Future<ui.ImageDescriptor?> getAppIcon(String packageName, double size) async {
try {
final result = await _platform.invokeMethod('getAppIcon', <String, dynamic>{
'packageName': packageName,
'sizeDip': size,
});
if (result != null) return result as Uint8List;
if (result != null) {
final bytes = result as Uint8List;
return InteropDecoding.bytesToCodec(bytes);
}
} on PlatformException catch (_) {
// ignore, as some packages legitimately do not have icons
}
return Uint8List(0);
return null;
}
@override

View file

@ -0,0 +1,25 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class InteropDecoding {
static Future<ui.ImageDescriptor?> bytesToCodec(Uint8List bytes) async {
const trailerLength = 4 * 2;
if (bytes.length < trailerLength) return null;
final trailerOffset = bytes.length - trailerLength;
final trailer = ByteData.sublistView(bytes, trailerOffset);
final bitmapWidth = trailer.getUint32(0);
final bitmapHeight = trailer.getUint32(4);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return ui.ImageDescriptor.raw(
buffer,
width: bitmapWidth,
height: bitmapHeight,
pixelFormat: ui.PixelFormat.rgba8888,
);
}
}

View file

@ -4,6 +4,7 @@ import 'dart:math';
import 'package:aves/model/app/support.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/decoding.dart';
import 'package:aves/services/common/output_buffer.dart';
import 'package:aves/services/common/service_policy.dart';
import 'package:aves/services/common/services.dart';
@ -194,7 +195,7 @@ class PlatformMediaFetchService implements MediaFetchService {
});
if (result != null) {
final bytes = result as Uint8List;
return _bytesToCodec(bytes);
return InteropDecoding.bytesToCodec(bytes);
}
} on PlatformException catch (e, stack) {
if (_isUnknownVisual(mimeType)) {
@ -237,7 +238,7 @@ class PlatformMediaFetchService implements MediaFetchService {
});
if (result != null) {
final bytes = result as Uint8List;
return _bytesToCodec(bytes);
return InteropDecoding.bytesToCodec(bytes);
}
} on PlatformException catch (e, stack) {
if (_isUnknownVisual(mimeType)) {
@ -271,24 +272,6 @@ class PlatformMediaFetchService implements MediaFetchService {
// convenience methods
Future<ui.ImageDescriptor?> _bytesToCodec(Uint8List bytes) async {
const trailerLength = 4 * 2;
if (bytes.length < trailerLength) return null;
final trailerOffset = bytes.length - trailerLength;
final trailer = ByteData.sublistView(bytes, trailerOffset);
final bitmapWidth = trailer.getUint32(0);
final bitmapHeight = trailer.getUint32(4);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return ui.ImageDescriptor.raw(
buffer,
width: bitmapWidth,
height: bitmapHeight,
pixelFormat: ui.PixelFormat.rgba8888,
);
}
bool _isUnknownVisual(String mimeType) => !_knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType);
static const Set<String> _knownOpaqueImages = {

View file

@ -1525,14 +1525,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.8"
transparent_image:
dependency: "direct main"
description:
name: transparent_image
sha256: e8991d955a2094e197ca24c645efec2faf4285772a4746126ca12875e54ca02f
url: "https://pub.dev"
source: hosted
version: "2.0.1"
typed_data:
dependency: transitive
description:

View file

@ -123,7 +123,6 @@ dependencies:
streams_channel:
git:
url: https://github.com/deckerst/aves_streams_channel.git
transparent_image:
url_launcher:
vector_map_tiles: ^8.0.0 # vector_map_tiles v9.0.0-beta.6 has a buggy cross-platform definition for `cacheFolder`
vector_math: