mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Migrated filepicker from Java to Kotlin (#5997)
* Rename .java to .kt * Migrated filepicker module from Java to Kotlin * Rename .java to .kt * Migrated filepicker module from Java to Kotlin * fix: test cases
This commit is contained in:
parent
3777f18bf9
commit
f8d519e8eb
21 changed files with 970 additions and 929 deletions
|
|
@ -1,23 +0,0 @@
|
||||||
package fr.free.nrw.commons.filepicker;
|
|
||||||
|
|
||||||
public interface Constants {
|
|
||||||
String DEFAULT_FOLDER_NAME = "CommonsContributions";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the request codes for permission handling
|
|
||||||
*/
|
|
||||||
interface RequestCodes {
|
|
||||||
int LOCATION = 1;
|
|
||||||
int STORAGE = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides locations as string for corresponding operations
|
|
||||||
*/
|
|
||||||
interface BundleKeys {
|
|
||||||
String FOLDER_NAME = "fr.free.nrw.commons.folder_name";
|
|
||||||
String ALLOW_MULTIPLE = "fr.free.nrw.commons.allow_multiple";
|
|
||||||
String COPY_TAKEN_PHOTOS = "fr.free.nrw.commons.copy_taken_photos";
|
|
||||||
String COPY_PICKED_IMAGES = "fr.free.nrw.commons.copy_picked_images";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
app/src/main/java/fr/free/nrw/commons/filepicker/Costants.kt
Normal file
29
app/src/main/java/fr/free/nrw/commons/filepicker/Costants.kt
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
package fr.free.nrw.commons.filepicker
|
||||||
|
|
||||||
|
interface Constants {
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_FOLDER_NAME = "CommonsContributions"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the request codes for permission handling
|
||||||
|
*/
|
||||||
|
interface RequestCodes {
|
||||||
|
companion object {
|
||||||
|
const val LOCATION = 1
|
||||||
|
const val STORAGE = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides locations as string for corresponding operations
|
||||||
|
*/
|
||||||
|
interface BundleKeys {
|
||||||
|
companion object {
|
||||||
|
const val FOLDER_NAME = "fr.free.nrw.commons.folder_name"
|
||||||
|
const val ALLOW_MULTIPLE = "fr.free.nrw.commons.allow_multiple"
|
||||||
|
const val COPY_TAKEN_PHOTOS = "fr.free.nrw.commons.copy_taken_photos"
|
||||||
|
const val COPY_PICKED_IMAGES = "fr.free.nrw.commons.copy_picked_images"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package fr.free.nrw.commons.filepicker;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides abstract methods which are overridden while handling Contribution Results
|
|
||||||
* inside the ContributionsController
|
|
||||||
*/
|
|
||||||
public abstract class DefaultCallback implements FilePicker.Callbacks {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onImagePickerError(Exception e, FilePicker.ImageSource source, int type) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCanceled(FilePicker.ImageSource source, int type) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package fr.free.nrw.commons.filepicker
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides abstract methods which are overridden while handling Contribution Results
|
||||||
|
* inside the ContributionsController
|
||||||
|
*/
|
||||||
|
abstract class DefaultCallback: FilePicker.Callbacks {
|
||||||
|
|
||||||
|
override fun onImagePickerError(e: Exception, source: FilePicker.ImageSource, type: Int) {}
|
||||||
|
|
||||||
|
override fun onCanceled(source: FilePicker.ImageSource, type: Int) {}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package fr.free.nrw.commons.filepicker;
|
|
||||||
|
|
||||||
import androidx.core.content.FileProvider;
|
|
||||||
|
|
||||||
public class ExtendedFileProvider extends FileProvider {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package fr.free.nrw.commons.filepicker
|
||||||
|
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
|
||||||
|
class ExtendedFileProvider: FileProvider() {}
|
||||||
|
|
@ -1,355 +0,0 @@
|
||||||
package fr.free.nrw.commons.filepicker;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.filepicker.PickedFiles.singleFileList;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import androidx.activity.result.ActivityResult;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
import fr.free.nrw.commons.customselector.model.Image;
|
|
||||||
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class FilePicker implements Constants {
|
|
||||||
|
|
||||||
private static final String KEY_PHOTO_URI = "photo_uri";
|
|
||||||
private static final String KEY_VIDEO_URI = "video_uri";
|
|
||||||
private static final String KEY_LAST_CAMERA_PHOTO = "last_photo";
|
|
||||||
private static final String KEY_LAST_CAMERA_VIDEO = "last_video";
|
|
||||||
private static final String KEY_TYPE = "type";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the uri of the clicked image so that it can be put in MediaStore
|
|
||||||
*/
|
|
||||||
private static Uri createCameraPictureFile(@NonNull Context context) throws IOException {
|
|
||||||
File imagePath = PickedFiles.getCameraPicturesLocation(context);
|
|
||||||
Uri uri = PickedFiles.getUriToFile(context, imagePath);
|
|
||||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
|
||||||
editor.putString(KEY_PHOTO_URI, uri.toString());
|
|
||||||
editor.putString(KEY_LAST_CAMERA_PHOTO, imagePath.toString());
|
|
||||||
editor.apply();
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Intent createGalleryIntent(@NonNull Context context, int type,
|
|
||||||
boolean openDocumentIntentPreferred) {
|
|
||||||
// storing picked image type to shared preferences
|
|
||||||
storeType(context, type);
|
|
||||||
//Supported types are SVG, PNG and JPEG,GIF, TIFF, WebP, XCF
|
|
||||||
final String[] mimeTypes = { "image/jpg","image/png","image/jpeg", "image/gif", "image/tiff", "image/webp", "image/xcf", "image/svg+xml", "image/webp"};
|
|
||||||
return plainGalleryPickerIntent(openDocumentIntentPreferred)
|
|
||||||
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, configuration(context).allowsMultiplePickingInGallery())
|
|
||||||
.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CreateCustomSectorIntent, creates intent for custom selector activity.
|
|
||||||
* @param context
|
|
||||||
* @param type
|
|
||||||
* @return Custom selector intent
|
|
||||||
*/
|
|
||||||
private static Intent createCustomSelectorIntent(@NonNull Context context, int type) {
|
|
||||||
storeType(context, type);
|
|
||||||
return new Intent(context, CustomSelectorActivity.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Intent createCameraForImageIntent(@NonNull Context context, int type) {
|
|
||||||
storeType(context, type);
|
|
||||||
|
|
||||||
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
|
||||||
try {
|
|
||||||
Uri capturedImageUri = createCameraPictureFile(context);
|
|
||||||
//We have to explicitly grant the write permission since Intent.setFlag works only on API Level >=20
|
|
||||||
grantWritePermission(context, intent, capturedImageUri);
|
|
||||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void revokeWritePermission(@NonNull Context context, Uri uri) {
|
|
||||||
context.revokeUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void grantWritePermission(@NonNull Context context, Intent intent, Uri uri) {
|
|
||||||
List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
|
|
||||||
for (ResolveInfo resolveInfo : resInfoList) {
|
|
||||||
String packageName = resolveInfo.activityInfo.packageName;
|
|
||||||
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void storeType(@NonNull Context context, int type) {
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(KEY_TYPE, type).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int restoreType(@NonNull Context context) {
|
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_TYPE, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens default galery or a available galleries picker if there is no default
|
|
||||||
*
|
|
||||||
* @param type Custom type of your choice, which will be returned with the images
|
|
||||||
*/
|
|
||||||
public static void openGallery(Activity activity, ActivityResultLauncher<Intent> resultLauncher, int type, boolean openDocumentIntentPreferred) {
|
|
||||||
Intent intent = createGalleryIntent(activity, type, openDocumentIntentPreferred);
|
|
||||||
resultLauncher.launch(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens Custom Selector
|
|
||||||
*/
|
|
||||||
public static void openCustomSelector(Activity activity, ActivityResultLauncher<Intent> resultLauncher, int type) {
|
|
||||||
Intent intent = createCustomSelectorIntent(activity, type);
|
|
||||||
resultLauncher.launch(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the camera app to pick image clicked by user
|
|
||||||
*/
|
|
||||||
public static void openCameraForImage(Activity activity, ActivityResultLauncher<Intent> resultLauncher, int type) {
|
|
||||||
Intent intent = createCameraForImageIntent(activity, type);
|
|
||||||
resultLauncher.launch(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static UploadableFile takenCameraPicture(Context context) throws URISyntaxException {
|
|
||||||
String lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_PHOTO, null);
|
|
||||||
if (lastCameraPhoto != null) {
|
|
||||||
return new UploadableFile(new File(lastCameraPhoto));
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static UploadableFile takenCameraVideo(Context context) throws URISyntaxException {
|
|
||||||
String lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_VIDEO, null);
|
|
||||||
if (lastCameraPhoto != null) {
|
|
||||||
return new UploadableFile(new File(lastCameraPhoto));
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<UploadableFile> handleExternalImagesPicked(Intent data, Activity activity) {
|
|
||||||
try {
|
|
||||||
return getFilesFromGalleryPictures(data, activity);
|
|
||||||
} catch (IOException | SecurityException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isPhoto(Intent data) {
|
|
||||||
return data == null || (data.getData() == null && data.getClipData() == null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Intent plainGalleryPickerIntent(boolean openDocumentIntentPreferred) {
|
|
||||||
/*
|
|
||||||
* Asking for ACCESS_MEDIA_LOCATION at runtime solved the location-loss issue
|
|
||||||
* in the custom selector in Contributions fragment.
|
|
||||||
* Detailed discussion: https://github.com/commons-app/apps-android-commons/issues/5015
|
|
||||||
*
|
|
||||||
* This permission check, however, was insufficient to fix location-loss in
|
|
||||||
* the regular selector in Contributions fragment and Nearby fragment,
|
|
||||||
* especially on some devices running Android 13 that use the new Photo Picker by default.
|
|
||||||
*
|
|
||||||
* New Photo Picker: https://developer.android.com/training/data-storage/shared/photopicker
|
|
||||||
*
|
|
||||||
* The new Photo Picker introduced by Android redacts location tags from EXIF metadata.
|
|
||||||
* Reported on the Google Issue Tracker: https://issuetracker.google.com/issues/243294058
|
|
||||||
* Status: Won't fix (Intended behaviour)
|
|
||||||
*
|
|
||||||
* Switched intent from ACTION_GET_CONTENT to ACTION_OPEN_DOCUMENT (by default; can
|
|
||||||
* be changed through the Setting page) as:
|
|
||||||
*
|
|
||||||
* ACTION_GET_CONTENT opens the 'best application' for choosing that kind of data
|
|
||||||
* The best application is the new Photo Picker that redacts the location tags
|
|
||||||
*
|
|
||||||
* ACTION_OPEN_DOCUMENT, however, displays the various DocumentsProvider instances
|
|
||||||
* installed on the device, letting the user interactively navigate through them.
|
|
||||||
*
|
|
||||||
* So, this allows us to use the traditional file picker that does not redact location tags
|
|
||||||
* from EXIF.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
Intent intent;
|
|
||||||
if (openDocumentIntentPreferred) {
|
|
||||||
intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
|
||||||
} else {
|
|
||||||
intent = new Intent(Intent.ACTION_GET_CONTENT);
|
|
||||||
}
|
|
||||||
intent.setType("image/*");
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onPictureReturnedFromDocuments(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) {
|
|
||||||
if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){
|
|
||||||
try {
|
|
||||||
Uri photoPath = result.getData().getData();
|
|
||||||
UploadableFile photoFile = PickedFiles.pickedExistingPicture(activity, photoPath);
|
|
||||||
callbacks.onImagesPicked(singleFileList(photoFile), FilePicker.ImageSource.DOCUMENTS, restoreType(activity));
|
|
||||||
|
|
||||||
if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) {
|
|
||||||
PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
callbacks.onImagePickerError(e, FilePicker.ImageSource.DOCUMENTS, restoreType(activity));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callbacks.onCanceled(FilePicker.ImageSource.DOCUMENTS, restoreType(activity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onPictureReturnedFromCustomSelector.
|
|
||||||
* Retrieve and forward the images to upload wizard through callback.
|
|
||||||
*/
|
|
||||||
public static void onPictureReturnedFromCustomSelector(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) {
|
|
||||||
if(result.getResultCode() == Activity.RESULT_OK){
|
|
||||||
try {
|
|
||||||
List<UploadableFile> files = getFilesFromCustomSelector(result.getData(), activity);
|
|
||||||
callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity));
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callbacks.onCanceled(ImageSource.CUSTOM_SELECTOR, restoreType(activity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get files from custom selector
|
|
||||||
* Retrieve and process the selected images from the custom selector.
|
|
||||||
*/
|
|
||||||
private static List<UploadableFile> getFilesFromCustomSelector(Intent data, Activity activity) throws IOException, SecurityException {
|
|
||||||
List<UploadableFile> files = new ArrayList<>();
|
|
||||||
ArrayList<Image> images = data.getParcelableArrayListExtra("Images");
|
|
||||||
for(Image image : images) {
|
|
||||||
Uri uri = image.getUri();
|
|
||||||
UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri);
|
|
||||||
files.add(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) {
|
|
||||||
PickedFiles.copyFilesInSeparateThread(activity, files);
|
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onPictureReturnedFromGallery(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) {
|
|
||||||
if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){
|
|
||||||
try {
|
|
||||||
List<UploadableFile> files = getFilesFromGalleryPictures(result.getData(), activity);
|
|
||||||
callbacks.onImagesPicked(files, FilePicker.ImageSource.GALLERY, restoreType(activity));
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
callbacks.onImagePickerError(e, FilePicker.ImageSource.GALLERY, restoreType(activity));
|
|
||||||
}
|
|
||||||
} else{
|
|
||||||
callbacks.onCanceled(FilePicker.ImageSource.GALLERY, restoreType(activity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<UploadableFile> getFilesFromGalleryPictures(Intent data, Activity activity) throws IOException, SecurityException {
|
|
||||||
List<UploadableFile> files = new ArrayList<>();
|
|
||||||
ClipData clipData = data.getClipData();
|
|
||||||
if (clipData == null) {
|
|
||||||
Uri uri = data.getData();
|
|
||||||
UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri);
|
|
||||||
files.add(file);
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < clipData.getItemCount(); i++) {
|
|
||||||
Uri uri = clipData.getItemAt(i).getUri();
|
|
||||||
UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri);
|
|
||||||
files.add(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) {
|
|
||||||
PickedFiles.copyFilesInSeparateThread(activity, files);
|
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onPictureReturnedFromCamera(ActivityResult activityResult, Activity activity, @NonNull FilePicker.Callbacks callbacks) {
|
|
||||||
if(activityResult.getResultCode() == Activity.RESULT_OK){
|
|
||||||
try {
|
|
||||||
String lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_PHOTO_URI, null);
|
|
||||||
if (!TextUtils.isEmpty(lastImageUri)) {
|
|
||||||
revokeWritePermission(activity, Uri.parse(lastImageUri));
|
|
||||||
}
|
|
||||||
|
|
||||||
UploadableFile photoFile = FilePicker.takenCameraPicture(activity);
|
|
||||||
List<UploadableFile> files = new ArrayList<>();
|
|
||||||
files.add(photoFile);
|
|
||||||
|
|
||||||
if (photoFile == null) {
|
|
||||||
Exception e = new IllegalStateException("Unable to get the picture returned from camera");
|
|
||||||
callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity));
|
|
||||||
} else {
|
|
||||||
if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) {
|
|
||||||
PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
callbacks.onImagesPicked(files, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity));
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
|
||||||
.edit()
|
|
||||||
.remove(KEY_LAST_CAMERA_PHOTO)
|
|
||||||
.remove(KEY_PHOTO_URI)
|
|
||||||
.apply();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callbacks.onCanceled(FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FilePickerConfiguration configuration(@NonNull Context context) {
|
|
||||||
return new FilePickerConfiguration(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public enum ImageSource {
|
|
||||||
GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO, CUSTOM_SELECTOR
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Callbacks {
|
|
||||||
void onImagePickerError(Exception e, FilePicker.ImageSource source, int type);
|
|
||||||
|
|
||||||
void onImagesPicked(@NonNull List<UploadableFile> imageFiles, FilePicker.ImageSource source, int type);
|
|
||||||
|
|
||||||
void onCanceled(FilePicker.ImageSource source, int type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface HandleActivityResult{
|
|
||||||
void onHandleActivityResult(FilePicker.Callbacks callbacks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
441
app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt
Normal file
441
app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
package fr.free.nrw.commons.filepicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
|
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity
|
||||||
|
import fr.free.nrw.commons.filepicker.PickedFiles.singleFileList
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
|
||||||
|
|
||||||
|
object FilePicker : Constants {
|
||||||
|
|
||||||
|
private const val KEY_PHOTO_URI = "photo_uri"
|
||||||
|
private const val KEY_VIDEO_URI = "video_uri"
|
||||||
|
private const val KEY_LAST_CAMERA_PHOTO = "last_photo"
|
||||||
|
private const val KEY_LAST_CAMERA_VIDEO = "last_video"
|
||||||
|
private const val KEY_TYPE = "type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the uri of the clicked image so that it can be put in MediaStore
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
@JvmStatic
|
||||||
|
private fun createCameraPictureFile(context: Context): Uri {
|
||||||
|
val imagePath = PickedFiles.getCameraPicturesLocation(context)
|
||||||
|
val uri = PickedFiles.getUriToFile(context, imagePath)
|
||||||
|
val editor = PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||||
|
editor.putString(KEY_PHOTO_URI, uri.toString())
|
||||||
|
editor.putString(KEY_LAST_CAMERA_PHOTO, imagePath.toString())
|
||||||
|
editor.apply()
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private fun createGalleryIntent(
|
||||||
|
context: Context,
|
||||||
|
type: Int,
|
||||||
|
openDocumentIntentPreferred: Boolean
|
||||||
|
): Intent {
|
||||||
|
// storing picked image type to shared preferences
|
||||||
|
storeType(context, type)
|
||||||
|
// Supported types are SVG, PNG and JPEG, GIF, TIFF, WebP, XCF
|
||||||
|
val mimeTypes = arrayOf(
|
||||||
|
"image/jpg",
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/gif",
|
||||||
|
"image/tiff",
|
||||||
|
"image/webp",
|
||||||
|
"image/xcf",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/webp"
|
||||||
|
)
|
||||||
|
return plainGalleryPickerIntent(openDocumentIntentPreferred)
|
||||||
|
.putExtra(
|
||||||
|
Intent.EXTRA_ALLOW_MULTIPLE,
|
||||||
|
configuration(context).allowsMultiplePickingInGallery()
|
||||||
|
)
|
||||||
|
.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateCustomSectorIntent, creates intent for custom selector activity.
|
||||||
|
* @param context
|
||||||
|
* @param type
|
||||||
|
* @return Custom selector intent
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
private fun createCustomSelectorIntent(context: Context, type: Int): Intent {
|
||||||
|
storeType(context, type)
|
||||||
|
return Intent(context, CustomSelectorActivity::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private fun createCameraForImageIntent(context: Context, type: Int): Intent {
|
||||||
|
storeType(context, type)
|
||||||
|
|
||||||
|
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||||
|
try {
|
||||||
|
val capturedImageUri = createCameraPictureFile(context)
|
||||||
|
// We have to explicitly grant the write permission since Intent.setFlag works only on API Level >=20
|
||||||
|
grantWritePermission(context, intent, capturedImageUri)
|
||||||
|
intent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private fun revokeWritePermission(context: Context, uri: Uri) {
|
||||||
|
context.revokeUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private fun grantWritePermission(context: Context, intent: Intent, uri: Uri) {
|
||||||
|
val resInfoList =
|
||||||
|
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
for (resolveInfo in resInfoList) {
|
||||||
|
val packageName = resolveInfo.activityInfo.packageName
|
||||||
|
context.grantUriPermission(
|
||||||
|
packageName,
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private fun storeType(context: Context, type: Int) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(KEY_TYPE, type).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private fun restoreType(context: Context): Int {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_TYPE, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens default gallery or available galleries picker if there is no default
|
||||||
|
*
|
||||||
|
* @param type Custom type of your choice, which will be returned with the images
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun openGallery(
|
||||||
|
activity: Activity,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>,
|
||||||
|
type: Int,
|
||||||
|
openDocumentIntentPreferred: Boolean
|
||||||
|
) {
|
||||||
|
val intent = createGalleryIntent(activity, type, openDocumentIntentPreferred)
|
||||||
|
resultLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens Custom Selector
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun openCustomSelector(
|
||||||
|
activity: Activity,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>,
|
||||||
|
type: Int
|
||||||
|
) {
|
||||||
|
val intent = createCustomSelectorIntent(activity, type)
|
||||||
|
resultLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the camera app to pick image clicked by user
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun openCameraForImage(
|
||||||
|
activity: Activity,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>,
|
||||||
|
type: Int
|
||||||
|
) {
|
||||||
|
val intent = createCameraForImageIntent(activity, type)
|
||||||
|
resultLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(URISyntaxException::class)
|
||||||
|
@JvmStatic
|
||||||
|
private fun takenCameraPicture(context: Context): UploadableFile? {
|
||||||
|
val lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getString(KEY_LAST_CAMERA_PHOTO, null)
|
||||||
|
return if (lastCameraPhoto != null) {
|
||||||
|
UploadableFile(File(lastCameraPhoto))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(URISyntaxException::class)
|
||||||
|
@JvmStatic
|
||||||
|
private fun takenCameraVideo(context: Context): UploadableFile? {
|
||||||
|
val lastCameraVideo = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getString(KEY_LAST_CAMERA_VIDEO, null)
|
||||||
|
return if (lastCameraVideo != null) {
|
||||||
|
UploadableFile(File(lastCameraVideo))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun handleExternalImagesPicked(data: Intent?, activity: Activity): List<UploadableFile> {
|
||||||
|
return try {
|
||||||
|
getFilesFromGalleryPictures(data, activity)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
emptyList()
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private fun isPhoto(data: Intent?): Boolean {
|
||||||
|
return data == null || (data.data == null && data.clipData == null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private fun plainGalleryPickerIntent(
|
||||||
|
openDocumentIntentPreferred: Boolean
|
||||||
|
): Intent {
|
||||||
|
/*
|
||||||
|
* Asking for ACCESS_MEDIA_LOCATION at runtime solved the location-loss issue
|
||||||
|
* in the custom selector in Contributions fragment.
|
||||||
|
* Detailed discussion: https://github.com/commons-app/apps-android-commons/issues/5015
|
||||||
|
*
|
||||||
|
* This permission check, however, was insufficient to fix location-loss in
|
||||||
|
* the regular selector in Contributions fragment and Nearby fragment,
|
||||||
|
* especially on some devices running Android 13 that use the new Photo Picker by default.
|
||||||
|
*
|
||||||
|
* New Photo Picker: https://developer.android.com/training/data-storage/shared/photopicker
|
||||||
|
*
|
||||||
|
* The new Photo Picker introduced by Android redacts location tags from EXIF metadata.
|
||||||
|
* Reported on the Google Issue Tracker: https://issuetracker.google.com/issues/243294058
|
||||||
|
* Status: Won't fix (Intended behaviour)
|
||||||
|
*
|
||||||
|
* Switched intent from ACTION_GET_CONTENT to ACTION_OPEN_DOCUMENT (by default; can
|
||||||
|
* be changed through the Setting page) as:
|
||||||
|
*
|
||||||
|
* ACTION_GET_CONTENT opens the 'best application' for choosing that kind of data
|
||||||
|
* The best application is the new Photo Picker that redacts the location tags
|
||||||
|
*
|
||||||
|
* ACTION_OPEN_DOCUMENT, however, displays the various DocumentsProvider instances
|
||||||
|
* installed on the device, letting the user interactively navigate through them.
|
||||||
|
*
|
||||||
|
* So, this allows us to use the traditional file picker that does not redact location tags
|
||||||
|
* from EXIF.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
val intent = if (openDocumentIntentPreferred) {
|
||||||
|
Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
|
} else {
|
||||||
|
Intent(Intent.ACTION_GET_CONTENT)
|
||||||
|
}
|
||||||
|
intent.type = "image/*"
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun onPictureReturnedFromDocuments(
|
||||||
|
result: ActivityResult,
|
||||||
|
activity: Activity,
|
||||||
|
callbacks: Callbacks
|
||||||
|
) {
|
||||||
|
if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) {
|
||||||
|
try {
|
||||||
|
val photoPath = result.data?.data
|
||||||
|
val photoFile = PickedFiles.pickedExistingPicture(activity, photoPath!!)
|
||||||
|
callbacks.onImagesPicked(
|
||||||
|
singleFileList(photoFile),
|
||||||
|
ImageSource.DOCUMENTS,
|
||||||
|
restoreType(activity)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) {
|
||||||
|
PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
callbacks.onImagePickerError(e, ImageSource.DOCUMENTS, restoreType(activity))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callbacks.onCanceled(ImageSource.DOCUMENTS, restoreType(activity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onPictureReturnedFromCustomSelector.
|
||||||
|
* Retrieve and forward the images to upload wizard through callback.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun onPictureReturnedFromCustomSelector(
|
||||||
|
result: ActivityResult,
|
||||||
|
activity: Activity,
|
||||||
|
callbacks: Callbacks
|
||||||
|
) {
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
try {
|
||||||
|
val files = getFilesFromCustomSelector(result.data, activity)
|
||||||
|
callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callbacks.onCanceled(ImageSource.CUSTOM_SELECTOR, restoreType(activity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get files from custom selector
|
||||||
|
* Retrieve and process the selected images from the custom selector.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class, SecurityException::class)
|
||||||
|
@JvmStatic
|
||||||
|
private fun getFilesFromCustomSelector(
|
||||||
|
data: Intent?,
|
||||||
|
activity: Activity
|
||||||
|
): List<UploadableFile> {
|
||||||
|
val files = mutableListOf<UploadableFile>()
|
||||||
|
val images = data?.getParcelableArrayListExtra<Image>("Images")
|
||||||
|
images?.forEach { image ->
|
||||||
|
val uri = image.uri
|
||||||
|
val file = PickedFiles.pickedExistingPicture(activity, uri)
|
||||||
|
files.add(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) {
|
||||||
|
PickedFiles.copyFilesInSeparateThread(activity, files)
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun onPictureReturnedFromGallery(
|
||||||
|
result: ActivityResult,
|
||||||
|
activity: Activity,
|
||||||
|
callbacks: Callbacks
|
||||||
|
) {
|
||||||
|
if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) {
|
||||||
|
try {
|
||||||
|
val files = getFilesFromGalleryPictures(result.data, activity)
|
||||||
|
callbacks.onImagesPicked(files, ImageSource.GALLERY, restoreType(activity))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
callbacks.onImagePickerError(e, ImageSource.GALLERY, restoreType(activity))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callbacks.onCanceled(ImageSource.GALLERY, restoreType(activity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, SecurityException::class)
|
||||||
|
@JvmStatic
|
||||||
|
private fun getFilesFromGalleryPictures(
|
||||||
|
data: Intent?,
|
||||||
|
activity: Activity
|
||||||
|
): List<UploadableFile> {
|
||||||
|
val files = mutableListOf<UploadableFile>()
|
||||||
|
val clipData = data?.clipData
|
||||||
|
if (clipData == null) {
|
||||||
|
val uri = data?.data
|
||||||
|
val file = PickedFiles.pickedExistingPicture(activity, uri!!)
|
||||||
|
files.add(file)
|
||||||
|
} else {
|
||||||
|
for (i in 0 until clipData.itemCount) {
|
||||||
|
val uri = clipData.getItemAt(i).uri
|
||||||
|
val file = PickedFiles.pickedExistingPicture(activity, uri)
|
||||||
|
files.add(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) {
|
||||||
|
PickedFiles.copyFilesInSeparateThread(activity, files)
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun onPictureReturnedFromCamera(
|
||||||
|
activityResult: ActivityResult,
|
||||||
|
activity: Activity,
|
||||||
|
callbacks: Callbacks
|
||||||
|
) {
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
try {
|
||||||
|
val lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
|
.getString(KEY_PHOTO_URI, null)
|
||||||
|
if (!lastImageUri.isNullOrEmpty()) {
|
||||||
|
revokeWritePermission(activity, Uri.parse(lastImageUri))
|
||||||
|
}
|
||||||
|
|
||||||
|
val photoFile = takenCameraPicture(activity)
|
||||||
|
val files = mutableListOf<UploadableFile>()
|
||||||
|
photoFile?.let { files.add(it) }
|
||||||
|
|
||||||
|
if (photoFile == null) {
|
||||||
|
val e = IllegalStateException("Unable to get the picture returned from camera")
|
||||||
|
callbacks.onImagePickerError(e, ImageSource.CAMERA_IMAGE, restoreType(activity))
|
||||||
|
} else {
|
||||||
|
if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) {
|
||||||
|
PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile))
|
||||||
|
}
|
||||||
|
callbacks.onImagesPicked(files, ImageSource.CAMERA_IMAGE, restoreType(activity))
|
||||||
|
}
|
||||||
|
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(activity).edit()
|
||||||
|
.remove(KEY_LAST_CAMERA_PHOTO)
|
||||||
|
.remove(KEY_PHOTO_URI)
|
||||||
|
.apply()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
callbacks.onImagePickerError(e, ImageSource.CAMERA_IMAGE, restoreType(activity))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callbacks.onCanceled(ImageSource.CAMERA_IMAGE, restoreType(activity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun configuration(context: Context): FilePickerConfiguration {
|
||||||
|
return FilePickerConfiguration(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ImageSource {
|
||||||
|
GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO, CUSTOM_SELECTOR
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callbacks {
|
||||||
|
fun onImagePickerError(e: Exception, source: ImageSource, type: Int)
|
||||||
|
|
||||||
|
fun onImagesPicked(imageFiles: List<UploadableFile>, source: ImageSource, type: Int)
|
||||||
|
|
||||||
|
fun onCanceled(source: ImageSource, type: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HandleActivityResult {
|
||||||
|
fun onHandleActivityResult(callbacks: Callbacks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package fr.free.nrw.commons.filepicker;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
public class FilePickerConfiguration implements Constants {
|
|
||||||
|
|
||||||
private Context context;
|
|
||||||
|
|
||||||
FilePickerConfiguration(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FilePickerConfiguration setAllowMultiplePickInGallery(boolean allowMultiple) {
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
|
||||||
.putBoolean(BundleKeys.ALLOW_MULTIPLE, allowMultiple)
|
|
||||||
.apply();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FilePickerConfiguration setCopyTakenPhotosToPublicGalleryAppFolder(boolean copy) {
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
|
||||||
.putBoolean(BundleKeys.COPY_TAKEN_PHOTOS, copy)
|
|
||||||
.apply();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFolderName() {
|
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context).getString(BundleKeys.FOLDER_NAME, DEFAULT_FOLDER_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean allowsMultiplePickingInGallery() {
|
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.ALLOW_MULTIPLE, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean shouldCopyTakenPhotosToPublicGalleryAppFolder() {
|
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.COPY_TAKEN_PHOTOS, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean shouldCopyPickedImagesToPublicGalleryAppFolder() {
|
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.COPY_PICKED_IMAGES, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package fr.free.nrw.commons.filepicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
|
||||||
|
class FilePickerConfiguration(
|
||||||
|
private val context: Context
|
||||||
|
): Constants {
|
||||||
|
|
||||||
|
fun setAllowMultiplePickInGallery(allowMultiple: Boolean): FilePickerConfiguration {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||||
|
.putBoolean(Constants.BundleKeys.ALLOW_MULTIPLE, allowMultiple)
|
||||||
|
.apply()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCopyTakenPhotosToPublicGalleryAppFolder(copy: Boolean): FilePickerConfiguration {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||||
|
.putBoolean(Constants.BundleKeys.COPY_TAKEN_PHOTOS, copy)
|
||||||
|
.apply()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFolderName(): String {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getString(
|
||||||
|
Constants.BundleKeys.FOLDER_NAME,
|
||||||
|
Constants.DEFAULT_FOLDER_NAME
|
||||||
|
) ?: Constants.DEFAULT_FOLDER_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allowsMultiplePickingInGallery(): Boolean {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(Constants.BundleKeys.ALLOW_MULTIPLE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldCopyTakenPhotosToPublicGalleryAppFolder(): Boolean {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(Constants.BundleKeys.COPY_TAKEN_PHOTOS, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldCopyPickedImagesToPublicGalleryAppFolder(): Boolean {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(Constants.BundleKeys.COPY_PICKED_IMAGES, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package fr.free.nrw.commons.filepicker;
|
|
||||||
|
|
||||||
import android.webkit.MimeTypeMap;
|
|
||||||
|
|
||||||
import com.facebook.common.internal.ImmutableMap;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class MimeTypeMapWrapper {
|
|
||||||
|
|
||||||
private static final MimeTypeMap sMimeTypeMap = MimeTypeMap.getSingleton();
|
|
||||||
|
|
||||||
private static final Map<String, String> sMimeTypeToExtensionMap =
|
|
||||||
ImmutableMap.of(
|
|
||||||
"image/heif", "heif",
|
|
||||||
"image/heic", "heic");
|
|
||||||
|
|
||||||
public static String getExtensionFromMimeType(String mimeType) {
|
|
||||||
String result = sMimeTypeToExtensionMap.get(mimeType);
|
|
||||||
if (result != null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return sMimeTypeMap.getExtensionFromMimeType(mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package fr.free.nrw.commons.filepicker
|
||||||
|
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
|
||||||
|
class MimeTypeMapWrapper {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val sMimeTypeMap = MimeTypeMap.getSingleton()
|
||||||
|
|
||||||
|
private val sMimeTypeToExtensionMap = mapOf(
|
||||||
|
"image/heif" to "heif",
|
||||||
|
"image/heic" to "heic"
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getExtensionFromMimeType(mimeType: String): String? {
|
||||||
|
val result = sMimeTypeToExtensionMap[mimeType]
|
||||||
|
if (result != null) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return sMimeTypeMap.getExtensionFromMimeType(mimeType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
package fr.free.nrw.commons.filepicker;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.media.MediaScannerConnection;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.webkit.MimeTypeMap;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.core.content.FileProvider;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PickedFiles.
|
|
||||||
* Process the upload items.
|
|
||||||
*/
|
|
||||||
public class PickedFiles implements Constants {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Folder Name
|
|
||||||
* @param context
|
|
||||||
* @return default application folder name.
|
|
||||||
*/
|
|
||||||
private static String getFolderName(@NonNull Context context) {
|
|
||||||
return FilePicker.configuration(context).getFolderName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* tempImageDirectory
|
|
||||||
* @param context
|
|
||||||
* @return temporary image directory to copy and perform exif changes.
|
|
||||||
*/
|
|
||||||
private static File tempImageDirectory(@NonNull Context context) {
|
|
||||||
File privateTempDir = new File(context.getCacheDir(), DEFAULT_FOLDER_NAME);
|
|
||||||
if (!privateTempDir.exists()) privateTempDir.mkdirs();
|
|
||||||
return privateTempDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* writeToFile
|
|
||||||
* writes inputStream data to the destination file.
|
|
||||||
* @param in input stream of source file.
|
|
||||||
* @param file destination file
|
|
||||||
*/
|
|
||||||
private static void writeToFile(InputStream in, File file) throws IOException {
|
|
||||||
try (OutputStream out = new FileOutputStream(file)) {
|
|
||||||
byte[] buf = new byte[1024];
|
|
||||||
int len;
|
|
||||||
while ((len = in.read(buf)) > 0) {
|
|
||||||
out.write(buf, 0, len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy file function.
|
|
||||||
* Copies source file to destination file.
|
|
||||||
* @param src source file
|
|
||||||
* @param dst destination file
|
|
||||||
* @throws IOException (File input stream exception)
|
|
||||||
*/
|
|
||||||
private static void copyFile(File src, File dst) throws IOException {
|
|
||||||
try (InputStream in = new FileInputStream(src)) {
|
|
||||||
writeToFile(in, dst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy files in separate thread.
|
|
||||||
* Copies all the uploadable files to the temp image folder on background thread.
|
|
||||||
* @param context
|
|
||||||
* @param filesToCopy uploadable file list to be copied.
|
|
||||||
*/
|
|
||||||
static void copyFilesInSeparateThread(final Context context, final List<UploadableFile> filesToCopy) {
|
|
||||||
new Thread(() -> {
|
|
||||||
List<File> copiedFiles = new ArrayList<>();
|
|
||||||
int i = 1;
|
|
||||||
for (UploadableFile uploadableFile : filesToCopy) {
|
|
||||||
File fileToCopy = uploadableFile.getFile();
|
|
||||||
File dstDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), getFolderName(context));
|
|
||||||
if (!dstDir.exists()) {
|
|
||||||
dstDir.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] filenameSplit = fileToCopy.getName().split("\\.");
|
|
||||||
String extension = "." + filenameSplit[filenameSplit.length - 1];
|
|
||||||
String filename = String.format("IMG_%s_%d.%s", new SimpleDateFormat("yyyyMMdd_HHmmss").format(Calendar.getInstance().getTime()), i, extension);
|
|
||||||
|
|
||||||
File dstFile = new File(dstDir, filename);
|
|
||||||
try {
|
|
||||||
dstFile.createNewFile();
|
|
||||||
copyFile(fileToCopy, dstFile);
|
|
||||||
copiedFiles.add(dstFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
scanCopiedImages(context, copiedFiles);
|
|
||||||
}).run();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* singleFileList.
|
|
||||||
* converts a single uploadableFile to list of uploadableFile.
|
|
||||||
* @param file uploadable file
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
static List<UploadableFile> singleFileList(UploadableFile file) {
|
|
||||||
List<UploadableFile> list = new ArrayList<>();
|
|
||||||
list.add(file);
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ScanCopiedImages
|
|
||||||
* Scan copied images metadata using media scanner.
|
|
||||||
* @param context
|
|
||||||
* @param copiedImages copied images list.
|
|
||||||
*/
|
|
||||||
static void scanCopiedImages(Context context, List<File> copiedImages) {
|
|
||||||
String[] paths = new String[copiedImages.size()];
|
|
||||||
for (int i = 0; i < copiedImages.size(); i++) {
|
|
||||||
paths[i] = copiedImages.get(i).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaScannerConnection.scanFile(context,
|
|
||||||
paths, null,
|
|
||||||
(path, uri) -> {
|
|
||||||
Timber.d("Scanned " + path + ":");
|
|
||||||
Timber.d("-> uri=%s", uri);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pickedExistingPicture
|
|
||||||
* convert the image into uploadable file.
|
|
||||||
* @param photoUri Uri of the image.
|
|
||||||
* @return Uploadable file ready for tag redaction.
|
|
||||||
*/
|
|
||||||
public static UploadableFile pickedExistingPicture(@NonNull Context context, Uri photoUri) throws IOException, SecurityException {// SecurityException for those file providers who share URI but forget to grant necessary permissions
|
|
||||||
File directory = tempImageDirectory(context);
|
|
||||||
File photoFile = new File(directory, UUID.randomUUID().toString() + "." + getMimeType(context, photoUri));
|
|
||||||
if (photoFile.createNewFile()) {
|
|
||||||
try (InputStream pictureInputStream = context.getContentResolver().openInputStream(photoUri)) {
|
|
||||||
writeToFile(pictureInputStream, photoFile);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new IOException("could not create photoFile to write upon");
|
|
||||||
}
|
|
||||||
return new UploadableFile(photoUri, photoFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getCameraPictureLocation
|
|
||||||
*/
|
|
||||||
static File getCameraPicturesLocation(@NonNull Context context) throws IOException {
|
|
||||||
File dir = tempImageDirectory(context);
|
|
||||||
return File.createTempFile(UUID.randomUUID().toString(), ".jpg", dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To find out the extension of required object in given uri
|
|
||||||
* Solution by http://stackoverflow.com/a/36514823/1171484
|
|
||||||
*/
|
|
||||||
private static String getMimeType(@NonNull Context context, @NonNull Uri uri) {
|
|
||||||
String extension;
|
|
||||||
|
|
||||||
//Check uri format to avoid null
|
|
||||||
if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
|
|
||||||
//If scheme is a content
|
|
||||||
extension = MimeTypeMapWrapper.getExtensionFromMimeType(context.getContentResolver().getType(uri));
|
|
||||||
} else {
|
|
||||||
//If scheme is a File
|
|
||||||
//This will replace white spaces with %20 and also other special characters. This will avoid returning null values on file name with spaces and special characters.
|
|
||||||
extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(new File(uri.getPath())).toString());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return extension;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GetUriToFile
|
|
||||||
* @param file get uri of file
|
|
||||||
* @return uri of requested file.
|
|
||||||
*/
|
|
||||||
static Uri getUriToFile(@NonNull Context context, @NonNull File file) {
|
|
||||||
String packageName = context.getApplicationContext().getPackageName();
|
|
||||||
String authority = packageName + ".provider";
|
|
||||||
return FileProvider.getUriForFile(context, authority, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
195
app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.kt
Normal file
195
app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.kt
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
package fr.free.nrw.commons.filepicker
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import fr.free.nrw.commons.filepicker.Constants.Companion.DEFAULT_FOLDER_NAME
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PickedFiles.
|
||||||
|
* Process the upload items.
|
||||||
|
*/
|
||||||
|
object PickedFiles : Constants {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Folder Name
|
||||||
|
* @return default application folder name.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
private fun getFolderName(context: Context): String {
|
||||||
|
return FilePicker.configuration(context).getFolderName()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tempImageDirectory
|
||||||
|
* @return temporary image directory to copy and perform exif changes.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
private fun tempImageDirectory(context: Context): File {
|
||||||
|
val privateTempDir = File(context.cacheDir, DEFAULT_FOLDER_NAME)
|
||||||
|
if (!privateTempDir.exists()) privateTempDir.mkdirs()
|
||||||
|
return privateTempDir
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* writeToFile
|
||||||
|
* Writes inputStream data to the destination file.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun writeToFile(inputStream: InputStream, file: File) {
|
||||||
|
inputStream.use { input ->
|
||||||
|
FileOutputStream(file).use { output ->
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
var length: Int
|
||||||
|
while (input.read(buffer).also { length = it } > 0) {
|
||||||
|
output.write(buffer, 0, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy file function.
|
||||||
|
* Copies source file to destination file.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
@JvmStatic
|
||||||
|
private fun copyFile(src: File, dst: File) {
|
||||||
|
FileInputStream(src).use { inputStream ->
|
||||||
|
writeToFile(inputStream, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy files in separate thread.
|
||||||
|
* Copies all the uploadable files to the temp image folder on background thread.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun copyFilesInSeparateThread(context: Context, filesToCopy: List<UploadableFile>) {
|
||||||
|
Thread {
|
||||||
|
val copiedFiles = mutableListOf<File>()
|
||||||
|
var index = 1
|
||||||
|
filesToCopy.forEach { uploadableFile ->
|
||||||
|
val fileToCopy = uploadableFile.file
|
||||||
|
val dstDir = File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
||||||
|
getFolderName(context)
|
||||||
|
)
|
||||||
|
if (!dstDir.exists()) dstDir.mkdirs()
|
||||||
|
|
||||||
|
val filenameSplit = fileToCopy.name.split(".")
|
||||||
|
val extension = ".${filenameSplit.last()}"
|
||||||
|
val filename = "IMG_${SimpleDateFormat(
|
||||||
|
"yyyyMMdd_HHmmss",
|
||||||
|
Locale.getDefault()).format(Date())}_$index$extension"
|
||||||
|
val dstFile = File(dstDir, filename)
|
||||||
|
|
||||||
|
try {
|
||||||
|
dstFile.createNewFile()
|
||||||
|
copyFile(fileToCopy, dstFile)
|
||||||
|
copiedFiles.add(dstFile)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
scanCopiedImages(context, copiedFiles)
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* singleFileList
|
||||||
|
* Converts a single uploadableFile to list of uploadableFile.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun singleFileList(file: UploadableFile): List<UploadableFile> {
|
||||||
|
return listOf(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScanCopiedImages
|
||||||
|
* Scans copied images metadata using media scanner.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun scanCopiedImages(context: Context, copiedImages: List<File>) {
|
||||||
|
val paths = copiedImages.map { it.toString() }.toTypedArray()
|
||||||
|
MediaScannerConnection.scanFile(context, paths, null) { path, uri ->
|
||||||
|
Timber.d("Scanned $path:")
|
||||||
|
Timber.d("-> uri=$uri")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pickedExistingPicture
|
||||||
|
* Convert the image into uploadable file.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class, SecurityException::class)
|
||||||
|
@JvmStatic
|
||||||
|
fun pickedExistingPicture(context: Context, photoUri: Uri): UploadableFile {
|
||||||
|
val directory = tempImageDirectory(context)
|
||||||
|
val mimeType = getMimeType(context, photoUri)
|
||||||
|
val photoFile = File(directory, "${UUID.randomUUID()}.$mimeType")
|
||||||
|
|
||||||
|
if (photoFile.createNewFile()) {
|
||||||
|
context.contentResolver.openInputStream(photoUri)?.use { inputStream ->
|
||||||
|
writeToFile(inputStream, photoFile)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw IOException("Could not create photoFile to write upon")
|
||||||
|
}
|
||||||
|
return UploadableFile(photoUri, photoFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getCameraPictureLocation
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
@JvmStatic
|
||||||
|
fun getCameraPicturesLocation(context: Context): File {
|
||||||
|
val dir = tempImageDirectory(context)
|
||||||
|
return File.createTempFile(UUID.randomUUID().toString(), ".jpg", dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To find out the extension of the required object in a given uri
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
private fun getMimeType(context: Context, uri: Uri): String {
|
||||||
|
return if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
|
||||||
|
context.contentResolver.getType(uri)
|
||||||
|
?.let { MimeTypeMapWrapper.getExtensionFromMimeType(it) }
|
||||||
|
} else {
|
||||||
|
MimeTypeMap.getFileExtensionFromUrl(
|
||||||
|
Uri.fromFile(uri.path?.let { File(it) }).toString()
|
||||||
|
)
|
||||||
|
} ?: "jpg" // Default to jpg if unable to determine type
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetUriToFile
|
||||||
|
* @param file get uri of file
|
||||||
|
* @return uri of requested file.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getUriToFile(context: Context, file: File): Uri {
|
||||||
|
val packageName = context.applicationContext.packageName
|
||||||
|
val authority = "$packageName.provider"
|
||||||
|
return FileProvider.getUriForFile(context, authority, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
package fr.free.nrw.commons.filepicker;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Parcel;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.exifinterface.media.ExifInterface;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.upload.FileUtils;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Date;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class UploadableFile implements Parcelable {
|
|
||||||
public static final Creator<UploadableFile> CREATOR = new Creator<UploadableFile>() {
|
|
||||||
@Override
|
|
||||||
public UploadableFile createFromParcel(Parcel in) {
|
|
||||||
return new UploadableFile(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UploadableFile[] newArray(int size) {
|
|
||||||
return new UploadableFile[size];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final Uri contentUri;
|
|
||||||
private final File file;
|
|
||||||
|
|
||||||
public UploadableFile(Uri contentUri, File file) {
|
|
||||||
this.contentUri = contentUri;
|
|
||||||
this.file = file;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UploadableFile(File file) {
|
|
||||||
this.file = file;
|
|
||||||
this.contentUri = Uri.fromFile(new File(file.getPath()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public UploadableFile(Parcel in) {
|
|
||||||
this.contentUri = in.readParcelable(Uri.class.getClassLoader());
|
|
||||||
file = (File) in.readSerializable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri getContentUri() {
|
|
||||||
return contentUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public File getFile() {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFilePath() {
|
|
||||||
return file.getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri getMediaUri() {
|
|
||||||
return Uri.parse(getFilePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMimeType(Context context) {
|
|
||||||
return FileUtils.getMimeType(context, getMediaUri());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int describeContents() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* First try to get the file creation date from EXIF else fall back to CP
|
|
||||||
* @param context
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public DateTimeWithSource getFileCreatedDate(Context context) {
|
|
||||||
DateTimeWithSource dateTimeFromExif = getDateTimeFromExif();
|
|
||||||
if (dateTimeFromExif == null) {
|
|
||||||
return getFileCreatedDateFromCP(context);
|
|
||||||
} else {
|
|
||||||
return dateTimeFromExif;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get filePath creation date from uri from all possible content providers
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private DateTimeWithSource getFileCreatedDateFromCP(Context context) {
|
|
||||||
try {
|
|
||||||
Cursor cursor = context.getContentResolver().query(contentUri, null, null, null, null);
|
|
||||||
if (cursor == null) {
|
|
||||||
return null;//Could not fetch last_modified
|
|
||||||
}
|
|
||||||
//Content provider contracts for opening gallery from the app and that by sharing from gallery from outside are different and we need to handle both the cases
|
|
||||||
int lastModifiedColumnIndex = cursor.getColumnIndex("last_modified");//If gallery is opened from in app
|
|
||||||
if (lastModifiedColumnIndex == -1) {
|
|
||||||
lastModifiedColumnIndex = cursor.getColumnIndex("datetaken");
|
|
||||||
}
|
|
||||||
//If both the content providers do not give the data, lets leave it to Jesus
|
|
||||||
if (lastModifiedColumnIndex == -1) {
|
|
||||||
cursor.close();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
cursor.moveToFirst();
|
|
||||||
return new DateTimeWithSource(cursor.getLong(lastModifiedColumnIndex), DateTimeWithSource.CP_SOURCE);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;////Could not fetch last_modified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicate whether the EXIF contains the location (both latitude and longitude).
|
|
||||||
*
|
|
||||||
* @return whether the location exists for the file's EXIF
|
|
||||||
*/
|
|
||||||
public boolean hasLocation() {
|
|
||||||
try {
|
|
||||||
ExifInterface exif = new ExifInterface(file.getAbsolutePath());
|
|
||||||
final String latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
|
|
||||||
final String longitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
|
|
||||||
return latitude != null && longitude != null;
|
|
||||||
} catch (IOException | NumberFormatException | IndexOutOfBoundsException e) {
|
|
||||||
Timber.tag("UploadableFile");
|
|
||||||
Timber.d(e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get filePath creation date from uri from EXIF
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private DateTimeWithSource getDateTimeFromExif() {
|
|
||||||
try {
|
|
||||||
ExifInterface exif = new ExifInterface(file.getAbsolutePath());
|
|
||||||
// TAG_DATETIME returns the last edited date, we need TAG_DATETIME_ORIGINAL for creation date
|
|
||||||
// See issue https://github.com/commons-app/apps-android-commons/issues/1971
|
|
||||||
String dateTimeSubString = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL);
|
|
||||||
if (dateTimeSubString!=null) { //getAttribute may return null
|
|
||||||
String year = dateTimeSubString.substring(0,4);
|
|
||||||
String month = dateTimeSubString.substring(5,7);
|
|
||||||
String day = dateTimeSubString.substring(8,10);
|
|
||||||
// This date is stored as a string (not as a date), the rason is we don't want to include timezones
|
|
||||||
String dateCreatedString = String.format("%04d-%02d-%02d", Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day));
|
|
||||||
if (dateCreatedString.length() == 10) { //yyyy-MM-dd format of date is expected
|
|
||||||
@SuppressLint("RestrictedApi") Long dateTime = exif.getDateTimeOriginal();
|
|
||||||
if(dateTime != null){
|
|
||||||
Date date = new Date(dateTime);
|
|
||||||
return new DateTimeWithSource(date, dateCreatedString, DateTimeWithSource.EXIF_SOURCE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException | NumberFormatException | IndexOutOfBoundsException e) {
|
|
||||||
Timber.tag("UploadableFile");
|
|
||||||
Timber.d(e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeToParcel(Parcel parcel, int i) {
|
|
||||||
parcel.writeParcelable(contentUri, 0);
|
|
||||||
parcel.writeSerializable(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class contains the epochDate along with the source from which it was extracted
|
|
||||||
*/
|
|
||||||
public class DateTimeWithSource {
|
|
||||||
public static final String CP_SOURCE = "contentProvider";
|
|
||||||
public static final String EXIF_SOURCE = "exif";
|
|
||||||
|
|
||||||
private final long epochDate;
|
|
||||||
private String dateString; // this does not includes timezone information
|
|
||||||
private final String source;
|
|
||||||
|
|
||||||
public DateTimeWithSource(long epochDate, String source) {
|
|
||||||
this.epochDate = epochDate;
|
|
||||||
this.source = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DateTimeWithSource(Date date, String source) {
|
|
||||||
this.epochDate = date.getTime();
|
|
||||||
this.source = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DateTimeWithSource(Date date, String dateString, String source) {
|
|
||||||
this.epochDate = date.getTime();
|
|
||||||
this.dateString = dateString;
|
|
||||||
this.source = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getEpochDate() {
|
|
||||||
return epochDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDateString() {
|
|
||||||
return dateString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSource() {
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
package fr.free.nrw.commons.filepicker
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.upload.FileUtils
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Date
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class UploadableFile : Parcelable {
|
||||||
|
|
||||||
|
val contentUri: Uri
|
||||||
|
val file: File
|
||||||
|
|
||||||
|
constructor(contentUri: Uri, file: File) {
|
||||||
|
this.contentUri = contentUri
|
||||||
|
this.file = file
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
this.file = file
|
||||||
|
this.contentUri = Uri.fromFile(File(file.path))
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(parcel: Parcel) {
|
||||||
|
contentUri = parcel.readParcelable(Uri::class.java.classLoader)!!
|
||||||
|
file = parcel.readSerializable() as File
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFilePath(): String {
|
||||||
|
return file.path
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMediaUri(): Uri {
|
||||||
|
return Uri.parse(getFilePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMimeType(context: Context): String? {
|
||||||
|
return FileUtils.getMimeType(context, getMediaUri())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First try to get the file creation date from EXIF, else fall back to Content Provider (CP)
|
||||||
|
*/
|
||||||
|
fun getFileCreatedDate(context: Context): DateTimeWithSource? {
|
||||||
|
return getDateTimeFromExif() ?: getFileCreatedDateFromCP(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filePath creation date from URI using all possible content providers
|
||||||
|
*/
|
||||||
|
private fun getFileCreatedDateFromCP(context: Context): DateTimeWithSource? {
|
||||||
|
return try {
|
||||||
|
val cursor: Cursor? = context.contentResolver.query(contentUri, null, null, null, null)
|
||||||
|
cursor?.use {
|
||||||
|
val lastModifiedColumnIndex = cursor
|
||||||
|
.getColumnIndex(
|
||||||
|
"last_modified"
|
||||||
|
).takeIf { it != -1 }
|
||||||
|
?: cursor.getColumnIndex("datetaken")
|
||||||
|
if (lastModifiedColumnIndex == -1) return null // No valid column found
|
||||||
|
cursor.moveToFirst()
|
||||||
|
DateTimeWithSource(
|
||||||
|
cursor.getLong(
|
||||||
|
lastModifiedColumnIndex
|
||||||
|
), DateTimeWithSource.CP_SOURCE)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("UploadableFile").d(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the EXIF contains the location (both latitude and longitude).
|
||||||
|
*/
|
||||||
|
fun hasLocation(): Boolean {
|
||||||
|
return try {
|
||||||
|
val exif = ExifInterface(file.absolutePath)
|
||||||
|
val latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)
|
||||||
|
val longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)
|
||||||
|
latitude != null && longitude != null
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.tag("UploadableFile").d(e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filePath creation date from URI using EXIF data
|
||||||
|
*/
|
||||||
|
private fun getDateTimeFromExif(): DateTimeWithSource? {
|
||||||
|
return try {
|
||||||
|
val exif = ExifInterface(file.absolutePath)
|
||||||
|
val dateTimeSubString = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)
|
||||||
|
if (dateTimeSubString != null) {
|
||||||
|
val year = dateTimeSubString.substring(0, 4).toInt()
|
||||||
|
val month = dateTimeSubString.substring(5, 7).toInt()
|
||||||
|
val day = dateTimeSubString.substring(8, 10).toInt()
|
||||||
|
val dateCreatedString = "%04d-%02d-%02d".format(year, month, day)
|
||||||
|
if (dateCreatedString.length == 10) {
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
val dateTime = exif.dateTimeOriginal
|
||||||
|
if (dateTime != null) {
|
||||||
|
val date = Date(dateTime)
|
||||||
|
return DateTimeWithSource(date, dateCreatedString, DateTimeWithSource.EXIF_SOURCE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("UploadableFile").d(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeParcelable(contentUri, flags)
|
||||||
|
parcel.writeSerializable(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateTimeWithSource {
|
||||||
|
companion object {
|
||||||
|
const val CP_SOURCE = "contentProvider"
|
||||||
|
const val EXIF_SOURCE = "exif"
|
||||||
|
}
|
||||||
|
|
||||||
|
val epochDate: Long
|
||||||
|
var dateString: String? = null
|
||||||
|
val source: String
|
||||||
|
|
||||||
|
constructor(epochDate: Long, source: String) {
|
||||||
|
this.epochDate = epochDate
|
||||||
|
this.source = source
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(date: Date, source: String) {
|
||||||
|
epochDate = date.time
|
||||||
|
this.source = source
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(date: Date, dateString: String, source: String) {
|
||||||
|
epochDate = date.time
|
||||||
|
this.dateString = dateString
|
||||||
|
this.source = source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<UploadableFile> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): UploadableFile {
|
||||||
|
return UploadableFile(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<UploadableFile?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ import fr.free.nrw.commons.campaigns.CampaignView
|
||||||
import fr.free.nrw.commons.contributions.ContributionController
|
import fr.free.nrw.commons.contributions.ContributionController
|
||||||
import fr.free.nrw.commons.contributions.MainActivity
|
import fr.free.nrw.commons.contributions.MainActivity
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection
|
import fr.free.nrw.commons.di.ApplicationlessInjection
|
||||||
|
import fr.free.nrw.commons.filepicker.FilePicker
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager
|
import fr.free.nrw.commons.location.LocationServiceManager
|
||||||
import fr.free.nrw.commons.logging.CommonsLogSender
|
import fr.free.nrw.commons.logging.CommonsLogSender
|
||||||
|
|
@ -83,9 +84,17 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
private val cameraPickLauncherForResult: ActivityResultLauncher<Intent> =
|
private val cameraPickLauncherForResult: ActivityResultLauncher<Intent> =
|
||||||
registerForActivityResult(StartActivityForResult()) { result ->
|
registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
contributionController.handleActivityResultWithCallback(requireActivity()) { callbacks ->
|
contributionController.handleActivityResultWithCallback(
|
||||||
contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks)
|
requireActivity(),
|
||||||
|
object: FilePicker.HandleActivityResult {
|
||||||
|
override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) {
|
||||||
|
contributionController.onPictureReturnedFromCamera(
|
||||||
|
result,
|
||||||
|
requireActivity(),
|
||||||
|
callbacks
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class CustomSelectorUtils {
|
||||||
fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact())
|
fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact())
|
||||||
val sha1 =
|
val sha1 =
|
||||||
fileUtilsWrapper.getSHA1(
|
fileUtilsWrapper.getSHA1(
|
||||||
fileUtilsWrapper.getFileInputStream(uploadableFile.filePath),
|
fileUtilsWrapper.getFileInputStream(uploadableFile.getFilePath()),
|
||||||
)
|
)
|
||||||
uploadableFile.file.delete()
|
uploadableFile.file.delete()
|
||||||
sha1
|
sha1
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package fr.free.nrw.commons.filepicker;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.MatrixCursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.provider.OpenableColumns;
|
|
||||||
import androidx.core.content.FileProvider;
|
|
||||||
import org.robolectric.annotation.Implementation;
|
|
||||||
import org.robolectric.annotation.Implements;
|
|
||||||
|
|
||||||
@Implements(FileProvider.class)
|
|
||||||
public class ShadowFileProvider {
|
|
||||||
|
|
||||||
@Implementation
|
|
||||||
public Cursor query(final Uri uri, final String[] projection, final String selection,
|
|
||||||
final String[] selectionArgs,
|
|
||||||
final String sortOrder) {
|
|
||||||
|
|
||||||
if (uri == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String[] columns = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
|
|
||||||
final Object[] values = {"dummy", 500};
|
|
||||||
final MatrixCursor cursor = new MatrixCursor(columns, 1);
|
|
||||||
|
|
||||||
if (!uri.equals(Uri.EMPTY)) {
|
|
||||||
cursor.addRow(values);
|
|
||||||
}
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package fr.free.nrw.commons.filepicker
|
||||||
|
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.MatrixCursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import org.robolectric.annotation.Implementation
|
||||||
|
import org.robolectric.annotation.Implements
|
||||||
|
|
||||||
|
@Implements(FileProvider::class)
|
||||||
|
class ShadowFileProvider {
|
||||||
|
|
||||||
|
@Implementation
|
||||||
|
fun query(
|
||||||
|
uri: Uri?,
|
||||||
|
projection: Array<String>?,
|
||||||
|
selection: String?,
|
||||||
|
selectionArgs: Array<String>?,
|
||||||
|
sortOrder: String?
|
||||||
|
): Cursor? {
|
||||||
|
|
||||||
|
if (uri == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val columns = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
|
||||||
|
val values = arrayOf<Any>("dummy", 500)
|
||||||
|
val cursor = MatrixCursor(columns, 1)
|
||||||
|
|
||||||
|
if (uri != Uri.EMPTY) {
|
||||||
|
cursor.addRow(values)
|
||||||
|
}
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -62,7 +62,7 @@ class UploadPresenterTest {
|
||||||
`when`(repository.buildContributions()).thenReturn(Observable.just(contribution))
|
`when`(repository.buildContributions()).thenReturn(Observable.just(contribution))
|
||||||
uploadableFiles.add(uploadableFile)
|
uploadableFiles.add(uploadableFile)
|
||||||
`when`(view.uploadableFiles).thenReturn(uploadableFiles)
|
`when`(view.uploadableFiles).thenReturn(uploadableFiles)
|
||||||
`when`(uploadableFile.filePath).thenReturn("data://test")
|
`when`(uploadableFile.getFilePath()).thenReturn("data://test")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue