service review

This commit is contained in:
Thibault Deckers 2021-09-11 14:34:05 +09:00
parent 9fdb42892e
commit 4ae828710d
43 changed files with 227 additions and 282 deletions

View file

@ -53,18 +53,16 @@ class MainActivity : FlutterActivity() {
// dart -> platform -> dart
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(AppShortcutHandler(this))
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler())
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(ImageFileHandler(this))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
MethodChannel(messenger, GlobalSearchHandler.CHANNEL).setMethodCallHandler(GlobalSearchHandler(this))
MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
MethodChannel(messenger, TimeHandler.CHANNEL).setMethodCallHandler(TimeHandler())
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this))
// result streaming: dart -> platform ->->-> dart

View file

@ -4,17 +4,25 @@ import android.content.*
import android.content.pm.ApplicationInfo
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.FileProvider
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.R
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.getBytes
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall
@ -39,6 +47,8 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
"openMap" -> safe(call, result, ::openMap)
"setAs" -> safe(call, result, ::setAs)
"share" -> safe(call, result, ::share)
"canPin" -> safe(call, result, ::canPin)
"pin" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pin) }
else -> result.notImplemented()
}
}
@ -307,6 +317,64 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}
}
// shortcuts
private fun isPinSupported() = ShortcutManagerCompat.isRequestPinShortcutSupported(context)
private fun canPin(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(isPinSupported())
}
private fun pin(call: MethodCall, result: MethodChannel.Result) {
val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes")
val filters = call.argument<List<String>>("filters")
if (label == null || filters == null) {
result.error("pin-args", "failed because of missing arguments", null)
return
}
if (!isPinSupported()) {
result.error("pin-unsupported", "failed because the launcher does not support pinning shortcuts", null)
return
}
var icon: IconCompat? = null
if (iconBytes?.isNotEmpty() == true) {
var bitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size)
bitmap = BitmapUtils.centerSquareCrop(context, bitmap, 256)
if (bitmap != null) {
// adaptive, so the bitmap is used as background and covers the whole icon
icon = IconCompat.createWithAdaptiveBitmap(bitmap)
}
}
if (icon == null) {
// shortcut adaptive icons are placed in `mipmap`, not `drawable`,
// so that foreground is rendered at the intended scale
val supportAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
icon = IconCompat.createWithResource(context, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_collection else R.drawable.ic_shortcut_collection)
}
val intent = Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra("page", "/collection")
.putExtra("filters", filters.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback
.putExtra("filtersString", filters.joinToString(MainActivity.EXTRA_STRING_ARRAY_SEPARATOR))
// multiple shortcuts sharing the same ID cannot be created with different labels or icons
// so we provide a unique ID for each one, and let the user manage duplicates (i.e. same filter set), if any
val shortcut = ShortcutInfoCompat.Builder(context, UUID.randomUUID().toString())
.setShortLabel(label)
.setIcon(icon)
.setIntent(intent)
.build()
ShortcutManagerCompat.requestPinShortcut(context, shortcut, null)
result.success(true)
}
companion object {
private val LOG_TAG = LogUtils.createTag<AppAdapterHandler>()
const val CHANNEL = "deckers.thibault/aves/app"

View file

@ -1,90 +0,0 @@
package deckers.thibault.aves.channel.calls
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Build
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.utils.BitmapUtils.centerSquareCrop
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.*
class AppShortcutHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"canPin" -> safe(call, result, ::canPin)
"pin" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pin) }
else -> result.notImplemented()
}
}
private fun isSupported() = ShortcutManagerCompat.isRequestPinShortcutSupported(context)
private fun canPin(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(isSupported())
}
private fun pin(call: MethodCall, result: MethodChannel.Result) {
val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes")
val filters = call.argument<List<String>>("filters")
if (label == null || filters == null) {
result.error("pin-args", "failed because of missing arguments", null)
return
}
if (!isSupported()) {
result.error("pin-unsupported", "failed because the launcher does not support pinning shortcuts", null)
return
}
var icon: IconCompat? = null
if (iconBytes?.isNotEmpty() == true) {
var bitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size)
bitmap = centerSquareCrop(context, bitmap, 256)
if (bitmap != null) {
// adaptive, so the bitmap is used as background and covers the whole icon
icon = IconCompat.createWithAdaptiveBitmap(bitmap)
}
}
if (icon == null) {
// shortcut adaptive icons are placed in `mipmap`, not `drawable`,
// so that foreground is rendered at the intended scale
val supportAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
icon = IconCompat.createWithResource(context, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_collection else R.drawable.ic_shortcut_collection)
}
val intent = Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra("page", "/collection")
.putExtra("filters", filters.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback
.putExtra("filtersString", filters.joinToString(MainActivity.EXTRA_STRING_ARRAY_SEPARATOR))
// multiple shortcuts sharing the same ID cannot be created with different labels or icons
// so we provide a unique ID for each one, and let the user manage duplicates (i.e. same filter set), if any
val shortcut = ShortcutInfoCompat.Builder(context, UUID.randomUUID().toString())
.setShortLabel(label)
.setIcon(icon)
.setIntent(intent)
.build()
ShortcutManagerCompat.requestPinShortcut(context, shortcut, null)
result.success(true)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/shortcut"
}
}

