Fix app stuck and memory issues while uploading images (#2287)

* Do not use an image array to store all bitmap pixels at once

* Extract image preprocessing to a different service and use computation thread

* Add java docs

* Cleanup code to remove temp file logic

* Add logs in upload flow

* Fix tests

* Fix more tests
This commit is contained in:
Vivek Maskara 2019-01-16 22:02:25 +05:30 committed by neslihanturan
parent 21f82dd346
commit 559127dfa3
21 changed files with 320 additions and 891 deletions

View file

@ -42,7 +42,6 @@ import fr.free.nrw.commons.logging.LogUtils;
import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.upload.FileUtils; import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.ContributionUtils;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
@ -113,9 +112,6 @@ public class CommonsApplication extends Application {
// TODO: Remove when we're able to initialize Fresco in test builds. // TODO: Remove when we're able to initialize Fresco in test builds.
} }
// Empty temp directory in case some temp files are created and never removed.
ContributionUtils.emptyTemporaryDirectory();
if (BuildConfig.DEBUG && !isRoboUnitTest()) { if (BuildConfig.DEBUG && !isRoboUnitTest()) {
Stetho.initializeWithDefaults(this); Stetho.initializeWithDefaults(this);
} }

View file

@ -421,7 +421,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
} }
public void startMainActivity() { public void startMainActivity() {
NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP);
finish(); finish();
} }

View file

