#100 fixed wrong app album color on startup
This commit is contained in:
parent
051c6f5846
commit
9a2451ea0c
13 changed files with 86 additions and 41 deletions
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
9
test/fake/android_app_service.dart
Normal file
9
test/fake/android_app_service.dart
Normal 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({});
|
||||
}
|
|
@ -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());
|
||||
|
|
Loading…
Reference in a new issue