Migrated filepicker module from Java to Kotlin

This commit is contained in:
Saifuddin 2024-12-04 19:59:18 +05:30
parent 5c2a68102e
commit 3e1208ee3d
11 changed files with 595 additions and 568 deletions

View file

@ -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";
}
}

View 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"
}
}
}

View file

@ -1,16 +1,12 @@
package fr.free.nrw.commons.filepicker; package fr.free.nrw.commons.filepicker
/** /**
* Provides abstract methods which are overridden while handling Contribution Results * Provides abstract methods which are overridden while handling Contribution Results
* inside the ContributionsController * inside the ContributionsController
*/ */
public abstract class DefaultCallback implements FilePicker.Callbacks { abstract class DefaultCallback: FilePicker.Callbacks {
@Override override fun onImagePickerError(e: Exception, source: FilePicker.ImageSource, type: Int) {}
public void onImagePickerError(Exception e, FilePicker.ImageSource source, int type) {
}
@Override override fun onCanceled(source: FilePicker.ImageSource, type: Int) {}
public void onCanceled(FilePicker.ImageSource source, int type) {
}
} }

View file

@ -1,7 +1,5 @@
package fr.free.nrw.commons.filepicker; package fr.free.nrw.commons.filepicker
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider
public class ExtendedFileProvider extends FileProvider { class ExtendedFileProvider: FileProvider() {}
}

View file

@ -1,60 +1,73 @@
package fr.free.nrw.commons.filepicker; package fr.free.nrw.commons.filepicker
import static fr.free.nrw.commons.filepicker.PickedFiles.singleFileList; 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
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 { object FilePicker : Constants {
private static final String KEY_PHOTO_URI = "photo_uri"; private const val KEY_PHOTO_URI = "photo_uri"
private static final String KEY_VIDEO_URI = "video_uri"; private const val KEY_VIDEO_URI = "video_uri"
private static final String KEY_LAST_CAMERA_PHOTO = "last_photo"; private const val KEY_LAST_CAMERA_PHOTO = "last_photo"
private static final String KEY_LAST_CAMERA_VIDEO = "last_video"; private const val KEY_LAST_CAMERA_VIDEO = "last_video"
private static final String KEY_TYPE = "type"; private const val KEY_TYPE = "type"
/** /**
* Returns the uri of the clicked image so that it can be put in MediaStore * Returns the uri of the clicked image so that it can be put in MediaStore
*/ */
private static Uri createCameraPictureFile(@NonNull Context context) throws IOException { @Throws(IOException::class)
File imagePath = PickedFiles.getCameraPicturesLocation(context);
Uri uri = PickedFiles.getUriToFile(context, imagePath); @JvmStatic
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); private fun createCameraPictureFile(context: Context): Uri {
editor.putString(KEY_PHOTO_URI, uri.toString()); val imagePath = PickedFiles.getCameraPicturesLocation(context)
editor.putString(KEY_LAST_CAMERA_PHOTO, imagePath.toString()); val uri = PickedFiles.getUriToFile(context, imagePath)
editor.apply(); val editor = PreferenceManager.getDefaultSharedPreferences(context).edit()
return uri; 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) { @JvmStatic
private fun createGalleryIntent(
context: Context,
type: Int,
openDocumentIntentPreferred: Boolean
): Intent {
// storing picked image type to shared preferences // storing picked image type to shared preferences
storeType(context, type); storeType(context, type)
//Supported types are SVG, PNG and JPEG,GIF, TIFF, WebP, XCF // 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"}; 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) return plainGalleryPickerIntent(openDocumentIntentPreferred)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, configuration(context).allowsMultiplePickingInGallery()) .putExtra(
.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); Intent.EXTRA_ALLOW_MULTIPLE,
configuration(context).allowsMultiplePickingInGallery()
)
.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
} }
/** /**
@ -63,107 +76,149 @@ public class FilePicker implements Constants {
* @param type * @param type
* @return Custom selector intent * @return Custom selector intent
*/ */
private static Intent createCustomSelectorIntent(@NonNull Context context, int type) { @JvmStatic
storeType(context, type); private fun createCustomSelectorIntent(context: Context, type: Int): Intent {
return new Intent(context, CustomSelectorActivity.class); storeType(context, type)
return Intent(context, CustomSelectorActivity::class.java)
} }
private static Intent createCameraForImageIntent(@NonNull Context context, int type) { @JvmStatic
storeType(context, type); private fun createCameraForImageIntent(context: Context, type: Int): Intent {
storeType(context, type)
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
try { try {
Uri capturedImageUri = createCameraPictureFile(context); val capturedImageUri = createCameraPictureFile(context)
//We have to explicitly grant the write permission since Intent.setFlag works only on API Level >=20 // We have to explicitly grant the write permission since Intent.setFlag works only on API Level >=20
grantWritePermission(context, intent, capturedImageUri); grantWritePermission(context, intent, capturedImageUri)
intent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri); intent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri)
} catch (Exception e) { } catch (e: Exception) {
e.printStackTrace(); e.printStackTrace()
} }
return intent; return intent
} }
private static void revokeWritePermission(@NonNull Context context, Uri uri) { @JvmStatic
context.revokeUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); private fun revokeWritePermission(context: Context, uri: Uri) {
context.revokeUriPermission(
uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} }
private static void grantWritePermission(@NonNull Context context, Intent intent, Uri uri) { @JvmStatic
List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); private fun grantWritePermission(context: Context, intent: Intent, uri: Uri) {
for (ResolveInfo resolveInfo : resInfoList) { val resInfoList =
String packageName = resolveInfo.activityInfo.packageName; context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); 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
)
} }
} }
private static void storeType(@NonNull Context context, int type) { @JvmStatic
PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(KEY_TYPE, type).apply(); private fun storeType(context: Context, type: Int) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(KEY_TYPE, type).apply()
} }
private static int restoreType(@NonNull Context context) { @JvmStatic
return PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_TYPE, 0); private fun restoreType(context: Context): Int {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_TYPE, 0)
} }
/** /**
* Opens default galery or a available galleries picker if there is no default * 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 * @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) { @JvmStatic
Intent intent = createGalleryIntent(activity, type, openDocumentIntentPreferred); fun openGallery(
resultLauncher.launch(intent); activity: Activity,
resultLauncher: ActivityResultLauncher<Intent>,
type: Int,
openDocumentIntentPreferred: Boolean
) {
val intent = createGalleryIntent(activity, type, openDocumentIntentPreferred)
resultLauncher.launch(intent)
} }
/** /**
* Opens Custom Selector * Opens Custom Selector
*/ */
public static void openCustomSelector(Activity activity, ActivityResultLauncher<Intent> resultLauncher, int type) { @JvmStatic
Intent intent = createCustomSelectorIntent(activity, type); fun openCustomSelector(
resultLauncher.launch(intent); 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 * Opens the camera app to pick image clicked by user
*/ */
public static void openCameraForImage(Activity activity, ActivityResultLauncher<Intent> resultLauncher, int type) { @JvmStatic
Intent intent = createCameraForImageIntent(activity, type); fun openCameraForImage(
resultLauncher.launch(intent); activity: Activity,
resultLauncher: ActivityResultLauncher<Intent>,
type: Int
) {
val intent = createCameraForImageIntent(activity, type)
resultLauncher.launch(intent)
} }
@Nullable @Throws(URISyntaxException::class)
private static UploadableFile takenCameraPicture(Context context) throws URISyntaxException { @JvmStatic
String lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_PHOTO, null); private fun takenCameraPicture(context: Context): UploadableFile? {
if (lastCameraPhoto != null) { val lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context)
return new UploadableFile(new File(lastCameraPhoto)); .getString(KEY_LAST_CAMERA_PHOTO, null)
return if (lastCameraPhoto != null) {
UploadableFile(File(lastCameraPhoto))
} else { } else {
return null; null
} }
} }
@Nullable @Throws(URISyntaxException::class)
private static UploadableFile takenCameraVideo(Context context) throws URISyntaxException { @JvmStatic
String lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_VIDEO, null); private fun takenCameraVideo(context: Context): UploadableFile? {
if (lastCameraPhoto != null) { val lastCameraVideo = PreferenceManager.getDefaultSharedPreferences(context)
return new UploadableFile(new File(lastCameraPhoto)); .getString(KEY_LAST_CAMERA_VIDEO, null)
return if (lastCameraVideo != null) {
UploadableFile(File(lastCameraVideo))
} else { } else {
return null; null
} }
} }
public static List<UploadableFile> handleExternalImagesPicked(Intent data, Activity activity) { @JvmStatic
try { fun handleExternalImagesPicked(data: Intent?, activity: Activity): List<UploadableFile> {
return getFilesFromGalleryPictures(data, activity); return try {
} catch (IOException | SecurityException e) { getFilesFromGalleryPictures(data, activity)
e.printStackTrace(); } catch (e: IOException) {
e.printStackTrace()
emptyList()
} catch (e: SecurityException) {
e.printStackTrace()
emptyList()
} }
return new ArrayList<>();
} }
private static boolean isPhoto(Intent data) { @JvmStatic
return data == null || (data.getData() == null && data.getClipData() == null); private fun isPhoto(data: Intent?): Boolean {
return data == null || (data.data == null && data.clipData == null)
} }
private static Intent plainGalleryPickerIntent(boolean openDocumentIntentPreferred) { @JvmStatic
private fun plainGalleryPickerIntent(
openDocumentIntentPreferred: Boolean
): Intent {
/* /*
* Asking for ACCESS_MEDIA_LOCATION at runtime solved the location-loss issue * Asking for ACCESS_MEDIA_LOCATION at runtime solved the location-loss issue
* in the custom selector in Contributions fragment. * in the custom selector in Contributions fragment.
@ -192,32 +247,40 @@ public class FilePicker implements Constants {
* from EXIF. * from EXIF.
* *
*/ */
Intent intent; val intent = if (openDocumentIntentPreferred) {
if (openDocumentIntentPreferred) { Intent(Intent.ACTION_OPEN_DOCUMENT)
intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
} else { } else {
intent = new Intent(Intent.ACTION_GET_CONTENT); Intent(Intent.ACTION_GET_CONTENT)
} }
intent.setType("image/*"); intent.type = "image/*"
return intent; return intent
} }
public static void onPictureReturnedFromDocuments(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { @JvmStatic
if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){ fun onPictureReturnedFromDocuments(
result: ActivityResult,
activity: Activity,
callbacks: Callbacks
) {
if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) {
try { try {
Uri photoPath = result.getData().getData(); val photoPath = result.data?.data
UploadableFile photoFile = PickedFiles.pickedExistingPicture(activity, photoPath); val photoFile = PickedFiles.pickedExistingPicture(activity, photoPath!!)
callbacks.onImagesPicked(singleFileList(photoFile), FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); callbacks.onImagesPicked(
singleFileList(photoFile),
ImageSource.DOCUMENTS,
restoreType(activity)
)
if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) {
PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile))
} }
} catch (Exception e) { } catch (e: Exception) {
e.printStackTrace(); e.printStackTrace()
callbacks.onImagePickerError(e, FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); callbacks.onImagePickerError(e, ImageSource.DOCUMENTS, restoreType(activity))
} }
} else { } else {
callbacks.onCanceled(FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); callbacks.onCanceled(ImageSource.DOCUMENTS, restoreType(activity))
} }
} }
@ -225,131 +288,155 @@ public class FilePicker implements Constants {
* onPictureReturnedFromCustomSelector. * onPictureReturnedFromCustomSelector.
* Retrieve and forward the images to upload wizard through callback. * Retrieve and forward the images to upload wizard through callback.
*/ */
public static void onPictureReturnedFromCustomSelector(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { @JvmStatic
if(result.getResultCode() == Activity.RESULT_OK){ fun onPictureReturnedFromCustomSelector(
try { result: ActivityResult,
List<UploadableFile> files = getFilesFromCustomSelector(result.getData(), activity); activity: Activity,
callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); callbacks: Callbacks
} catch (Exception e) { ) {
e.printStackTrace(); if (result.resultCode == Activity.RESULT_OK) {
callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); try {
} val files = getFilesFromCustomSelector(result.data, activity)
} else { callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity))
callbacks.onCanceled(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 * Get files from custom selector
* Retrieve and process the selected images from the custom selector. * Retrieve and process the selected images from the custom selector.
*/ */
private static List<UploadableFile> getFilesFromCustomSelector(Intent data, Activity activity) throws IOException, SecurityException { @Throws(IOException::class, SecurityException::class)
List<UploadableFile> files = new ArrayList<>(); @JvmStatic
ArrayList<Image> images = data.getParcelableArrayListExtra("Images"); private fun getFilesFromCustomSelector(
for(Image image : images) { data: Intent?,
Uri uri = image.getUri(); activity: Activity
UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri); ): List<UploadableFile> {
files.add(file); 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()) { if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) {
PickedFiles.copyFilesInSeparateThread(activity, files); PickedFiles.copyFilesInSeparateThread(activity, files)
} }
return files; return files
} }
public static void onPictureReturnedFromGallery(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { @JvmStatic
if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){ fun onPictureReturnedFromGallery(
result: ActivityResult,
activity: Activity,
callbacks: Callbacks
) {
if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) {
try { try {
List<UploadableFile> files = getFilesFromGalleryPictures(result.getData(), activity); val files = getFilesFromGalleryPictures(result.data, activity)
callbacks.onImagesPicked(files, FilePicker.ImageSource.GALLERY, restoreType(activity)); callbacks.onImagesPicked(files, ImageSource.GALLERY, restoreType(activity))
} catch (Exception e) { } catch (e: Exception) {
e.printStackTrace(); e.printStackTrace()
callbacks.onImagePickerError(e, FilePicker.ImageSource.GALLERY, restoreType(activity)); callbacks.onImagePickerError(e, 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 { } else {
for (int i = 0; i < clipData.getItemCount(); i++) { callbacks.onCanceled(ImageSource.GALLERY, restoreType(activity))
Uri uri = clipData.getItemAt(i).getUri(); }
UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri); }
files.add(file);
@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()) { if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) {
PickedFiles.copyFilesInSeparateThread(activity, files); PickedFiles.copyFilesInSeparateThread(activity, files)
} }
return files; return files
} }
public static void onPictureReturnedFromCamera(ActivityResult activityResult, Activity activity, @NonNull FilePicker.Callbacks callbacks) { @JvmStatic
if(activityResult.getResultCode() == Activity.RESULT_OK){ fun onPictureReturnedFromCamera(
activityResult: ActivityResult,
activity: Activity,
callbacks: Callbacks
) {
if (activityResult.resultCode == Activity.RESULT_OK) {
try { try {
String lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_PHOTO_URI, null); val lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity)
if (!TextUtils.isEmpty(lastImageUri)) { .getString(KEY_PHOTO_URI, null)
revokeWritePermission(activity, Uri.parse(lastImageUri)); if (!lastImageUri.isNullOrEmpty()) {
revokeWritePermission(activity, Uri.parse(lastImageUri))
} }
UploadableFile photoFile = FilePicker.takenCameraPicture(activity); val photoFile = takenCameraPicture(activity)
List<UploadableFile> files = new ArrayList<>(); val files = mutableListOf<UploadableFile>()
files.add(photoFile); photoFile?.let { files.add(it) }
if (photoFile == null) { if (photoFile == null) {
Exception e = new IllegalStateException("Unable to get the picture returned from camera"); val e = IllegalStateException("Unable to get the picture returned from camera")
callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); callbacks.onImagePickerError(e, ImageSource.CAMERA_IMAGE, restoreType(activity))
} else { } else {
if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) {
PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile))
} }
callbacks.onImagesPicked(files, ImageSource.CAMERA_IMAGE, restoreType(activity))
callbacks.onImagesPicked(files, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity));
} }
PreferenceManager.getDefaultSharedPreferences(activity) PreferenceManager.getDefaultSharedPreferences(activity).edit()
.edit()
.remove(KEY_LAST_CAMERA_PHOTO) .remove(KEY_LAST_CAMERA_PHOTO)
.remove(KEY_PHOTO_URI) .remove(KEY_PHOTO_URI)
.apply(); .apply()
} catch (Exception e) { } catch (e: Exception) {
e.printStackTrace(); e.printStackTrace()
callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); callbacks.onImagePickerError(e, ImageSource.CAMERA_IMAGE, restoreType(activity))
} }
} else { } else {
callbacks.onCanceled(FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); callbacks.onCanceled(ImageSource.CAMERA_IMAGE, restoreType(activity))
} }
} }
public static FilePickerConfiguration configuration(@NonNull Context context) { @JvmStatic
return new FilePickerConfiguration(context); fun configuration(context: Context): FilePickerConfiguration {
return FilePickerConfiguration(context)
} }
enum class ImageSource {
public enum ImageSource {
GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO, CUSTOM_SELECTOR GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO, CUSTOM_SELECTOR
} }
public interface Callbacks { interface Callbacks {
void onImagePickerError(Exception e, FilePicker.ImageSource source, int type); fun onImagePickerError(e: Exception, source: ImageSource, type: Int)
void onImagesPicked(@NonNull List<UploadableFile> imageFiles, FilePicker.ImageSource source, int type); fun onImagesPicked(imageFiles: List<UploadableFile>, source: ImageSource, type: Int)
void onCanceled(FilePicker.ImageSource source, int type); fun onCanceled(source: ImageSource, type: Int)
} }
public interface HandleActivityResult{ interface HandleActivityResult {
void onHandleActivityResult(FilePicker.Callbacks callbacks); fun onHandleActivityResult(callbacks: Callbacks)
} }
} }

View file

@ -1,44 +1,46 @@
package fr.free.nrw.commons.filepicker; package fr.free.nrw.commons.filepicker
import android.content.Context; import android.content.Context
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager
public class FilePickerConfiguration implements Constants { class FilePickerConfiguration(
private val context: Context
): Constants {
private Context context; fun setAllowMultiplePickInGallery(allowMultiple: Boolean): FilePickerConfiguration {
FilePickerConfiguration(Context context) {
this.context = context;
}
public FilePickerConfiguration setAllowMultiplePickInGallery(boolean allowMultiple) {
PreferenceManager.getDefaultSharedPreferences(context).edit() PreferenceManager.getDefaultSharedPreferences(context).edit()
.putBoolean(BundleKeys.ALLOW_MULTIPLE, allowMultiple) .putBoolean(Constants.BundleKeys.ALLOW_MULTIPLE, allowMultiple)
.apply(); .apply()
return this; return this
} }
public FilePickerConfiguration setCopyTakenPhotosToPublicGalleryAppFolder(boolean copy) { fun setCopyTakenPhotosToPublicGalleryAppFolder(copy: Boolean): FilePickerConfiguration {
PreferenceManager.getDefaultSharedPreferences(context).edit() PreferenceManager.getDefaultSharedPreferences(context).edit()
.putBoolean(BundleKeys.COPY_TAKEN_PHOTOS, copy) .putBoolean(Constants.BundleKeys.COPY_TAKEN_PHOTOS, copy)
.apply(); .apply()
return this; return this
} }
public String getFolderName() { fun getFolderName(): String {
return PreferenceManager.getDefaultSharedPreferences(context).getString(BundleKeys.FOLDER_NAME, DEFAULT_FOLDER_NAME); return PreferenceManager.getDefaultSharedPreferences(context)
.getString(
Constants.BundleKeys.FOLDER_NAME,
Constants.DEFAULT_FOLDER_NAME
) ?: Constants.DEFAULT_FOLDER_NAME
} }
public boolean allowsMultiplePickingInGallery() { fun allowsMultiplePickingInGallery(): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.ALLOW_MULTIPLE, false); return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Constants.BundleKeys.ALLOW_MULTIPLE, false)
} }
public boolean shouldCopyTakenPhotosToPublicGalleryAppFolder() { fun shouldCopyTakenPhotosToPublicGalleryAppFolder(): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.COPY_TAKEN_PHOTOS, false); return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Constants.BundleKeys.COPY_TAKEN_PHOTOS, false)
} }
public boolean shouldCopyPickedImagesToPublicGalleryAppFolder() { fun shouldCopyPickedImagesToPublicGalleryAppFolder(): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.COPY_PICKED_IMAGES, false); return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Constants.BundleKeys.COPY_PICKED_IMAGES, false)
} }
} }

