diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3b0746918..5711c7d39 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -60,3 +60,30 @@ jobs:
with:
artifacts: "build/app/outputs/apk/release/*.apk,build/app/outputs/bundle/release/*.aab"
token: ${{ secrets.RELEASE_WORKFLOW_TOKEN }}
+
+ - name: Upload app bundle
+ uses: actions/upload-artifact@v2
+ with:
+ name: appbundle
+ path: build/app/outputs/bundle/release/app-release.aab
+
+ release:
+ name: Create beta release on Play Store.
+ needs: [ build ]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Get appbundle from artifacts.
+ uses: actions/download-artifact@v2
+ with:
+ name: appbundle
+
+ - name: Release app to beta channel.
+ uses: r0adkll/upload-google-play@v1
+ with:
+ serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}
+ packageName: deckers.thibault.aves
+ releaseFile: app-release.aab
+ track: beta
+ whatsNewDirectory: whatsnew
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 09202053a..3f54529e3 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -30,7 +30,7 @@
-
diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.java
index 7d13671b5..82f3d3825 100644
--- a/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.java
+++ b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.java
@@ -6,6 +6,7 @@ import android.os.Handler;
import android.os.Looper;
import android.util.Log;
+import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -94,6 +95,10 @@ public class ImageOpStreamHandler implements EventChannel.StreamHandler {
String destinationDir = (String) argMap.get("destinationPath");
if (copy == null || destinationDir == null) return;
+ if (!destinationDir.endsWith(File.separator)) {
+ destinationDir += File.separator;
+ }
+
List entries = entryMapList.stream().map(AvesImageEntry::new).collect(Collectors.toList());
provider.moveMultiple(activity, copy, destinationDir, entries, new ImageProvider.ImageOpCallback() {
@Override
diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java
index d05ad556a..391d1ca3c 100644
--- a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java
+++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java
@@ -56,7 +56,7 @@ public abstract class ImageProvider {
return Futures.immediateFailedFuture(new UnsupportedOperationException());
}
- public void moveMultiple(final Activity activity, Boolean copy, String destinationDir, List entries, @NonNull ImageOpCallback callback) {
+ public void moveMultiple(final Activity activity, final Boolean copy, final String destinationDir, final List entries, @NonNull final ImageOpCallback callback) {
callback.onFailure(new UnsupportedOperationException());
}
@@ -66,12 +66,11 @@ public abstract class ImageProvider {
return;
}
- Map newFields = new HashMap<>();
File oldFile = new File(oldPath);
File newFile = new File(oldFile.getParent(), newFilename);
if (oldFile.equals(newFile)) {
Log.w(LOG_TAG, "new name and old name are the same, path=" + oldPath);
- callback.onSuccess(newFields);
+ callback.onSuccess(new HashMap<>());
return;
}
@@ -94,40 +93,7 @@ public abstract class ImageProvider {
}
MediaScannerConnection.scanFile(activity, new String[]{oldPath}, new String[]{mimeType}, null);
- MediaScannerConnection.scanFile(activity, new String[]{newFile.getPath()}, new String[]{mimeType}, (newPath, newUri) -> {
- Log.d(LOG_TAG, "onScanCompleted with newPath=" + newPath + ", newUri=" + newUri);
- if (newUri != null) {
- // newURI is a file media URI (e.g. "content://media/12a9-8b42/file/62872")
- // but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
- long contentId = ContentUris.parseId(newUri);
- Uri contentUri = null;
- if (mimeType.startsWith(MimeTypes.IMAGE)) {
- contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId);
- } else if (mimeType.startsWith(MimeTypes.VIDEO)) {
- contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId);
- }
- if (contentUri != null) {
- // we retrieve updated fields as the renamed file became a new entry in the Media Store
- String[] projection = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.TITLE};
- try {
- Cursor cursor = activity.getContentResolver().query(contentUri, projection, null, null, null);
- if (cursor != null) {
- if (cursor.moveToNext()) {
- newFields.put("uri", contentUri.toString());
- newFields.put("contentId", contentId);
- newFields.put("path", cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)));
- newFields.put("title", cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.TITLE)));
- }
- cursor.close();
- }
- } catch (Exception e) {
- callback.onFailure(e);
- return;
- }
- }
- }
- callback.onSuccess(newFields);
- });
+ scanNewPath(activity, newFile.getPath(), mimeType, callback);
}
public void rotate(final Activity activity, final String path, final Uri uri, final String mimeType, final boolean clockwise, final ImageOpCallback callback) {
@@ -291,6 +257,54 @@ public abstract class ImageProvider {
// }
}
+ protected void scanNewPath(final Activity activity, final String path, final String mimeType, final ImageOpCallback callback) {
+ MediaScannerConnection.scanFile(activity, new String[]{path}, new String[]{mimeType}, (newPath, newUri) -> {
+ Log.d(LOG_TAG, "scanNewPath onScanCompleted with newPath=" + newPath + ", newUri=" + newUri);
+
+ long contentId = 0;
+ Uri contentUri = null;
+ if (newUri != null) {
+ // newURI is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
+ // but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
+ contentId = ContentUris.parseId(newUri);
+ if (mimeType.startsWith(MimeTypes.IMAGE)) {
+ contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId);
+ } else if (mimeType.startsWith(MimeTypes.VIDEO)) {
+ contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId);
+ }
+ }
+ if (contentUri == null) {
+ callback.onFailure(new Exception("failed to get content URI of item at path=" + path));
+ return;
+ }
+
+ Map newFields = new HashMap<>();
+ // we retrieve updated fields as the renamed file became a new entry in the Media Store
+ String[] projection = {MediaStore.MediaColumns.TITLE};
+ try {
+ Cursor cursor = activity.getContentResolver().query(contentUri, projection, null, null, null);
+ if (cursor != null) {
+ if (cursor.moveToNext()) {
+ newFields.put("uri", contentUri.toString());
+ newFields.put("contentId", contentId);
+ newFields.put("path", path);
+ newFields.put("title", cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.TITLE)));
+ }
+ cursor.close();
+ }
+ } catch (Exception e) {
+ callback.onFailure(e);
+ return;
+ }
+
+ if (newFields.isEmpty()) {
+ callback.onFailure(new Exception("failed to get item details from provider at contentUri=" + contentUri));
+ } else {
+ callback.onSuccess(newFields);
+ }
+ });
+ }
+
public interface ImageOpCallback {
void onSuccess(Map fields);
diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java
index 90abe66dc..5a5f0fae8 100644
--- a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java
+++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java
@@ -6,7 +6,6 @@ import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
-import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
@@ -17,6 +16,7 @@ import android.provider.MediaStore;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import com.commonsware.cwac.document.DocumentFileCompat;
import com.google.common.util.concurrent.ListenableFuture;
@@ -275,7 +275,7 @@ public class MediaStoreImageProvider extends ImageProvider {
}
@Override
- public void moveMultiple(final Activity activity, Boolean copy, String destinationDir, List entries, @NonNull ImageOpCallback callback) {
+ public void moveMultiple(final Activity activity, final Boolean copy, final String destinationDir, final List entries, @NonNull final ImageOpCallback callback) {
if (PermissionManager.requireVolumeAccessDialog(activity, destinationDir)) {
Runnable runnable = () -> moveMultiple(activity, copy, destinationDir, entries, callback);
new Handler(Looper.getMainLooper()).post(() -> PermissionManager.showVolumeAccessDialog(activity, destinationDir, runnable));
@@ -288,7 +288,11 @@ public class MediaStoreImageProvider extends ImageProvider {
return;
}
- String volumeName = null;
+ MediaStoreMoveDestination destination = new MediaStoreMoveDestination(activity, destinationDir);
+ if (destination.volumePath == null) {
+ callback.onFailure(new Exception("failed to set up destination volume path for path=" + destinationDir));
+ return;
+ }
for (AvesImageEntry entry : entries) {
Uri sourceUri = entry.uri;
@@ -298,16 +302,16 @@ public class MediaStoreImageProvider extends ImageProvider {
Map result = new HashMap() {{
put("uri", sourceUri.toString());
}};
+
+ // TODO TLAD check if there is any downside to use tree document files with scoped storage on API 30+
+ // when testing scoped storage on API 29, it seems less constraining to use tree document files than to rely on the Media Store
try {
ListenableFuture