From 52ab39381eccb1d55231338b8c8ce7d6e57c4d29 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Mon, 4 Feb 2019 02:10:31 +0530 Subject: [PATCH] Add module for file picker for camera and gallery uploads (#2375) * Use easy image for image picker * Do not use harcoded mime type * Use uploadable file for image uploads * Add picker files in filepicker module * Remove redundant checks for file * Make usage of file extensions consistent * Add javadocs * Fix tests * Enable image upload using bookmark activity * Fix multiple uploads * Fix external image uploads * Fix chooser intents * Fix image quality checks * Segregate internal and external upload intents * Invoke all error messages from one place * Minor changes * Fix tests * Add image processing service tests --- app/build.gradle | 3 - app/src/main/AndroidManifest.xml | 2 +- .../commons/bookmarks/BookmarksActivity.java | 12 + .../locations/BookmarkLocationsFragment.java | 12 +- .../contributions/ContributionController.java | 159 ++--- .../ContributionsListFragment.java | 7 +- .../commons/contributions/MainActivity.java | 14 +- .../commons/contributions/UploadableFile.java | 85 +++ .../nrw/commons/filepicker/Constants.java | 22 + .../commons/filepicker/DefaultCallback.java | 12 + .../filepicker/ExtendedFileProvider.java | 7 + .../nrw/commons/filepicker/FilePicker.java | 550 ++++++++++++++++++ .../filepicker/FilePickerConfiguration.java | 58 ++ .../filepicker/MimeTypeMapWrapper.java | 46 ++ .../nrw/commons/filepicker/PickedFiles.java | 155 +++++ .../nrw/commons/media/FrescoImageLoader.java | 53 -- .../nrw/commons/nearby/NearbyFragment.java | 5 +- .../nrw/commons/nearby/NearbyMapFragment.java | 13 +- .../nrw/commons/nearby/PlaceRenderer.java | 11 +- .../nrw/commons/upload/FileProcessor.java | 6 +- .../fr/free/nrw/commons/upload/FileUtils.java | 40 +- .../upload/ImageProcessingService.java | 63 +- .../fr/free/nrw/commons/upload/Title.java | 2 +- .../nrw/commons/upload/UploadActivity.java | 80 ++- .../free/nrw/commons/upload/UploadModel.java | 192 +++--- .../nrw/commons/upload/UploadPresenter.java | 134 ++--- .../nrw/commons/upload/UploadService.java | 66 +-- .../upload/UploadThumbnailRenderer.java | 8 +- .../free/nrw/commons/upload/UploadView.java | 6 +- .../fr/free/nrw/commons/utils/ImageUtils.java | 18 +- .../free/nrw/commons/utils/IntentUtils.java | 33 -- .../commons/wikidata/WikidataEditService.java | 4 +- .../layout/activity_upload_bottom_card.xml | 2 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/provider_paths.xml | 1 + .../contributions/ContributionDaoTest.kt | 8 +- .../upload/ImageProcessingServiceTest.kt | 137 +++++ .../nrw/commons/upload/UploadModelTest.kt | 84 +-- .../nrw/commons/upload/UploadPresenterTest.kt | 13 +- 39 files changed, 1553 insertions(+), 574 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/contributions/UploadableFile.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/media/FrescoImageLoader.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/IntentUtils.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt diff --git a/app/build.gradle b/app/build.gradle index eb2c64b62..4b6f83fdd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,9 +39,6 @@ dependencies { implementation files('libs/simplemagic-1.9.jar') implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" - implementation('com.github.esafirm.android-image-picker:imagepicker:1.13.1', { - exclude group: 'com.github.bumptech.glide', module: 'glide' - }) // Logging implementation 'ch.acra:acra:4.9.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c5ce14bb0..2aa4b2fce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -158,7 +158,7 @@ android:process=":acra" /> diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksActivity.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksActivity.java index 84966ed20..6406ea8d9 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksActivity.java @@ -10,11 +10,14 @@ import android.support.v4.view.ViewPager; import android.view.View; import android.widget.AdapterView; +import javax.inject.Inject; + import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.theme.NavigationBaseActivity; @@ -31,6 +34,9 @@ public class BookmarksActivity extends NavigationBaseActivity @BindView(R.id.tabLayoutBookmarks) TabLayout tabLayout; + @Inject + ContributionController controller; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -67,6 +73,12 @@ public class BookmarksActivity extends NavigationBaseActivity } } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + controller.handleActivityResult(this, requestCode, resultCode, data); + } + /** * This method is called onClick of media inside category details (CategoryImageListFragment). */ diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java index e656de9be..66be5dc1b 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java @@ -13,8 +13,6 @@ import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; -import com.esafirm.imagepicker.features.ImagePicker; -import com.esafirm.imagepicker.model.Image; import com.pedrogomez.renderers.RVRendererAdapter; import java.util.ArrayList; @@ -32,8 +30,6 @@ import fr.free.nrw.commons.kvstore.BasicKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.nearby.NearbyAdapterFactory; import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.utils.ImageUtils; -import fr.free.nrw.commons.utils.IntentUtils; public class BookmarkLocationsFragment extends DaggerFragment { @@ -99,12 +95,6 @@ public class BookmarkLocationsFragment extends DaggerFragment { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (IntentUtils.shouldBookmarksHandle(requestCode, resultCode, data)) { - List images = ImagePicker.getImages(data); - Intent shareIntent = contributionController.handleImagesPicked(ImageUtils.getUriListFromImages(images), requestCode); - startActivity(shareIntent); - } else { - super.onActivityResult(requestCode, resultCode, data); - } + contributionController.handleActivityResult(getActivity(), requestCode, resultCode, data); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 575749d0d..0e1a4ecb8 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -4,119 +4,142 @@ import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.net.Uri; import android.os.Build; +import android.support.annotation.NonNull; -import com.esafirm.imagepicker.features.ImagePicker; - +import java.io.File; import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.media.FrescoImageLoader; +import fr.free.nrw.commons.filepicker.DefaultCallback; +import fr.free.nrw.commons.filepicker.FilePicker; import fr.free.nrw.commons.kvstore.BasicKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.upload.UploadActivity; import fr.free.nrw.commons.utils.PermissionUtils; +import fr.free.nrw.commons.utils.ViewUtil; -import static android.content.Intent.ACTION_SEND_MULTIPLE; -import static android.content.Intent.EXTRA_STREAM; import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA; -import static fr.free.nrw.commons.contributions.Contribution.SOURCE_EXTERNAL; import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY; +import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES; import static fr.free.nrw.commons.upload.UploadService.EXTRA_SOURCE; import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; @Singleton public class ContributionController { - //request codes - public static final int CAMERA_UPLOAD_REQUEST_CODE = 10011; - public static final int GALLERY_UPLOAD_REQUEST_CODE = 10012; - public static final int NEARBY_CAMERA_UPLOAD_REQUEST_CODE = 10013; - public static final int NEARBY_GALLERY_UPLOAD_REQUEST_CODE = 10014; - public static final int BOOKMARK_CAMERA_UPLOAD_REQUEST_CODE = 10015; - public static final int BOOKMARK_GALLERY_UPLOAD_REQUEST_CODE = 10016; + public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads"; - //upload limits - public static final int MULTIPLE_UPLOAD_IMAGE_LIMIT = 5; - public static final int NEARBY_UPLOAD_IMAGE_LIMIT = 1; - - private final Context context; private final BasicKvStore defaultKvStore; private final JsonKvStore directKvStore; @Inject - public ContributionController(Context context, - @Named("default_preferences") BasicKvStore defaultKvStore, + public ContributionController(@Named("default_preferences") BasicKvStore defaultKvStore, @Named("direct_nearby_upload_prefs") JsonKvStore directKvStore) { - this.context = context; this.defaultKvStore = defaultKvStore; this.directKvStore = directKvStore; } - public void initiateCameraPick(Activity activity, - int requestCode) { + /** + * Check for permissions and initiate camera click + */ + public void initiateCameraPick(Activity activity) { boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true); if (!useExtStorage) { - initiateCameraUpload(activity, requestCode); + initiateCameraUpload(activity); return; } PermissionUtils.checkPermissionsAndPerformAction(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, - () -> initiateCameraUpload(activity, requestCode), + () -> initiateCameraUpload(activity), R.string.storage_permission_title, R.string.write_storage_permission_rationale); } - public void initiateGalleryPick(Activity activity, - int imageLimit, - int requestCode) { + /** + * Check for permissions and initiate gallery picker + */ + public void initiateGalleryPick(Activity activity, boolean allowMultipleUploads) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN) { - initiateGalleryUpload(activity, imageLimit, requestCode); + initiateGalleryUpload(activity, allowMultipleUploads); } else { PermissionUtils.checkPermissionsAndPerformAction(activity, Manifest.permission.READ_EXTERNAL_STORAGE, - () -> initiateGalleryUpload(activity, imageLimit, requestCode), + () -> initiateGalleryUpload(activity, allowMultipleUploads), R.string.storage_permission_title, R.string.read_storage_permission_rationale); } } - private void initiateGalleryUpload(Activity activity, - int imageLimit, - int requestCode) { - ImagePicker imagePicker = ImagePicker.ImagePickerWithFragment - .create(activity) - .showCamera(false) - .folderMode(true) - .includeVideo(false) - .imageLoader(new FrescoImageLoader()) - .enableLog(true); - - if (imageLimit > 1) { - imagePicker.multi().limit(imageLimit).start(requestCode); - } else { - imagePicker.single().start(requestCode); - } + /** + * Open chooser for gallery uploads + */ + private void initiateGalleryUpload(Activity activity, boolean allowMultipleUploads) { + setPickerConfiguration(activity, allowMultipleUploads); + FilePicker.openGallery(activity, 0); } - private void initiateCameraUpload(Activity activity, int requestCode) { - ImagePicker.cameraOnly() - .start(activity, requestCode); + /** + * Sets configuration for file picker + */ + private void setPickerConfiguration(Activity activity, + boolean allowMultipleUploads) { + boolean copyToExternalStorage = defaultKvStore.getBoolean("useExternalStorage", true); + FilePicker.configuration(activity) + .setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage) + .setAllowMultiplePickInGallery(allowMultipleUploads); } - public Intent handleImagesPicked(ArrayList uriList, int requestCode) { + /** + * Initiate camera upload by opening camera + */ + private void initiateCameraUpload(Activity activity) { + setPickerConfiguration(activity, false); + FilePicker.openCameraForImage(activity, 0); + } + + /** + * Attaches callback for file picker. + */ + public void handleActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + FilePicker.handleActivityResult(requestCode, resultCode, data, activity, new DefaultCallback() { + @Override + public void onImagePickerError(Exception e, FilePicker.ImageSource source, int type) { + ViewUtil.showShortToast(activity, R.string.error_occurred_in_picking_images); + } + + @Override + public void onImagesPicked(@NonNull List imagesFiles, FilePicker.ImageSource source, int type) { + Intent intent = handleImagesPicked(activity, imagesFiles, getSourceFromImageSource(source)); + activity.startActivity(intent); + } + }); + } + + public List handleExternalImagesPicked(Activity activity, + Intent data) { + return getUploadableFiles(FilePicker.handleExternalImagesPicked(data, activity)); + } + + /** + * Returns intent to be passed to upload activity + * Attaches place object for nearby uploads + */ + private Intent handleImagesPicked(Context context, + List imagesFiles, + String source) { + ArrayList uploadableFiles = getUploadableFiles(imagesFiles); Intent shareIntent = new Intent(context, UploadActivity.class); - shareIntent.setAction(ACTION_SEND_MULTIPLE); - shareIntent.putExtra(EXTRA_SOURCE, getSourceFromRequestCode(requestCode)); - shareIntent.putExtra(EXTRA_STREAM, uriList); - shareIntent.setType("image/jpeg"); + shareIntent.setAction(ACTION_INTERNAL_UPLOADS); + shareIntent.putExtra(EXTRA_SOURCE, source); + shareIntent.putParcelableArrayListExtra(EXTRA_FILES, uploadableFiles); Place place = directKvStore.getJson(PLACE_OBJECT, Place.class); if (place != null) { shareIntent.putExtra(PLACE_OBJECT, place); @@ -125,18 +148,22 @@ public class ContributionController { return shareIntent; } - private String getSourceFromRequestCode(int requestCode) { - switch (requestCode) { - case CAMERA_UPLOAD_REQUEST_CODE: - case NEARBY_CAMERA_UPLOAD_REQUEST_CODE: - case BOOKMARK_CAMERA_UPLOAD_REQUEST_CODE: - return SOURCE_CAMERA; - case GALLERY_UPLOAD_REQUEST_CODE: - case NEARBY_GALLERY_UPLOAD_REQUEST_CODE: - case BOOKMARK_GALLERY_UPLOAD_REQUEST_CODE: - return SOURCE_GALLERY; + @NonNull + private ArrayList getUploadableFiles(List imagesFiles) { + ArrayList uploadableFiles = new ArrayList<>(); + for (File file : imagesFiles) { + uploadableFiles.add(new UploadableFile(file)); } + return uploadableFiles; + } - return SOURCE_EXTERNAL; + /** + * Get image upload source + */ + private String getSourceFromImageSource(FilePicker.ImageSource source) { + if (source.equals(FilePicker.ImageSource.CAMERA_IMAGE)) { + return SOURCE_CAMERA; + } + return SOURCE_GALLERY; } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index 23f71d546..5987a1236 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -27,9 +27,6 @@ import fr.free.nrw.commons.utils.ConfigUtils; import static android.view.View.GONE; import static android.view.View.VISIBLE; -import static fr.free.nrw.commons.contributions.ContributionController.CAMERA_UPLOAD_REQUEST_CODE; -import static fr.free.nrw.commons.contributions.ContributionController.GALLERY_UPLOAD_REQUEST_CODE; -import static fr.free.nrw.commons.contributions.ContributionController.MULTIPLE_UPLOAD_IMAGE_LIMIT; /** * Created by root on 01.06.2018. @@ -95,8 +92,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { private void setListeners() { fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); - fabCamera.setOnClickListener(view -> controller.initiateCameraPick(getActivity(), CAMERA_UPLOAD_REQUEST_CODE)); - fabGallery.setOnClickListener(view -> controller.initiateGalleryPick(getActivity(), MULTIPLE_UPLOAD_IMAGE_LIMIT, GALLERY_UPLOAD_REQUEST_CODE)); + fabCamera.setOnClickListener(view -> controller.initiateCameraPick(getActivity())); + fabGallery.setOnClickListener(view -> controller.initiateGalleryPick(getActivity(), true)); } private void animateFAB(boolean isFabOpen) { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 4948d026a..622f9461d 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -18,9 +18,6 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import com.esafirm.imagepicker.features.ImagePicker; -import com.esafirm.imagepicker.model.Image; - import java.util.List; import javax.inject.Inject; @@ -40,8 +37,6 @@ import fr.free.nrw.commons.notification.Notification; import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.notification.NotificationController; import fr.free.nrw.commons.upload.UploadService; -import fr.free.nrw.commons.utils.ImageUtils; -import fr.free.nrw.commons.utils.IntentUtils; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; @@ -457,13 +452,8 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (IntentUtils.shouldContributionsHandle(requestCode, resultCode, data)) { - List images = ImagePicker.getImages(data); - Intent shareIntent = controller.handleImagesPicked(ImageUtils.getUriListFromImages(images), requestCode); - startActivity(shareIntent); - } else { - super.onActivityResult(requestCode, resultCode, data); - } + super.onActivityResult(requestCode, resultCode, data); + controller.handleActivityResult(this, requestCode, resultCode, data); } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/UploadableFile.java b/app/src/main/java/fr/free/nrw/commons/contributions/UploadableFile.java new file mode 100644 index 000000000..c6fc50513 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/UploadableFile.java @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.contributions; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.File; + +import fr.free.nrw.commons.upload.FileUtils; + +public class UploadableFile implements Parcelable { + public static final Creator CREATOR = new Creator() { + @Override + public UploadableFile createFromParcel(Parcel in) { + return new UploadableFile(in); + } + + @Override + public UploadableFile[] newArray(int size) { + return new UploadableFile[size]; + } + }; + private final File file; + + public UploadableFile(File file) { + this.file = file; + } + + public UploadableFile(Parcel in) { + file = (File) in.readSerializable(); + } + + 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; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeSerializable(file); + } + + /** + * Get filePath creation date from uri from all possible content providers + * + * @return + */ + public long getFileCreatedDate(Context context) { + try { + ContentResolver contentResolver = context.getContentResolver(); + Cursor cursor = contentResolver.query(getMediaUri(), null, null, null, null); + if (cursor == null) { + return -1;//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) { + return -1L; + } + cursor.moveToFirst(); + return cursor.getLong(lastModifiedColumnIndex); + } catch (Exception e) { + return -1;////Could not fetch last_modified + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java new file mode 100644 index 000000000..fefb50311 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java @@ -0,0 +1,22 @@ +package fr.free.nrw.commons.filepicker; + +public interface Constants { + String DEFAULT_FOLDER_NAME = "CommonsContributions"; + + interface RequestCodes { + int FILE_PICKER_IMAGE_IDENTIFICATOR = 0b1101101100; //876 + int SOURCE_CHOOSER = 1 << 15; + + int PICK_PICTURE_FROM_DOCUMENTS = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 11); + int PICK_PICTURE_FROM_GALLERY = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 12); + int TAKE_PICTURE = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 13); + int CAPTURE_VIDEO = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 14); + } + + 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"; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java b/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java new file mode 100644 index 000000000..4448adb5f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.filepicker; + +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) { + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java b/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java new file mode 100644 index 000000000..ee0ef6af9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java @@ -0,0 +1,7 @@ +package fr.free.nrw.commons.filepicker; + +import android.support.v4.content.FileProvider; + +public class ExtendedFileProvider extends FileProvider { + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java new file mode 100644 index 000000000..aaa9f1caf --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java @@ -0,0 +1,550 @@ +package fr.free.nrw.commons.filepicker; + +import android.app.Activity; +import android.content.ClipData; +import android.content.ComponentName; +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.os.Build; +import android.os.Parcelable; +import android.preference.PreferenceManager; +import android.provider.MediaStore; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.text.TextUtils; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +import static fr.free.nrw.commons.filepicker.PickedFiles.singleFileList; + +@SuppressWarnings({"unused", "FieldCanBeLocal", "ResultOfMethodCallIgnored"}) +public class FilePicker implements Constants { + + private static final boolean SHOW_GALLERY_IN_CHOOSER = false; + 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"; + + 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 Uri createCameraVideoFile(@NonNull Context context) throws IOException { + File imagePath = PickedFiles.getCameraVideoLocation(context); + Uri uri = PickedFiles.getUriToFile(context, imagePath); + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); + editor.putString(KEY_VIDEO_URI, uri.toString()); + editor.putString(KEY_LAST_CAMERA_VIDEO, imagePath.toString()); + editor.apply(); + return uri; + } + + private static Intent createDocumentsIntent(@NonNull Context context, int type) { + storeType(context, type); + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + return intent; + } + + private static Intent createGalleryIntent(@NonNull Context context, int type) { + storeType(context, type); + Intent intent = plainGalleryPickerIntent(); + if (Build.VERSION.SDK_INT >= 18) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, configuration(context).allowsMultiplePickingInGallery()); + } + return intent; + } + + 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 Intent createCameraForVideoIntent(@NonNull Context context, int type) { + storeType(context, type); + + Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + try { + Uri capturedImageUri = createCameraVideoFile(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 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 Intent createChooserIntent(@NonNull Context context, @Nullable String chooserTitle, int type) throws IOException { + return createChooserIntent(context, chooserTitle, SHOW_GALLERY_IN_CHOOSER, type); + } + + private static Intent createChooserIntent(@NonNull Context context, @Nullable String chooserTitle, boolean showGallery, int type) throws IOException { + storeType(context, type); + + Uri outputFileUri = createCameraPictureFile(context); + List cameraIntents = new ArrayList<>(); + Intent captureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); + PackageManager packageManager = context.getPackageManager(); + List camList = packageManager.queryIntentActivities(captureIntent, 0); + for (ResolveInfo res : camList) { + final String packageName = res.activityInfo.packageName; + final Intent intent = new Intent(captureIntent); + intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name)); + intent.setPackage(packageName); + intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri); + grantWritePermission(context, intent, outputFileUri); + cameraIntents.add(intent); + } + Intent galleryIntent; + + if (showGallery) { + galleryIntent = createGalleryIntent(context, type); + } else { + galleryIntent = createDocumentsIntent(context, type); + } + + Intent chooserIntent = Intent.createChooser(galleryIntent, chooserTitle); + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(new Parcelable[cameraIntents.size()])); + + return chooserIntent; + } + + private static void storeType(@NonNull Context context, int type) { + PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(KEY_TYPE, type).commit(); + } + + private static int restoreType(@NonNull Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_TYPE, 0); + } + + public static void openChooserWithDocuments(Activity activity, @Nullable String chooserTitle, int type) { + try { + Intent intent = createChooserIntent(activity, chooserTitle, type); + activity.startActivityForResult(intent, RequestCodes.SOURCE_CHOOSER | RequestCodes.PICK_PICTURE_FROM_DOCUMENTS); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void openChooserWithDocuments(Fragment fragment, @Nullable String chooserTitle, int type) { + try { + Intent intent = createChooserIntent(fragment.getActivity(), chooserTitle, type); + fragment.startActivityForResult(intent, RequestCodes.SOURCE_CHOOSER | RequestCodes.PICK_PICTURE_FROM_DOCUMENTS); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void openChooserWithDocuments(android.app.Fragment fragment, @Nullable String chooserTitle, int type) { + try { + Intent intent = createChooserIntent(fragment.getActivity(), chooserTitle, type); + fragment.startActivityForResult(intent, RequestCodes.SOURCE_CHOOSER | RequestCodes.PICK_PICTURE_FROM_DOCUMENTS); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void openChooserWithGallery(Activity activity, @Nullable String chooserTitle, int type) { + try { + Intent intent = createChooserIntent(activity, chooserTitle, true, type); + activity.startActivityForResult(intent, RequestCodes.SOURCE_CHOOSER | RequestCodes.PICK_PICTURE_FROM_GALLERY); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void openChooserWithGallery(Fragment fragment, @Nullable String chooserTitle, int type) { + try { + Intent intent = createChooserIntent(fragment.getActivity(), chooserTitle, true, type); + fragment.startActivityForResult(intent, RequestCodes.SOURCE_CHOOSER | RequestCodes.PICK_PICTURE_FROM_GALLERY); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void openChooserWithGallery(android.app.Fragment fragment, @Nullable String chooserTitle, int type) { + try { + Intent intent = createChooserIntent(fragment.getActivity(), chooserTitle, true, type); + fragment.startActivityForResult(intent, RequestCodes.SOURCE_CHOOSER | RequestCodes.PICK_PICTURE_FROM_GALLERY); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void openDocuments(Activity activity, int type) { + Intent intent = createDocumentsIntent(activity, type); + activity.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_DOCUMENTS); + } + + public static void openDocuments(Fragment fragment, int type) { + Intent intent = createDocumentsIntent(fragment.getContext(), type); + fragment.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_DOCUMENTS); + } + + public static void openDocuments(android.app.Fragment fragment, int type) { + Intent intent = createDocumentsIntent(fragment.getActivity(), type); + fragment.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_DOCUMENTS); + } + + /** + * 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, int type) { + Intent intent = createGalleryIntent(activity, type); + activity.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_GALLERY); + } + + /** + * 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(Fragment fragment, int type) { + Intent intent = createGalleryIntent(fragment.getContext(), type); + fragment.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_GALLERY); + } + + /** + * 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(android.app.Fragment fragment, int type) { + Intent intent = createGalleryIntent(fragment.getActivity(), type); + fragment.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_GALLERY); + } + + public static void openCameraForImage(Activity activity, int type) { + Intent intent = createCameraForImageIntent(activity, type); + activity.startActivityForResult(intent, RequestCodes.TAKE_PICTURE); + } + + public static void openCameraForImage(Fragment fragment, int type) { + Intent intent = createCameraForImageIntent(fragment.getActivity(), type); + fragment.startActivityForResult(intent, RequestCodes.TAKE_PICTURE); + } + + public static void openCameraForImage(android.app.Fragment fragment, int type) { + Intent intent = createCameraForImageIntent(fragment.getActivity(), type); + fragment.startActivityForResult(intent, RequestCodes.TAKE_PICTURE); + } + + public static void openCameraForVideo(Activity activity, int type) { + Intent intent = createCameraForVideoIntent(activity, type); + activity.startActivityForResult(intent, RequestCodes.CAPTURE_VIDEO); + } + + public static void openCameraForVideo(Fragment fragment, int type) { + Intent intent = createCameraForVideoIntent(fragment.getActivity(), type); + fragment.startActivityForResult(intent, RequestCodes.CAPTURE_VIDEO); + } + + public static void openCameraForVideo(android.app.Fragment fragment, int type) { + Intent intent = createCameraForVideoIntent(fragment.getActivity(), type); + fragment.startActivityForResult(intent, RequestCodes.CAPTURE_VIDEO); + } + + @Nullable + private static File takenCameraPicture(Context context) throws IOException, URISyntaxException { + String lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_PHOTO, null); + if (lastCameraPhoto != null) { + return new File(lastCameraPhoto); + } else { + return null; + } + } + + @Nullable + private static File takenCameraVideo(Context context) throws IOException, URISyntaxException { + String lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_VIDEO, null); + if (lastCameraPhoto != null) { + return new File(lastCameraPhoto); + } else { + return null; + } + } + + public static void handleActivityResult(int requestCode, int resultCode, Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { + boolean isHandledPickedFile = (requestCode & RequestCodes.FILE_PICKER_IMAGE_IDENTIFICATOR) > 0; + if (isHandledPickedFile) { + requestCode &= ~RequestCodes.SOURCE_CHOOSER; + if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY || + requestCode == RequestCodes.TAKE_PICTURE || + requestCode == RequestCodes.CAPTURE_VIDEO || + requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS) { + if (resultCode == Activity.RESULT_OK) { + if (requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS && !isPhoto(data)) { + onPictureReturnedFromDocuments(data, activity, callbacks); + } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY && !isPhoto(data)) { + onPictureReturnedFromGallery(data, activity, callbacks); + } else if (requestCode == RequestCodes.TAKE_PICTURE) { + onPictureReturnedFromCamera(activity, callbacks); + } else if (requestCode == RequestCodes.CAPTURE_VIDEO) { + onVideoReturnedFromCamera(activity, callbacks); + } else if (isPhoto(data)) { + onPictureReturnedFromCamera(activity, callbacks); + } else { + onPictureReturnedFromDocuments(data, activity, callbacks); + } + } else { + if (requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS) { + callbacks.onCanceled(FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); + } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY) { + callbacks.onCanceled(FilePicker.ImageSource.GALLERY, restoreType(activity)); + } else { + callbacks.onCanceled(FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); + } + } + } + } + } + + public static List handleExternalImagesPicked(Intent data, Activity activity) { + try { + return getFilesFromGalleryPictures(data, activity); + } catch (IOException e) { + e.printStackTrace(); + } + return new ArrayList<>(); + } + + private static boolean isPhoto(Intent data) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return data == null || (data.getData() == null && data.getClipData() == null); + } else { + return data == null || (data.getData() == null); + } + } + + public static boolean willHandleActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == RequestCodes.SOURCE_CHOOSER || requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY || requestCode == RequestCodes.TAKE_PICTURE || requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS) { + return true; + } + return false; + } + + private static Intent plainGalleryPickerIntent() { + return new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + } + + public static boolean canDeviceHandleGallery(@NonNull Context context) { + return plainGalleryPickerIntent().resolveActivity(context.getPackageManager()) != null; + } + + /** + * @param context context + * @return File containing lastly taken (using camera) photo. Returns null if there was no photo taken or it doesn't exist anymore. + */ + public static File lastlyTakenButCanceledPhoto(@NonNull Context context) { + String filePath = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_PHOTO, null); + if (filePath == null) return null; + File file = new File(filePath); + if (file.exists()) { + return file; + } else { + return null; + } + } + + public static File lastlyTakenButCanceledVideo(@NonNull Context context) { + String filePath = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_VIDEO, null); + if (filePath == null) return null; + File file = new File(filePath); + if (file.exists()) { + return file; + } else { + return null; + } + } + + private static void onPictureReturnedFromDocuments(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { + try { + Uri photoPath = data.getData(); + File 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)); + } + } + + private static void onPictureReturnedFromGallery(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { + try { + List files = getFilesFromGalleryPictures(data, activity); + callbacks.onImagesPicked(files, FilePicker.ImageSource.GALLERY, restoreType(activity)); + } catch (Exception e) { + e.printStackTrace(); + callbacks.onImagePickerError(e, FilePicker.ImageSource.GALLERY, restoreType(activity)); + } + } + + private static List getFilesFromGalleryPictures(Intent data, Activity activity) throws IOException { + List files = new ArrayList<>(); + ClipData clipData = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + clipData = data.getClipData(); + } + if (clipData == null) { + Uri uri = data.getData(); + File file = PickedFiles.pickedExistingPicture(activity, uri); + files.add(file); + } else { + for (int i = 0; i < clipData.getItemCount(); i++) { + Uri uri = clipData.getItemAt(i).getUri(); + File file = PickedFiles.pickedExistingPicture(activity, uri); + files.add(file); + } + } + + if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, files); + } + + return files; + } + + private static void onPictureReturnedFromCamera(Activity activity, @NonNull FilePicker.Callbacks callbacks) { + try { + String lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_PHOTO_URI, null); + if (!TextUtils.isEmpty(lastImageUri)) { + revokeWritePermission(activity, Uri.parse(lastImageUri)); + } + + File photoFile = FilePicker.takenCameraPicture(activity); + List 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)); + } + } + + private static void onVideoReturnedFromCamera(Activity activity, @NonNull FilePicker.Callbacks callbacks) { + try { + String lastVideoUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_VIDEO_URI, null); + if (!TextUtils.isEmpty(lastVideoUri)) { + revokeWritePermission(activity, Uri.parse(lastVideoUri)); + } + + File photoFile = FilePicker.takenCameraVideo(activity); + List files = new ArrayList<>(); + files.add(photoFile); + + if (photoFile == null) { + Exception e = new IllegalStateException("Unable to get the video returned from camera"); + callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_VIDEO, restoreType(activity)); + } else { + if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); + } + + callbacks.onImagesPicked(files, FilePicker.ImageSource.CAMERA_VIDEO, restoreType(activity)); + } + + PreferenceManager.getDefaultSharedPreferences(activity) + .edit() + .remove(KEY_LAST_CAMERA_VIDEO) + .remove(KEY_VIDEO_URI) + .apply(); + } catch (Exception e) { + e.printStackTrace(); + callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_VIDEO, restoreType(activity)); + } + } + + /** + * Method to clear configuration. Would likely be used in onDestroy(), onDestroyView()... + * + * @param context context + */ + public static void clearConfiguration(@NonNull Context context) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .remove(BundleKeys.FOLDER_NAME) + .remove(BundleKeys.ALLOW_MULTIPLE) + .remove(BundleKeys.COPY_TAKEN_PHOTOS) + .remove(BundleKeys.COPY_PICKED_IMAGES) + .apply(); + } + + public static FilePickerConfiguration configuration(@NonNull Context context) { + return new FilePickerConfiguration(context); + } + + + public enum ImageSource { + GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO + } + + public interface Callbacks { + void onImagePickerError(Exception e, FilePicker.ImageSource source, int type); + + void onImagesPicked(@NonNull List imageFiles, FilePicker.ImageSource source, int type); + + void onCanceled(FilePicker.ImageSource source, int type); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java new file mode 100644 index 000000000..5a69b27b4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java @@ -0,0 +1,58 @@ +package fr.free.nrw.commons.filepicker; + +import android.content.Context; +import android.preference.PreferenceManager; + +public class FilePickerConfiguration implements Constants { + + private Context context; + + FilePickerConfiguration(Context context) { + this.context = context; + } + + public FilePickerConfiguration setImagesFolderName(String folderName) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(BundleKeys.FOLDER_NAME, folderName) + .commit(); + return this; + } + + public FilePickerConfiguration setAllowMultiplePickInGallery(boolean allowMultiple) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putBoolean(BundleKeys.ALLOW_MULTIPLE, allowMultiple) + .commit(); + return this; + } + + public FilePickerConfiguration setCopyTakenPhotosToPublicGalleryAppFolder(boolean copy) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putBoolean(BundleKeys.COPY_TAKEN_PHOTOS, copy) + .commit(); + return this; + } + + public FilePickerConfiguration setCopyPickedImagesToPublicGalleryAppFolder(boolean copy) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putBoolean(BundleKeys.COPY_PICKED_IMAGES, copy) + .commit(); + 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); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java b/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java new file mode 100644 index 000000000..ca5d1c59e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java @@ -0,0 +1,46 @@ +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 sMimeTypeToExtensionMap = + ImmutableMap.of( + "image/heif", "heif", + "image/heic", "heic"); + + private static final Map sExtensionToMimeTypeMap = + ImmutableMap.of( + "heif", "image/heif", + "heic", "image/heic"); + + public static String getExtensionFromMimeType(String mimeType) { + String result = sMimeTypeToExtensionMap.get(mimeType); + if (result != null) { + return result; + } + return sMimeTypeMap.getExtensionFromMimeType(mimeType); + } + + public static String getMimeTypeFromExtension(String extension) { + String result = sExtensionToMimeTypeMap.get(extension); + if (result != null) { + return result; + } + return sMimeTypeMap.getMimeTypeFromExtension(extension); + } + + public static boolean hasExtension(String extension) { + return sExtensionToMimeTypeMap.containsKey(extension) || sMimeTypeMap.hasExtension(extension); + } + + public static boolean hasMimeType(String mimeType) { + return sMimeTypeToExtensionMap.containsKey(mimeType) || sMimeTypeMap.hasMimeType(mimeType); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java new file mode 100644 index 000000000..fb5663d2b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java @@ -0,0 +1,155 @@ +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.support.annotation.NonNull; +import android.support.v4.content.FileProvider; +import android.util.Log; +import android.webkit.MimeTypeMap; + +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; + + +class PickedFiles implements Constants { + + private static String getFolderName(@NonNull Context context) { + return FilePicker.configuration(context).getFolderName(); + } + + private static File tempImageDirectory(@NonNull Context context) { + File privateTempDir = new File(context.getCacheDir(), DEFAULT_FOLDER_NAME); + if (!privateTempDir.exists()) privateTempDir.mkdirs(); + return privateTempDir; + } + + private static void writeToFile(InputStream in, File file) { + try { + OutputStream out = new FileOutputStream(file); + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.close(); + in.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void copyFile(File src, File dst) throws IOException { + InputStream in = new FileInputStream(src); + writeToFile(in, dst); + } + + static void copyFilesInSeparateThread(final Context context, final List filesToCopy) { + new Thread(new Runnable() { + @Override + public void run() { + List copiedFiles = new ArrayList<>(); + int i = 1; + for (File fileToCopy : filesToCopy) { + 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(); + } + + static List singleFileList(File file) { + List list = new ArrayList<>(); + list.add(file); + return list; + } + + static void scanCopiedImages(Context context, List 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, + new MediaScannerConnection.OnScanCompletedListener() { + public void onScanCompleted(String path, Uri uri) { + Log.d(getClass().getSimpleName(), "Scanned " + path + ":"); + Log.d(getClass().getSimpleName(), "-> uri=" + uri); + } + }); + } + + static File pickedExistingPicture(@NonNull Context context, Uri photoUri) throws IOException { + InputStream pictureInputStream = context.getContentResolver().openInputStream(photoUri); + File directory = tempImageDirectory(context); + File photoFile = new File(directory, UUID.randomUUID().toString() + "." + getMimeType(context, photoUri)); + photoFile.createNewFile(); + writeToFile(pictureInputStream, photoFile); + return photoFile; + } + + static File getCameraPicturesLocation(@NonNull Context context) throws IOException { + File dir = tempImageDirectory(context); + return File.createTempFile(UUID.randomUUID().toString(), ".jpg", dir); + } + + static File getCameraVideoLocation(@NonNull Context context) throws IOException { + File dir = tempImageDirectory(context); + return File.createTempFile(UUID.randomUUID().toString(), ".mp4", 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; + } + + static Uri getUriToFile(@NonNull Context context, @NonNull File file) { + String packageName = context.getApplicationContext().getPackageName(); + String authority = packageName + ".provider"; + return FileProvider.getUriForFile(context, authority, file); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/media/FrescoImageLoader.java b/app/src/main/java/fr/free/nrw/commons/media/FrescoImageLoader.java deleted file mode 100644 index abdf61b00..000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/FrescoImageLoader.java +++ /dev/null @@ -1,53 +0,0 @@ -package fr.free.nrw.commons.media; - -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.widget.ImageView; - -import com.esafirm.imagepicker.features.imageloader.ImageLoader; -import com.esafirm.imagepicker.features.imageloader.ImageType; -import com.facebook.common.util.UriUtil; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.drawee.drawable.ProgressBarDrawable; -import com.facebook.drawee.generic.GenericDraweeHierarchy; -import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; -import com.facebook.drawee.interfaces.DraweeController; -import com.facebook.drawee.view.DraweeHolder; -import com.facebook.imagepipeline.common.ResizeOptions; -import com.facebook.imagepipeline.request.ImageRequest; -import com.facebook.imagepipeline.request.ImageRequestBuilder; - -public class FrescoImageLoader implements ImageLoader { - - @Override - public void loadImage(String path, ImageView imageView, ImageType imageType) { - Drawable defaultDrawable = imageView.getDrawable(); - Resources resources = imageView.getContext().getResources(); - GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(resources) - .build(); - final DraweeHolder draweeHolder = DraweeHolder.create(hierarchy, imageView.getContext()); - draweeHolder.getHierarchy().setProgressBarImage(new ProgressBarDrawable()); - Drawable drawable = draweeHolder.getHierarchy().getTopLevelDrawable(); - if (drawable == null) { - imageView.setImageDrawable(defaultDrawable); - } else { - imageView.setImageDrawable(drawable); - } - - Uri uri = new Uri.Builder() - .scheme(UriUtil.LOCAL_FILE_SCHEME) - .path(path) - .build(); - ImageRequest imageRequest = ImageRequestBuilder - .newBuilderWithSource(uri) - .setResizeOptions(new ResizeOptions(200, 200)) - .build(); - DraweeController controller = Fresco.newDraweeControllerBuilder() - .setOldController(draweeHolder.getController()) - .setImageRequest(imageRequest) - .build(); - draweeHolder.setController(controller); - - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java index d440b5c68..e8ba35d6f 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java @@ -4,7 +4,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; @@ -22,7 +21,6 @@ import android.widget.LinearLayout; import android.widget.ProgressBar; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import java.util.List; @@ -40,7 +38,6 @@ import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.utils.FragmentUtils; import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.UriSerializer; import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.wikidata.WikidataEditListener; import io.reactivex.Observable; @@ -245,7 +242,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment @Override public void onWikidataEditSuccessful() { // Do not refresh nearby map if we are checking other areas with search this area button - if (!nearbyMapFragment.searchThisAreaModeOn) { + if (nearbyMapFragment != null && !nearbyMapFragment.searchThisAreaModeOn) { refreshView(MAP_UPDATED); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index 6ac512fbc..57a3065d2 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -4,7 +4,6 @@ import android.animation.ObjectAnimator; import android.animation.TypeEvaluator; import android.animation.ValueAnimator; import android.content.Intent; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Color; import android.net.Uri; @@ -30,10 +29,8 @@ import android.widget.ProgressBar; import android.widget.TextView; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.mapbox.mapboxsdk.Mapbox; -import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions; import com.mapbox.mapboxsdk.annotations.Icon; import com.mapbox.mapboxsdk.annotations.IconFactory; import com.mapbox.mapboxsdk.annotations.Marker; @@ -63,17 +60,11 @@ import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.kvstore.BasicKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.utils.ImageUtils; -import fr.free.nrw.commons.utils.IntentUtils; import fr.free.nrw.commons.utils.LocationUtils; import fr.free.nrw.commons.utils.UiUtils; -import fr.free.nrw.commons.utils.UriDeserializer; import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; -import static fr.free.nrw.commons.contributions.ContributionController.NEARBY_CAMERA_UPLOAD_REQUEST_CODE; -import static fr.free.nrw.commons.contributions.ContributionController.NEARBY_GALLERY_UPLOAD_REQUEST_CODE; -import static fr.free.nrw.commons.contributions.ContributionController.NEARBY_UPLOAD_IMAGE_LIMIT; import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; @@ -867,7 +858,7 @@ public class NearbyMapFragment extends DaggerFragment { if (fabCamera.isShown()) { Timber.d("Camera button tapped. Place: %s", place.toString()); storeSharedPrefs(); - controller.initiateCameraPick(getActivity(), NEARBY_CAMERA_UPLOAD_REQUEST_CODE); + controller.initiateCameraPick(getActivity()); } }); @@ -875,7 +866,7 @@ public class NearbyMapFragment extends DaggerFragment { if (fabGallery.isShown()) { Timber.d("Gallery button tapped. Place: %s", place.toString()); storeSharedPrefs(); - controller.initiateGalleryPick(getActivity(), NEARBY_UPLOAD_IMAGE_LIMIT, NEARBY_GALLERY_UPLOAD_REQUEST_CODE); + controller.initiateGalleryPick(getActivity(), false); } }); } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index debd80f57..447a5aef2 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java @@ -33,17 +33,10 @@ import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.kvstore.BasicKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.utils.PlaceUtils; import timber.log.Timber; -import static fr.free.nrw.commons.contributions.ContributionController.NEARBY_CAMERA_UPLOAD_REQUEST_CODE; -import static fr.free.nrw.commons.contributions.ContributionController.NEARBY_GALLERY_UPLOAD_REQUEST_CODE; -import static fr.free.nrw.commons.contributions.ContributionController.NEARBY_UPLOAD_IMAGE_LIMIT; import static fr.free.nrw.commons.theme.NavigationBaseActivity.startActivityWithFlags; -import static fr.free.nrw.commons.wikidata.WikidataConstants.IS_DIRECT_UPLOAD; import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; -import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF; -import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION; public class PlaceRenderer extends Renderer { @@ -146,7 +139,7 @@ public class PlaceRenderer extends Renderer { } else { Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); storeSharedPrefs(); - controller.initiateCameraPick(fragment.getActivity(), NEARBY_CAMERA_UPLOAD_REQUEST_CODE); + controller.initiateCameraPick(fragment.getActivity()); } }); @@ -166,7 +159,7 @@ public class PlaceRenderer extends Renderer { }else { Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); storeSharedPrefs(); - controller.initiateGalleryPick(fragment.getActivity(), NEARBY_UPLOAD_IMAGE_LIMIT, NEARBY_GALLERY_UPLOAD_REQUEST_CODE); + controller.initiateGalleryPick(fragment.getActivity(), false); } }); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java index fa7a49f5e..b16150817 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java @@ -24,7 +24,7 @@ import io.reactivex.schedulers.Schedulers; import timber.log.Timber; /** - * Processing of the image file that is about to be uploaded via ShareActivity is done here + * Processing of the image filePath that is about to be uploaded via ShareActivity is done here */ @Singleton public class FileProcessor implements SimilarImageDialogFragment.onResponse { @@ -61,7 +61,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { } /** - * Processes file coordinates, either from EXIF data or user location + * Processes filePath coordinates, either from EXIF data or user location */ GPSExtractor processFileCoordinates(SimilarImageInterface similarImageInterface) { Timber.d("Calling GPSExtractor"); @@ -116,7 +116,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { Timber.d("not null fild EXIF" + tempImageObj.imageCoordsExists + " coords" + tempImageObj.getCoords()); if (tempImageObj.getCoords() != null && tempImageObj.imageCoordsExists) { // Current image has gps coordinates and it's not current gps locaiton - Timber.d("This file has image coords:" + file.getAbsolutePath()); + Timber.d("This filePath has image coords:" + file.getAbsolutePath()); similarImageInterface.showSimilarImageFragment(filePath, file.getAbsolutePath()); break; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 18dd9e4dd..1485d716a 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -1,20 +1,10 @@ package fr.free.nrw.commons.upload; -import android.annotation.SuppressLint; import android.content.ContentResolver; -import android.content.ContentUris; import android.content.Context; -import android.database.Cursor; import android.media.ExifInterface; import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.os.ParcelFileDescriptor; -import android.provider.DocumentsContract; -import android.provider.MediaStore; -import android.provider.OpenableColumns; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import android.webkit.MimeTypeMap; import java.io.BufferedReader; import java.io.File; @@ -32,7 +22,7 @@ import timber.log.Timber; public class FileUtils { /** - * Get SHA1 of file from input stream + * Get SHA1 of filePath from input stream */ static String getSHA1(InputStream is) { @@ -71,7 +61,7 @@ public class FileUtils { } /** - * Get Geolocation of file from input file path + * Get Geolocation of filePath from input filePath path */ static String getGeolocationOfFile(String filePath) { @@ -91,10 +81,10 @@ public class FileUtils { /** - * Read and return the content of a resource file as string. + * Read and return the content of a resource filePath as string. * - * @param fileName asset file's path (e.g. "/queries/nearby_query.rq") - * @return the content of the file + * @param fileName asset filePath's path (e.g. "/queries/nearby_query.rq") + * @return the content of the filePath */ public static String readFromResource(String fileName) throws IOException { StringBuilder buffer = new StringBuilder(); @@ -138,8 +128,22 @@ public class FileUtils { return deletedAll; } - static String getFileExt(String fileName) { - //Default file extension + public static String getMimeType(Context context, Uri uri) { + String mimeType; + if (uri.getScheme()!=null && uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { + ContentResolver cr = context.getContentResolver(); + mimeType = cr.getType(uri); + } else { + String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri + .toString()); + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( + fileExtension.toLowerCase()); + } + return mimeType; + } + + public static String getFileExt(String fileName) { + //Default filePath extension String extension = ".jpg"; int i = fileName.lastIndexOf('.'); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java index 93bbd4d50..5992e4b34 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java @@ -10,6 +10,11 @@ import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtilsWrapper; import fr.free.nrw.commons.utils.StringUtils; import io.reactivex.Single; +import timber.log.Timber; + +import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE; +import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS; +import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; /** * Methods for pre-processing images to be uploaded @@ -28,7 +33,6 @@ public class ImageProcessingService { MediaWikiApi mwApi) { this.fileUtilsWrapper = fileUtilsWrapper; this.bitmapRegionDecoderWrapper = bitmapRegionDecoderWrapper; - this.imageUtilsWrapper = imageUtilsWrapper; this.mwApi = mwApi; } @@ -37,23 +41,48 @@ public class ImageProcessingService { * Check image quality before upload * - checks duplicate image * - checks dark image + * - checks geolocation for image + * - check for valid title */ - public Single checkImageQuality(String filePath) { - return checkImageQuality(null, filePath); + Single validateImage(UploadModel.UploadItem uploadItem, boolean checkTitle) { + int currentImageQuality = uploadItem.getImageQuality(); + Timber.d("Current image quality is %d", currentImageQuality); + if (currentImageQuality == ImageUtils.IMAGE_KEEP) { + return Single.just(ImageUtils.IMAGE_OK); + } + Timber.d("Checking the validity of image"); + String filePath = uploadItem.getMediaUri().getPath(); + Single duplicateImage = checkDuplicateImage(filePath); + Single wrongGeoLocation = checkImageGeoLocation(uploadItem.getPlace(), filePath); + Single darkImage = checkDarkImage(filePath); + Single itemTitle = checkTitle ? validateItemTitle(uploadItem) : Single.just(ImageUtils.IMAGE_OK); + + return Single.zip(duplicateImage, wrongGeoLocation, darkImage, itemTitle, + (duplicate, wrongGeo, dark, title) -> { + Timber.d("Result for duplicate: %d, geo: %d, dark: %d, title: %d", duplicate, wrongGeo, dark, title); + return duplicate | wrongGeo | dark | title; + }); } /** - * Check image quality before upload - * - checks duplicate image - * - checks dark image - * - checks geolocation for image + * Checks item title + * - empty title + * - existing title + * @param uploadItem + * @return */ - public Single checkImageQuality(Place place, String filePath) { - return Single.zip( - checkDuplicateImage(filePath), - checkImageGeoLocation(place, filePath), - checkDarkImage(filePath), //Returns IMAGE_DARK or IMAGE_OK - (dupe, wrongGeo, dark) -> dupe | wrongGeo | dark); + private Single validateItemTitle(UploadModel.UploadItem uploadItem) { + Timber.d("Checking for image title %s", uploadItem.getTitle()); + Title title = uploadItem.getTitle(); + if (title.isEmpty()) { + return Single.just(EMPTY_TITLE); + } + + return Single.fromCallable(() -> mwApi.fileExistsWithName(uploadItem.getFileName())) + .map(doesFileExist -> { + Timber.d("Result for valid title is %s", doesFileExist); + return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK; + }); } /** @@ -62,11 +91,15 @@ public class ImageProcessingService { * @return IMAGE_DUPLICATE or IMAGE_OK */ private Single checkDuplicateImage(String filePath) { + Timber.d("Checking for duplicate image %s", filePath); return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath)) .map(fileUtilsWrapper::getSHA1) .map(mwApi::existingFile) - .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK); + .map(b -> { + Timber.d("Result for duplicate image %s", b); + return b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK; + }); } /** @@ -75,6 +108,7 @@ public class ImageProcessingService { * @return IMAGE_DARK or IMAGE_OK */ private Single checkDarkImage(String filePath) { + Timber.d("Checking for dark image %s", filePath); return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath)) .map(file -> bitmapRegionDecoderWrapper.newInstance(file, false)) @@ -87,6 +121,7 @@ public class ImageProcessingService { * @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK */ private Single checkImageGeoLocation(Place place, String filePath) { + Timber.d("Checking for image geolocation %s", filePath); if (place == null || StringUtils.isNullOrWhiteSpace(place.getWikiDataEntityId())) { return Single.just(ImageUtils.IMAGE_OK); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Title.java b/app/src/main/java/fr/free/nrw/commons/upload/Title.java index e0781ce67..96624ad17 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/Title.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/Title.java @@ -5,7 +5,7 @@ import android.text.TextUtils; import io.reactivex.subjects.BehaviorSubject; import timber.log.Timber; -class Title{ +public class Title{ private String titleText; private boolean set; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java index 5559f750e..32056079f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java @@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload; import android.Manifest; import android.annotation.SuppressLint; +import android.app.ProgressDialog; import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -52,6 +53,8 @@ import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.category.CategoriesModel; import fr.free.nrw.commons.category.CategoryItem; import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.ContributionController; +import fr.free.nrw.commons.contributions.UploadableFile; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.nearby.Place; @@ -67,12 +70,17 @@ import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; +import static fr.free.nrw.commons.contributions.Contribution.SOURCE_EXTERNAL; +import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS; +import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES; import static fr.free.nrw.commons.utils.ImageUtils.Result; import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult; import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; public class UploadActivity extends BaseActivity implements UploadView, SimilarImageInterface { @Inject MediaWikiApi mwApi; + @Inject + ContributionController contributionController; @Inject @Named("direct_nearby_upload_prefs") JsonKvStore directKvStore; @Inject UploadPresenter presenter; @Inject CategoriesModel categoriesModel; @@ -128,6 +136,7 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI private DescriptionsAdapter descriptionsAdapter; private RVRendererAdapter categoriesAdapter; private CompositeDisposable compositeDisposable; + private ProgressDialog progressDialog; @SuppressLint("CheckResult") @@ -241,7 +250,7 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI dismissKeyboard(); } if(isShowingItem) { - descriptionsAdapter.setItems(uploadItem.title, uploadItem.descriptions); + descriptionsAdapter.setItems(uploadItem.getTitle(), uploadItem.getDescriptions()); rvDescriptions.setAdapter(descriptionsAdapter); } } @@ -372,18 +381,10 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI } @Override - public void showBadPicturePopup(@Result int result) { - if (result >= 8 ) { // If location of image and nearby does not match, then set shared preferences to disable wikidata edits - directKvStore.putBoolean("Picture_Has_Correct_Location", false); - } - String errorMessageForResult = getErrorMessageForResult(this, result); - if (StringUtils.isNullOrWhiteSpace(errorMessageForResult)) { - return; - } - + public void showBadPicturePopup(String errorMessage) { DialogUtil.showAlertDialog(this, getString(R.string.warning), - errorMessageForResult, + errorMessage, () -> presenter.deletePicture(), () -> presenter.keepPicture()); } @@ -410,6 +411,22 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI () -> presenter.handleCategoryNext(categoriesModel, true)); } + @Override + public void showProgressDialog() { + if (progressDialog == null) { + progressDialog = new ProgressDialog(this); + } + progressDialog.setMessage(getString(R.string.please_wait)); + progressDialog.show(); + } + + @Override + public void hideProgressDialog() { + if (progressDialog != null && !isFinishing()) { + progressDialog.dismiss(); + } + } + @Override public void launchMapActivity(String decCoords) { Utils.handleGeoCoordinates(this, decCoords); @@ -596,7 +613,26 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI private void receiveSharedItems() { Intent intent = getIntent(); - String mimeType = intent.getType(); + String action = intent.getAction(); + if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) { + receiveExternalSharedItems(); + } else if (ACTION_INTERNAL_UPLOADS.equals(action)) { + receiveInternalSharedItems(); + } + } + + private void receiveExternalSharedItems() { + List uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent()); + if (uploadableFiles.isEmpty()) { + handleNullMedia(); + return; + } + + presenter.receive(uploadableFiles, SOURCE_EXTERNAL, null); + } + + private void receiveInternalSharedItems() { + Intent intent = getIntent(); String source; if (intent.hasExtra(UploadService.EXTRA_SOURCE)) { @@ -605,31 +641,21 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI source = Contribution.SOURCE_EXTERNAL; } - Timber.d("Received intent %s with action %s and mimeType %s from source %s", + Timber.d("Received intent %s with action %s and from source %s", intent.toString(), intent.getAction(), - mimeType, source); - ArrayList urisList = new ArrayList<>(); + ArrayList uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES); + Timber.i("Received multiple upload %s", uploadableFiles.size()); - if (Intent.ACTION_SEND.equals(intent.getAction())) { - Uri mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - if (mediaUri != null) { - urisList.add(mediaUri); - } - } else if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { - urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - Timber.i("Received multiple upload %s", urisList.size()); - } - - if (urisList.isEmpty()) { + if (uploadableFiles.isEmpty()) { handleNullMedia(); return; } Place place = intent.getParcelableExtra(PLACE_OBJECT); - presenter.receive(urisList, mimeType, source, place); + presenter.receive(uploadableFiles, source, place); resetDirectPrefs(); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java index 490baccb0..2a97f6714 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -1,9 +1,7 @@ package fr.free.nrw.commons.upload; import android.annotation.SuppressLint; -import android.content.ContentResolver; import android.content.Context; -import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -18,13 +16,17 @@ import javax.inject.Inject; import javax.inject.Named; import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.UploadableFile; +import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper; import fr.free.nrw.commons.kvstore.BasicKvStore; import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.utils.ImageUtils; import io.reactivex.Observable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.functions.Consumer; @@ -40,7 +42,6 @@ public class UploadModel { "", "", GPSExtractor.DUMMY, - "", null, -1L) { }; @@ -54,11 +55,9 @@ public class UploadModel { private boolean rightCardState = true; private int currentStepIndex = 0; private Context context; - private ContentResolver contentResolver; private Disposable badImageSubscription; private SessionManager sessionManager; - private FileUtilsWrapper fileUtilsWrapper; private FileProcessor fileProcessor; private final ImageProcessingService imageProcessingService; @@ -68,52 +67,41 @@ public class UploadModel { @Named("licenses_by_name") Map licensesByName, Context context, SessionManager sessionManager, - FileUtilsWrapper fileUtilsWrapper, - FileProcessor fileProcessor, ImageProcessingService imageProcessingService) { + FileProcessor fileProcessor, + ImageProcessingService imageProcessingService) { this.licenses = licenses; this.basicKvStore = basicKvStore; this.license = basicKvStore.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); this.licensesByName = licensesByName; this.context = context; - this.contentResolver = context.getContentResolver(); this.sessionManager = sessionManager; - this.fileUtilsWrapper = fileUtilsWrapper; this.fileProcessor = fileProcessor; this.imageProcessingService = imageProcessingService; } @SuppressLint("CheckResult") - Observable preProcessImages(List mediaUris, - String mimeType, + Observable preProcessImages(List uploadableFiles, Place place, String source, SimilarImageInterface similarImageInterface) { initDefaultValues(); + return Observable.fromIterable(uploadableFiles) + .map(uploadableFile -> getUploadItem(uploadableFile, place, source, similarImageInterface)); + } - return Observable.fromIterable(mediaUris) - .map(mediaUri -> { - UploadItem item = getUploadItem(mimeType, place, source, similarImageInterface, mediaUri); - imageProcessingService.checkImageQuality(place, mediaUri.getPath()) - .subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(item.imageQuality::onNext, Timber::e); - return item; - }); + Single getImageQuality(UploadItem uploadItem, boolean checkTitle) { + return imageProcessingService.validateImage(uploadItem, checkTitle); } @NonNull - private UploadItem getUploadItem(String mimeType, + private UploadItem getUploadItem(UploadableFile uploadableFile, Place place, String source, - SimilarImageInterface similarImageInterface, - Uri mediaUri) { - fileProcessor - .initFileDetails(Objects.requireNonNull(mediaUri.getPath()), context.getContentResolver()); - long fileCreatedDate = getFileCreatedDate(mediaUri); - String fileExt = fileUtilsWrapper.getFileExt(mediaUri.getPath()); + SimilarImageInterface similarImageInterface) { + fileProcessor.initFileDetails(Objects.requireNonNull(uploadableFile.getFilePath()), context.getContentResolver()); + long fileCreatedDate = uploadableFile.getFileCreatedDate(context); GPSExtractor gpsExtractor = fileProcessor.processFileCoordinates(similarImageInterface); - return new UploadItem(mediaUri, mimeType, source, gpsExtractor, - fileExt, place, fileCreatedDate); + return new UploadItem(Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate); } void onItemsProcessed(Place place, List uploadItems) { @@ -121,9 +109,11 @@ public class UploadModel { if (items.isEmpty()) { return; } + UploadItem uploadItem = items.get(0); uploadItem.selected = true; uploadItem.first = true; + if (place != null) { uploadItem.title.setTitleText(place.getName()); uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription()); @@ -140,34 +130,6 @@ public class UploadModel { items = new ArrayList<>(); } - /** - * Get file creation date from uri from all possible content providers - * - * @param media - * @return - */ - private long getFileCreatedDate(Uri media) { - try { - Cursor cursor = contentResolver.query(media, null, null, null, null); - if (cursor == null) { - return -1;//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) { - return -1L; - } - cursor.moveToFirst(); - return cursor.getLong(lastModifiedColumnIndex); - } catch (Exception e) { - return -1;////Could not fetch last_modified - } - } - boolean isPreviousAvailable() { return currentStepIndex > 0; } @@ -226,11 +188,8 @@ public class UploadModel { this.bottomCardState = bottomCardState; } + @SuppressLint("CheckResult") public void next() { - Timber.d("UploadModel:next; Handling next"); - if (badImageSubscription != null) - badImageSubscription.dispose(); - Timber.d("UploadModel:next; disposing badImageSubscription"); markCurrentUploadVisited(); if (currentStepIndex < items.size() + 1) { currentStepIndex++; @@ -312,7 +271,8 @@ public class UploadModel { Observable buildContributions(List categoryStringList) { return Observable.fromIterable(items).map(item -> { - Contribution contribution = new Contribution(item.mediaUri, null, item.title + "." + item.fileExt, + Contribution contribution = new Contribution(item.mediaUri, null, + item.getFileName(), Description.formatList(item.descriptions), -1, null, null, sessionManager.getAuthorName(), CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getCoords()); @@ -332,17 +292,21 @@ public class UploadModel { } void keepPicture() { - items.get(currentStepIndex).imageQuality.onNext(ImageUtils.IMAGE_KEEP); + items.get(currentStepIndex).setImageQuality(ImageUtils.IMAGE_KEEP); } void deletePicture() { badImageSubscription.dispose(); - items.remove(currentStepIndex).imageQuality.onComplete(); updateItemState(); } - void subscribeBadPicture(Consumer consumer) { - badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e); + void subscribeBadPicture(Consumer consumer, boolean checkTitle) { + if (isShowingItem()) { + badImageSubscription = getImageQuality(getCurrentItem(), checkTitle) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(consumer, Timber::e); + } } public List getItems() { @@ -351,24 +315,23 @@ public class UploadModel { @SuppressWarnings("WeakerAccess") static class UploadItem { - public final Uri mediaUri; - public final String mimeType; - public final String source; - public final GPSExtractor gpsCoords; + private final Uri mediaUri; + private final String mimeType; + private final String source; + private final GPSExtractor gpsCoords; - public boolean selected = false; - public boolean first = false; - public String fileExt; - public BehaviorSubject imageQuality; - Title title; - List descriptions; - public Place place; - public boolean visited; - public boolean error; - public long createdTimestamp; + private boolean selected = false; + private boolean first = false; + private Title title; + private List descriptions; + private Place place; + private boolean visited; + private boolean error; + private long createdTimestamp; + private BehaviorSubject imageQuality; @SuppressLint("CheckResult") - UploadItem(Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, String fileExt, @Nullable Place place, long createdTimestamp) { + UploadItem(Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, @Nullable Place place, long createdTimestamp) { title = new Title(); descriptions = new ArrayList<>(); descriptions.add(new Description()); @@ -377,9 +340,72 @@ public class UploadModel { this.mimeType = mimeType; this.source = source; this.gpsCoords = gpsCoords; - this.fileExt = fileExt; - imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); this.createdTimestamp = createdTimestamp; + imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); + } + + public String getMimeType() { + return mimeType; + } + + public String getSource() { + return source; + } + + public GPSExtractor getGpsCoords() { + return gpsCoords; + } + + public boolean isSelected() { + return selected; + } + + public boolean isFirst() { + return first; + } + + public List getDescriptions() { + return descriptions; + } + + public boolean isVisited() { + return visited; + } + + public boolean isError() { + return error; + } + + public long getCreatedTimestamp() { + return createdTimestamp; + } + + public Title getTitle() { + return title; + } + + public Uri getMediaUri() { + return mediaUri; + } + + public int getImageQuality() { + return this.imageQuality.getValue(); + } + + public void setImageQuality(int imageQuality) { + this.imageQuality.onNext(imageQuality); + } + + public String getFileExt() { + return MimeTypeMapWrapper.getExtensionFromMimeType(mimeType); + } + + public String getFileName() { + return Utils.fixExtension(title.toString(), getFileExt()); + } + + public Place getPlace() { + return place; } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java index 0896bf8e9..ee42460ca 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java @@ -1,11 +1,12 @@ package fr.free.nrw.commons.upload; import android.annotation.SuppressLint; -import android.net.Uri; +import android.content.Context; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Named; @@ -14,11 +15,12 @@ import javax.inject.Singleton; import fr.free.nrw.commons.R; import fr.free.nrw.commons.category.CategoriesModel; import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.UploadableFile; import fr.free.nrw.commons.kvstore.BasicKvStore; -import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.settings.Prefs; -import fr.free.nrw.commons.utils.ImageUtils; +import fr.free.nrw.commons.utils.StringUtils; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; @@ -29,6 +31,7 @@ import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE; import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS; import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP; import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; +import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult; /** * The MVP pattern presenter of Upload GUI @@ -36,10 +39,6 @@ import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; @Singleton public class UploadPresenter { - private final UploadModel uploadModel; - private final UploadController uploadController; - private final MediaWikiApi mediaWikiApi; - private static final UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(), new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null); private UploadView view = DUMMY; @@ -51,31 +50,37 @@ public class UploadPresenter { @UploadView.UploadPage private int currentPage = UploadView.PLEASE_WAIT; - @Inject @Named("default_preferences") BasicKvStore defaultKvStore; + private final UploadModel uploadModel; + private final UploadController uploadController; + private final Context context; + private final BasicKvStore defaultKvStore; + private final JsonKvStore directKvStore; @Inject UploadPresenter(UploadModel uploadModel, UploadController uploadController, - MediaWikiApi mediaWikiApi) { + Context context, + @Named("default_preferences") BasicKvStore defaultKvStore, + @Named("direct_nearby_upload_prefs") JsonKvStore directKvStore) { this.uploadModel = uploadModel; this.uploadController = uploadController; - this.mediaWikiApi = mediaWikiApi; + this.context = context; + this.defaultKvStore = defaultKvStore; + this.directKvStore = directKvStore; } /** * Passes the items received to {@link #uploadModel} and displays the items. * * @param media The Uri's of the media being uploaded. - * @param mimeType the mimeType of the files. * @param source File source from {@link Contribution.FileSource} */ @SuppressLint("CheckResult") - void receive(List media, - String mimeType, + void receive(List media, @Contribution.FileSource String source, Place place) { Observable uploadItemObservable = uploadModel - .preProcessImages(media, mimeType, place, source, similarImageInterface); + .preProcessImages(media, place, source, similarImageInterface); uploadItemObservable .toList() @@ -90,7 +95,7 @@ public class UploadPresenter { updateCards(); updateLicenses(); updateContent(); - uploadModel.subscribeBadPicture(this::handleBadPicture); + uploadModel.subscribeBadPicture(this::handleBadImage, false); } /** @@ -112,13 +117,25 @@ public class UploadPresenter { void handleNext(Title title, List descriptions) { Timber.e("Inside handleNext"); - validateCurrentItemTitle() + view.showProgressDialog(); + uploadModel.getImageQuality(uploadModel.getCurrentItem(), true) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(errorCode -> handleImage(errorCode, title, descriptions), + .subscribe(imageResult -> handleImage(title, descriptions, imageResult), throwable -> Timber.e(throwable, "Error occurred while handling image")); } + private void handleImage(Title title, List descriptions, Integer imageResult) { + view.hideProgressDialog(); + if (imageResult == IMAGE_KEEP || imageResult == IMAGE_OK) { + Timber.d("Set title and desc; Show next uploaded item"); + setTitleAndDescription(title, descriptions); + nextUploadedItem(); + } else { + handleBadImage(imageResult); + } + } + /** * Called by the next button in {@link UploadActivity} */ @@ -132,28 +149,27 @@ public class UploadPresenter { } } - private void handleImage(Integer errorCode, Title title, List descriptions) { + private void handleBadImage(Integer errorCode) { + Timber.d("Handle bad picture with error code %d", errorCode); + if (errorCode >= 8) { // If location of image and nearby does not match, then set shared preferences to disable wikidata edits + directKvStore.putBoolean("Picture_Has_Correct_Location", false); + } + switch (errorCode) { case EMPTY_TITLE: Timber.d("Title is empty. Showing toast"); view.showErrorMessage(R.string.add_title_toast); break; case FILE_NAME_EXISTS: - if(getCurrentItem().imageQuality.getValue().equals(IMAGE_KEEP)) { - Timber.d("Set title and desc; Show next uploaded item"); - setTitleAndDescription(title, descriptions); - nextUploadedItem(); - } else { - Timber.d("Trying to show duplicate picture popup"); - view.showDuplicatePicturePopup(); - } + Timber.d("Trying to show duplicate picture popup"); + view.showDuplicatePicturePopup(); break; - case IMAGE_OK: - Timber.d("Image is OK. Proceeding"); default: - Timber.d("Default: Setting title and desc; Show next uploaded item"); - setTitleAndDescription(title, descriptions); - nextUploadedItem(); + String errorMessageForResult = getErrorMessageForResult(context, errorCode); + if (StringUtils.isNullOrWhiteSpace(errorMessageForResult)) { + return; + } + view.showBadPicturePopup(errorMessageForResult); } } @@ -161,10 +177,7 @@ public class UploadPresenter { Timber.d("Trying to show next uploaded item"); uploadModel.next(); updateContent(); - if (uploadModel.isShowingItem()) { - Timber.d("Is showing item is true"); - uploadModel.subscribeBadPicture(this::handleBadPicture); - } + uploadModel.subscribeBadPicture(this::handleBadImage, false); view.dismissKeyboard(); } @@ -173,31 +186,9 @@ public class UploadPresenter { uploadModel.setCurrentTitleAndDescriptions(title, descriptions); } - private Title getCurrentImageTitle() { - return getCurrentItem().title; - } - String getCurrentImageFileName() { UploadItem currentItem = getCurrentItem(); - return currentItem.title + "." + uploadModel.getCurrentItem().fileExt; - } - - @SuppressLint("CheckResult") - private Observable validateCurrentItemTitle() { - Title title = getCurrentImageTitle(); - if (title.isEmpty()) { - view.showErrorMessage(R.string.add_title_toast); - return Observable.just(EMPTY_TITLE); - } - - return Observable.fromCallable(() -> mediaWikiApi.fileExistsWithName(getCurrentImageFileName())) - .subscribeOn(Schedulers.io()) - .map(doesFileExist -> { - if (doesFileExist) { - return FILE_NAME_EXISTS; - } - return IMAGE_OK; - }); + return currentItem.getFileName(); } /** @@ -206,9 +197,7 @@ public class UploadPresenter { void handlePrevious() { uploadModel.previous(); updateContent(); - if (uploadModel.isShowingItem()) { - uploadModel.subscribeBadPicture(this::handleBadPicture); - } + uploadModel.subscribeBadPicture(this::handleBadImage, false); view.dismissKeyboard(); } @@ -235,22 +224,12 @@ public class UploadPresenter { * Called by the map button on the right card in {@link UploadActivity} */ void openCoordinateMap() { - GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords; + GPSExtractor gpsObj = uploadModel.getCurrentItem().getGpsCoords(); if (gpsObj != null && gpsObj.imageCoordsExists) { view.launchMapActivity(gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude()); } } - - /** - * Called by the image processors when a result is obtained. - * - * @param result the result returned by the image procesors. - */ - private void handleBadPicture(@ImageUtils.Result int result) { - view.showBadPicturePopup(result); - } - void keepPicture() { uploadModel.keepPicture(); } @@ -262,8 +241,7 @@ public class UploadPresenter { uploadModel.deletePicture(); updateCards(); updateContent(); - if (uploadModel.isShowingItem()) - uploadModel.subscribeBadPicture(this::handleBadPicture); + uploadModel.subscribeBadPicture(this::handleBadImage, false); view.dismissKeyboard(); } } @@ -367,7 +345,7 @@ public class UploadPresenter { view.setPreviousEnabled(uploadModel.isPreviousAvailable()); view.setSubmitEnabled(uploadModel.isSubmitAvailable()); - view.setBackground(uploadModel.getCurrentItem().mediaUri); + view.setBackground(uploadModel.getCurrentItem().getMediaUri()); view.updateBottomCardContent(uploadModel.getCurrentStep(), uploadModel.getStepCount(), @@ -376,7 +354,7 @@ public class UploadPresenter { view.updateTopCardContent(); - GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords; + GPSExtractor gpsObj = uploadModel.getCurrentItem().getGpsCoords(); view.updateRightCardContent(gpsObj != null && gpsObj.imageCoordsExists); view.updateSubtitleVisibility(uploadModel.getCount()); @@ -421,8 +399,8 @@ public class UploadPresenter { List getImageTitleList() { List titleList = new ArrayList<>(); for (UploadItem item : uploadModel.getUploads()) { - if (item.title.isSet()) { - titleList.add(item.title.toString()); + if (item.getTitle().isSet()) { + titleList.add(item.getTitle().toString()); } } return titleList; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index c4ec36c2e..efc26bcba 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -8,18 +8,14 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.content.Intent; import android.graphics.BitmapFactory; +import android.net.Uri; import android.os.Bundle; import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.webkit.MimeTypeMap; import android.widget.Toast; -import com.j256.simplemagic.ContentInfo; -import com.j256.simplemagic.ContentInfoUtil; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashSet; @@ -33,12 +29,11 @@ import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionDao; -import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.contributions.ContributionsContentProvider; +import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.UploadResult; import fr.free.nrw.commons.wikidata.WikidataEditService; @@ -52,6 +47,7 @@ public class UploadService extends HandlerService { public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload"; public static final String EXTRA_SOURCE = EXTRA_PREFIX + ".source"; + public static final String EXTRA_FILES = EXTRA_PREFIX + ".files"; public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign"; @Inject MediaWikiApi mwApi; @@ -64,7 +60,7 @@ public class UploadService extends HandlerService { private int toUpload; /** - * The file names of unfinished uploads, used to prevent overwriting + * The filePath names of unfinished uploads, used to prevent overwriting */ private Set unfinishedUploads = new HashSet<>(); @@ -74,7 +70,6 @@ public class UploadService extends HandlerService { public static final int NOTIFICATION_UPLOAD_IN_PROGRESS = 1; public static final int NOTIFICATION_UPLOAD_COMPLETE = 2; public static final int NOTIFICATION_UPLOAD_FAILED = 3; - private ContentInfoUtil contentInfoUtil; public UploadService() { super("UploadService"); @@ -201,41 +196,22 @@ public class UploadService extends HandlerService { } private void uploadContribution(Contribution contribution) { - InputStream fileInputStream = null; - InputStream tempFileInputStream = null; - ContentInfo contentInfo = null; - String notificationTag = contribution.getLocalUri().toString(); + InputStream fileInputStream; + Uri localUri = contribution.getLocalUri(); + if (localUri == null || localUri.getPath() == null) { + Timber.d("localUri/path is null"); + return; + } + String notificationTag = localUri.toString(); try { - File file1 = new File(contribution.getLocalUri().getPath()); + File file1 = new File(localUri.getPath()); fileInputStream = new FileInputStream(file1); - tempFileInputStream = new FileInputStream(file1); - if (contentInfoUtil == null) { - contentInfoUtil = new ContentInfoUtil(); - } - contentInfo = contentInfoUtil.findMatch(tempFileInputStream); } catch (FileNotFoundException e) { Timber.d("File not found"); Toast fileNotFound = Toast.makeText(this, R.string.upload_failed, Toast.LENGTH_LONG); fileNotFound.show(); return; - } catch (IOException e) { - Timber.d("exception while fetching MIME type: "+e); - } finally { - try { - if (null != tempFileInputStream) { - tempFileInputStream.close(); - } - } catch (IOException e) { - Timber.d("File not found"); - } - } - - //As the fileInputStream is null there's no point in continuing the upload process - //mwapi.upload accepts a NonNull input stream - if (fileInputStream == null) { - Timber.d("File not found"); - return; } Timber.d("Before execution!"); @@ -244,20 +220,8 @@ public class UploadService extends HandlerService { CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL); this.startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build()); - String filename = null; + String filename = contribution.getFilename(); try { - //try to fetch the MIME type from contentInfo first and then use the tag to do it - //Note : the tag has not proven trustworthy in the past - String mimeType; - if (contentInfo == null || contentInfo.getMimeType() == null) { - mimeType = (String) contribution.getTag("mimeType"); - } else { - mimeType = contentInfo.getMimeType(); - } - filename = Utils.fixExtension( - contribution.getFilename(), - MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)); - synchronized (unfinishedUploads) { Timber.d("making sure of uniqueness of name: %s", filename); filename = findUniqueFilename(filename); @@ -282,7 +246,7 @@ public class UploadService extends HandlerService { contribution ); UploadResult uploadResult = mwApi.uploadFile(filename, fileInputStream, contribution.getDataLength(), - contribution.getPageContents(getApplicationContext()), contribution.getEditSummary(), contribution.getLocalUri(), contribution.getContentProviderUri(), notificationUpdater); + contribution.getPageContents(getApplicationContext()), contribution.getEditSummary(), localUri, contribution.getContentProviderUri(), notificationUpdater); Timber.d("Response is %s", uploadResult.toString()); @@ -341,7 +305,7 @@ public class UploadService extends HandlerService { sequenceFileName = fileName; } else { if (fileName.indexOf('.') == -1) { - // We really should have appended a file type suffix already. + // We really should have appended a filePath type suffix already. // But... we might not. sequenceFileName = fileName + " " + sequenceNumber; } else { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java index 485cb1add..afcc42aed 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java @@ -43,11 +43,11 @@ class UploadThumbnailRenderer extends Renderer { @Override public void render() { UploadModel.UploadItem content = getContent(); - Uri uri = Uri.parse(content.mediaUri.toString()); + Uri uri = Uri.parse(content.getMediaUri().toString()); background.setImageURI(Uri.fromFile(new File(String.valueOf(uri)))); - background.setAlpha(content.selected ? 1.0f : 0.5f); - space.setVisibility(content.first ? View.VISIBLE : View.GONE); - error.setVisibility(content.visited && content.error ? View.VISIBLE : View.GONE); + background.setAlpha(content.isSelected() ? 1.0f : 0.5f); + space.setVisibility(content.isFirst() ? View.VISIBLE : View.GONE); + error.setVisibility(content.isVisited() && content.isError() ? View.VISIBLE : View.GONE); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java index 0dddaf788..19c27119b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java @@ -68,7 +68,7 @@ public interface UploadView { void dismissKeyboard(); - void showBadPicturePopup(@ImageUtils.Result int errorMessage); + void showBadPicturePopup(String errorMessage); void showDuplicatePicturePopup(); @@ -81,4 +81,8 @@ public interface UploadView { void initDefaultCategories(); void showNoCategorySelectedWarning(); + + void showProgressDialog(); + + void hideProgressDialog(); } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java index 620f52dcb..1ff4839bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java @@ -10,7 +10,6 @@ import android.net.Uri; import android.support.annotation.IntDef; import android.support.annotation.Nullable; -import com.esafirm.imagepicker.model.Image; import com.facebook.common.executors.CallerThreadExecutor; import com.facebook.common.references.CloseableReference; import com.facebook.datasource.DataSource; @@ -24,8 +23,6 @@ import com.facebook.imagepipeline.request.ImageRequestBuilder; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.List; import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; @@ -37,9 +34,9 @@ import timber.log.Timber; public class ImageUtils { - static final int IMAGE_DARK = 1; + public static final int IMAGE_DARK = 1; static final int IMAGE_BLURRY = 1 << 1; - public static final int IMAGE_DUPLICATE = 1 << 2; + public static final int IMAGE_DUPLICATE = 1 << 2; //4 public static final int IMAGE_GEOLOCATION_DIFFERENT = 1 << 3; public static final int IMAGE_OK = 0; public static final int IMAGE_KEEP = -1; @@ -251,15 +248,4 @@ public class ImageUtils { return errorMessage.toString(); } - - public static ArrayList getUriListFromImages(List imageList) { - ArrayList uriList = new ArrayList<>(); - for (Image imagePath : imageList) { - if (!StringUtils.isNullOrWhiteSpace(imagePath.getPath())) { - uriList.add(Uri.parse(imagePath.getPath())); - } - } - - return uriList; - } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/IntentUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/IntentUtils.java deleted file mode 100644 index 664300b3e..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/IntentUtils.java +++ /dev/null @@ -1,33 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.app.Activity; -import android.content.Intent; - -import static fr.free.nrw.commons.contributions.ContributionController.BOOKMARK_CAMERA_UPLOAD_REQUEST_CODE; -import static fr.free.nrw.commons.contributions.ContributionController.BOOKMARK_GALLERY_UPLOAD_REQUEST_CODE; -import static fr.free.nrw.commons.contributions.ContributionController.CAMERA_UPLOAD_REQUEST_CODE; -import static fr.free.nrw.commons.contributions.ContributionController.GALLERY_UPLOAD_REQUEST_CODE; -import static fr.free.nrw.commons.contributions.ContributionController.NEARBY_CAMERA_UPLOAD_REQUEST_CODE; -import static fr.free.nrw.commons.contributions.ContributionController.NEARBY_GALLERY_UPLOAD_REQUEST_CODE; - -public class IntentUtils { - - /** - * Check if the intent should be handled by contributions list fragment - */ - public static boolean shouldContributionsHandle(int requestCode, int resultCode, Intent data) { - return resultCode == Activity.RESULT_OK - && (requestCode == GALLERY_UPLOAD_REQUEST_CODE || requestCode == CAMERA_UPLOAD_REQUEST_CODE - || requestCode == NEARBY_CAMERA_UPLOAD_REQUEST_CODE || requestCode == NEARBY_GALLERY_UPLOAD_REQUEST_CODE) - && data != null; - } - - /** - * Check if the intent should be handled by contributions list fragment - */ - public static boolean shouldBookmarksHandle(int requestCode, int resultCode, Intent data) { - return resultCode == Activity.RESULT_OK - && (requestCode == BOOKMARK_CAMERA_UPLOAD_REQUEST_CODE || requestCode == BOOKMARK_GALLERY_UPLOAD_REQUEST_CODE) - && data != null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java index eaa779744..fe7a183df 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java @@ -92,7 +92,9 @@ public class WikidataEditService { private void handleClaimResult(String wikidataEntityId, String revisionId) { if (revisionId != null) { - wikidataEditListener.onSuccessfulWikidataEdit(); + if (wikidataEditListener != null) { + wikidataEditListener.onSuccessfulWikidataEdit(); + } showSuccessToast(); logEdit(revisionId); } else { diff --git a/app/src/main/res/layout/activity_upload_bottom_card.xml b/app/src/main/res/layout/activity_upload_bottom_card.xml index 0107f25c2..5d529335a 100644 --- a/app/src/main/res/layout/activity_upload_bottom_card.xml +++ b/app/src/main/res/layout/activity_upload_bottom_card.xml @@ -144,6 +144,8 @@ android:layout_height="wrap_content" android:gravity="center_vertical" android:textSize="@dimen/subtitle_text" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/bottom_card_title" tools:text="1st image" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7ac904783..4ab1866f4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -463,4 +463,8 @@ Upload your first media by touching the camera or gallery icon above. You have no unread Notification Share logs using + Error occurred while picking images + Choose Images to upload + + Please wait… diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml index 97ce9c0eb..9d466c397 100644 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionDaoTest.kt index 124886b74..b414b04a7 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionDaoTest.kt @@ -176,7 +176,7 @@ class ContributionDaoTest { @Test fun saveNewContribution_nullableImageUrlUsesFileAsBackup() { whenever(client.insert(isA(), isA())).thenReturn(contentUri) - val contribution = createContribution(true, null, null, null, "file") + val contribution = createContribution(true, null, null, null, "filePath") testObject.save(contribution) @@ -186,7 +186,7 @@ class ContributionDaoTest { // Nullable fields are absent if null assertFalse(it.containsKey(Table.COLUMN_LOCAL_URI)) assertFalse(it.containsKey(Table.COLUMN_UPLOADED)) - assertEquals(Utils.makeThumbBaseUrl("file"), it.getAsString(Table.COLUMN_IMAGE_URL)) + assertEquals(Utils.makeThumbBaseUrl("filePath"), it.getAsString(Table.COLUMN_IMAGE_URL)) } } @@ -285,7 +285,7 @@ class ContributionDaoTest { createCursor(created, uploaded, false, localUri).let { mc -> testObject.fromCursor(mc).let { assertEquals(uriForId(111), it.contentUri) - assertEquals("file", it.filename) + assertEquals("filePath", it.filename) assertEquals(localUri, it.localUri.toString()) assertEquals("image", it.imageUrl) assertEquals(created, it.dateCreated.time) @@ -335,7 +335,7 @@ class ContributionDaoTest { private fun createCursor(created: Long, uploaded: Long, multiple: Boolean, localUri: String) = MatrixCursor(Table.ALL_FIELDS, 1).apply { - addRow(listOf("111", "file", localUri, "image", + addRow(listOf("111", "filePath", localUri, "image", created, STATE_QUEUED, 222L, uploaded, 88L, SOURCE_GALLERY, "desc", "create", if (multiple) 1 else 0, 640, 480, "007", "Q1")) moveToFirst() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt new file mode 100644 index 000000000..4bdda3ddd --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt @@ -0,0 +1,137 @@ +package fr.free.nrw.commons.upload + +import android.graphics.BitmapRegionDecoder +import android.net.Uri +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.mwapi.MediaWikiApi +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.utils.BitmapRegionDecoderWrapper +import fr.free.nrw.commons.utils.ImageUtils +import fr.free.nrw.commons.utils.ImageUtilsWrapper +import io.reactivex.Single +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations +import java.io.FileInputStream + +class ImageProcessingServiceTest { + @Mock + internal var fileUtilsWrapper: FileUtilsWrapper? = null + @Mock + internal var bitmapRegionDecoderWrapper: BitmapRegionDecoderWrapper? = null + @Mock + internal var imageUtilsWrapper: ImageUtilsWrapper? = null + @Mock + internal var mwApi: MediaWikiApi? = null + + @InjectMocks + var imageProcessingService: ImageProcessingService? = null + + @Mock + internal lateinit var uploadItem: UploadModel.UploadItem + + @Before + @Throws(Exception::class) + fun setUp() { + MockitoAnnotations.initMocks(this) + val mediaUri = mock(Uri::class.java) + val mockPlace = mock(Place::class.java) + val mockTitle = mock(Title::class.java) + + `when`(mockPlace.wikiDataEntityId).thenReturn("Q1") + `when`(mockPlace.getLocation()).thenReturn(mock(LatLng::class.java)) + `when`(mediaUri.path).thenReturn("filePath") + `when`(mockTitle.isEmpty).thenReturn(false) + `when`(mockTitle.isSet).thenReturn(true) + + `when`(uploadItem.mediaUri).thenReturn(mediaUri) + `when`(uploadItem.imageQuality).thenReturn(ImageUtils.IMAGE_WAIT) + + `when`(uploadItem.title).thenReturn(mockTitle) + + `when`(uploadItem.place).thenReturn(mockPlace) + + `when`(fileUtilsWrapper!!.getFileInputStream(ArgumentMatchers.anyString())) + .thenReturn(mock(FileInputStream::class.java)) + `when`(fileUtilsWrapper!!.getSHA1(any(FileInputStream::class.java))) + .thenReturn("fileSha") + + `when`(fileUtilsWrapper!!.getGeolocationOfFile(ArgumentMatchers.anyString())) + .thenReturn("latLng") + + `when`(bitmapRegionDecoderWrapper!!.newInstance(any(FileInputStream::class.java), anyBoolean())) + .thenReturn(mock(BitmapRegionDecoder::class.java)) + `when`(imageUtilsWrapper!!.checkIfImageIsTooDark(any(BitmapRegionDecoder::class.java))) + .thenReturn(Single.just(ImageUtils.IMAGE_OK)) + + `when`(imageUtilsWrapper!!.checkImageGeolocationIsDifferent(ArgumentMatchers.anyString(), any(LatLng::class.java))) + .thenReturn(Single.just(ImageUtils.IMAGE_OK)) + + `when`(fileUtilsWrapper!!.getFileInputStream(ArgumentMatchers.anyString())) + .thenReturn(mock(FileInputStream::class.java)) + `when`(fileUtilsWrapper!!.getSHA1(any(FileInputStream::class.java))) + .thenReturn("fileSha") + `when`(mwApi!!.existingFile(ArgumentMatchers.anyString())) + .thenReturn(false) + `when`(mwApi!!.fileExistsWithName(ArgumentMatchers.anyString())) + .thenReturn(false) + } + + @Test + fun validateImageForKeepImage() { + `when`(uploadItem.imageQuality).thenReturn(ImageUtils.IMAGE_KEEP) + val validateImage = imageProcessingService!!.validateImage(uploadItem, false) + assertEquals(ImageUtils.IMAGE_OK, validateImage.blockingGet()) + } + + @Test + fun validateImageForDuplicateImage() { + `when`(mwApi!!.existingFile(ArgumentMatchers.anyString())) + .thenReturn(true) + val validateImage = imageProcessingService!!.validateImage(uploadItem, false) + assertEquals(ImageUtils.IMAGE_DUPLICATE, validateImage.blockingGet()) + } + + @Test + fun validateImageForOkImage() { + val validateImage = imageProcessingService!!.validateImage(uploadItem, false) + assertEquals(ImageUtils.IMAGE_OK, validateImage.blockingGet()) + } + + @Test + fun validateImageForDarkImage() { + `when`(imageUtilsWrapper!!.checkIfImageIsTooDark(any(BitmapRegionDecoder::class.java))) + .thenReturn(Single.just(ImageUtils.IMAGE_DARK)) + val validateImage = imageProcessingService!!.validateImage(uploadItem, false) + assertEquals(ImageUtils.IMAGE_DARK, validateImage.blockingGet()) + } + + @Test + fun validateImageForWrongGeoLocation() { + `when`(imageUtilsWrapper!!.checkImageGeolocationIsDifferent(ArgumentMatchers.anyString(), any(LatLng::class.java))) + .thenReturn(Single.just(ImageUtils.IMAGE_GEOLOCATION_DIFFERENT)) + val validateImage = imageProcessingService!!.validateImage(uploadItem, false) + assertEquals(ImageUtils.IMAGE_GEOLOCATION_DIFFERENT, validateImage.blockingGet()) + } + + @Test + fun validateImageForFileNameExistsWithCheckTitleOff() { + `when`(mwApi!!.fileExistsWithName(ArgumentMatchers.anyString())) + .thenReturn(true) + val validateImage = imageProcessingService!!.validateImage(uploadItem, false) + assertEquals(ImageUtils.IMAGE_OK, validateImage.blockingGet()) + } + + @Test + fun validateImageForFileNameExistsWithCheckTitleOn() { + `when`(mwApi!!.fileExistsWithName(ArgumentMatchers.nullable(String::class.java))) + .thenReturn(true) + val validateImage = imageProcessingService!!.validateImage(uploadItem, true) + assertEquals(ImageUtils.FILE_NAME_EXISTS, validateImage.blockingGet()) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt index 52e3290fd..94473dcd2 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt @@ -2,8 +2,8 @@ package fr.free.nrw.commons.upload import android.app.Application import android.content.Context -import android.net.Uri import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.contributions.UploadableFile import fr.free.nrw.commons.kvstore.BasicKvStore import fr.free.nrw.commons.mwapi.MediaWikiApi import fr.free.nrw.commons.nearby.Place @@ -14,8 +14,7 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.anyString +import org.mockito.ArgumentMatchers.* import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.Mockito.`when` @@ -70,10 +69,8 @@ class UploadModelTest { .thenReturn(mock(FileInputStream::class.java)) `when`(fileUtilsWrapper!!.getGeolocationOfFile(anyString())) .thenReturn("") - `when`(imageProcessingService!!.checkImageQuality(anyString())) - .thenReturn(Single.just(IMAGE_OK)) - `when`(imageProcessingService!!.checkImageQuality(any(Place::class.java), anyString())) - .thenReturn(Single.just(IMAGE_OK)) + `when`(imageProcessingService!!.validateImage(any(UploadModel.UploadItem::class.java), anyBoolean())) + .thenReturn(Single.just(IMAGE_OK)); } @@ -84,10 +81,7 @@ class UploadModelTest { @Test fun receive() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - val preProcessImages = uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + val preProcessImages = uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } preProcessImages.doOnComplete { assertTrue(uploadModel!!.items.size == 2) } @@ -95,46 +89,31 @@ class UploadModelTest { @Test fun verifyPreviousNotAvailable() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } assertFalse(uploadModel!!.isPreviousAvailable) } @Test fun verifyNextAvailable() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } assertTrue(uploadModel!!.isNextAvailable) } @Test fun isSubmitAvailable() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } assertTrue(uploadModel!!.isNextAvailable) } @Test fun getCurrentStep() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } assertTrue(uploadModel!!.currentStep == 1) } @Test fun getStepCount() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - val preProcessImages = uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + val preProcessImages = uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } preProcessImages.doOnComplete { assertTrue(uploadModel!!.stepCount == 4) } @@ -142,10 +121,7 @@ class UploadModelTest { @Test fun getCount() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - val preProcessImages = uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + val preProcessImages = uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } preProcessImages.doOnComplete { assertTrue(uploadModel!!.count == 2) } @@ -153,10 +129,7 @@ class UploadModelTest { @Test fun getUploads() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - val preProcessImages = uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + val preProcessImages = uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } preProcessImages.doOnComplete { assertTrue(uploadModel!!.uploads.size == 2) } @@ -164,19 +137,13 @@ class UploadModelTest { @Test fun isTopCardState() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } assertTrue(uploadModel!!.isTopCardState) } @Test fun next() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } assertTrue(uploadModel!!.currentStep == 1) uploadModel!!.next() assertTrue(uploadModel!!.currentStep == 2) @@ -184,10 +151,7 @@ class UploadModelTest { @Test fun previous() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } assertTrue(uploadModel!!.currentStep == 1) uploadModel!!.next() assertTrue(uploadModel!!.currentStep == 2) @@ -197,18 +161,22 @@ class UploadModelTest { @Test fun isShowingItem() { - val element = getElement() - val element2 = getElement() - var uriList: List = mutableListOf(element, element2) - val preProcessImages = uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> } + val preProcessImages = uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } preProcessImages.doOnComplete { assertTrue(uploadModel!!.isShowingItem) } } - private fun getElement(): Uri { - val mock = mock(Uri::class.java) - `when`(mock.path).thenReturn(UUID.randomUUID().toString() + "/file.jpg") + private fun getMediaList(): List { + val element = getElement() + val element2 = getElement() + var uriList: List = mutableListOf(element, element2) + return uriList + } + + private fun getElement(): UploadableFile { + val mock = mock(UploadableFile::class.java) + `when`(mock.filePath).thenReturn(UUID.randomUUID().toString() + "/filePath.jpg") return mock } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt index f10e19664..64d5af976 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt @@ -1,6 +1,6 @@ package fr.free.nrw.commons.upload -import android.net.Uri +import fr.free.nrw.commons.contributions.UploadableFile import fr.free.nrw.commons.mwapi.MediaWikiApi import fr.free.nrw.commons.nearby.Place import io.reactivex.Observable @@ -26,8 +26,7 @@ class UploadPresenterTest { @Throws(Exception::class) fun setUp() { MockitoAnnotations.initMocks(this) - `when`(uploadModel!!.preProcessImages(ArgumentMatchers.anyListOf(Uri::class.java), - ArgumentMatchers.anyString(), + `when`(uploadModel!!.preProcessImages(ArgumentMatchers.anyListOf(UploadableFile::class.java), ArgumentMatchers.any(Place::class.java), ArgumentMatchers.anyString(), ArgumentMatchers.any(SimilarImageInterface::class.java))) @@ -36,9 +35,9 @@ class UploadPresenterTest { @Test fun receiveMultipleItems() { - val element = Mockito.mock(Uri::class.java) - val element2 = Mockito.mock(Uri::class.java) - var uriList: List = mutableListOf(element, element2) - uploadPresenter!!.receive(uriList, "image/jpeg", "external", mock(Place::class.java)) + val element = Mockito.mock(UploadableFile::class.java) + val element2 = Mockito.mock(UploadableFile::class.java) + var uriList: List = mutableListOf(element, element2) + uploadPresenter!!.receive(uriList, "external", mock(Place::class.java)) } } \ No newline at end of file