View file

@ -1,26 +1,24 @@
package fr.free.nrw.commons.filepicker; package fr.free.nrw.commons.filepicker
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap
import com.facebook.common.internal.ImmutableMap; class MimeTypeMapWrapper {
import java.util.Map; companion object {
private val sMimeTypeMap = MimeTypeMap.getSingleton()
public class MimeTypeMapWrapper { private val sMimeTypeToExtensionMap = mapOf(
"image/heif" to "heif",
"image/heic" to "heic"
)
private static final MimeTypeMap sMimeTypeMap = MimeTypeMap.getSingleton(); @JvmStatic
fun getExtensionFromMimeType(mimeType: String): String? {
private static final Map<String, String> sMimeTypeToExtensionMap = val result = sMimeTypeToExtensionMap[mimeType]
ImmutableMap.of( if (result != null) {
"image/heif", "heif", return result
"image/heic", "heic"); }
return sMimeTypeMap.getExtensionFromMimeType(mimeType)
public static String getExtensionFromMimeType(String mimeType) {
String result = sMimeTypeToExtensionMap.get(mimeType);
if (result != null) {
return result;
} }
return sMimeTypeMap.getExtensionFromMimeType(mimeType);
} }
} }

View file

@ -1,67 +1,62 @@
package fr.free.nrw.commons.filepicker; package fr.free.nrw.commons.filepicker
import android.content.ContentResolver; import android.content.ContentResolver
import android.content.Context; import android.content.Context
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection
import android.net.Uri; import android.net.Uri
import android.os.Environment; import android.os.Environment
import android.webkit.MimeTypeMap; 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
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. * PickedFiles.
* Process the upload items. * Process the upload items.
*/ */
public class PickedFiles implements Constants { object PickedFiles : Constants {
/** /**
* Get Folder Name * Get Folder Name
* @param context
* @return default application folder name. * @return default application folder name.
*/ */
private static String getFolderName(@NonNull Context context) { private fun getFolderName(context: Context): String {
return FilePicker.configuration(context).getFolderName(); return FilePicker.configuration(context).getFolderName()
} }
/** /**
* tempImageDirectory * tempImageDirectory
* @param context
* @return temporary image directory to copy and perform exif changes. * @return temporary image directory to copy and perform exif changes.
*/ */
private static File tempImageDirectory(@NonNull Context context) { private fun tempImageDirectory(context: Context): File {
File privateTempDir = new File(context.getCacheDir(), DEFAULT_FOLDER_NAME); val privateTempDir = File(context.cacheDir, DEFAULT_FOLDER_NAME)
if (!privateTempDir.exists()) privateTempDir.mkdirs(); if (!privateTempDir.exists()) privateTempDir.mkdirs()
return privateTempDir; return privateTempDir
} }
/** /**
* writeToFile * writeToFile
* writes inputStream data to the destination file. * 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 { @Throws(IOException::class)
try (OutputStream out = new FileOutputStream(file)) { private fun writeToFile(inputStream: InputStream, file: File) {
byte[] buf = new byte[1024]; inputStream.use { input ->
int len; FileOutputStream(file).use { output ->
while ((len = in.read(buf)) > 0) { val buffer = ByteArray(1024)
out.write(buf, 0, len); var length: Int
while (input.read(buffer).also { length = it } > 0) {
output.write(buffer, 0, length)
}
} }
} }
} }
@ -69,129 +64,111 @@ public class PickedFiles implements Constants {
/** /**
* Copy file function. * Copy file function.
* Copies source file to destination file. * 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 { @Throws(IOException::class)
try (InputStream in = new FileInputStream(src)) { private fun copyFile(src: File, dst: File) {
writeToFile(in, dst); FileInputStream(src).use { inputStream ->
writeToFile(inputStream, dst)
} }
} }
/** /**
* Copy files in separate thread. * Copy files in separate thread.
* Copies all the uploadable files to the temp image folder on background 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) { fun copyFilesInSeparateThread(context: Context, filesToCopy: List<UploadableFile>) {
new Thread(() -> { Thread {
List<File> copiedFiles = new ArrayList<>(); val copiedFiles = mutableListOf<File>()
int i = 1; var index = 1
for (UploadableFile uploadableFile : filesToCopy) { filesToCopy.forEach { uploadableFile ->
File fileToCopy = uploadableFile.getFile(); val fileToCopy = uploadableFile.file
File dstDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), getFolderName(context)); val dstDir = File(
if (!dstDir.exists()) { Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
dstDir.mkdirs(); getFolderName(context)
} )
if (!dstDir.exists()) dstDir.mkdirs()
String[] filenameSplit = fileToCopy.getName().split("\\."); val filenameSplit = fileToCopy.name.split(".")
String extension = "." + filenameSplit[filenameSplit.length - 1]; val extension = ".${filenameSplit.last()}"
String filename = String.format("IMG_%s_%d.%s", new SimpleDateFormat("yyyyMMdd_HHmmss").format(Calendar.getInstance().getTime()), i, extension); val filename = "IMG_${SimpleDateFormat(
"yyyyMMdd_HHmmss",
Locale.getDefault()).format(Date())}_$index$extension"
val dstFile = File(dstDir, filename)
File dstFile = new File(dstDir, filename);
try { try {
dstFile.createNewFile(); dstFile.createNewFile()
copyFile(fileToCopy, dstFile); copyFile(fileToCopy, dstFile)
copiedFiles.add(dstFile); copiedFiles.add(dstFile)
} catch (IOException e) { } catch (e: IOException) {
e.printStackTrace(); e.printStackTrace()
} }
i++; index++
} }
scanCopiedImages(context, copiedFiles); scanCopiedImages(context, copiedFiles)
}).run(); }.start()
} }
/** /**
* singleFileList. * singleFileList
* converts a single uploadableFile to list of uploadableFile. * Converts a single uploadableFile to list of uploadableFile.
* @param file uploadable file
* @return
*/ */
static List<UploadableFile> singleFileList(UploadableFile file) { fun singleFileList(file: UploadableFile): List<UploadableFile> {
List<UploadableFile> list = new ArrayList<>(); return listOf(file)
list.add(file);
return list;
} }
/** /**
* ScanCopiedImages * ScanCopiedImages
* Scan copied images metadata using media scanner. * Scans copied images metadata using media scanner.
* @param context
* @param copiedImages copied images list.
*/ */
static void scanCopiedImages(Context context, List<File> copiedImages) { fun scanCopiedImages(context: Context, copiedImages: List<File>) {
String[] paths = new String[copiedImages.size()]; val paths = copiedImages.map { it.toString() }.toTypedArray()
for (int i = 0; i < copiedImages.size(); i++) { MediaScannerConnection.scanFile(context, paths, null) { path, uri ->
paths[i] = copiedImages.get(i).toString(); Timber.d("Scanned $path:")
Timber.d("-> uri=$uri")
} }
MediaScannerConnection.scanFile(context,
paths, null,
(path, uri) -> {
Timber.d("Scanned " + path + ":");
Timber.d("-> uri=%s", uri);
});
} }
/** /**
* pickedExistingPicture * pickedExistingPicture
* convert the image into uploadable file. * 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 @Throws(IOException::class, SecurityException::class)
File directory = tempImageDirectory(context); fun pickedExistingPicture(context: Context, photoUri: Uri): UploadableFile {
File photoFile = new File(directory, UUID.randomUUID().toString() + "." + getMimeType(context, photoUri)); val directory = tempImageDirectory(context)
val mimeType = getMimeType(context, photoUri)
val photoFile = File(directory, "${UUID.randomUUID()}.$mimeType")
if (photoFile.createNewFile()) { if (photoFile.createNewFile()) {
try (InputStream pictureInputStream = context.getContentResolver().openInputStream(photoUri)) { context.contentResolver.openInputStream(photoUri)?.use { inputStream ->
writeToFile(pictureInputStream, photoFile); writeToFile(inputStream, photoFile)
} }
} else { } else {
throw new IOException("could not create photoFile to write upon"); throw IOException("Could not create photoFile to write upon")
} }
return new UploadableFile(photoUri, photoFile); return UploadableFile(photoUri, photoFile)
} }
/** /**
* getCameraPictureLocation * getCameraPictureLocation
*/ */
static File getCameraPicturesLocation(@NonNull Context context) throws IOException { @Throws(IOException::class)
File dir = tempImageDirectory(context); fun getCameraPicturesLocation(context: Context): File {
return File.createTempFile(UUID.randomUUID().toString(), ".jpg", dir); val dir = tempImageDirectory(context)
return File.createTempFile(UUID.randomUUID().toString(), ".jpg", dir)
} }
/** /**
* To find out the extension of required object in given uri * To find out the extension of the required object in a given uri
* Solution by http://stackoverflow.com/a/36514823/1171484
*/ */
private static String getMimeType(@NonNull Context context, @NonNull Uri uri) { private fun getMimeType(context: Context, uri: Uri): String {
String extension; return if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
context.contentResolver.getType(uri)
//Check uri format to avoid null ?.let { MimeTypeMapWrapper.getExtensionFromMimeType(it) }
if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
//If scheme is a content
extension = MimeTypeMapWrapper.getExtensionFromMimeType(context.getContentResolver().getType(uri));
} else { } else {
//If scheme is a File MimeTypeMap.getFileExtensionFromUrl(
//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. Uri.fromFile(uri.path?.let { File(it) }).toString()
extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(new File(uri.getPath())).toString()); )
} ?: "jpg" // Default to jpg if unable to determine type
}
return extension;
} }
/** /**
@ -199,10 +176,9 @@ public class PickedFiles implements Constants {
* @param file get uri of file * @param file get uri of file
* @return uri of requested file. * @return uri of requested file.
*/ */
static Uri getUriToFile(@NonNull Context context, @NonNull File file) { fun getUriToFile(context: Context, file: File): Uri {
String packageName = context.getApplicationContext().getPackageName(); val packageName = context.applicationContext.packageName
String authority = packageName + ".provider"; val authority = "$packageName.provider"
return FileProvider.getUriForFile(context, authority, file); return FileProvider.getUriForFile(context, authority, file)
} }
}
}

