copy: fix for non primary volumes, update collection
This commit is contained in:
parent
1cd333d419
commit
487ac5c677
7 changed files with 100 additions and 45 deletions
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue