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">
<action android:name="android.intent.action.VIEW" />
<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.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
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="false" />

View file

@ -32,7 +32,7 @@ public class MainActivity extends FlutterActivity {
public static final String VIEWER_CHANNEL = "deckers.thibault/aves/viewer";
private Map<String, String> sharedEntryMap;
private Map<String, String> intentDataMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -60,23 +60,47 @@ public class MainActivity extends FlutterActivity {
new MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler(
(call, result) -> {
if (call.method.contentEquals("getSharedEntry")) {
result.success(sharedEntryMap);
sharedEntryMap = null;
if (call.method.contentEquals("getIntentData")) {
result.success(intentDataMap);
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) {
Log.i(LOG_TAG, "handleIntent intent=" + intent);
if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) {
if (intent == null) return;
String action = intent.getAction();
if (action == null) return;
switch (action) {
case Intent.ACTION_VIEW:
Uri uri = intent.getData();
String mimeType = intent.getType();
if (uri != null && mimeType != null) {
sharedEntryMap = new HashMap<>();
sharedEntryMap.put("uri", uri.toString());
sharedEntryMap.put("mimeType", mimeType);
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());
}
enum AppMode { main, pick, view }
class AvesApp extends StatelessWidget {
static AppMode mode = AppMode.main;
@override
Widget build(BuildContext context) {
return MaterialApp(
@ -56,7 +60,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> {
MediaStoreSource _mediaStore;
ImageEntry _sharedEntry;
ImageEntry _viewerEntry;
Future<void> _appSetup;
@override
@ -87,18 +91,36 @@ class _HomePageState extends State<HomePage> {
await settings.init(); // <20ms
final sharedExtra = await ViewerService.getSharedEntry();
if (sharedExtra != null) {
_sharedEntry = await ImageFileService.getImageEntry(sharedExtra['uri'], sharedExtra['mimeType']);
// cataloging is essential for geolocation and video rotation
await _sharedEntry.catalog();
unawaited(_sharedEntry.locate());
} else {
final intentData = await ViewerService.getIntentData();
if (intentData != null) {
final action = intentData['action'];
switch (action) {
case 'view':
AvesApp.mode = AppMode.view;
await _initViewerEntry(
uri: intentData['uri'],
mimeType: intentData['mimeType'],
);
break;
case 'pick':
AvesApp.mode = AppMode.pick;
break;
}
}
if (AvesApp.mode != AppMode.view) {
_mediaStore = MediaStoreSource();
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
Widget build(BuildContext context) {
return FutureBuilder(
@ -107,8 +129,8 @@ class _HomePageState extends State<HomePage> {
if (snapshot.hasError) return const Icon(AIcons.error);
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
debugPrint('$runtimeType app setup future complete');
if (_sharedEntry != null) {
return SingleFullscreenPage(entry: _sharedEntry);
if (AvesApp.mode == AppMode.view) {
return SingleFullscreenPage(entry: _viewerEntry);
}
if (_mediaStore != null) {
return CollectionPage(CollectionLens(

View file

@ -37,7 +37,7 @@ class AlbumFilter extends CollectionFilter {
Future<Color> color(BuildContext context) {
// do not use async/await and rely on `SynchronousFuture`
// 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]);
return PaletteGenerator.fromImageProvider(

View file

@ -4,13 +4,23 @@ import 'package:flutter/services.dart';
class ViewerService {
static const platform = MethodChannel('deckers.thibault/aves/viewer');
static Future<Map> getSharedEntry() async {
static Future<Map> getIntentData() async {
try {
// return nullable map with: 'uri' 'mimeType'
return await platform.invokeMethod('getSharedEntry') as Map;
// return nullable map with 'action' and possibly 'uri' 'mimeType'
return await platform.invokeMethod('getIntentData') as Map;
} 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 {};
}
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) {
if (albumDirectory != null) {
if (androidFileUtils.isCameraPath(albumDirectory)) return AlbumType.Camera;
if (androidFileUtils.isDownloadPath(albumDirectory)) return AlbumType.Download;
if (androidFileUtils.isScreenRecordingsPath(albumDirectory)) return AlbumType.ScreenRecordings;
if (androidFileUtils.isScreenshotsPath(albumDirectory)) return AlbumType.Screenshots;
if (androidFileUtils.isCameraPath(albumDirectory)) return AlbumType.camera;
if (androidFileUtils.isDownloadPath(albumDirectory)) return AlbumType.download;
if (androidFileUtils.isScreenRecordingsPath(albumDirectory)) return AlbumType.screenRecordings;
if (androidFileUtils.isScreenshotsPath(albumDirectory)) return AlbumType.screenshots;
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) {
@ -55,14 +55,7 @@ class AndroidFileUtils {
}
}
enum AlbumType {
Default,
App,
Camera,
Download,
ScreenRecordings,
Screenshots,
}
enum AlbumType { regular, app, camera, download, screenRecordings, screenshots }
class StorageVolume {
final String description, path, state;

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:aves/main.dart';
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/settings.dart';
import 'package:aves/utils/constants.dart';
@ -121,6 +122,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
Widget _buildAppBarTitle() {
if (collection.isBrowsing) {
final title = AvesApp.mode == AppMode.pick ? 'Select' : 'Aves';
return GestureDetector(
onTap: _goToSearch,
// 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),
color: Colors.transparent,
height: kToolbarHeight,
child: const Text('Aves'),
child: Text(title),
),
);
} else if (collection.isSelecting) {
@ -169,6 +171,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
..._buildSortMenuItems(),
..._buildGroupMenuItems(),
if (collection.isBrowsing) ...[
if (AvesApp.mode == AppMode.main)
const PopupMenuItem(
value: CollectionAction.select,
child: MenuRow(text: 'Select', icon: AIcons.select),

View file

@ -185,7 +185,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
builder: (context, snapshot) {
final specialAlbums = source.sortedAlbums.where((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();
@ -205,10 +205,10 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
final regularAlbums = <String>[], appAlbums = <String>[];
for (var album in source.sortedAlbums) {
switch (androidFileUtils.getAlbumType(album)) {
case AlbumType.Default:
case AlbumType.regular:
regularAlbums.add(album);
break;
case AlbumType.App:
case AlbumType.app:
appAlbums.add(album);
break;
default:

View file

@ -71,7 +71,7 @@ class SectionHeader extends StatelessWidget {
var headerExtent = 0.0;
if (sectionKey is String) {
// 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 text = source.getUniqueAlbumName(sectionKey);
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/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_section_layout.dart';
import 'package:aves/widgets/album/thumbnail/decorated.dart';
@ -52,14 +54,18 @@ class GridThumbnail extends StatelessWidget {
return GestureDetector(
key: ValueKey(entry.uri),
onTap: () {
if (AvesApp.mode == AppMode.main) {
if (collection.isBrowsing) {
_goToFullscreen(context);
} else {
} else if (collection.isSelecting) {
collection.toggleSelection(entry);
}
} else if (AvesApp.mode == AppMode.pick) {
ViewerService.pick(entry.uri);
}
},
onLongPress: () {
if (collection.isBrowsing) {
if (AvesApp.mode == AppMode.main && collection.isBrowsing) {
collection.toggleSelection(entry);
}
},

View file

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