View file

@ -5,15 +5,21 @@ import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import java.util.*
class DeviceHandler : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
else -> result.notImplemented()
}
}
private fun getDefaultTimeZone(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(TimeZone.getDefault().id)
}
private fun getPerformanceClass(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
result.success(Build.VERSION.MEDIA_PERFORMANCE_CLASS)

View file

@ -23,7 +23,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
private val density = activity.resources.displayMetrics.density
private val regionFetcher = RegionFetcher(activity)
@ -196,6 +196,6 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
}
companion object {
const val CHANNEL = "deckers.thibault/aves/image"
const val CHANNEL = "deckers.thibault/aves/media_file"
}
}

View file

@ -38,6 +38,6 @@ class MediaStoreHandler(private val activity: Activity) : MethodCallHandler {
}
companion object {
const val CHANNEL = "deckers.thibault/aves/mediastore"
const val CHANNEL = "deckers.thibault/aves/media_store"
}
}

View file

@ -1,24 +0,0 @@
package deckers.thibault.aves.channel.calls
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import java.util.*
class TimeHandler : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
else -> result.notImplemented()
}
}
private fun getDefaultTimeZone(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(TimeZone.getDefault().id)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/time"
}
}

View file

@ -187,7 +187,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
companion object {
private val LOG_TAG = LogUtils.createTag<ImageByteStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/image_byte_stream"
const val CHANNEL = "deckers.thibault/aves/media_byte_stream"
private const val BUFFER_SIZE = 2 shl 17 // 256kB

View file

@ -177,6 +177,6 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
companion object {
private val LOG_TAG = LogUtils.createTag<ImageOpStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/image_op_stream"
const val CHANNEL = "deckers.thibault/aves/media_op_stream"
}
}

View file

@ -59,6 +59,6 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
companion object {
private val LOG_TAG = LogUtils.createTag<MediaStoreChangeStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/mediastorechange"
const val CHANNEL = "deckers.thibault/aves/media_store_change"
}
}

View file

@ -62,6 +62,6 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
companion object {
private val LOG_TAG = LogUtils.createTag<MediaStoreStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/mediastorestream"
const val CHANNEL = "deckers.thibault/aves/media_store_stream"
}
}

View file