@ -1,11 +1,11 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.Manifest; import android.Manifest;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.support.v4.app.Fragment;
import com.esafirm.imagepicker.features.ImagePicker; import com.esafirm.imagepicker.features.ImagePicker;
@ -59,40 +59,40 @@ public class ContributionController {
this.directKvStore = directKvStore; this.directKvStore = directKvStore;
} }
public void initiateCameraPick(Fragment fragment, public void initiateCameraPick(Activity activity,
int requestCode) { int requestCode) {
boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true); boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true);
if (!useExtStorage) { if (!useExtStorage) {
initiateCameraUpload(fragment, requestCode); initiateCameraUpload(activity, requestCode);
return; return;
} }
PermissionUtils.checkPermissionsAndPerformAction(fragment.getActivity(), PermissionUtils.checkPermissionsAndPerformAction(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE,
() -> initiateCameraUpload(fragment, requestCode), () -> initiateCameraUpload(activity, requestCode),
R.string.storage_permission_title, R.string.storage_permission_title,
R.string.write_storage_permission_rationale); R.string.write_storage_permission_rationale);
} }
public void initiateGalleryPick(Fragment fragment, public void initiateGalleryPick(Activity activity,
int imageLimit, int imageLimit,
int requestCode) { int requestCode) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN) {
initiateGalleryUpload(fragment, imageLimit, requestCode); initiateGalleryUpload(activity, imageLimit, requestCode);
} else { } else {
PermissionUtils.checkPermissionsAndPerformAction(fragment.getActivity(), PermissionUtils.checkPermissionsAndPerformAction(activity,
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE,
() -> initiateGalleryUpload(fragment, imageLimit, requestCode), () -> initiateGalleryUpload(activity, imageLimit, requestCode),
R.string.storage_permission_title, R.string.storage_permission_title,
R.string.read_storage_permission_rationale); R.string.read_storage_permission_rationale);
} }
} }
private void initiateGalleryUpload(Fragment fragment, private void initiateGalleryUpload(Activity activity,
int imageLimit, int imageLimit,
int requestCode) { int requestCode) {
ImagePicker imagePicker = ImagePicker.ImagePickerWithFragment ImagePicker imagePicker = ImagePicker.ImagePickerWithFragment
.create(fragment) .create(activity)
.showCamera(false) .showCamera(false)
.folderMode(true) .folderMode(true)
.includeVideo(false) .includeVideo(false)
@ -106,9 +106,9 @@ public class ContributionController {
} }
} }
private void initiateCameraUpload(Fragment fragment, int requestCode) { private void initiateCameraUpload(Activity activity, int requestCode) {
ImagePicker.cameraOnly() ImagePicker.cameraOnly()
.start(fragment, requestCode); .start(activity, requestCode);
} }
public Intent handleImagesPicked(ArrayList<Uri> uriList, int requestCode) { public Intent handleImagesPicked(ArrayList<Uri> uriList, int requestCode) {

View file

@ -16,11 +16,6 @@ import android.widget.ListAdapter;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import com.esafirm.imagepicker.features.ImagePicker;
import com.esafirm.imagepicker.model.Image;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -31,8 +26,6 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.BasicKvStore; import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.IntentUtils;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
@ -104,8 +97,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
private void setListeners() { private void setListeners() {
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
fabCamera.setOnClickListener(view -> controller.initiateCameraPick(this, CAMERA_UPLOAD_REQUEST_CODE)); fabCamera.setOnClickListener(view -> controller.initiateCameraPick(getActivity(), CAMERA_UPLOAD_REQUEST_CODE));
fabGallery.setOnClickListener(view -> controller.initiateGalleryPick(this, MULTIPLE_UPLOAD_IMAGE_LIMIT, GALLERY_UPLOAD_REQUEST_CODE)); fabGallery.setOnClickListener(view -> controller.initiateGalleryPick(getActivity(), MULTIPLE_UPLOAD_IMAGE_LIMIT, GALLERY_UPLOAD_REQUEST_CODE));
} }
private void animateFAB(boolean isFabOpen) { private void animateFAB(boolean isFabOpen) {
@ -135,18 +128,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
parentFragment.waitForContributionsListFragment.countDown(); parentFragment.waitForContributionsListFragment.countDown();
} }
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (IntentUtils.shouldContributionsListHandle(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);
}
}
/** /**
* Responsible to set progress bar invisible and visible * Responsible to set progress bar invisible and visible
* @param isVisible True when contributions list should be hidden. * @param isVisible True when contributions list should be hidden.

View file

@ -17,6 +17,11 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import com.esafirm.imagepicker.features.ImagePicker;
import com.esafirm.imagepicker.model.Image;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -32,6 +37,8 @@ import fr.free.nrw.commons.nearby.NearbyFragment;
import fr.free.nrw.commons.nearby.NearbyNotificationCardView; import fr.free.nrw.commons.nearby.NearbyNotificationCardView;
import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.upload.UploadService; import fr.free.nrw.commons.upload.UploadService;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.IntentUtils;
import timber.log.Timber; import timber.log.Timber;
import static android.content.ContentResolver.requestSync; import static android.content.ContentResolver.requestSync;
@ -41,6 +48,7 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
@Inject @Inject
SessionManager sessionManager; SessionManager sessionManager;
@Inject ContributionController controller;
@BindView(R.id.tab_layout) @BindView(R.id.tab_layout)
TabLayout tabLayout; TabLayout tabLayout;
@BindView(R.id.pager) @BindView(R.id.pager)
@ -438,12 +446,13 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (IntentUtils.shouldContributionsListHandle(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);
ContributionsListFragment contributionsListFragment = }
(ContributionsListFragment) contributionsActivityPagerAdapter
.getItem(0).getChildFragmentManager()
.findFragmentByTag(ContributionsFragment.CONTRIBUTION_LIST_FRAGMENT_TAG);
contributionsListFragment.onActivityResult(requestCode, resultCode, data);
} }
@Override @Override

View file

@ -52,7 +52,6 @@ import fr.free.nrw.commons.category.QueryContinue;
import fr.free.nrw.commons.kvstore.BasicKvStore; import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.notification.Notification; import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationUtils; import fr.free.nrw.commons.notification.NotificationUtils;
import fr.free.nrw.commons.utils.ContributionUtils;
import fr.free.nrw.commons.utils.DateUtils; import fr.free.nrw.commons.utils.DateUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import in.yuvi.http.fluent.Http; import in.yuvi.http.fluent.Http;
@ -910,8 +909,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
} }
return new UploadResult(resultStatus, errorCode); return new UploadResult(resultStatus, errorCode);
} else { } else {
// If success we have to remove file from temp directory
ContributionUtils.removeTemporaryFile(fileUri);
Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename
String imageUrl = result.getString("/api/upload/imageinfo/@url"); String imageUrl = result.getString("/api/upload/imageinfo/@url");

View file

@ -10,8 +10,6 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.esafirm.imagepicker.features.ImagePicker;
import com.esafirm.imagepicker.model.Image;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
@ -31,12 +29,9 @@ import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.kvstore.BasicKvStore; import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.UriDeserializer; import fr.free.nrw.commons.utils.UriDeserializer;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.utils.IntentUtils.shouldNearbyHandle;
public class NearbyListFragment extends DaggerFragment { public class NearbyListFragment extends DaggerFragment {
private Bundle bundleForUpdates; // Carry information from activity about changed nearby places and current location private Bundle bundleForUpdates; // Carry information from activity about changed nearby places and current location
@ -134,17 +129,6 @@ public class NearbyListFragment extends DaggerFragment {
return placeList; return placeList;
} }
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (shouldNearbyHandle(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);
}
}
/** /**
* Sets bundles for updates in map. Ie. user is moved too much so we need to update nearby markers. * Sets bundles for updates in map. Ie. user is moved too much so we need to update nearby markers.
* @param bundleForUpdates includes new calculated nearby places. * @param bundleForUpdates includes new calculated nearby places.

View file

@ -26,8 +26,6 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import com.esafirm.imagepicker.features.ImagePicker;
import com.esafirm.imagepicker.model.Image;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
@ -868,7 +866,7 @@ public class NearbyMapFragment extends DaggerFragment {
if (fabCamera.isShown()) { if (fabCamera.isShown()) {
Timber.d("Camera button tapped. Place: %s", place.toString()); Timber.d("Camera button tapped. Place: %s", place.toString());
storeSharedPrefs(); storeSharedPrefs();
controller.initiateCameraPick(this, NEARBY_CAMERA_UPLOAD_REQUEST_CODE); controller.initiateCameraPick(getActivity(), NEARBY_CAMERA_UPLOAD_REQUEST_CODE);
} }
}); });
@ -876,7 +874,7 @@ public class NearbyMapFragment extends DaggerFragment {
if (fabGallery.isShown()) { if (fabGallery.isShown()) {
Timber.d("Gallery button tapped. Place: %s", place.toString()); Timber.d("Gallery button tapped. Place: %s", place.toString());
storeSharedPrefs(); storeSharedPrefs();
controller.initiateGalleryPick(this, NEARBY_UPLOAD_IMAGE_LIMIT, NEARBY_GALLERY_UPLOAD_REQUEST_CODE); controller.initiateGalleryPick(getActivity(), NEARBY_UPLOAD_IMAGE_LIMIT, NEARBY_GALLERY_UPLOAD_REQUEST_CODE);
} }
}); });
} }
@ -886,17 +884,6 @@ public class NearbyMapFragment extends DaggerFragment {
directKvStore.putJson(PLACE_OBJECT, place); directKvStore.putJson(PLACE_OBJECT, place);
} }
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (IntentUtils.shouldNearbyHandle(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);
}
}
private void openWebView(Uri link) { private void openWebView(Uri link) {
Utils.handleWebUrl(getContext(), link); Utils.handleWebUrl(getContext(), link);
} }

View file

@ -144,7 +144,7 @@ public class PlaceRenderer extends Renderer<Place> {
} else { } else {
Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
storeSharedPrefs(); storeSharedPrefs();
controller.initiateCameraPick(fragment, NEARBY_CAMERA_UPLOAD_REQUEST_CODE); controller.initiateCameraPick(fragment.getActivity(), NEARBY_CAMERA_UPLOAD_REQUEST_CODE);
} }
}); });
@ -164,7 +164,7 @@ public class PlaceRenderer extends Renderer<Place> {
}else { }else {
Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
storeSharedPrefs(); storeSharedPrefs();
controller.initiateGalleryPick(fragment, NEARBY_UPLOAD_IMAGE_LIMIT, NEARBY_GALLERY_UPLOAD_REQUEST_CODE); controller.initiateGalleryPick(fragment.getActivity(), NEARBY_UPLOAD_IMAGE_LIMIT, NEARBY_GALLERY_UPLOAD_REQUEST_CODE);
} }
}); });

View file

@ -18,19 +18,14 @@ import android.support.annotation.Nullable;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.channels.FileChannel;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Date;
import timber.log.Timber; import timber.log.Timber;
@ -94,256 +89,6 @@ public class FileUtils {
} }
} }
static String createCopyPathAndCopy(boolean useExternalStorage,
Uri uri,
ContentResolver contentResolver,
Context context) throws IOException {
return useExternalStorage ? createExternalCopyPathAndCopy(uri, contentResolver) :
createCopyPathAndCopy(uri, context);
}
/**
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
*
* @return path of copy
*/
@Nullable
private static String createExternalCopyPathAndCopy(Uri uri, ContentResolver contentResolver) throws IOException {
ParcelFileDescriptor parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r");
if (parcelFileDescriptor == null) {
return null;
}
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + "." + getFileExt(uri, contentResolver);
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
newFile.mkdir();
FileUtils.copy(fileDescriptor, copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
/**
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
*
* @return path of copy
*/
@Nullable
private static String createCopyPathAndCopy(Uri uri, Context context) throws IOException {
FileDescriptor fileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r").getFileDescriptor();
String copyPath = context.getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + "." + getFileExt(uri, context.getContentResolver());
FileUtils.copy(fileDescriptor, copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
* other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @author paulburke
*/
// Can be safely suppressed, checks for isKitKat before running isDocumentUri
@SuppressLint("NewApi")
@Nullable
public static String getPath(Context context,
Uri uri,
boolean useExternalStorage) {
String returnPath = null;
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
returnPath = Environment.getExternalStorageDirectory() + "/" + split[1];
}
} else if (isDownloadsDocument(uri)) { // DownloadsProvider
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/document"), Long.valueOf(id));
returnPath = getDataColumn(context, contentUri, null, null);
} else if (isMediaDocument(uri)) { // MediaProvider
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
switch (type) {
case "image":
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
break;
case "video":
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
break;
case "audio":
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
break;
default:
break;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[]{
split[1]
};
returnPath = getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
returnPath = getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
returnPath = uri.getPath();
}
if (returnPath == null) {
//fetching path may fail depending on the source URI and all hope is lost
//so we will create and use a copy of the file, which seems to work
String copyPath = null;
try {
ParcelFileDescriptor descriptor
= context.getContentResolver().openFileDescriptor(uri, "r");
if (descriptor != null) {
if (useExternalStorage) {
copyPath = Environment.getExternalStorageDirectory().toString()
+ "/CommonsApp/" + new Date().getTime() + ".jpg";
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
newFile.mkdir();
FileUtils.copy(
descriptor.getFileDescriptor(),
copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
copyPath = context.getCacheDir().getAbsolutePath()
+ "/" + new Date().getTime() + ".jpg";
FileUtils.copy(
descriptor.getFileDescriptor(),
copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
} catch (IOException e) {
Timber.w(e, "Error in file " + copyPath);
return null;
}
} else {
return returnPath;
}
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
@Nullable
private static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = MediaStore.Images.ImageColumns.DATA;
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} catch (IllegalArgumentException e) {
Timber.d(e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
/**
* Check if the URI is owned by the current app.
*/
public static boolean isSelfOwned(Context context, Uri uri) {
return uri.getAuthority().equals(context.getPackageName() + ".provider");
}
/**
* Copy content from source file to destination file.
*
* @param source stream copied from
* @param destination stream copied to
* @throws IOException thrown when failing to read source or opening destination file
*/
public static void copy(@NonNull FileInputStream source, @NonNull FileOutputStream destination)
throws IOException {
FileChannel sourceChannel = source.getChannel();
FileChannel destinationChannel = destination.getChannel();
sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel);
}
/**
* Copy content from source file to destination file.
*
* @param source file descriptor copied from
* @param destination file path copied to
* @throws IOException thrown when failing to read source or opening destination file
*/
private static void copy(@NonNull FileDescriptor source, @NonNull String destination)
throws IOException {
copy(new FileInputStream(source), new FileOutputStream(destination));
}
/** /**
* Read and return the content of a resource file as string. * Read and return the content of a resource file as string.
@ -393,56 +138,6 @@ public class FileUtils {
return deletedAll; return deletedAll;
} }
public static File createAndGetAppLogsFile(String logs) {
try {
File commonsAppDirectory = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
if (!commonsAppDirectory.exists()) {
commonsAppDirectory.mkdir();
}
File logsFile = new File(commonsAppDirectory, "logs.txt");
if (logsFile.exists()) {
//old logs file is useless
logsFile.delete();
}
logsFile.createNewFile();
FileOutputStream outputStream = new FileOutputStream(logsFile);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
outputStreamWriter.append(logs);
outputStreamWriter.close();
outputStream.flush();
outputStream.close();
return logsFile;
} catch (IOException ioe) {
Timber.e(ioe);
return null;
}
}
public static String getFilename(Uri uri, ContentResolver contentResolver) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN)
return "";
String result = null;
if (uri.getScheme().equals("content")) {
try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
}
}
if (result == null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
return result;
}
static String getFileExt(String fileName) { static String getFileExt(String fileName) {
//Default file extension //Default file extension
String extension = ".jpg"; String extension = ".jpg";
@ -454,10 +149,6 @@ public class FileUtils {
return extension; return extension;
} }
private static String getFileExt(Uri uri, ContentResolver contentResolver) {
return getFileExt(getFilename(uri, contentResolver));
}
public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException { public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return new FileInputStream(filePath); return new FileInputStream(filePath);
} }

View file

@ -20,16 +20,6 @@ public class FileUtilsWrapper {
} }
public String createCopyPathAndCopy(boolean useExtStorage,
Uri uri,
ContentResolver contentResolver,
Context context) throws IOException {
return FileUtils.createCopyPathAndCopy(useExtStorage,
uri,
contentResolver,
context);
}
public String getFileExt(String fileName) { public String getFileExt(String fileName) {
return FileUtils.getFileExt(fileName); return FileUtils.getFileExt(fileName);
} }

View file

@ -0,0 +1,97 @@
package fr.free.nrw.commons.upload;
import javax.inject.Inject;
import javax.inject.Singleton;
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 fr.free.nrw.commons.utils.StringUtils;
import io.reactivex.Single;
/**
* Methods for pre-processing images to be uploaded
*/
@Singleton
public class ImageProcessingService {
private final FileUtilsWrapper fileUtilsWrapper;
private final BitmapRegionDecoderWrapper bitmapRegionDecoderWrapper;
private final ImageUtilsWrapper imageUtilsWrapper;
private final MediaWikiApi mwApi;
@Inject
public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper,
BitmapRegionDecoderWrapper bitmapRegionDecoderWrapper,
ImageUtilsWrapper imageUtilsWrapper,
MediaWikiApi mwApi) {
this.fileUtilsWrapper = fileUtilsWrapper;
this.bitmapRegionDecoderWrapper = bitmapRegionDecoderWrapper;
this.imageUtilsWrapper = imageUtilsWrapper;
this.mwApi = mwApi;
}
/**
* Check image quality before upload
* - checks duplicate image
* - checks dark image
*/
public Single<Integer> checkImageQuality(String filePath) {
return checkImageQuality(null, filePath);
}
/**
* Check image quality before upload
* - checks duplicate image
* - checks dark image
* - checks geolocation for image
*/
public Single<Integer> 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);
}
/**
* Checks for duplicate image
* @param filePath file to be checked
* @return IMAGE_DUPLICATE or IMAGE_OK
*/
private Single<Integer> checkDuplicateImage(String filePath) {
return Single.fromCallable(() ->
fileUtilsWrapper.getFileInputStream(filePath))
.map(fileUtilsWrapper::getSHA1)
.map(mwApi::existingFile)
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK);
}
/**
* Checks for dark image
* @param filePath file to be checked
* @return IMAGE_DARK or IMAGE_OK
*/
private Single<Integer> checkDarkImage(String filePath) {
return Single.fromCallable(() ->
fileUtilsWrapper.getFileInputStream(filePath))
.map(file -> bitmapRegionDecoderWrapper.newInstance(file, false))
.flatMap(imageUtilsWrapper::checkIfImageIsTooDark);
}
/**
* Checks for image geolocation
* @param filePath file to be checked
* @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK
*/
private Single<Integer> checkImageGeoLocation(Place place, String filePath) {
if (place == null || StringUtils.isNullOrWhiteSpace(place.getWikiDataEntityId())) {
return Single.just(ImageUtils.IMAGE_OK);
}
return Single.fromCallable(() -> filePath)
.map(fileUtilsWrapper::getGeolocationOfFile)
.flatMap(geoLocation -> imageUtilsWrapper.checkImageGeolocationIsDifferent(geoLocation, place.getLocation()));
}
}

View file

@ -69,10 +69,7 @@ import timber.log.Timber;
import static fr.free.nrw.commons.utils.ImageUtils.Result; import static fr.free.nrw.commons.utils.ImageUtils.Result;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult; import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
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.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 UploadActivity extends AuthenticatedActivity implements UploadView, SimilarImageInterface { public class UploadActivity extends AuthenticatedActivity implements UploadView, SimilarImageInterface {
@Inject MediaWikiApi mwApi; @Inject MediaWikiApi mwApi;
@ -643,12 +640,8 @@ public class UploadActivity extends AuthenticatedActivity implements UploadView,
return; return;
} }
if (intent.hasExtra(PLACE_OBJECT)) {
Place place = intent.getParcelableExtra(PLACE_OBJECT); Place place = intent.getParcelableExtra(PLACE_OBJECT);
presenter.receiveDirect(urisList.get(0), mimeType, source, place); presenter.receive(urisList, mimeType, source, place);
} else {
presenter.receive(urisList, mimeType, source);
}
resetDirectPrefs(); resetDirectPrefs();
} }

View file

@ -7,7 +7,6 @@ import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -20,24 +19,17 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.kvstore.BasicKvStore; import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.BitmapRegionDecoderWrapper;
import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ImageUtilsWrapper;
import fr.free.nrw.commons.utils.StringUtils;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer; import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.BehaviorSubject; import io.reactivex.subjects.BehaviorSubject;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
public class UploadModel { public class UploadModel {
private MediaWikiApi mwApi; private MediaWikiApi mwApi;
@ -67,9 +59,8 @@ public class UploadModel {
private SessionManager sessionManager; private SessionManager sessionManager;
private Uri currentMediaUri; private Uri currentMediaUri;
private FileUtilsWrapper fileUtilsWrapper; private FileUtilsWrapper fileUtilsWrapper;
private ImageUtilsWrapper imageUtilsWrapper;
private BitmapRegionDecoderWrapper bitmapRegionDecoderWrapper;
private FileProcessor fileProcessor; private FileProcessor fileProcessor;
private final ImageProcessingService imageProcessingService;
@Inject @Inject
UploadModel(@Named("licenses") List<String> licenses, UploadModel(@Named("licenses") List<String> licenses,
@ -79,13 +70,10 @@ public class UploadModel {
MediaWikiApi mwApi, MediaWikiApi mwApi,
SessionManager sessionManager, SessionManager sessionManager,
FileUtilsWrapper fileUtilsWrapper, FileUtilsWrapper fileUtilsWrapper,
ImageUtilsWrapper imageUtilsWrapper, FileProcessor fileProcessor, ImageProcessingService imageProcessingService) {
BitmapRegionDecoderWrapper bitmapRegionDecoderWrapper,
FileProcessor fileProcessor) {
this.licenses = licenses; this.licenses = licenses;
this.basicKvStore = basicKvStore; this.basicKvStore = basicKvStore;
this.license = basicKvStore.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); this.license = basicKvStore.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
this.bitmapRegionDecoderWrapper = bitmapRegionDecoderWrapper;
this.licensesByName = licensesByName; this.licensesByName = licensesByName;
this.context = context; this.context = context;
this.mwApi = mwApi; this.mwApi = mwApi;
@ -93,90 +81,53 @@ public class UploadModel {
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
this.fileUtilsWrapper = fileUtilsWrapper; this.fileUtilsWrapper = fileUtilsWrapper;
this.fileProcessor = fileProcessor; this.fileProcessor = fileProcessor;
this.imageUtilsWrapper = imageUtilsWrapper; useExtStorage = this.basicKvStore.getBoolean("useExternalStorage", false);
this.imageProcessingService = imageProcessingService;
useExtStorage = this.basicKvStore.getBoolean("useExternalStorage", false); useExtStorage = this.basicKvStore.getBoolean("useExternalStorage", false);
} }
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
void receive(List<Uri> mediaUri, Observable<UploadItem> preProcessImages(List<Uri> mediaUris,
String mimeType, String mimeType,
Place place,
String source, String source,
SimilarImageInterface similarImageInterface) { SimilarImageInterface similarImageInterface) {
initDefaultValues(); initDefaultValues();
Observable<UploadItem> itemObservable = Observable.fromIterable(mediaUri) return Observable.fromIterable(mediaUris)
.map(media -> { .map(mediaUri -> {
currentMediaUri = media; if (mediaUri == null || mediaUri.getPath() == null) {
return cacheFileUpload(media); return null;
}) }
.map(filePath -> { String filePath = mediaUri.getPath();
long fileCreatedDate = getFileCreatedDate(currentMediaUri); long fileCreatedDate = getFileCreatedDate(currentMediaUri);
Uri uri = Uri.fromFile(new File(filePath)); String fileExt = fileUtilsWrapper.getFileExt(filePath);
GPSExtractor gpsExtractor = fileProcessor.processFileCoordinates(similarImageInterface);
fileProcessor.initFileDetails(filePath, context.getContentResolver()); fileProcessor.initFileDetails(filePath, context.getContentResolver());
UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface), UploadItem item = new UploadItem(mediaUri, mimeType, source, gpsExtractor,
fileUtilsWrapper.getFileExt(filePath), null, fileCreatedDate); fileExt, place.getWikiDataEntityId(), fileCreatedDate);
checkImageQuality(null, null, filePath) imageProcessingService.checkImageQuality(place, filePath)
.observeOn(Schedulers.io()) .subscribeOn(Schedulers.computation())
.subscribe(item.imageQuality::onNext, Timber::e); .subscribe(item.imageQuality::onNext, Timber::e);
return item; return item;
}); });
items = itemObservable.toList().blockingGet();
items.get(0).selected = true;
items.get(0).first = true;
} }
@SuppressLint("CheckResult") void onItemsProcessed(Place place, List<UploadItem> uploadItems) {
void receiveDirect(Uri media, String mimeType, String source, Place place, SimilarImageInterface similarImageInterface) { items = uploadItems;
initDefaultValues(); if (items.isEmpty()) {
long fileCreatedDate = getFileCreatedDate(media); return;
String filePath = this.cacheFileUpload(media); }
Uri uri = Uri.fromFile(new File(filePath)); UploadItem uploadItem = items.get(0);
fileProcessor.initFileDetails(filePath, context.getContentResolver()); uploadItem.selected = true;
UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface), uploadItem.first = true;
fileUtilsWrapper.getFileExt(filePath), place.getWikiDataEntityId(), fileCreatedDate); if (place != null) {
item.title.setTitleText(place.getName()); uploadItem.title.setTitleText(place.getName());
item.descriptions.get(0).setDescriptionText(place.getLongDescription()); uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription());
//TODO figure out if default descriptions in other languages exist //TODO figure out if default descriptions in other languages exist
item.descriptions.get(0).setLanguageCode("en"); uploadItem.descriptions.get(0).setLanguageCode("en");
checkImageQuality(place.getWikiDataEntityId(), place.getLocation(), filePath)
.observeOn(Schedulers.io())
.subscribe(item.imageQuality::onNext, Timber::e);
items.add(item);
items.get(0).selected = true;
items.get(0).first = true;
} }
private Single<Integer> checkImageQuality(String wikiDataEntityId, LatLng latLng, String filePath) {
return Single.zip(
checkDuplicateFile(filePath),
checkImageCoordinates(wikiDataEntityId, latLng, filePath),
checkDarkImage(filePath), //Returns IMAGE_DARK or IMAGE_OK
(dupe, wrongGeo, dark) -> dupe | wrongGeo | dark);
}
private Single<Integer> checkDarkImage(String filePath) {
return Single.fromCallable(() ->
fileUtilsWrapper.getFileInputStream(filePath))
.map(file -> bitmapRegionDecoderWrapper.newInstance(file, false))
.map(imageUtilsWrapper::checkIfImageIsTooDark);
}
private Single<Integer> checkImageCoordinates(String wikiDataEntityId, LatLng latLng, String filePath) {
if (StringUtils.isNullOrWhiteSpace(wikiDataEntityId)) {
return Single.just(IMAGE_OK);
}
return Single.fromCallable(() -> filePath)
.map(fileUtilsWrapper::getGeolocationOfFile)
.map(geoLocation -> imageUtilsWrapper.checkImageGeolocationIsDifferent(geoLocation, latLng))
.map(r -> r ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT : IMAGE_OK);
}
private Single<Integer> checkDuplicateFile(String filePath) {
return Single.fromCallable(() ->
fileUtilsWrapper.getFileInputStream(filePath))
.map(fileUtilsWrapper::getSHA1)
.map(mwApi::existingFile)
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : IMAGE_OK);
} }
private void initDefaultValues() { private void initDefaultValues() {
@ -274,8 +225,10 @@ public class UploadModel {
} }
public void next() { public void next() {
Timber.d("UploadModel:next; Handling next");
if (badImageSubscription != null) if (badImageSubscription != null)
badImageSubscription.dispose(); badImageSubscription.dispose();
Timber.d("UploadModel:next; disposing badImageSubscription");
markCurrentUploadVisited(); markCurrentUploadVisited();
if (currentStepIndex < items.size() + 1) { if (currentStepIndex < items.size() + 1) {
currentStepIndex++; currentStepIndex++;
@ -325,6 +278,7 @@ public class UploadModel {
} }
private void updateItemState() { private void updateItemState() {
Timber.d("Updating item state");
int count = items.size(); int count = items.size();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
UploadItem item = items.get(i); UploadItem item = items.get(i);
@ -334,6 +288,7 @@ public class UploadModel {
} }
private void markCurrentUploadVisited() { private void markCurrentUploadVisited() {
Timber.d("Marking current upload visited");
if (currentStepIndex < items.size() && currentStepIndex >= 0) { if (currentStepIndex < items.size() && currentStepIndex >= 0) {
items.get(currentStepIndex).visited = true; items.get(currentStepIndex).visited = true;
} }
@ -372,29 +327,6 @@ public class UploadModel {
}); });
} }
/**
* Copy files into local storage and return file path
* If somehow copy fails, it returns the original path
* @param media Uri of the file
* @return path of the enw file
*/
private String cacheFileUpload(Uri media) {
String finalFilePath;
try {
String copyFilePath = fileUtilsWrapper.createCopyPathAndCopy(useExtStorage, media, contentResolver, context);
Timber.i("Copied file path is %s", copyFilePath);
finalFilePath = copyFilePath;
} catch (Exception e) {
Timber.w(e, "Error in copying URI %s. Using original file path instead", media.getPath());
finalFilePath = media.getPath();
}
if (StringUtils.isNullOrWhiteSpace(finalFilePath)) {
finalFilePath = media.getPath();
}
return finalFilePath;
}
void keepPicture() { void keepPicture() {
items.get(currentStepIndex).imageQuality.onNext(ImageUtils.IMAGE_KEEP); items.get(currentStepIndex).imageQuality.onNext(ImageUtils.IMAGE_KEEP);
} }

View file

@ -5,7 +5,6 @@ import android.net.Uri;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
@ -20,7 +19,6 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Completable;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
@ -64,10 +62,6 @@ public class UploadPresenter {
this.mediaWikiApi = mediaWikiApi; this.mediaWikiApi = mediaWikiApi;
} }
void receive(Uri mediaUri, String mimeType, String source) {
receive(Collections.singletonList(mediaUri), mimeType, source);
}
/** /**
* Passes the items received to {@link #uploadModel} and displays the items. * Passes the items received to {@link #uploadModel} and displays the items.
* *
@ -76,41 +70,30 @@ public class UploadPresenter {
* @param source File source from {@link Contribution.FileSource} * @param source File source from {@link Contribution.FileSource}
*/ */
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
void receive(List<Uri> media, String mimeType, @Contribution.FileSource String source) { void receive(List<Uri> media,
Completable.fromRunnable(() -> uploadModel.receive(media, mimeType, source, similarImageInterface)) String mimeType,
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
updateCards();
updateLicenses();
updateContent();
if (uploadModel.isShowingItem())
uploadModel.subscribeBadPicture(this::handleBadPicture);
}, Timber::e);
}
/**
* Passes the direct upload item 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 receiveDirect(Uri media, String mimeType,
@Contribution.FileSource String source, @Contribution.FileSource String source,
Place place) { Place place) {
Completable.fromRunnable(() -> uploadModel.receiveDirect(media, mimeType, source, place, similarImageInterface)) Observable<UploadItem> uploadItemObservable = uploadModel
.preProcessImages(media, mimeType, place, source, similarImageInterface);
uploadItemObservable
.toList()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> { .subscribe(uploadItems -> onImagesProcessed(uploadItems, place),
throwable -> Timber.e(throwable, "Error occurred in processing images"));
}
private void onImagesProcessed(List<UploadItem> uploadItems, Place place) {
uploadModel.onItemsProcessed(place, uploadItems);
updateCards(); updateCards();
updateLicenses(); updateLicenses();
updateContent(); updateContent();
if (uploadModel.isShowingItem()) if (uploadModel.isShowingItem())
uploadModel.subscribeBadPicture(this::handleBadPicture); uploadModel.subscribeBadPicture(this::handleBadPicture);
}, Timber::e);
} }
/** /**
* Sets the license to parameter and updates {@link UploadActivity} * Sets the license to parameter and updates {@link UploadActivity}
* *
@ -129,10 +112,12 @@ public class UploadPresenter {
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
void handleNext(Title title, void handleNext(Title title,
List<Description> descriptions) { List<Description> descriptions) {
Timber.e("Inside handleNext");
validateCurrentItemTitle() validateCurrentItemTitle()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(errorCode -> handleImage(errorCode, title, descriptions)); .subscribe(errorCode -> handleImage(errorCode, title, descriptions),
throwable -> Timber.e(throwable, "Error occurred while handling image"));
} }
/** /**
@ -151,33 +136,41 @@ public class UploadPresenter {
private void handleImage(Integer errorCode, Title title, List<Description> descriptions) { private void handleImage(Integer errorCode, Title title, List<Description> descriptions) {
switch (errorCode) { switch (errorCode) {
case EMPTY_TITLE: case EMPTY_TITLE:
Timber.d("Title is empty. Showing toast");
view.showErrorMessage(R.string.add_title_toast); view.showErrorMessage(R.string.add_title_toast);
break; break;
case FILE_NAME_EXISTS: case FILE_NAME_EXISTS:
if(getCurrentItem().imageQuality.getValue().equals(IMAGE_KEEP)) { if(getCurrentItem().imageQuality.getValue().equals(IMAGE_KEEP)) {
Timber.d("Set title and desc; Show next uploaded item");
setTitleAndDescription(title, descriptions); setTitleAndDescription(title, descriptions);
nextUploadedItem(); nextUploadedItem();
} else { } else {
Timber.d("Trying to show duplicate picture popup");
view.showDuplicatePicturePopup(); view.showDuplicatePicturePopup();
} }
break; break;
case IMAGE_OK: case IMAGE_OK:
Timber.d("Image is OK. Proceeding");
default: default:
Timber.d("Default: Setting title and desc; Show next uploaded item");
setTitleAndDescription(title, descriptions); setTitleAndDescription(title, descriptions);
nextUploadedItem(); nextUploadedItem();
} }
} }
private void nextUploadedItem() { private void nextUploadedItem() {
Timber.d("Trying to show next uploaded item");
uploadModel.next(); uploadModel.next();
updateContent(); updateContent();
if (uploadModel.isShowingItem()) { if (uploadModel.isShowingItem()) {
Timber.d("Is showing item is true");
uploadModel.subscribeBadPicture(this::handleBadPicture); uploadModel.subscribeBadPicture(this::handleBadPicture);
} }
view.dismissKeyboard(); view.dismissKeyboard();
} }
private void setTitleAndDescription(Title title, List<Description> descriptions) { private void setTitleAndDescription(Title title, List<Description> descriptions) {
Timber.d("setTitleAndDescription: Setting title and desc");
uploadModel.setCurrentTitleAndDescriptions(title, descriptions); uploadModel.setCurrentTitleAndDescriptions(title, descriptions);
} }

View file

@ -1,155 +0,0 @@
package fr.free.nrw.commons.utils;
import android.content.Context;
import android.net.Uri;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Random;
import timber.log.Timber;
/**
* This class includes utility methods for uploading process of images.
*/
public class ContributionUtils {
private static String TEMP_EXTERNAL_DIRECTORY =
android.os.Environment.getExternalStorageDirectory().getPath()+
File.separatorChar+"UploadingByCommonsApp";
/**
* Saves images temporarily to a fixed folder and use Uri of that file during upload process.
* Otherwise, temporary Uri provided by content provider sometimes points to a null space and
* consequently upload fails. See: issue #1400A and E.
* Not: Saved image will be deleted, our directory will be empty after upload process.
* @return URI of saved image
*/
public static Uri saveFileBeingUploadedTemporarily(Context context, Uri URIfromContentProvider) {
// TODO add exceptions for Google Drive URİ is needed
Uri result = null;
if (checkIfDirectoryExists(TEMP_EXTERNAL_DIRECTORY)) {
String destinationFilename = decideTempDestinationFileName();
result = saveFileFromURI(context, URIfromContentProvider, destinationFilename);
} else { // If directory doesn't exist, create it and recursive call current method to check again
File file = new File(TEMP_EXTERNAL_DIRECTORY);
if (file.mkdirs()) {
Timber.d("saveFileBeingUploadedTemporarily() parameters: URI from Content Provider %s", URIfromContentProvider);
result = saveFileBeingUploadedTemporarily(context, URIfromContentProvider); // If directory is created
} else { //An error occurred to create directory
Timber.e("saveFileBeingUploadedTemporarily() parameters: URI from Content Provider %s", URIfromContentProvider);
}
}
return result;
}
/**
* Removes temp file created during upload
* @param tempFileUri
*/
public static void removeTemporaryFile(Uri tempFileUri) {
//TODO: do I have to notify file system about deletion?
File tempFile = new File(tempFileUri.getPath());
if (tempFile.exists()) {
boolean isDeleted = tempFile.delete();
Timber.e("removeTemporaryFile() parameters: URI tempFileUri %s, deleted status %b", tempFileUri, isDeleted);
}
}
/**
* Creates a temporary directory and returns pathname
* @return
*/
private static String decideTempDestinationFileName() {
int i = 0;
while (new File(TEMP_EXTERNAL_DIRECTORY + File.separatorChar + i + "_tmp").exists()) {
i++;
}
// Use time stamp for file name, so that two temporary file never has same file name
// to prevent previous file reference bug
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
// For multiple uploads, time randomisation should be combined with another random
// parameter, since they created at same time
int multipleUploadRandomParameter = new Random().nextInt(100);
return TEMP_EXTERNAL_DIRECTORY + File.separatorChar + timeStamp + multipleUploadRandomParameter + "_tmp";
}
/**
* Empties files in Temporary Directory
*/
public static void emptyTemporaryDirectory() {
File dir = new File(TEMP_EXTERNAL_DIRECTORY);
if (dir.isDirectory()) {
String[] children = dir.list();
if (children == null) return;
for (String child : children) {
new File(dir, child).delete();
}
}
}
/**
* Saves file from source URI to destination.
* @param sourceUri Uri which points to file to be saved
* @param destinationFilename where file will be located at
* @return Uri points to file saved
*/
private static Uri saveFileFromURI(Context context, Uri sourceUri, String destinationFilename) {
File file = new File(destinationFilename);
if (file.exists()) {
file.delete();
}
InputStream in = null;
OutputStream out = null;
try {
in = context.getContentResolver().openInputStream(sourceUri);
out = new FileOutputStream(new File(destinationFilename));
byte[] buf = new byte[1024];
int length;
while ((length = in.read(buf)) > 0) {
out.write(buf, 0, length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null) out.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (in != null) in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return Uri.parse("file://" + destinationFilename);
}
/**
* Checks if directory exists
* @param pathToCheck path of directory to check
* @return true if directory exists, false otherwise
*/
private static boolean checkIfDirectoryExists(String pathToCheck) {
File dir = new File(pathToCheck);
return dir.exists() && dir.isDirectory();
}
}

View file

@ -128,44 +128,42 @@ public class ImageUtils {
int bitmapHeight = bitmap.getHeight(); int bitmapHeight = bitmap.getHeight();
int allPixelsCount = bitmapWidth * bitmapHeight; int allPixelsCount = bitmapWidth * bitmapHeight;
int[] bitmapPixels = new int[allPixelsCount];
Timber.d("total %s", Integer.toString(allPixelsCount)); Timber.d("total %s", Integer.toString(allPixelsCount));
bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight);
int numberOfBrightPixels = 0; int numberOfBrightPixels = 0;
int numberOfMediumBrightnessPixels = 0; int numberOfMediumBrightnessPixels = 0;
double brightPixelThreshold = 0.025*allPixelsCount; double brightPixelThreshold = 0.025 * allPixelsCount;
double mediumBrightPixelThreshold = 0.3*allPixelsCount; double mediumBrightPixelThreshold = 0.3 * allPixelsCount;
for (int pixel : bitmapPixels) { for (int x = 0; x < bitmapWidth; x++) {
for (int y = 0; y < bitmapHeight; y++) {
int pixel = bitmap.getPixel(x, y);
int r = Color.red(pixel); int r = Color.red(pixel);
int g = Color.green(pixel); int g = Color.green(pixel);
int b = Color.blue(pixel); int b = Color.blue(pixel);
int secondMax = r>g ? r:g; int secondMax = r > g ? r : g;
double max = (secondMax>b ? secondMax:b)/255.0; double max = (secondMax > b ? secondMax : b) / 255.0;
int secondMin = r<g ? r:g; int secondMin = r < g ? r : g;
double min = (secondMin<b ? secondMin:b)/255.0; double min = (secondMin < b ? secondMin : b) / 255.0;
double luminance = ((max+min)/2.0)*100; double luminance = ((max + min) / 2.0) * 100;
int highBrightnessLuminance = 40; int highBrightnessLuminance = 40;
int mediumBrightnessLuminance = 26; int mediumBrightnessLuminance = 26;
if (luminance<highBrightnessLuminance){ if (luminance < highBrightnessLuminance) {
if (luminance>mediumBrightnessLuminance){ if (luminance > mediumBrightnessLuminance) {
numberOfMediumBrightnessPixels++; numberOfMediumBrightnessPixels++;
} }
} } else {
else {
numberOfBrightPixels++; numberOfBrightPixels++;
} }
if (numberOfBrightPixels>=brightPixelThreshold || numberOfMediumBrightnessPixels>=mediumBrightPixelThreshold){ if (numberOfBrightPixels >= brightPixelThreshold || numberOfMediumBrightnessPixels >= mediumBrightPixelThreshold) {
return false; return false;
} }
}
} }
return true; return true;
} }

View file

@ -5,6 +5,8 @@ import android.graphics.BitmapRegionDecoder;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import static fr.free.nrw.commons.utils.ImageUtils.*; import static fr.free.nrw.commons.utils.ImageUtils.*;
@ -17,11 +19,18 @@ public class ImageUtilsWrapper {
} }
public @Result int checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) { public Single<Integer> checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
return ImageUtils.checkIfImageIsTooDark(bitmapRegionDecoder); int isImageDark = ImageUtils.checkIfImageIsTooDark(bitmapRegionDecoder);
return Single.just(isImageDark)
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation());
} }
public boolean checkImageGeolocationIsDifferent(String geolocationOfFileString, LatLng latLng) { public Single<Integer> checkImageGeolocationIsDifferent(String geolocationOfFileString, LatLng latLng) {
return ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng); boolean isImageGeoLocationDifferent = ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng);
return Single.just(isImageGeoLocationDifferent)
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT : ImageUtils.IMAGE_OK);
} }
} }

