#100 fixed wrong app album color on startup

This commit is contained in:
Thibault Deckers 2021-10-07 16:38:33 +09:00
parent 051c6f5846
commit 9a2451ea0c
13 changed files with 86 additions and 41 deletions

View file

@ -1,6 +1,6 @@
import 'dart:ui' as ui show Codec;
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/common/services.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
@ -39,7 +39,7 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderCallback decode) async {
try {
final bytes = await AndroidAppService.getAppIcon(key.packageName, key.size);
final bytes = await androidAppService.getAppIcon(key.packageName, key.size);
return await decode(bytes.isEmpty ? kTransparentImage : bytes);
} catch (error) {
debugPrint('$runtimeType _loadAsync failed with packageName=$packageName, error=$error');

View file

@ -10,10 +10,35 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart';
class AndroidAppService {
abstract class AndroidAppService {
Future<Set<Package>> getPackages();
Future<Uint8List> getAppIcon(String packageName, double size);
Future<bool> copyToClipboard(String uri, String? label);
Future<bool> edit(String uri, String mimeType);
Future<bool> open(String uri, String mimeType);
Future<bool> openMap(LatLng latLng);
Future<bool> setAs(String uri, String mimeType);
Future<bool> shareEntries(Iterable<AvesEntry> entries);
Future<bool> shareSingle(String uri, String mimeType);
Future<bool> canPinToHomeScreen();
Future<void> pinToHomeScreen(String label, AvesEntry? entry, Set<CollectionFilter> filters);
}
class PlatformAndroidAppService implements AndroidAppService {
static const platform = MethodChannel('deckers.thibault/aves/app');
static Future<Set<Package>> getPackages() async {
@override
Future<Set<Package>> getPackages() async {
try {
final result = await platform.invokeMethod('getPackages');
final packages = (result as List).cast<Map>().map((map) => Package.fromMap(map)).toSet();
@ -29,7 +54,8 @@ class AndroidAppService {
return {};
}
static Future<Uint8List> getAppIcon(String packageName, double size) async {
@override
Future<Uint8List> getAppIcon(String packageName, double size) async {
try {
final result = await platform.invokeMethod('getAppIcon', <String, dynamic>{
'packageName': packageName,
@ -42,7 +68,8 @@ class AndroidAppService {
return Uint8List(0);
}
static Future<bool> copyToClipboard(String uri, String? label) async {
@override
Future<bool> copyToClipboard(String uri, String? label) async {
try {
final result = await platform.invokeMethod('copyToClipboard', <String, dynamic>{
'uri': uri,
@ -55,7 +82,8 @@ class AndroidAppService {
return false;
}
static Future<bool> edit(String uri, String mimeType) async {
@override
Future<bool> edit(String uri, String mimeType) async {
try {
final result = await platform.invokeMethod('edit', <String, dynamic>{
'uri': uri,
@ -68,7 +96,8 @@ class AndroidAppService {
return false;
}
static Future<bool> open(String uri, String mimeType) async {
@override
Future<bool> open(String uri, String mimeType) async {
try {
final result = await platform.invokeMethod('open', <String, dynamic>{
'uri': uri,
@ -81,7 +110,8 @@ class AndroidAppService {
return false;
}
static Future<bool> openMap(LatLng latLng) async {
@override
Future<bool> openMap(LatLng latLng) async {
final latitude = roundToPrecision(latLng.latitude, decimals: 6);
final longitude = roundToPrecision(latLng.longitude, decimals: 6);
final geoUri = 'geo:$latitude,$longitude?q=$latitude,$longitude';
@ -97,7 +127,8 @@ class AndroidAppService {
return false;
}
static Future<bool> setAs(String uri, String mimeType) async {
@override
Future<bool> setAs(String uri, String mimeType) async {
try {
final result = await platform.invokeMethod('setAs', <String, dynamic>{
'uri': uri,
@ -110,7 +141,8 @@ class AndroidAppService {
return false;
}
static Future<bool> shareEntries(Iterable<AvesEntry> entries) async {
@override
Future<bool> shareEntries(Iterable<AvesEntry> entries) async {
// loosen mime type to a generic one, so we can share with badly defined apps
// e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats
final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
@ -125,7 +157,8 @@ class AndroidAppService {
return false;
}
static Future<bool> shareSingle(String uri, String mimeType) async {
@override
Future<bool> shareSingle(String uri, String mimeType) async {
try {
final result = await platform.invokeMethod('share', <String, dynamic>{
'urisByMimeType': {
@ -142,9 +175,10 @@ class AndroidAppService {
// app shortcuts
// this ability will not change over the lifetime of the app
static bool? _canPin;
bool? _canPin;
static Future<bool> canPinToHomeScreen() async {
@override
Future<bool> canPinToHomeScreen() async {
if (_canPin != null) return SynchronousFuture(_canPin!);
try {
@ -159,7 +193,8 @@ class AndroidAppService {
return false;
}
static Future<void> pinToHomeScreen(String label, AvesEntry? entry, Set<CollectionFilter> filters) async {
@override
Future<void> pinToHomeScreen(String label, AvesEntry? entry, Set<CollectionFilter> filters) async {
Uint8List? iconBytes;
if (entry != null) {
final size = entry.isVideo ? 0.0 : 256.0;

View file

@ -1,5 +1,6 @@
import 'package:aves/model/availability.dart';
import 'package:aves/model/metadata_db.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/device_service.dart';
import 'package:aves/services/media/embedded_data_service.dart';
import 'package:aves/services/media/media_file_service.dart';
@ -18,6 +19,7 @@ final p.Context pContext = getIt<p.Context>();
final AvesAvailability availability = getIt<AvesAvailability>();
final MetadataDb metadataDb = getIt<MetadataDb>();
final AndroidAppService androidAppService = getIt<AndroidAppService>();
final DeviceService deviceService = getIt<DeviceService>();
final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
final MediaFileService mediaFileService = getIt<MediaFileService>();
@ -33,6 +35,7 @@ void initPlatformServices() {
getIt.registerLazySingleton<AvesAvailability>(() => LiveAvesAvailability());
getIt.registerLazySingleton<MetadataDb>(() => SqfliteMetadataDb());
getIt.registerLazySingleton<AndroidAppService>(() => PlatformAndroidAppService());
getIt.registerLazySingleton<DeviceService>(() => PlatformDeviceService());
getIt.registerLazySingleton<EmbeddedDataService>(() => PlatformEmbeddedDataService());
getIt.registerLazySingleton<MediaFileService>(() => PlatformMediaFileService());

View file

@ -1,4 +1,3 @@
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/change_notifier.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
@ -36,15 +35,15 @@ class AndroidFileUtils {
// from Aves
videoCapturesPath = pContext.join(dcimPath, 'Video Captures');
_initialized = true;
}
Future<void> initAppNames() async {
// include package fetching in initialization
// to avoid app album color flickering
if (_packages.isEmpty) {
_packages = await AndroidAppService.getPackages();
_packages = await androidAppService.getPackages();
_potentialAppDirs = _launcherPackages.expand((package) => package.potentialDirs).toList();
appNameChangeNotifier.notifyListeners();
}
_initialized = true;
}
bool isCameraPath(String path) => path.startsWith(dcimPath) && (path.endsWith('${separator}Camera') || path.endsWith('${separator}100ANDRO'));

View file

@ -9,7 +9,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
import 'package:aves/widgets/collection/filter_bar.dart';
@ -61,7 +61,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
vsync: this,
);
_isSelectingNotifier.addListener(_onActivityChange);
_canAddShortcutsLoader = AndroidAppService.canPinToHomeScreen();
_canAddShortcutsLoader = androidAppService.canPinToHomeScreen();
_registerWidget(widget);
WidgetsBinding.instance!.addPostFrameCallback((_) => _onFilterChanged());
}
@ -363,7 +363,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
final name = result.item2;
if (name.isEmpty) return;
unawaited(AndroidAppService.pinToHomeScreen(name, coverEntry, filters));
unawaited(androidAppService.pinToHomeScreen(name, coverEntry, filters));
}
void _goToSearch() {

View file

@ -9,7 +9,6 @@ import 'package:aves/model/highlight.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/enums.dart';
@ -66,7 +65,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
void _share(BuildContext context) {
final selection = context.read<Selection<AvesEntry>>();
final selectedItems = _getExpandedSelectedItems(selection);
AndroidAppService.shareEntries(selectedItems).then((success) {
androidAppService.shareEntries(selectedItems).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
}

View file

@ -1,5 +1,5 @@
import 'package:aves/image_providers/app_icon_image_provider.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/viewer/info/common.dart';
@ -21,7 +21,7 @@ class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with Au
@override
void initState() {
super.initState();
_loader = AndroidAppService.getPackages();
_loader = androidAppService.getPackages();
}
@override

View file

@ -66,7 +66,6 @@ class _HomePageState extends State<HomePage> {
}
await androidFileUtils.init();
unawaited(androidFileUtils.initAppNames());
var appMode = AppMode.main;
final intentData = widget.intentData ?? await ViewerService.getIntentData();

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:aves/model/entry.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
@ -56,10 +55,10 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin {
final uri = fields['uri']!;
if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) {
// open with another app
unawaited(AndroidAppService.open(uri, mimeType).then((success) {
unawaited(androidAppService.open(uri, mimeType).then((success) {
if (!success) {
// fallback to sharing, so that the file can be saved somewhere
AndroidAppService.shareSingle(uri, mimeType).then((success) {
androidAppService.shareSingle(uri, mimeType).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
}

View file

@ -10,7 +10,6 @@ import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/enums.dart';
@ -39,7 +38,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
entry.toggleFavourite();
break;
case EntryAction.copyToClipboard:
AndroidAppService.copyToClipboard(entry.uri, entry.bestTitle).then((success) {
androidAppService.copyToClipboard(entry.uri, entry.bestTitle).then((success) {
showFeedback(context, success ? context.l10n.genericSuccessFeedback : context.l10n.genericFailureFeedback);
});
break;
@ -68,17 +67,17 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
_flip(context, entry);
break;
case EntryAction.edit:
AndroidAppService.edit(entry.uri, entry.mimeType).then((success) {
androidAppService.edit(entry.uri, entry.mimeType).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
break;
case EntryAction.open:
AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype).then((success) {
androidAppService.open(entry.uri, entry.mimeTypeAnySubtype).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
break;
case EntryAction.openMap:
AndroidAppService.openMap(entry.latLng!).then((success) {
androidAppService.openMap(entry.latLng!).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
break;
@ -86,12 +85,12 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
_rotateScreen(context);
break;
case EntryAction.setAs:
AndroidAppService.setAs(entry.uri, entry.mimeType).then((success) {
androidAppService.setAs(entry.uri, entry.mimeType).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
break;
case EntryAction.share:
AndroidAppService.shareEntries({entry}).then((success) {
androidAppService.shareEntries({entry}).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
break;

View file

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:aves/model/actions/video_actions.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/theme/icons.dart';
@ -74,7 +74,7 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
scale: scale,
child: IconButton(
icon: const Icon(AIcons.openOutside),
onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype),
onPressed: () => androidAppService.open(entry.uri, entry.mimeTypeAnySubtype),
tooltip: context.l10n.viewerOpenTooltip,
),
),

View file

@ -0,0 +1,9 @@
import 'package:aves/services/android_app_service.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeAndroidAppService extends Fake implements AndroidAppService {
@override
Future<Set<Package>> getPackages() => SynchronousFuture({});
}

View file

@ -8,6 +8,7 @@ import 'package:aves/model/metadata_db.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/media_store_source.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/device_service.dart';
import 'package:aves/services/media/media_file_service.dart';
@ -21,6 +22,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import '../fake/android_app_service.dart';
import '../fake/availability.dart';
import '../fake/device_service.dart';
import '../fake/media_file_service.dart';
@ -42,6 +44,7 @@ void main() {
getIt.registerLazySingleton<AvesAvailability>(() => FakeAvesAvailability());
getIt.registerLazySingleton<MetadataDb>(() => FakeMetadataDb());
getIt.registerLazySingleton<AndroidAppService>(() => FakeAndroidAppService());
getIt.registerLazySingleton<DeviceService>(() => FakeDeviceService());
getIt.registerLazySingleton<MediaFileService>(() => FakeMediaFileService());
getIt.registerLazySingleton<MediaStoreService>(() => FakeMediaStoreService());