@ -33,7 +33,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
final mimeType = key.mimeType;
final pageId = key.pageId;
try {
final bytes = await imageFileService.getRegion(
final bytes = await mediaFileService.getRegion(
uri,
mimeType,
key.rotationDegrees,
@ -56,11 +56,11 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
@override
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, RegionProviderKey key, ImageErrorListener handleError) {
imageFileService.resumeLoading(key);
mediaFileService.resumeLoading(key);
super.resolveStreamForKey(configuration, stream, key, handleError);
}
void pause() => imageFileService.cancelRegion(key);
void pause() => mediaFileService.cancelRegion(key);
}
@immutable

View file

@ -35,7 +35,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
final mimeType = key.mimeType;
final pageId = key.pageId;
try {
final bytes = await imageFileService.getThumbnail(
final bytes = await mediaFileService.getThumbnail(
uri: uri,
mimeType: mimeType,
pageId: pageId,
@ -57,11 +57,11 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
@override
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) {
imageFileService.resumeLoading(key);
mediaFileService.resumeLoading(key);
super.resolveStreamForKey(configuration, stream, key, handleError);
}
void pause() => imageFileService.cancelThumbnail(key);
void pause() => mediaFileService.cancelThumbnail(key);
}
@immutable

View file

@ -49,7 +49,7 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
assert(key == this);
try {
final bytes = await imageFileService.getImage(
final bytes = await mediaFileService.getImage(
uri,
mimeType,
rotationDegrees,

View file

@ -615,7 +615,7 @@ class AvesEntry {
Future<bool> delete() {
final completer = Completer<bool>();
imageFileService.delete([this]).listen(
mediaFileService.delete([this]).listen(
(event) => completer.complete(event.success),
onError: completer.completeError,
onDone: () {

View file

@ -11,7 +11,6 @@ import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/map_style.dart';
import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/services/device_service.dart';
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
import 'package:firebase_core/firebase_core.dart';
@ -128,7 +127,7 @@ class Settings extends ChangeNotifier {
Future<void> setContextualDefaults() async {
// performance
final performanceClass = await DeviceService.getPerformanceClass();
final performanceClass = await deviceService.getPerformanceClass();
enableOverlayBlurEffect = performanceClass >= 30;
// availability

View file

@ -159,7 +159,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
Future<bool> renameEntry(AvesEntry entry, String newName, {required bool persist}) async {
if (newName == entry.filenameWithoutExtension) return true;
final newFields = await imageFileService.rename(entry, '$newName${entry.extension}');
final newFields = await mediaFileService.rename(entry, '$newName${entry.extension}');
if (newFields.isEmpty) return false;
await _moveEntry(entry, newFields, persist: persist);

View file

@ -25,7 +25,7 @@ class MediaStoreSource extends CollectionSource {
await metadataDb.init();
await favourites.init();
await covers.init();
final currentTimeZone = await timeService.getDefaultTimeZone();
final currentTimeZone = await deviceService.getDefaultTimeZone();
if (currentTimeZone != null) {
final catalogTimeZone = settings.catalogTimeZone;
if (currentTimeZone != catalogTimeZone) {
@ -153,7 +153,7 @@ class MediaStoreSource extends CollectionSource {
for (final kv in uriByContentId.entries) {
final contentId = kv.key;
final uri = kv.value;
final sourceEntry = await imageFileService.getEntry(uri, null);
final sourceEntry = await mediaFileService.getEntry(uri, null);
if (sourceEntry != null) {
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
// compare paths because some apps move files without updating their `last modified date`

View file

@ -1,10 +1,12 @@
import 'dart:typed_data';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart';
@ -136,4 +138,49 @@ class AndroidAppService {
}
return false;
}
// app shortcuts
// this ability will not change over the lifetime of the app
static bool? _canPin;
static Future<bool> canPinToHomeScreen() async {
if (_canPin != null) return SynchronousFuture(_canPin!);
try {
final result = await platform.invokeMethod('canPin');
if (result != null) {
_canPin = result;
return result;
}
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
static Future<void> pinToHomeScreen(String label, AvesEntry? entry, Set<CollectionFilter> filters) async {
Uint8List? iconBytes;
if (entry != null) {
final size = entry.isVideo ? 0.0 : 256.0;
iconBytes = await mediaFileService.getThumbnail(
uri: entry.uri,
mimeType: entry.mimeType,
pageId: entry.pageId,
rotationDegrees: entry.rotationDegrees,
isFlipped: entry.isFlipped,
dateModifiedSecs: entry.dateModifiedSecs,
extent: size,
);
}
try {
await platform.invokeMethod('pin', <String, dynamic>{
'label': label,
'iconBytes': iconBytes,
'filters': filters.map((filter) => filter.toJson()).toList(),
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
}

View file

@ -1,54 +0,0 @@
import 'dart:typed_data';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class AppShortcutService {
static const platform = MethodChannel('deckers.thibault/aves/shortcut');
// this ability will not change over the lifetime of the app
static bool? _canPin;
static Future<bool> canPin() async {
if (_canPin != null) return SynchronousFuture(_canPin!);
try {
final result = await platform.invokeMethod('canPin');
if (result != null) {
_canPin = result;
return result;
}
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
static Future<void> pin(String label, AvesEntry? entry, Set<CollectionFilter> filters) async {
Uint8List? iconBytes;
if (entry != null) {
final size = entry.isVideo ? 0.0 : 256.0;
iconBytes = await imageFileService.getThumbnail(
uri: entry.uri,
mimeType: entry.mimeType,
pageId: entry.pageId,
rotationDegrees: entry.rotationDegrees,
isFlipped: entry.isFlipped,
dateModifiedSecs: entry.dateModifiedSecs,
extent: size,
);
}
try {
await platform.invokeMethod('pin', <String, dynamic>{
'label': label,
'iconBytes': iconBytes,
'filters': filters.map((filter) => filter.toJson()).toList(),
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
}

View file

@ -1,13 +1,13 @@
import 'package:aves/model/availability.dart';
import 'package:aves/model/metadata_db.dart';
import 'package:aves/services/embedded_data_service.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/services/media_store_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';
import 'package:aves/services/media/media_store_service.dart';
import 'package:aves/services/metadata/metadata_edit_service.dart';
import 'package:aves/services/metadata/metadata_fetch_service.dart';
import 'package:aves/services/report_service.dart';
import 'package:aves/services/storage_service.dart';
import 'package:aves/services/time_service.dart';
import 'package:aves/services/window_service.dart';
import 'package:get_it/get_it.dart';
import 'package:path/path.dart' as p;
@ -18,14 +18,14 @@ final p.Context pContext = getIt<p.Context>();
final AvesAvailability availability = getIt<AvesAvailability>();
final MetadataDb metadataDb = getIt<MetadataDb>();
final DeviceService deviceService = getIt<DeviceService>();
final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
final ImageFileService imageFileService = getIt<ImageFileService>();
final MediaFileService mediaFileService = getIt<MediaFileService>();
final MediaStoreService mediaStoreService = getIt<MediaStoreService>();
final MetadataEditService metadataEditService = getIt<MetadataEditService>();
final MetadataFetchService metadataFetchService = getIt<MetadataFetchService>();
final ReportService reportService = getIt<ReportService>();
final StorageService storageService = getIt<StorageService>();
final TimeService timeService = getIt<TimeService>();
final WindowService windowService = getIt<WindowService>();
void initPlatformServices() {
@ -33,13 +33,13 @@ void initPlatformServices() {
getIt.registerLazySingleton<AvesAvailability>(() => LiveAvesAvailability());
getIt.registerLazySingleton<MetadataDb>(() => SqfliteMetadataDb());
getIt.registerLazySingleton<DeviceService>(() => PlatformDeviceService());
getIt.registerLazySingleton<EmbeddedDataService>(() => PlatformEmbeddedDataService());
getIt.registerLazySingleton<ImageFileService>(() => PlatformImageFileService());
getIt.registerLazySingleton<MediaFileService>(() => PlatformMediaFileService());
getIt.registerLazySingleton<MediaStoreService>(() => PlatformMediaStoreService());
getIt.registerLazySingleton<MetadataEditService>(() => PlatformMetadataEditService());
getIt.registerLazySingleton<MetadataFetchService>(() => PlatformMetadataFetchService());
getIt.registerLazySingleton<ReportService>(() => CrashlyticsReportService());
getIt.registerLazySingleton<StorageService>(() => PlatformStorageService());
getIt.registerLazySingleton<TimeService>(() => PlatformTimeService());
getIt.registerLazySingleton<WindowService>(() => PlatformWindowService());
}

View file

@ -1,10 +1,27 @@
import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
class DeviceService {
abstract class DeviceService {
Future<String?> getDefaultTimeZone();
Future<int> getPerformanceClass();
}
class PlatformDeviceService implements DeviceService {
static const platform = MethodChannel('deckers.thibault/aves/device');
static Future<int> getPerformanceClass() async {
@override
Future<String?> getDefaultTimeZone() async {
try {
return await platform.invokeMethod('getDefaultTimeZone');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return null;
}
@override
Future<int> getPerformanceClass() async {
try {
await platform.invokeMethod('getPerformanceClass');
final result = await platform.invokeMethod('getPerformanceClass');

View file

@ -13,7 +13,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart';
abstract class ImageFileService {
abstract class MediaFileService {
Future<AvesEntry?> getEntry(String uri, String? mimeType);
Future<Uint8List> getSvg(
@ -92,10 +92,10 @@ abstract class ImageFileService {
Future<Map<String, dynamic>> rename(AvesEntry entry, String newName);
}
class PlatformImageFileService implements ImageFileService {
static const platform = MethodChannel('deckers.thibault/aves/image');
static final StreamsChannel _byteStreamChannel = StreamsChannel('deckers.thibault/aves/image_byte_stream');
static final StreamsChannel _opStreamChannel = StreamsChannel('deckers.thibault/aves/image_op_stream');
class PlatformMediaFileService implements MediaFileService {
static const platform = MethodChannel('deckers.thibault/aves/media_file');
static final StreamsChannel _byteStreamChannel = StreamsChannel('deckers.thibault/aves/media_byte_stream');
static final StreamsChannel _opStreamChannel = StreamsChannel('deckers.thibault/aves/media_op_stream');
static const double thumbnailDefaultSize = 64.0;
static Map<String, dynamic> _toPlatformEntryMap(AvesEntry entry) {

View file

@ -12,11 +12,14 @@ abstract class MediaStoreService {
// knownEntries: map of contentId -> dateModifiedSecs
Stream<AvesEntry> getEntries(Map<int, int> knownEntries);
// returns media URI
Future<Uri?> scanFile(String path, String mimeType);
}
class PlatformMediaStoreService implements MediaStoreService {
static const platform = MethodChannel('deckers.thibault/aves/mediastore');
static final StreamsChannel _streamChannel = StreamsChannel('deckers.thibault/aves/mediastorestream');
static const platform = MethodChannel('deckers.thibault/aves/media_store');
static final StreamsChannel _streamChannel = StreamsChannel('deckers.thibault/aves/media_store_stream');
@override
Future<List<int>> checkObsoleteContentIds(List<int> knownContentIds) async {
@ -55,4 +58,19 @@ class PlatformMediaStoreService implements MediaStoreService {
return Stream.error(e);
}
}
// returns media URI
@override
Future<Uri?> scanFile(String path, String mimeType) async {
try {
final result = await platform.invokeMethod('scanFile', <String, dynamic>{
'path': path,
'mimeType': mimeType,
});
if (result != null) return Uri.tryParse(result);
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return null;
}
}

View file

@ -18,7 +18,7 @@ class SvgMetadataService {
static Future<Size?> getSize(AvesEntry entry) async {
try {
final data = await imageFileService.getSvg(entry.uri, entry.mimeType);
final data = await mediaFileService.getSvg(entry.uri, entry.mimeType);
final document = XmlDocument.parse(utf8.decode(data));
final root = document.rootElement;
@ -64,7 +64,7 @@ class SvgMetadataService {
}
try {
final data = await imageFileService.getSvg(entry.uri, entry.mimeType);
final data = await mediaFileService.getSvg(entry.uri, entry.mimeType);
final document = XmlDocument.parse(utf8.decode(data));
final root = document.rootElement;

View file

@ -4,7 +4,6 @@ import 'dart:typed_data';
import 'package:aves/services/common/output_buffer.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart';
@ -27,9 +26,6 @@ abstract class StorageService {
// returns number of deleted directories
Future<int> deleteEmptyDirectories(Iterable<String> dirPaths);
// returns media URI
Future<Uri?> scanFile(String path, String mimeType);
// return whether operation succeeded (`null` if user cancelled)
Future<bool?> createFile(String name, String mimeType, Uint8List bytes);
@ -154,22 +150,6 @@ class PlatformStorageService implements StorageService {
return 0;
}
// returns media URI
@override
Future<Uri?> scanFile(String path, String mimeType) async {
debugPrint('scanFile with path=$path, mimeType=$mimeType');
try {
final result = await platform.invokeMethod('scanFile', <String, dynamic>{
'path': path,
'mimeType': mimeType,
});
if (result != null) return Uri.tryParse(result);
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return null;
}
@override
Future<bool?> createFile(String name, String mimeType, Uint8List bytes) async {
try {

View file

@ -1,20 +0,0 @@
import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
abstract class TimeService {
Future<String?> getDefaultTimeZone();
}
class PlatformTimeService implements TimeService {
static const platform = MethodChannel('deckers.thibault/aves/time');
@override
Future<String?> getDefaultTimeZone() async {
try {
return await platform.invokeMethod('getDefaultTimeZone');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return null;
}
}

View file

@ -41,7 +41,7 @@ class _AvesAppState extends State<AvesApp> {
// observers are not registered when using the same list object with different items
// the list itself needs to be reassigned
List<NavigatorObserver> _navigatorObservers = [];
final EventChannel _mediaStoreChangeChannel = const EventChannel('deckers.thibault/aves/mediastorechange');
final EventChannel _mediaStoreChangeChannel = const EventChannel('deckers.thibault/aves/media_store_change');
final EventChannel _newIntentChannel = const EventChannel('deckers.thibault/aves/intent');
final EventChannel _errorChannel = const EventChannel('deckers.thibault/aves/error');
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');

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/app_shortcut_service.dart';
import 'package:aves/services/android_app_service.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 = AppShortcutService.canPin();
_canAddShortcutsLoader = AndroidAppService.canPinToHomeScreen();
_registerWidget(widget);
WidgetsBinding.instance!.addPostFrameCallback((_) => _onFilterChanged());
}
@ -370,7 +370,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
final name = result.item2;
if (name.isEmpty) return;
unawaited(AppShortcutService.pin(name, coverEntry, filters));
unawaited(AndroidAppService.pinToHomeScreen(name, coverEntry, filters));
}
void _goToSearch() {

View file

@ -122,7 +122,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
source.pauseMonitoring();
showOpReport<MoveOpEvent>(
context: context,
opStream: imageFileService.move(todoEntries, copy: copy, destinationAlbum: destinationAlbum),
opStream: mediaFileService.move(todoEntries, copy: copy, destinationAlbum: destinationAlbum),
itemCount: todoCount,
onDone: (processed) async {
final movedOps = processed.where((e) => e.success).toSet();
@ -222,7 +222,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
source.pauseMonitoring();
showOpReport<ImageOpEvent>(
context: context,
opStream: imageFileService.delete(selectedItems),
opStream: mediaFileService.delete(selectedItems),
itemCount: todoCount,
onDone: (processed) async {
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();

View file

@ -155,7 +155,7 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
if (widget.cancellableNotifier?.value ?? false) {
final key = await _currentProviderStream?.provider.provider.obtainKey(ImageConfiguration.empty);
if (key is ThumbnailProviderKey) {
imageFileService.cancelThumbnail(key);
mediaFileService.cancelThumbnail(key);
}
}
}

View file

@ -45,7 +45,7 @@ class _DebugCacheSectionState extends State<DebugCacheSection> with AutomaticKee
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: imageFileService.clearSizedThumbnailDiskCache,
onPressed: mediaFileService.clearSizedThumbnailDiskCache,
child: const Text('Clear'),
),
],

View file

@ -167,7 +167,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
source.pauseMonitoring();
showOpReport<ImageOpEvent>(
context: context,
opStream: imageFileService.delete(todoEntries),
opStream: mediaFileService.delete(todoEntries),
itemCount: todoCount,
onDone: (processed) async {
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
@ -226,7 +226,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
source.pauseMonitoring();
showOpReport<MoveOpEvent>(
context: context,
opStream: imageFileService.move(todoEntries, copy: false, destinationAlbum: destinationAlbum),
opStream: mediaFileService.move(todoEntries, copy: false, destinationAlbum: destinationAlbum),
itemCount: todoCount,
onDone: (processed) async {
final movedOps = processed.where((e) => e.success).toSet();

View file

@ -125,7 +125,7 @@ class _HomePageState extends State<HomePage> {
}
Future<AvesEntry?> _initViewerEntry({required String uri, required String? mimeType}) async {
final entry = await imageFileService.getEntry(uri, mimeType);
final entry = await mediaFileService.getEntry(uri, mimeType);
if (entry != null) {
// cataloguing is essential for coordinates and video rotation
await entry.catalog(background: false, persist: false);

View file

@ -204,7 +204,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
showOpReport<ExportOpEvent>(
context: context,
// TODO TLAD [SVG] export separately from raster images (sending bytes, like frame captures)
opStream: imageFileService.export(
opStream: mediaFileService.export(
selection,
mimeType: MimeTypes.jpeg,
destinationAlbum: destinationAlbum,
@ -286,7 +286,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
MaterialPageRoute(
settings: const RouteSettings(name: SourceViewerPage.routeName),
builder: (context) => SourceViewerPage(
loader: () => imageFileService.getSvg(entry.uri, entry.mimeType).then(utf8.decode),
loader: () => mediaFileService.getSvg(entry.uri, entry.mimeType).then(utf8.decode),
),
),
);

View file

@ -74,7 +74,7 @@ class EntryPrinter with FeedbackMixin {
Future<pdf.Widget?> _buildPageImage(AvesEntry entry) async {
if (entry.isSvg) {
final bytes = await imageFileService.getSvg(entry.uri, entry.mimeType);
final bytes = await mediaFileService.getSvg(entry.uri, entry.mimeType);
if (bytes.isNotEmpty) {
return pdf.SvgImage(svg: utf8.decode(bytes));
}

View file

@ -85,7 +85,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
}
};
final newFields = await imageFileService.captureFrame(
final newFields = await mediaFileService.captureFrame(
entry,
desiredName: '${entry.bestTitle}_${'$positionMillis'.padLeft(8, '0')}',
exif: exif,

View file

@ -1,8 +1,8 @@
import 'package:aves/services/time_service.dart';
import 'package:aves/services/device_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeTimeService extends Fake implements TimeService {
class FakeDeviceService extends Fake implements DeviceService {
@override
Future<String> getDefaultTimeZone() => SynchronousFuture('');
}

View file

@ -1,11 +1,11 @@
import 'package:aves/model/entry.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/services/media/media_file_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'media_store_service.dart';
class FakeImageFileService extends Fake implements ImageFileService {
class FakeMediaFileService extends Fake implements MediaFileService {
@override
Future<Map<String, dynamic>> rename(AvesEntry entry, String newName) {
final contentId = FakeMediaStoreService.nextContentId;

View file

@ -1,7 +1,7 @@
import 'package:aves/model/entry.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/media_store_service.dart';
import 'package:aves/services/media/media_store_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';

View file

@ -8,12 +8,12 @@ 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/image_file_service.dart';
import 'package:aves/services/media_store_service.dart';
import 'package:aves/services/metadata/metadata_fetch_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';
import 'package:aves/services/media/media_store_service.dart';
import 'package:aves/services/metadata/metadata_fetch_service.dart';
import 'package:aves/services/storage_service.dart';
import 'package:aves/services/time_service.dart';
import 'package:aves/services/window_service.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:flutter/widgets.dart';
@ -21,12 +21,12 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import '../fake/availability.dart';
import '../fake/image_file_service.dart';
import '../fake/device_service.dart';
import '../fake/media_file_service.dart';
import '../fake/media_store_service.dart';
import '../fake/metadata_db.dart';
import '../fake/metadata_fetch_service.dart';
import '../fake/storage_service.dart';
import '../fake/time_service.dart';
import '../fake/window_service.dart';
void main() {
@ -40,11 +40,11 @@ void main() {
getIt.registerLazySingleton<AvesAvailability>(() => FakeAvesAvailability());
getIt.registerLazySingleton<MetadataDb>(() => FakeMetadataDb());
getIt.registerLazySingleton<ImageFileService>(() => FakeImageFileService());
getIt.registerLazySingleton<DeviceService>(() => FakeDeviceService());
getIt.registerLazySingleton<MediaFileService>(() => FakeMediaFileService());
getIt.registerLazySingleton<MediaStoreService>(() => FakeMediaStoreService());
getIt.registerLazySingleton<MetadataFetchService>(() => FakeMetadataFetchService());
getIt.registerLazySingleton<StorageService>(() => FakeStorageService());
getIt.registerLazySingleton<TimeService>(() => FakeTimeService());
getIt.registerLazySingleton<WindowService>(() => FakeWindowService());
await settings.init();

View file

@ -4,7 +4,7 @@ import 'package:aves/main.dart' as app;
import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/storage_service.dart';
import 'package:aves/services/media/media_store_service.dart';
import 'package:aves/services/window_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/src/widgets/media_query.dart';
@ -19,9 +19,9 @@ void main() {
// scan files copied from test assets
// we do it via the app instead of broadcasting via ADB
// because `MEDIA_SCANNER_SCAN_FILE` intent got deprecated in API 29
final storageService = PlatformStorageService();
storageService.scanFile(p.join(targetPicturesDir, 'aves_logo.svg'), 'image/svg+xml');
storageService.scanFile(p.join(targetPicturesDir, 'ipse.jpg'), 'image/jpeg');
final mediaStoreService = PlatformMediaStoreService();
mediaStoreService.scanFile(p.join(targetPicturesDir, 'aves_logo.svg'), 'image/svg+xml');
mediaStoreService.scanFile(p.join(targetPicturesDir, 'ipse.jpg'), 'image/jpeg');
configureAndLaunch();
}