app icon decoding: use raw image descriptor in Flutter on decoded bytes from Android
This commit is contained in:
parent
d4791df333
commit
c1a99d9be5
8 changed files with 49 additions and 48 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
25
lib/services/common/decoding.dart
Normal file
25
lib/services/common/decoding.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 = {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue