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
This commit is contained in:
Vivek Maskara 2019-02-04 02:10:31 +05:30 committed by neslihanturan
parent fb5a40bba5
commit 52ab39381e
39 changed files with 1553 additions and 574 deletions

View file

@ -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<Uri> 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<File> imagesFiles, FilePicker.ImageSource source, int type) {
Intent intent = handleImagesPicked(activity, imagesFiles, getSourceFromImageSource(source));
activity.startActivity(intent);
}
});
}
public List<UploadableFile> 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<File> imagesFiles,
String source) {
ArrayList<UploadableFile> 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<UploadableFile> getUploadableFiles(List<File> imagesFiles) {
ArrayList<UploadableFile> 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;
}
}

View file

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

View file

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

View file

@ -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<UploadableFile> CREATOR = new Creator<UploadableFile>() {
@Override
public UploadableFile createFromParcel(Parcel in) {
return new UploadableFile(in);
}
@Override
public UploadableFile[] newArray(int size) {
return new UploadableFile[size];
}
};
private final 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
}
}
}