copy: fix for non primary volumes, update collection

This commit is contained in:
Thibault Deckers 2020-05-27 14:34:11 +09:00
parent 1cd333d419
commit 487ac5c677
7 changed files with 100 additions and 45 deletions

View file

@ -35,7 +35,7 @@ android {
applicationId "deckers.thibault.aves"
// some Java 8 APIs (java.util.stream, etc.) require minSdkVersion 24
// but Android Studio 4.0 desugaring features allow targeting older SDKs
minSdkVersion 23
minSdkVersion 24
targetSdkVersion 29 // same as compileSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View file

@ -40,8 +40,6 @@ public class ImageEntry {
@Nullable
public String title;
@Nullable
private String bucketDisplayName;
@Nullable
public Integer width, height, orientationDegrees;
@Nullable
public Long sizeBytes;
@ -66,7 +64,6 @@ public class ImageEntry {
this.title = (String) map.get("title");
this.dateModifiedSecs = toLong(map.get("dateModifiedSecs"));
this.sourceDateTakenMillis = toLong(map.get("sourceDateTakenMillis"));
this.bucketDisplayName = (String) map.get("bucketDisplayName");
this.durationMillis = toLong(map.get("durationMillis"));
}
@ -82,7 +79,6 @@ public class ImageEntry {
put("title", title);
put("dateModifiedSecs", dateModifiedSecs);
put("sourceDateTakenMillis", sourceDateTakenMillis);
put("bucketDisplayName", bucketDisplayName);
put("durationMillis", durationMillis);
// only for map export
put("contentId", getContentId());

View file

@ -10,6 +10,8 @@ import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.MediaStore;
import android.util.Log;
@ -51,7 +53,6 @@ public class MediaStoreImageProvider extends ImageProvider {
private static final String[] IMAGE_PROJECTION = Stream.of(BASE_PROJECTION, new String[]{
// uses MediaStore.Images.Media instead of MediaStore.MediaColumns for APIs < Q
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
MediaStore.Images.Media.ORIENTATION,
}).flatMap(Stream::of).toArray(String[]::new);
@ -59,7 +60,6 @@ public class MediaStoreImageProvider extends ImageProvider {
private static final String[] VIDEO_PROJECTION = Stream.of(BASE_PROJECTION, new String[]{
// uses MediaStore.Video.Media instead of MediaStore.MediaColumns for APIs < Q
MediaStore.Video.Media.DATE_TAKEN,
MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
MediaStore.Video.Media.DURATION,
}, (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ?
new String[]{
@ -111,7 +111,6 @@ public class MediaStoreImageProvider extends ImageProvider {
int heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT);
int dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED);
int dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN);
int bucketDisplayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_DISPLAY_NAME);
// image & video for API >= Q, only for images for API < Q
int orientationColumn = cursor.getColumnIndex(MediaStore.MediaColumns.ORIENTATION);
@ -138,7 +137,6 @@ public class MediaStoreImageProvider extends ImageProvider {
put("title", cursor.getString(titleColumn));
put("dateModifiedSecs", cursor.getLong(dateModifiedColumn));
put("sourceDateTakenMillis", cursor.getLong(dateTakenColumn));
put("bucketDisplayName", cursor.getString(bucketDisplayNameColumn));
// only for map export
put("contentId", contentId);
}};
@ -228,21 +226,19 @@ public class MediaStoreImageProvider extends ImageProvider {
public ListenableFuture<Map<String, Object>> move(final Activity activity, final String sourcePath, final Uri sourceUri, String destinationDir, String mimeType, boolean copy) {
SettableFuture<Map<String, Object>> future = SettableFuture.create();
// if (Env.isOnSdCard(activity, path)) {
// Uri sdCardTreeUri = PermissionManager.getSdCardTreeUri(activity);
// if (sdCardTreeUri == null) {
// Runnable runnable = () -> move(activity, path, uri, copy, destinationPath, callback);
// new Handler(Looper.getMainLooper()).post(() -> PermissionManager.showSdCardAccessDialog(activity, runnable));
// return;
// }
//
// // if the file is on SD card, calling the content resolver delete() removes the entry from the Media Store
// // but it doesn't delete the file, even if the app has the permission
// StorageUtils.deleteFromSdCard(activity, sdCardTreeUri, Env.getStorageVolumes(activity), path);
// Log.d(LOG_TAG, "deleted from SD card at path=" + uri);
// callback.onSuccess(null);
// return;
// }
String volumeName = "external";
StorageManager sm = activity.getSystemService(StorageManager.class);
if (sm != null) {
StorageVolume volume = sm.getStorageVolume(new File(destinationDir));
if (volume != null && !volume.isPrimary()) {
String uuid = volume.getUuid();
if (uuid != null) {
// the UUID returned may be uppercase
// but it should be lowercase to work with the MediaStore
volumeName = volume.getUuid().toLowerCase();
}
}
}
try {
// from API 29, changing MediaColumns.RELATIVE_PATH can move files on disk (same storage device)
@ -251,9 +247,6 @@ public class MediaStoreImageProvider extends ImageProvider {
// DocumentFile.getParentFile() is null without picking a tree first
// DocumentsContract.copyDocument() and moveDocument() need parent doc uri
// TODO TLAD copy/move
// TODO TLAD cannot copy to SD card, even with the permission to the volume root, by inserting to MediaStore
PathComponents sourcePathComponents = new PathComponents(sourcePath, Env.getStorageVolumes(activity));
String destinationPath = destinationDir + File.separator + sourcePathComponents.getFilename();
@ -262,9 +255,8 @@ public class MediaStoreImageProvider extends ImageProvider {
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
// contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "");
// contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "");
Uri tableUrl = mimeType.startsWith(MimeTypes.VIDEO) ? MediaStore.Video.Media.EXTERNAL_CONTENT_URI : MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri tableUrl = mimeType.startsWith(MimeTypes.VIDEO) ? MediaStore.Video.Media.getContentUri(volumeName) : MediaStore.Images.Media.getContentUri(volumeName);
Uri destinationUri = activity.getContentResolver().insert(tableUrl, contentValues);
// Log.d("TLAD", "move copy from=" + sourcePath + " to=" + destinationPath + " (destinationUri=" + destinationUri + ")");
if (destinationUri == null) {
future.setException(new Exception("failed to insert row to content resolver"));
} else {
@ -272,6 +264,8 @@ public class MediaStoreImageProvider extends ImageProvider {
DocumentFileCompat destination = DocumentFileCompat.fromSingleUri(activity, destinationUri);
source.copyTo(destination);
// TODO TLAD delete source when it is a `move`
Map<String, Object> newFields = new HashMap<>();
newFields.put("uri", destinationUri.toString());
newFields.put("contentId", ContentUris.parseId(destinationUri));

View file

@ -28,7 +28,6 @@ class ImageEntry {
String sourceTitle;
final int dateModifiedSecs;
final int sourceDateTakenMillis;
final String bucketDisplayName;
final int durationMillis;
int _catalogDateMillis;
CatalogMetadata _catalogMetadata;
@ -49,12 +48,38 @@ class ImageEntry {
this.sourceTitle,
this.dateModifiedSecs,
this.sourceDateTakenMillis,
this.bucketDisplayName,
this.durationMillis,
}) : directory = path != null ? dirname(path) : null {
isFavouriteNotifier.value = isFavourite;
}
ImageEntry copyWith({
@required String uri,
@required String path,
@required int contentId,
}) {
final copyContentId = contentId ?? this.contentId;
final copied = ImageEntry(
uri: uri ?? uri,
path: path ?? this.path,
contentId: copyContentId,
mimeType: mimeType,
width: width,
height: height,
orientationDegrees: orientationDegrees,
sizeBytes: sizeBytes,
sourceTitle: sourceTitle,
dateModifiedSecs: dateModifiedSecs,
sourceDateTakenMillis: sourceDateTakenMillis,
durationMillis: durationMillis,
)
.._catalogDateMillis = _catalogDateMillis
.._catalogMetadata = _catalogMetadata?.copyWith(contentId: copyContentId)
.._addressDetails = _addressDetails?.copyWith(contentId: copyContentId);
return copied;
}
factory ImageEntry.fromMap(Map map) {
return ImageEntry(
uri: map['uri'] as String,
@ -68,7 +93,6 @@ class ImageEntry {
sourceTitle: map['title'] as String,
dateModifiedSecs: map['dateModifiedSecs'] as int,
sourceDateTakenMillis: map['sourceDateTakenMillis'] as int,
bucketDisplayName: map['bucketDisplayName'] as String,
durationMillis: map['durationMillis'] as int,
);
}
@ -86,7 +110,6 @@ class ImageEntry {
'title': sourceTitle,
'dateModifiedSecs': dateModifiedSecs,
'sourceDateTakenMillis': sourceDateTakenMillis,
'bucketDisplayName': bucketDisplayName,
'durationMillis': durationMillis,
};
}

View file

@ -43,11 +43,26 @@ class CatalogMetadata {
this.xmpTitleDescription,
double latitude,
double longitude,
})
})
// Geocoder throws an IllegalArgumentException when a coordinate has a funky values like 1.7056881853375E7
: latitude = latitude == null || latitude < -90.0 || latitude > 90.0 ? null : latitude,
longitude = longitude == null || longitude < -180.0 || longitude > 180.0 ? null : longitude;
CatalogMetadata copyWith({
@required int contentId,
}) {
return CatalogMetadata(
contentId: contentId ?? this.contentId,
dateMillis: dateMillis,
isAnimated: isAnimated,
videoRotation: videoRotation,
xmpSubjects: xmpSubjects,
xmpTitleDescription: xmpTitleDescription,
latitude: latitude,
longitude: longitude,
);
}
factory CatalogMetadata.fromMap(Map map, {bool boolAsInteger = false}) {
final isAnimated = map['isAnimated'] ?? (boolAsInteger ? 0 : false);
return CatalogMetadata(
@ -121,6 +136,19 @@ class AddressDetails {
this.locality,
});
AddressDetails copyWith({
@required int contentId,
}) {
return AddressDetails(
contentId: contentId ?? this.contentId,
addressLine: addressLine,
countryCode: countryCode,
countryName: countryName,
adminArea: adminArea,
locality: locality,
);
}
factory AddressDetails.fromMap(Map map) {
return AddressDetails(
contentId: map['contentId'],

View file

@ -188,6 +188,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
),
const PopupMenuItem(
value: CollectionAction.move,
// TODO TLAD enable when handled on native side
enabled: false,
child: MenuRow(text: 'Move to album'),
),
const PopupMenuItem(

View file

@ -62,11 +62,10 @@ class SelectionActionDelegate with PermissionAwareMixin {
leading: const BackButton(),
title: Text(copy ? 'Copy to Album' : 'Move to Album'),
actions: [
IconButton(
icon: const Icon(AIcons.createAlbum),
onPressed: () {
// TODO TLAD album creation
},
const IconButton(
icon: Icon(AIcons.createAlbum),
// TODO TLAD album creation
onPressed: null,
tooltip: 'Create album',
),
],
@ -90,19 +89,31 @@ class SelectionActionDelegate with PermissionAwareMixin {
opStream: ImageFileService.move(selection, copy: copy, destinationPath: filter.album),
onDone: (Set<MoveOpEvent> processed) {
debugPrint('$runtimeType _moveSelection onDone');
final movedUris = processed.where((e) => e.success).map((e) => e.uri);
final movedCount = movedUris.length;
final movedOps = processed.where((e) => e.success);
final movedCount = movedOps.length;
final selectionCount = selection.length;
if (movedCount < selectionCount) {
final count = selectionCount - movedCount;
_showFeedback(context, 'Failed to move ${Intl.plural(count, one: '${count} item', other: '${count} items')}');
}
if (movedCount > 0) {
processed.forEach((event) {
debugPrint('$runtimeType _moveSelection moved entry uri=${event.uri} newFields=${event.newFields}');
// TODO TLAD update source
});
if (copy) {
collection.source.addAll(movedOps.map((movedOp) {
final sourceUri = movedOp.uri;
final newFields = movedOp.newFields;
final sourceEntry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
return sourceEntry?.copyWith(
uri: newFields['uri'] as String,
path: newFields['path'] as String,
contentId: newFields['contentId'] as int,
);
}));
} else {
// TODO TLAD update old entries path/dir/ID
}
// TODO TLAD update DB for catalog/address/fav
}
collection.clearSelection();
collection.browse();
},
);
@ -149,6 +160,7 @@ class SelectionActionDelegate with PermissionAwareMixin {
if (deletedCount > 0) {
collection.source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)));
}
collection.clearSelection();
collection.browse();
},
);