View file

@ -1,213 +1,168 @@
package fr.free.nrw.commons.filepicker; package fr.free.nrw.commons.filepicker
import android.annotation.SuppressLint; import android.annotation.SuppressLint
import android.content.Context; import android.content.Context
import android.database.Cursor; import android.database.Cursor
import android.net.Uri; import android.net.Uri
import android.os.Parcel; import android.os.Parcel
import android.os.Parcelable; import android.os.Parcelable
import androidx.annotation.Nullable; import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterface;
import fr.free.nrw.commons.upload.FileUtils; import fr.free.nrw.commons.upload.FileUtils
import java.io.File; import java.io.File
import java.io.IOException; import java.io.IOException
import java.util.Date; import java.util.Date
import timber.log.Timber; import timber.log.Timber
public class UploadableFile implements Parcelable { class UploadableFile : Parcelable {
public static final Creator<UploadableFile> CREATOR = new Creator<UploadableFile>() {
@Override
public UploadableFile createFromParcel(Parcel in) {
return new UploadableFile(in);
}
@Override val contentUri: Uri
public UploadableFile[] newArray(int size) { val file: File
return new UploadableFile[size];
}
};
private final Uri contentUri; constructor(contentUri: Uri, file: File) {
private final File file; this.contentUri = contentUri
this.file = file
public UploadableFile(Uri contentUri, File file) {
this.contentUri = contentUri;
this.file = file;
} }
public UploadableFile(File file) { constructor(file: File) {
this.file = file; this.file = file
this.contentUri = Uri.fromFile(new File(file.getPath())); this.contentUri = Uri.fromFile(File(file.path))
} }
public UploadableFile(Parcel in) { private constructor(parcel: Parcel) {
this.contentUri = in.readParcelable(Uri.class.getClassLoader()); contentUri = parcel.readParcelable(Uri::class.java.classLoader)!!
file = (File) in.readSerializable(); file = parcel.readSerializable() as File
} }
public Uri getContentUri() { fun getFilePath(): String {
return contentUri; return file.path
} }
public File getFile() { fun getMediaUri(): Uri {
return file; return Uri.parse(getFilePath())
} }
public String getFilePath() { fun getMimeType(context: Context): String? {
return file.getPath(); return FileUtils.getMimeType(context, getMediaUri())
} }
public Uri getMediaUri() { override fun describeContents(): Int = 0
return Uri.parse(getFilePath());
}
public String getMimeType(Context context) { /**
return FileUtils.getMimeType(context, getMediaUri()); * First try to get the file creation date from EXIF, else fall back to Content Provider (CP)
} */
fun getFileCreatedDate(context: Context): DateTimeWithSource? {
@Override return getDateTimeFromExif() ?: getFileCreatedDateFromCP(context)
public int describeContents() {
return 0;
} }
/** /**
* First try to get the file creation date from EXIF else fall back to CP * Get filePath creation date from URI using all possible content providers
* @param context
* @return
*/ */
@Nullable private fun getFileCreatedDateFromCP(context: Context): DateTimeWithSource? {
public DateTimeWithSource getFileCreatedDate(Context context) { return try {
DateTimeWithSource dateTimeFromExif = getDateTimeFromExif(); val cursor: Cursor? = context.contentResolver.query(contentUri, null, null, null, null)
if (dateTimeFromExif == null) { cursor?.use {
return getFileCreatedDateFromCP(context); val lastModifiedColumnIndex = cursor
} else { .getColumnIndex(
return dateTimeFromExif; "last_modified"
} ).takeIf { it != -1 }
} ?: cursor.getColumnIndex("datetaken")
if (lastModifiedColumnIndex == -1) return null // No valid column found
/** cursor.moveToFirst()
* Get filePath creation date from uri from all possible content providers DateTimeWithSource(
* cursor.getLong(
* @return lastModifiedColumnIndex
*/ ), DateTimeWithSource.CP_SOURCE)
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 } catch (e: Exception) {
int lastModifiedColumnIndex = cursor.getColumnIndex("last_modified");//If gallery is opened from in app Timber.tag("UploadableFile").d(e)
if (lastModifiedColumnIndex == -1) { null
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). * Indicates whether the EXIF contains the location (both latitude and longitude).
*
* @return whether the location exists for the file's EXIF
*/ */
public boolean hasLocation() { fun hasLocation(): Boolean {
try { return try {
ExifInterface exif = new ExifInterface(file.getAbsolutePath()); val exif = ExifInterface(file.absolutePath)
final String latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); val latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)
final String longitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); val longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)
return latitude != null && longitude != null; latitude != null && longitude != null
} catch (IOException | NumberFormatException | IndexOutOfBoundsException e) { } catch (e: IOException) {
Timber.tag("UploadableFile"); Timber.tag("UploadableFile").d(e)
Timber.d(e); false
} }
return false;
} }
/** /**
* Get filePath creation date from uri from EXIF * Get filePath creation date from URI using EXIF data
*
* @return
*/ */
private DateTimeWithSource getDateTimeFromExif() { private fun getDateTimeFromExif(): DateTimeWithSource? {
try { return try {
ExifInterface exif = new ExifInterface(file.getAbsolutePath()); val exif = ExifInterface(file.absolutePath)
// TAG_DATETIME returns the last edited date, we need TAG_DATETIME_ORIGINAL for creation date val dateTimeSubString = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)
// See issue https://github.com/commons-app/apps-android-commons/issues/1971 if (dateTimeSubString != null) {
String dateTimeSubString = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL); val year = dateTimeSubString.substring(0, 4).toInt()
if (dateTimeSubString!=null) { //getAttribute may return null val month = dateTimeSubString.substring(5, 7).toInt()
String year = dateTimeSubString.substring(0,4); val day = dateTimeSubString.substring(8, 10).toInt()
String month = dateTimeSubString.substring(5,7); val dateCreatedString = "%04d-%02d-%02d".format(year, month, day)
String day = dateTimeSubString.substring(8,10); if (dateCreatedString.length == 10) {
// This date is stored as a string (not as a date), the rason is we don't want to include timezones @SuppressLint("RestrictedApi")
String dateCreatedString = String.format("%04d-%02d-%02d", Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day)); val dateTime = exif.dateTimeOriginal
if (dateCreatedString.length() == 10) { //yyyy-MM-dd format of date is expected if (dateTime != null) {
@SuppressLint("RestrictedApi") Long dateTime = exif.getDateTimeOriginal(); val date = Date(dateTime)
if(dateTime != null){ return DateTimeWithSource(date, dateCreatedString, DateTimeWithSource.EXIF_SOURCE)
Date date = new Date(dateTime);
return new DateTimeWithSource(date, dateCreatedString, DateTimeWithSource.EXIF_SOURCE);
} }
} }
} }
} catch (IOException | NumberFormatException | IndexOutOfBoundsException e) { null
Timber.tag("UploadableFile"); } catch (e: Exception) {
Timber.d(e); Timber.tag("UploadableFile").d(e)
null
} }
return null;
} }
@Override override fun writeToParcel(parcel: Parcel, flags: Int) {
public void writeToParcel(Parcel parcel, int i) { parcel.writeParcelable(contentUri, flags)
parcel.writeParcelable(contentUri, 0); parcel.writeSerializable(file)
parcel.writeSerializable(file);
} }
/** class DateTimeWithSource {
* This class contains the epochDate along with the source from which it was extracted companion object {
*/ const val CP_SOURCE = "contentProvider"
public class DateTimeWithSource { const val EXIF_SOURCE = "exif"
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) { val epochDate: Long
this.epochDate = date.getTime(); var dateString: String? = null
this.source = source; val source: String
constructor(epochDate: Long, source: String) {
this.epochDate = epochDate
this.source = source
} }
public DateTimeWithSource(Date date, String dateString, String source) { constructor(date: Date, source: String) {
this.epochDate = date.getTime(); epochDate = date.time
this.dateString = dateString; this.source = source
this.source = source;
} }
public long getEpochDate() { constructor(date: Date, dateString: String, source: String) {
return epochDate; epochDate = date.time
this.dateString = dateString
this.source = source
}
}
companion object CREATOR : Parcelable.Creator<UploadableFile> {
override fun createFromParcel(parcel: Parcel): UploadableFile {
return UploadableFile(parcel)
} }
public String getDateString() { override fun newArray(size: Int): Array<UploadableFile?> {
return dateString; return arrayOfNulls(size)
}
public String getSource() {
return source;
} }
} }
} }

View file

@ -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
)
}
})
} }
/** /**

View file

@ -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