View file

@ -1,25 +1,21 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import android.app.Application import android.app.Application
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.graphics.BitmapRegionDecoder
import android.net.Uri import android.net.Uri
import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.kvstore.BasicKvStore import fr.free.nrw.commons.kvstore.BasicKvStore
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.mwapi.MediaWikiApi import fr.free.nrw.commons.mwapi.MediaWikiApi
import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.utils.BitmapRegionDecoderWrapper
import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK
import fr.free.nrw.commons.utils.ImageUtilsWrapper import io.reactivex.Single
import org.junit.After import org.junit.After
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers.* import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyString
import org.mockito.InjectMocks import org.mockito.InjectMocks
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
@ -27,6 +23,7 @@ import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named import javax.inject.Named
@ -51,11 +48,9 @@ class UploadModelTest {
@Mock @Mock
internal var fileUtilsWrapper: FileUtilsWrapper? = null internal var fileUtilsWrapper: FileUtilsWrapper? = null
@Mock @Mock
internal var imageUtilsWrapper: ImageUtilsWrapper? = null
@Mock
internal var bitmapRegionDecoderWrapper: BitmapRegionDecoderWrapper? = null
@Mock
internal var fileProcessor: FileProcessor? = null internal var fileProcessor: FileProcessor? = null
@Mock
internal var imageProcessingService: ImageProcessingService? = null
@InjectMocks @InjectMocks
var uploadModel: UploadModel? = null var uploadModel: UploadModel? = null
@ -67,8 +62,6 @@ class UploadModelTest {
`when`(context!!.applicationContext) `when`(context!!.applicationContext)
.thenReturn(mock(Application::class.java)) .thenReturn(mock(Application::class.java))
`when`(fileUtilsWrapper!!.createCopyPathAndCopy(anyBoolean(), any(Uri::class.java), nullable(ContentResolver::class.java), any(Context::class.java)))
.thenReturn("file.jpg")
`when`(fileUtilsWrapper!!.getFileExt(anyString())) `when`(fileUtilsWrapper!!.getFileExt(anyString()))
.thenReturn("jpg") .thenReturn("jpg")
`when`(fileUtilsWrapper!!.getSHA1(any(InputStream::class.java))) `when`(fileUtilsWrapper!!.getSHA1(any(InputStream::class.java)))
@ -77,12 +70,10 @@ class UploadModelTest {
.thenReturn(mock(FileInputStream::class.java)) .thenReturn(mock(FileInputStream::class.java))
`when`(fileUtilsWrapper!!.getGeolocationOfFile(anyString())) `when`(fileUtilsWrapper!!.getGeolocationOfFile(anyString()))
.thenReturn("") .thenReturn("")
`when`(imageUtilsWrapper!!.checkIfImageIsTooDark(any(BitmapRegionDecoder::class.java))) `when`(imageProcessingService!!.checkImageQuality(anyString()))
.thenReturn(IMAGE_OK) .thenReturn(Single.just(IMAGE_OK))
`when`(imageUtilsWrapper!!.checkImageGeolocationIsDifferent(anyString(), any(LatLng::class.java))) `when`(imageProcessingService!!.checkImageQuality(any(Place::class.java), anyString()))
.thenReturn(false) .thenReturn(Single.just(IMAGE_OK))
`when`(bitmapRegionDecoderWrapper!!.newInstance(any(FileInputStream::class.java), anyBoolean()))
.thenReturn(mock(BitmapRegionDecoder::class.java))
} }
@ -93,154 +84,99 @@ class UploadModelTest {
@Test @Test
fun receive() { fun receive() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } val preProcessImages = uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
preProcessImages.doOnComplete {
assertTrue(uploadModel!!.items.size == 2) assertTrue(uploadModel!!.items.size == 2)
} }
@Test
fun receiveDirect() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", mock(Place::class.java)) { _, _ -> }
assertTrue(uploadModel!!.items.size == 1)
}
@Test
fun verifyPreviousNotAvailableForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", mock(Place::class.java)) { _, _ -> }
assertFalse(uploadModel!!.isPreviousAvailable)
}
@Test
fun verifyNextAvailableForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", mock(Place::class.java)) { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable)
} }
@Test @Test
fun verifyPreviousNotAvailable() { fun verifyPreviousNotAvailable() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
assertFalse(uploadModel!!.isPreviousAvailable) assertFalse(uploadModel!!.isPreviousAvailable)
} }
@Test @Test
fun verifyNextAvailable() { fun verifyNextAvailable() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable) assertTrue(uploadModel!!.isNextAvailable)
} }
@Test @Test
fun isSubmitAvailable() { fun isSubmitAvailable() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable) assertTrue(uploadModel!!.isNextAvailable)
} }
@Test
fun isSubmitAvailableForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", mock(Place::class.java)) { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable)
}
@Test
fun getCurrentStepForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", mock(Place::class.java)) { _, _ -> }
assertTrue(uploadModel!!.currentStep == 1)
}
@Test @Test
fun getCurrentStep() { fun getCurrentStep() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.currentStep == 1) assertTrue(uploadModel!!.currentStep == 1)
} }
@Test @Test
fun getStepCount() { fun getStepCount() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } val preProcessImages = uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
preProcessImages.doOnComplete {
assertTrue(uploadModel!!.stepCount == 4) assertTrue(uploadModel!!.stepCount == 4)
} }
@Test
fun getStepCountForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", mock(Place::class.java)) { _, _ -> }
assertTrue(uploadModel!!.stepCount == 3)
}
@Test
fun getDirectCount() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", mock(Place::class.java)) { _, _ -> }
assertTrue(uploadModel!!.count == 1)
} }
@Test @Test
fun getCount() { fun getCount() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } val preProcessImages = uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
preProcessImages.doOnComplete {
assertTrue(uploadModel!!.count == 2) assertTrue(uploadModel!!.count == 2)
} }
}
@Test @Test
fun getUploads() { fun getUploads() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } val preProcessImages = uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
preProcessImages.doOnComplete {
assertTrue(uploadModel!!.uploads.size == 2) assertTrue(uploadModel!!.uploads.size == 2)
} }
@Test
fun getDirectUploads() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", mock(Place::class.java)) { _, _ -> }
assertTrue(uploadModel!!.uploads.size == 1)
} }
@Test @Test
fun isTopCardState() { fun isTopCardState() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.isTopCardState)
}
@Test
fun isTopCardStateForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", mock(Place::class.java)) { _, _ -> }
assertTrue(uploadModel!!.isTopCardState) assertTrue(uploadModel!!.isTopCardState)
} }
@Test @Test
fun next() { fun next() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.currentStep == 1) assertTrue(uploadModel!!.currentStep == 1)
uploadModel!!.next() uploadModel!!.next()
assertTrue(uploadModel!!.currentStep == 2) assertTrue(uploadModel!!.currentStep == 2)
@ -248,10 +184,10 @@ class UploadModelTest {
@Test @Test
fun previous() { fun previous() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.currentStep == 1) assertTrue(uploadModel!!.currentStep == 1)
uploadModel!!.next() uploadModel!!.next()
assertTrue(uploadModel!!.currentStep == 2) assertTrue(uploadModel!!.currentStep == 2)
@ -261,12 +197,20 @@ class UploadModelTest {
@Test @Test
fun isShowingItem() { fun isShowingItem() {
val element = mock(Uri::class.java) val element = getElement()
val element2 = mock(Uri::class.java) val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } val preProcessImages = uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
preProcessImages.doOnComplete {
assertTrue(uploadModel!!.isShowingItem) assertTrue(uploadModel!!.isShowingItem)
} }
}
private fun getElement(): Uri {
val mock = mock(Uri::class.java)
`when`(mock.path).thenReturn(UUID.randomUUID().toString() + "/file.jpg")
return mock
}
@Test @Test
fun buildContributions() { fun buildContributions() {

View file

@ -3,12 +3,12 @@ package fr.free.nrw.commons.upload
import android.net.Uri import android.net.Uri
import fr.free.nrw.commons.mwapi.MediaWikiApi import fr.free.nrw.commons.mwapi.MediaWikiApi
import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.nearby.Place
import io.reactivex.Observable
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.InjectMocks import org.mockito.*
import org.mockito.Mock import org.mockito.Mockito.`when`
import org.mockito.Mockito import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
class UploadPresenterTest { class UploadPresenterTest {
@ -26,6 +26,12 @@ class UploadPresenterTest {
@Throws(Exception::class) @Throws(Exception::class)
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
`when`(uploadModel!!.preProcessImages(ArgumentMatchers.anyListOf(Uri::class.java),
ArgumentMatchers.anyString(),
ArgumentMatchers.any(Place::class.java),
ArgumentMatchers.anyString(),
ArgumentMatchers.any(SimilarImageInterface::class.java)))
.thenReturn(Observable.just(mock(UploadModel.UploadItem::class.java)))
} }
@Test @Test
@ -33,18 +39,6 @@ class UploadPresenterTest {
val element = Mockito.mock(Uri::class.java) val element = Mockito.mock(Uri::class.java)
val element2 = Mockito.mock(Uri::class.java) val element2 = Mockito.mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2) var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadPresenter!!.receive(uriList, "image/jpeg", "external") uploadPresenter!!.receive(uriList, "image/jpeg", "external", mock(Place::class.java))
}
@Test
fun receiveSingleItem() {
val element = Mockito.mock(Uri::class.java)
uploadPresenter!!.receive(element, "image/jpeg", "external")
}
@Test
fun receiveDirect() {
val element = Mockito.mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", Mockito.mock(Place::class.java)) { _, _ -> }
} }
} }

View file

@ -7,17 +7,6 @@ import org.junit.Test
import java.io.* import java.io.*
class FileUtilsTest { class FileUtilsTest {
@Test
fun copiedFileIsIdenticalToSource() {
val source = File.createTempFile("temp", "")
val dest = File.createTempFile("temp", "")
writeToFile(source, "Hello, World")
FileUtils.copy(FileInputStream(source), FileOutputStream(dest))
assertEquals(getString(source), getString(dest))
}
@Test @Test
fun deleteFile() { fun deleteFile() {
val file = File.createTempFile("testfile", "") val file = File.createTempFile("testfile", "")