handle pick intent

This commit is contained in:
Thibault Deckers 2020-05-18 18:02:46 +09:00
parent e2cb03909a
commit fb7df6fcf2
11 changed files with 133 additions and 64 deletions

View file

@ -55,11 +55,22 @@
<intent-filter tools:ignore="AppLinkUrlError"> <intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" /> <data android:mimeType="image/*" />
<data android:mimeType="video/*" /> <data android:mimeType="video/*" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.OPENABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PICK" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<meta-data <meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="false" /> android:value="false" />
@ -77,7 +88,7 @@
<meta-data <meta-data
android:name="com.google.android.geo.API_KEY" android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyDf-1dN6JivrQGKSmxAdxERLM2egOvzGWs" /> android:value="AIzaSyDf-1dN6JivrQGKSmxAdxERLM2egOvzGWs" />
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"

View file

@ -32,7 +32,7 @@ public class MainActivity extends FlutterActivity {
public static final String VIEWER_CHANNEL = "deckers.thibault/aves/viewer"; public static final String VIEWER_CHANNEL = "deckers.thibault/aves/viewer";
private Map<String, String> sharedEntryMap; private Map<String, String> intentDataMap;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -60,23 +60,47 @@ public class MainActivity extends FlutterActivity {
new MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler( new MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler(
(call, result) -> { (call, result) -> {
if (call.method.contentEquals("getSharedEntry")) { if (call.method.contentEquals("getIntentData")) {
result.success(sharedEntryMap); result.success(intentDataMap);
sharedEntryMap = null; intentDataMap = null;
} else if (call.method.contentEquals("pick")) {
result.success(intentDataMap);
intentDataMap = null;
String resultUri = call.argument("uri");
if (resultUri != null) {
Intent data = new Intent();
data.setData(Uri.parse(resultUri));
setResult(RESULT_OK, data);
finish();
} else {
setResult(RESULT_CANCELED);
finish();
}
} }
}); });
} }
private void handleIntent(Intent intent) { private void handleIntent(Intent intent) {
Log.i(LOG_TAG, "handleIntent intent=" + intent); Log.i(LOG_TAG, "handleIntent intent=" + intent);
if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) { if (intent == null) return;
Uri uri = intent.getData(); String action = intent.getAction();
String mimeType = intent.getType(); if (action == null) return;
if (uri != null && mimeType != null) { switch (action) {
sharedEntryMap = new HashMap<>(); case Intent.ACTION_VIEW:
sharedEntryMap.put("uri", uri.toString()); Uri uri = intent.getData();
sharedEntryMap.put("mimeType", mimeType); String mimeType = intent.getType();
} if (uri != null && mimeType != null) {
intentDataMap = new HashMap<>();
intentDataMap.put("action", "view");
intentDataMap.put("uri", uri.toString());
intentDataMap.put("mimeType", mimeType);
}
break;
case Intent.ACTION_GET_CONTENT:
case Intent.ACTION_PICK:
intentDataMap = new HashMap<>();
intentDataMap.put("action", "pick");
break;
} }
} }

View file

@ -20,7 +20,11 @@ void main() {
runApp(AvesApp()); runApp(AvesApp());
} }
enum AppMode { main, pick, view }
class AvesApp extends StatelessWidget { class AvesApp extends StatelessWidget {
static AppMode mode = AppMode.main;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
@ -56,7 +60,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
MediaStoreSource _mediaStore; MediaStoreSource _mediaStore;
ImageEntry _sharedEntry; ImageEntry _viewerEntry;
Future<void> _appSetup; Future<void> _appSetup;
@override @override
@ -87,18 +91,36 @@ class _HomePageState extends State<HomePage> {
await settings.init(); // <20ms await settings.init(); // <20ms
final sharedExtra = await ViewerService.getSharedEntry(); final intentData = await ViewerService.getIntentData();
if (sharedExtra != null) { if (intentData != null) {
_sharedEntry = await ImageFileService.getImageEntry(sharedExtra['uri'], sharedExtra['mimeType']); final action = intentData['action'];
// cataloging is essential for geolocation and video rotation switch (action) {
await _sharedEntry.catalog(); case 'view':
unawaited(_sharedEntry.locate()); AvesApp.mode = AppMode.view;
} else { await _initViewerEntry(
uri: intentData['uri'],
mimeType: intentData['mimeType'],
);
break;
case 'pick':
AvesApp.mode = AppMode.pick;
break;
}
}
if (AvesApp.mode != AppMode.view) {
_mediaStore = MediaStoreSource(); _mediaStore = MediaStoreSource();
unawaited(_mediaStore.fetch()); unawaited(_mediaStore.fetch());
} }
} }
Future<void> _initViewerEntry({@required String uri, @required String mimeType}) async {
_viewerEntry = await ImageFileService.getImageEntry(uri, mimeType);
// cataloging is essential for geolocation and video rotation
await _viewerEntry.catalog();
unawaited(_viewerEntry.locate());
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder( return FutureBuilder(
@ -107,8 +129,8 @@ class _HomePageState extends State<HomePage> {
if (snapshot.hasError) return const Icon(AIcons.error); if (snapshot.hasError) return const Icon(AIcons.error);
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
debugPrint('$runtimeType app setup future complete'); debugPrint('$runtimeType app setup future complete');
if (_sharedEntry != null) { if (AvesApp.mode == AppMode.view) {
return SingleFullscreenPage(entry: _sharedEntry); return SingleFullscreenPage(entry: _viewerEntry);
} }
if (_mediaStore != null) { if (_mediaStore != null) {
return CollectionPage(CollectionLens( return CollectionPage(CollectionLens(

View file

@ -37,7 +37,7 @@ class AlbumFilter extends CollectionFilter {
Future<Color> color(BuildContext context) { Future<Color> color(BuildContext context) {
// do not use async/await and rely on `SynchronousFuture` // do not use async/await and rely on `SynchronousFuture`
// to prevent rebuilding of the `FutureBuilder` listening on this future // to prevent rebuilding of the `FutureBuilder` listening on this future
if (androidFileUtils.getAlbumType(album) == AlbumType.App) { if (androidFileUtils.getAlbumType(album) == AlbumType.app) {
if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]); if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]);
return PaletteGenerator.fromImageProvider( return PaletteGenerator.fromImageProvider(

View file

@ -4,13 +4,23 @@ import 'package:flutter/services.dart';
class ViewerService { class ViewerService {
static const platform = MethodChannel('deckers.thibault/aves/viewer'); static const platform = MethodChannel('deckers.thibault/aves/viewer');
static Future<Map> getSharedEntry() async { static Future<Map> getIntentData() async {
try { try {
// return nullable map with: 'uri' 'mimeType' // return nullable map with 'action' and possibly 'uri' 'mimeType'
return await platform.invokeMethod('getSharedEntry') as Map; return await platform.invokeMethod('getIntentData') as Map;
} on PlatformException catch (e) { } on PlatformException catch (e) {
debugPrint('getSharedEntry failed with code=${e.code}, exception=${e.message}, details=${e.details}'); debugPrint('getIntentData failed with code=${e.code}, exception=${e.message}, details=${e.details}');
} }
return {}; return {};
} }
static Future<void> pick(String uri) async {
try {
await platform.invokeMethod('pick', <String, dynamic>{
'uri': uri,
});
} on PlatformException catch (e) {
debugPrint('pick failed with code=${e.code}, exception=${e.message}, details=${e.details}');
}
}
} }

View file

@ -38,15 +38,15 @@ class AndroidFileUtils {
AlbumType getAlbumType(String albumDirectory) { AlbumType getAlbumType(String albumDirectory) {
if (albumDirectory != null) { if (albumDirectory != null) {
if (androidFileUtils.isCameraPath(albumDirectory)) return AlbumType.Camera; if (androidFileUtils.isCameraPath(albumDirectory)) return AlbumType.camera;
if (androidFileUtils.isDownloadPath(albumDirectory)) return AlbumType.Download; if (androidFileUtils.isDownloadPath(albumDirectory)) return AlbumType.download;
if (androidFileUtils.isScreenRecordingsPath(albumDirectory)) return AlbumType.ScreenRecordings; if (androidFileUtils.isScreenRecordingsPath(albumDirectory)) return AlbumType.screenRecordings;
if (androidFileUtils.isScreenshotsPath(albumDirectory)) return AlbumType.Screenshots; if (androidFileUtils.isScreenshotsPath(albumDirectory)) return AlbumType.screenshots;
final parts = albumDirectory.split(separator); final parts = albumDirectory.split(separator);
if (albumDirectory.startsWith(androidFileUtils.externalStorage) && appNameMap.keys.contains(parts.last)) return AlbumType.App; if (albumDirectory.startsWith(androidFileUtils.externalStorage) && appNameMap.keys.contains(parts.last)) return AlbumType.app;
} }
return AlbumType.Default; return AlbumType.regular;
} }
String getAlbumAppPackageName(String albumDirectory) { String getAlbumAppPackageName(String albumDirectory) {
@ -55,14 +55,7 @@ class AndroidFileUtils {
} }
} }
enum AlbumType { enum AlbumType { regular, app, camera, download, screenRecordings, screenshots }
Default,
App,
Camera,
Download,
ScreenRecordings,
Screenshots,
}
class StorageVolume { class StorageVolume {
final String description, path, state; final String description, path, state;

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/main.dart';
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/settings.dart'; import 'package:aves/model/settings.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
@ -121,6 +122,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
Widget _buildAppBarTitle() { Widget _buildAppBarTitle() {
if (collection.isBrowsing) { if (collection.isBrowsing) {
final title = AvesApp.mode == AppMode.pick ? 'Select' : 'Aves';
return GestureDetector( return GestureDetector(
onTap: _goToSearch, onTap: _goToSearch,
// use a `Container` with a dummy color to make it expand // use a `Container` with a dummy color to make it expand
@ -130,7 +132,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
padding: const EdgeInsets.symmetric(horizontal: NavigationToolbar.kMiddleSpacing), padding: const EdgeInsets.symmetric(horizontal: NavigationToolbar.kMiddleSpacing),
color: Colors.transparent, color: Colors.transparent,
height: kToolbarHeight, height: kToolbarHeight,
child: const Text('Aves'), child: Text(title),
), ),
); );
} else if (collection.isSelecting) { } else if (collection.isSelecting) {
@ -169,10 +171,11 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
..._buildSortMenuItems(), ..._buildSortMenuItems(),
..._buildGroupMenuItems(), ..._buildGroupMenuItems(),
if (collection.isBrowsing) ...[ if (collection.isBrowsing) ...[
const PopupMenuItem( if (AvesApp.mode == AppMode.main)
value: CollectionAction.select, const PopupMenuItem(
child: MenuRow(text: 'Select', icon: AIcons.select), value: CollectionAction.select,
), child: MenuRow(text: 'Select', icon: AIcons.select),
),
const PopupMenuItem( const PopupMenuItem(
value: CollectionAction.stats, value: CollectionAction.stats,
child: MenuRow(text: 'Stats', icon: AIcons.stats), child: MenuRow(text: 'Stats', icon: AIcons.stats),

View file

@ -185,7 +185,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
builder: (context, snapshot) { builder: (context, snapshot) {
final specialAlbums = source.sortedAlbums.where((album) { final specialAlbums = source.sortedAlbums.where((album) {
final type = androidFileUtils.getAlbumType(album); final type = androidFileUtils.getAlbumType(album);
return type != AlbumType.Default && type != AlbumType.App; return type != AlbumType.regular && type != AlbumType.app;
}); });
if (specialAlbums.isEmpty) return const SizedBox.shrink(); if (specialAlbums.isEmpty) return const SizedBox.shrink();
@ -205,10 +205,10 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
final regularAlbums = <String>[], appAlbums = <String>[]; final regularAlbums = <String>[], appAlbums = <String>[];
for (var album in source.sortedAlbums) { for (var album in source.sortedAlbums) {
switch (androidFileUtils.getAlbumType(album)) { switch (androidFileUtils.getAlbumType(album)) {
case AlbumType.Default: case AlbumType.regular:
regularAlbums.add(album); regularAlbums.add(album);
break; break;
case AlbumType.App: case AlbumType.app:
appAlbums.add(album); appAlbums.add(album);
break; break;
default: default:

View file

@ -71,7 +71,7 @@ class SectionHeader extends StatelessWidget {
var headerExtent = 0.0; var headerExtent = 0.0;
if (sectionKey is String) { if (sectionKey is String) {
// only compute height for album headers, as they're the only likely ones to split on multiple lines // only compute height for album headers, as they're the only likely ones to split on multiple lines
final hasLeading = androidFileUtils.getAlbumType(sectionKey) != AlbumType.Default; final hasLeading = androidFileUtils.getAlbumType(sectionKey) != AlbumType.regular;
final hasTrailing = androidFileUtils.isOnRemovableStorage(sectionKey); final hasTrailing = androidFileUtils.isOnRemovableStorage(sectionKey);
final text = source.getUniqueAlbumName(sectionKey); final text = source.getUniqueAlbumName(sectionKey);
final maxWidth = scrollableWidth - TitleSectionHeader.padding.horizontal; final maxWidth = scrollableWidth - TitleSectionHeader.padding.horizontal;

View file

@ -1,5 +1,7 @@
import 'package:aves/main.dart';
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/services/viewer_service.dart';
import 'package:aves/widgets/album/grid/list_known_extent.dart'; import 'package:aves/widgets/album/grid/list_known_extent.dart';
import 'package:aves/widgets/album/grid/list_section_layout.dart'; import 'package:aves/widgets/album/grid/list_section_layout.dart';
import 'package:aves/widgets/album/thumbnail/decorated.dart'; import 'package:aves/widgets/album/thumbnail/decorated.dart';
@ -52,14 +54,18 @@ class GridThumbnail extends StatelessWidget {
return GestureDetector( return GestureDetector(
key: ValueKey(entry.uri), key: ValueKey(entry.uri),
onTap: () { onTap: () {
if (collection.isBrowsing) { if (AvesApp.mode == AppMode.main) {
_goToFullscreen(context); if (collection.isBrowsing) {
} else { _goToFullscreen(context);
collection.toggleSelection(entry); } else if (collection.isSelecting) {
collection.toggleSelection(entry);
}
} else if (AvesApp.mode == AppMode.pick) {
ViewerService.pick(entry.uri);
} }
}, },
onLongPress: () { onLongPress: () {
if (collection.isBrowsing) { if (AvesApp.mode == AppMode.main && collection.isBrowsing) {
collection.toggleSelection(entry); collection.toggleSelection(entry);
} }
}, },

View file

@ -158,14 +158,14 @@ class IconUtils {
double size = 24, double size = 24,
}) { }) {
switch (androidFileUtils.getAlbumType(album)) { switch (androidFileUtils.getAlbumType(album)) {
case AlbumType.Camera: case AlbumType.camera:
return Icon(AIcons.cameraAlbum, size: size); return Icon(AIcons.cameraAlbum, size: size);
case AlbumType.Screenshots: case AlbumType.screenshots:
case AlbumType.ScreenRecordings: case AlbumType.screenRecordings:
return Icon(AIcons.screenshotAlbum, size: size); return Icon(AIcons.screenshotAlbum, size: size);
case AlbumType.Download: case AlbumType.download:
return Icon(AIcons.downloadAlbum, size: size); return Icon(AIcons.downloadAlbum, size: size);
case AlbumType.App: case AlbumType.app:
return Image( return Image(
image: AppIconImage( image: AppIconImage(
packageName: androidFileUtils.getAlbumAppPackageName(album), packageName: androidFileUtils.getAlbumAppPackageName(album),
@ -174,7 +174,7 @@ class IconUtils {
width: size, width: size,
height: size, height: size,
); );
case AlbumType.Default: case AlbumType.regular:
default: default:
return null; return null;
} }