mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 14:53:59 +01:00 
			
		
		
		
	Merge branch 'main' into fix-issues
This commit is contained in:
		
						commit
						00866c565c
					
				
					 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
	
	 Nicolas Raoul
						Nicolas Raoul