import 'package:aves/convert/convert.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/media/geotiff.dart'; import 'package:aves/model/media/panorama.dart'; import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/overlay.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/services/common/service_policy.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/metadata/xmp.dart'; import 'package:aves/utils/time_utils.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; abstract class MetadataFetchService { // returns Map> (map of directories, each directory being a map of metadata label and value description) Future getAllMetadata(AvesEntry entry); Future getCatalogMetadata(AvesEntry entry, {bool background = false}); Future getOverlayMetadata(AvesEntry entry, Set fields); Future getGeoTiffInfo(AvesEntry entry); Future getMultiPageInfo(AvesEntry entry); Future getPanoramaInfo(AvesEntry entry); Future>?> getIptc(AvesEntry entry); Future getXmp(AvesEntry entry); Future hasContentResolverProp(String prop); Future getContentResolverProp(AvesEntry entry, String prop); Future getDate(AvesEntry entry, MetadataField field); Future> getFields(AvesEntry entry, Set fields); } class PlatformMetadataFetchService implements MetadataFetchService { static const _platform = MethodChannel('deckers.thibault/aves/metadata_fetch'); @override Future getAllMetadata(AvesEntry entry) async { if (entry.isSvg) return {}; try { final result = await _platform.invokeMethod('getAllMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, }); if (result != null) return result as Map; } on PlatformException catch (e, stack) { if (entry.isValid) { await reportService.recordError(e, stack); } } return {}; } @override Future getCatalogMetadata(AvesEntry entry, {bool background = false}) async { if (entry.isSvg) return null; Future call() async { try { // returns map with: // 'mimeType': MIME type as reported by metadata extractors, not Media Store (string) // 'dateMillis': date taken in milliseconds since Epoch (long) // 'isAnimated': animated gif/webp (bool) // 'isFlipped': flipped according to EXIF orientation (bool) // 'rating': rating in [-1,5] (int) // 'rotationDegrees': rotation degrees according to EXIF orientation or other metadata (int) // 'latitude': latitude (double) // 'longitude': longitude (double) // 'xmpSubjects': ';' separated XMP subjects (string) // 'xmpTitle': XMP title (string) final result = await _platform.invokeMethod('getCatalogMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'path': entry.path, 'sizeBytes': entry.sizeBytes, }) as Map; result['id'] = entry.id; AvesEntry.normalizeMimeTypeFields(result); return CatalogMetadata.fromMap(result); } on PlatformException catch (e, stack) { if (entry.isValid) { await reportService.recordError(e, stack); } } return null; } return background ? servicePolicy.call( call, priority: ServiceCallPriority.getMetadata, ) : call(); } @override Future getOverlayMetadata(AvesEntry entry, Set fields) async { if (fields.isNotEmpty && !entry.isSvg) { try { // returns fields on demand, with various value types: // 'aperture' (double), // 'description' (string) // 'exposureTime' (string), // 'focalLength' (double), // 'iso' (int), final result = await _platform.invokeMethod('getOverlayMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, 'fields': fields.map((v) => v.toPlatform).toList(), }) as Map; return OverlayMetadata.fromMap(result); } on PlatformException catch (e, stack) { if (entry.isValid) { await reportService.recordError(e, stack); } } } return const OverlayMetadata(); } @override Future getGeoTiffInfo(AvesEntry entry) async { try { final result = await _platform.invokeMethod('getGeoTiffInfo', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, }) as Map; return GeoTiffInfo.fromMap(result); } on PlatformException catch (e, stack) { if (entry.isValid) { await reportService.recordError(e, stack); } } return null; } @override Future getMultiPageInfo(AvesEntry entry) async { try { final result = await _platform.invokeMethod('getMultiPageInfo', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, 'isMotionPhoto': entry.isMotionPhoto, }); final pageMaps = ((result as List?) ?? []).cast(); if (entry.isMotionPhoto && pageMaps.isNotEmpty) { final imagePage = pageMaps[0]; imagePage['width'] = entry.width; imagePage['height'] = entry.height; imagePage['rotationDegrees'] = entry.rotationDegrees; } pageMaps.forEach(AvesEntry.normalizeMimeTypeFields); return MultiPageInfo.fromPageMaps(entry, pageMaps); } on PlatformException catch (e, stack) { if (entry.isValid) { await reportService.recordError(e, stack); } } return null; } @override Future getPanoramaInfo(AvesEntry entry) async { try { // returns map with values for: // 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int), // 'fullPanoWidth' (int), 'fullPanoHeight' (int) final result = await _platform.invokeMethod('getPanoramaInfo', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, }) as Map; return PanoramaInfo.fromMap(result); } on PlatformException catch (e, stack) { if (entry.isValid) { await reportService.recordError(e, stack); } } return null; } @override Future>?> getIptc(AvesEntry entry) async { try { final result = await _platform.invokeMethod('getIptc', { 'mimeType': entry.mimeType, 'uri': entry.uri, }); if (result != null) return (result as List).cast().map((fields) => fields.cast()).toList(); } on PlatformException catch (e, stack) { if (entry.isValid) { await reportService.recordError(e, stack); } } return null; } @override Future getXmp(AvesEntry entry) async { try { final result = await _platform.invokeMethod('getXmp', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, }); if (result != null) return AvesXmp.fromList((result as List).cast()); } on PlatformException catch (e, stack) { if (entry.isValid) { await reportService.recordError(e, stack); } } return null; } final Map _contentResolverProps = {}; @override Future hasContentResolverProp(String prop) async { var exists = _contentResolverProps[prop]; if (exists != null) return SynchronousFuture(exists); try { exists = await _platform.invokeMethod('hasContentResolverProp', { 'prop': prop, }); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } exists ??= false; _contentResolverProps[prop] = exists; return exists; } @override Future getContentResolverProp(AvesEntry entry, String prop) async { try { return await _platform.invokeMethod('getContentResolverProp', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'prop': prop, }); } on PlatformException catch (e, stack) { if (entry.isValid) { await reportService.recordError(e, stack); } } return null; } @override Future getDate(AvesEntry entry, MetadataField field) async { try { final result = await _platform.invokeMethod('getDate', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, 'field': field.toPlatform, }); if (result is int) { return dateTimeFromMillis(result, isUtc: false); } } on PlatformException catch (e, stack) { if (entry.isValid) { await reportService.recordError(e, stack); } } return null; } @override Future> getFields(AvesEntry entry, Set fields) async { if (fields.isNotEmpty && !entry.isSvg) { try { final result = await _platform.invokeMethod('getFields', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, 'fields': fields.map((v) => v.toPlatform).toList(), }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { if (entry.isValid) { await reportService.recordError(e, stack); } } } return {}; } }