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.upload.FileUtils;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.ContributionUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
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.
}
// Empty temp directory in case some temp files are created and never removed.
ContributionUtils.emptyTemporaryDirectory();
if (BuildConfig.DEBUG && !isRoboUnitTest()) {
Stetho.initializeWithDefaults(this);
}

View file

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

View file

@ -1,11 +1,11 @@
package fr.free.nrw.commons.contributions;
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.v4.app.Fragment;
import com.esafirm.imagepicker.features.ImagePicker;
@ -59,40 +59,40 @@ public class ContributionController {
this.directKvStore = directKvStore;
}
public void initiateCameraPick(Fragment fragment,
public void initiateCameraPick(Activity activity,
int requestCode) {
boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true);
if (!useExtStorage) {
initiateCameraUpload(fragment, requestCode);
initiateCameraUpload(activity, requestCode);
return;
}
PermissionUtils.checkPermissionsAndPerformAction(fragment.getActivity(),
PermissionUtils.checkPermissionsAndPerformAction(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
() -> initiateCameraUpload(fragment, requestCode),
() -> initiateCameraUpload(activity, requestCode),
R.string.storage_permission_title,
R.string.write_storage_permission_rationale);
}
public void initiateGalleryPick(Fragment fragment,
public void initiateGalleryPick(Activity activity,
int imageLimit,
int requestCode) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN) {
initiateGalleryUpload(fragment, imageLimit, requestCode);
initiateGalleryUpload(activity, imageLimit, requestCode);
} else {
PermissionUtils.checkPermissionsAndPerformAction(fragment.getActivity(),
PermissionUtils.checkPermissionsAndPerformAction(activity,
Manifest.permission.READ_EXTERNAL_STORAGE,
() -> initiateGalleryUpload(fragment, imageLimit, requestCode),
() -> initiateGalleryUpload(activity, imageLimit, requestCode),
R.string.storage_permission_title,
R.string.read_storage_permission_rationale);
}
}
private void initiateGalleryUpload(Fragment fragment,
private void initiateGalleryUpload(Activity activity,
int imageLimit,
int requestCode) {
ImagePicker imagePicker = ImagePicker.ImagePickerWithFragment
.create(fragment)
.create(activity)
.showCamera(false)
.folderMode(true)
.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()
.start(fragment, requestCode);
.start(activity, 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.TextView;
import com.esafirm.imagepicker.features.ImagePicker;
import com.esafirm.imagepicker.model.Image;
import java.util.List;
import javax.inject.Inject;
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.JsonKvStore;
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.VISIBLE;
@ -104,8 +97,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
private void setListeners() {
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
fabCamera.setOnClickListener(view -> controller.initiateCameraPick(this, CAMERA_UPLOAD_REQUEST_CODE));
fabGallery.setOnClickListener(view -> controller.initiateGalleryPick(this, MULTIPLE_UPLOAD_IMAGE_LIMIT, GALLERY_UPLOAD_REQUEST_CODE));
fabCamera.setOnClickListener(view -> controller.initiateCameraPick(getActivity(), CAMERA_UPLOAD_REQUEST_CODE));
fabGallery.setOnClickListener(view -> controller.initiateGalleryPick(getActivity(), MULTIPLE_UPLOAD_IMAGE_LIMIT, GALLERY_UPLOAD_REQUEST_CODE));
}
private void animateFAB(boolean isFabOpen) {
@ -135,18 +128,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
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
* @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.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.Named;
@ -32,6 +37,8 @@ import fr.free.nrw.commons.nearby.NearbyFragment;
import fr.free.nrw.commons.nearby.NearbyNotificationCardView;
import fr.free.nrw.commons.notification.NotificationActivity;
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 static android.content.ContentResolver.requestSync;
@ -41,6 +48,7 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
@Inject
SessionManager sessionManager;
@Inject ContributionController controller;
@BindView(R.id.tab_layout)
TabLayout tabLayout;
@BindView(R.id.pager)
@ -438,12 +446,13 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
@Override
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);
ContributionsListFragment contributionsListFragment =
(ContributionsListFragment) contributionsActivityPagerAdapter
.getItem(0).getChildFragmentManager()
.findFragmentByTag(ContributionsFragment.CONTRIBUTION_LIST_FRAGMENT_TAG);
contributionsListFragment.onActivityResult(requestCode, resultCode, data);
}
}
@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.notification.Notification;
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.ViewUtil;
import in.yuvi.http.fluent.Http;
@ -910,8 +909,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
}
return new UploadResult(resultStatus, errorCode);
} else {
// If success we have to remove file from temp directory
ContributionUtils.removeTemporaryFile(fileUri);
Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename
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.ViewGroup;
import com.esafirm.imagepicker.features.ImagePicker;
import com.esafirm.imagepicker.model.Image;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
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.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.UriDeserializer;
import timber.log.Timber;
import static fr.free.nrw.commons.utils.IntentUtils.shouldNearbyHandle;
public class NearbyListFragment extends DaggerFragment {
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;
}
@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.
* @param bundleForUpdates includes new calculated nearby places.

View file

@ -26,8 +26,6 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar;
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.GsonBuilder;
import com.google.gson.reflect.TypeToken;
@ -868,7 +866,7 @@ public class NearbyMapFragment extends DaggerFragment {
if (fabCamera.isShown()) {
Timber.d("Camera button tapped. Place: %s", place.toString());
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()) {
Timber.d("Gallery button tapped. Place: %s", place.toString());
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);
}
@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) {
Utils.handleWebUrl(getContext(), link);
}

View file

@ -144,7 +144,7 @@ public class PlaceRenderer extends Renderer<Place> {
} else {
Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
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 {
Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
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.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
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.
@ -393,56 +138,6 @@ public class FileUtils {
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) {
//Default file extension
String extension = ".jpg";
@ -454,10 +149,6 @@ public class FileUtils {
return extension;
}
private static String getFileExt(Uri uri, ContentResolver contentResolver) {
return getFileExt(getFilename(uri, contentResolver));
}
public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
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) {
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.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.WIKIDATA_ENTITY_ID_PREF;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ITEM_LOCATION;
public class UploadActivity extends AuthenticatedActivity implements UploadView, SimilarImageInterface {
@Inject MediaWikiApi mwApi;
@ -643,12 +640,8 @@ public class UploadActivity extends AuthenticatedActivity implements UploadView,
return;
}
if (intent.hasExtra(PLACE_OBJECT)) {
Place place = intent.getParcelableExtra(PLACE_OBJECT);
presenter.receiveDirect(urisList.get(0), mimeType, source, place);
} else {
presenter.receive(urisList, mimeType, source);
}
presenter.receive(urisList, mimeType, source, place);
resetDirectPrefs();
}

View file

@ -7,7 +7,6 @@ import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
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.contributions.Contribution;
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.nearby.Place;
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.ImageUtilsWrapper;
import fr.free.nrw.commons.utils.StringUtils;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.BehaviorSubject;
import timber.log.Timber;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
public class UploadModel {
private MediaWikiApi mwApi;
@ -67,9 +59,8 @@ public class UploadModel {
private SessionManager sessionManager;
private Uri currentMediaUri;
private FileUtilsWrapper fileUtilsWrapper;
private ImageUtilsWrapper imageUtilsWrapper;
private BitmapRegionDecoderWrapper bitmapRegionDecoderWrapper;
private FileProcessor fileProcessor;
private final ImageProcessingService imageProcessingService;
@Inject
UploadModel(@Named("licenses") List<String> licenses,
@ -79,13 +70,10 @@ public class UploadModel {
MediaWikiApi mwApi,
SessionManager sessionManager,
FileUtilsWrapper fileUtilsWrapper,
ImageUtilsWrapper imageUtilsWrapper,
BitmapRegionDecoderWrapper bitmapRegionDecoderWrapper,
FileProcessor fileProcessor) {
FileProcessor fileProcessor, ImageProcessingService imageProcessingService) {
this.licenses = licenses;
this.basicKvStore = basicKvStore;
this.license = basicKvStore.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
this.bitmapRegionDecoderWrapper = bitmapRegionDecoderWrapper;
this.licensesByName = licensesByName;
this.context = context;
this.mwApi = mwApi;
@ -93,90 +81,53 @@ public class UploadModel {
this.sessionManager = sessionManager;
this.fileUtilsWrapper = fileUtilsWrapper;
this.fileProcessor = fileProcessor;
this.imageUtilsWrapper = imageUtilsWrapper;
useExtStorage = this.basicKvStore.getBoolean("useExternalStorage", false);
this.imageProcessingService = imageProcessingService;
useExtStorage = this.basicKvStore.getBoolean("useExternalStorage", false);
}
@SuppressLint("CheckResult")
void receive(List<Uri> mediaUri,
Observable<UploadItem> preProcessImages(List<Uri> mediaUris,
String mimeType,
Place place,
String source,
SimilarImageInterface similarImageInterface) {
initDefaultValues();
Observable<UploadItem> itemObservable = Observable.fromIterable(mediaUri)
.map(media -> {
currentMediaUri = media;
return cacheFileUpload(media);
})
.map(filePath -> {
return Observable.fromIterable(mediaUris)
.map(mediaUri -> {
if (mediaUri == null || mediaUri.getPath() == null) {
return null;
}
String filePath = mediaUri.getPath();
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());
UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface),
fileUtilsWrapper.getFileExt(filePath), null, fileCreatedDate);
checkImageQuality(null, null, filePath)
.observeOn(Schedulers.io())
UploadItem item = new UploadItem(mediaUri, mimeType, source, gpsExtractor,
fileExt, place.getWikiDataEntityId(), fileCreatedDate);
imageProcessingService.checkImageQuality(place, filePath)
.subscribeOn(Schedulers.computation())
.subscribe(item.imageQuality::onNext, Timber::e);
return item;
});
items = itemObservable.toList().blockingGet();
items.get(0).selected = true;
items.get(0).first = true;
}
@SuppressLint("CheckResult")
void receiveDirect(Uri media, String mimeType, String source, Place place, SimilarImageInterface similarImageInterface) {
initDefaultValues();
long fileCreatedDate = getFileCreatedDate(media);
String filePath = this.cacheFileUpload(media);
Uri uri = Uri.fromFile(new File(filePath));
fileProcessor.initFileDetails(filePath, context.getContentResolver());
UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface),
fileUtilsWrapper.getFileExt(filePath), place.getWikiDataEntityId(), fileCreatedDate);
item.title.setTitleText(place.getName());
item.descriptions.get(0).setDescriptionText(place.getLongDescription());
void onItemsProcessed(Place place, List<UploadItem> uploadItems) {
items = uploadItems;
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());
//TODO figure out if default descriptions in other languages exist
item.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;
uploadItem.descriptions.get(0).setLanguageCode("en");
}
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() {
@ -274,8 +225,10 @@ public class UploadModel {
}
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++;
@ -325,6 +278,7 @@ public class UploadModel {
}
private void updateItemState() {
Timber.d("Updating item state");
int count = items.size();
for (int i = 0; i < count; i++) {
UploadItem item = items.get(i);
@ -334,6 +288,7 @@ public class UploadModel {
}
private void markCurrentUploadVisited() {
Timber.d("Marking current upload visited");
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
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() {
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.util.ArrayList;
import java.util.Collections;
import java.util.List;
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.settings.Prefs;
import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@ -64,10 +62,6 @@ public class UploadPresenter {
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.
*
@ -76,41 +70,30 @@ public class UploadPresenter {
* @param source File source from {@link Contribution.FileSource}
*/
@SuppressLint("CheckResult")
void receive(List<Uri> media, String mimeType, @Contribution.FileSource String source) {
Completable.fromRunnable(() -> uploadModel.receive(media, mimeType, source, similarImageInterface))
.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,
void receive(List<Uri> media,
String mimeType,
@Contribution.FileSource String source,
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())
.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();
updateLicenses();
updateContent();
if (uploadModel.isShowingItem())
uploadModel.subscribeBadPicture(this::handleBadPicture);
}, Timber::e);
}
/**
* Sets the license to parameter and updates {@link UploadActivity}
*
@ -129,10 +112,12 @@ public class UploadPresenter {
@SuppressLint("CheckResult")
void handleNext(Title title,
List<Description> descriptions) {
Timber.e("Inside handleNext");
validateCurrentItemTitle()
.subscribeOn(Schedulers.io())
.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) {
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();
}
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();
}
}
private void nextUploadedItem() {
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);
}
view.dismissKeyboard();
}
private void setTitleAndDescription(Title title, List<Description> descriptions) {
Timber.d("setTitleAndDescription: Setting title and desc");
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,16 +128,15 @@ public class ImageUtils {
int bitmapHeight = bitmap.getHeight();
int allPixelsCount = bitmapWidth * bitmapHeight;
int[] bitmapPixels = new int[allPixelsCount];
Timber.d("total %s", Integer.toString(allPixelsCount));
bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight);
int numberOfBrightPixels = 0;
int numberOfMediumBrightnessPixels = 0;
double brightPixelThreshold = 0.025 * 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 g = Color.green(pixel);
int b = Color.blue(pixel);
@ -157,15 +156,14 @@ public class ImageUtils {
if (luminance > mediumBrightnessLuminance) {
numberOfMediumBrightnessPixels++;
}
}
else {
} else {
numberOfBrightPixels++;
}
if (numberOfBrightPixels >= brightPixelThreshold || numberOfMediumBrightnessPixels >= mediumBrightPixelThreshold) {
return false;
}
}
}
return true;
}

View file

@ -5,6 +5,8 @@ import android.graphics.BitmapRegionDecoder;
import javax.inject.Inject;
import javax.inject.Singleton;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
import fr.free.nrw.commons.location.LatLng;
import static fr.free.nrw.commons.utils.ImageUtils.*;
@ -17,11 +19,18 @@ public class ImageUtilsWrapper {
}
public @Result int checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
return ImageUtils.checkIfImageIsTooDark(bitmapRegionDecoder);
public Single<Integer> checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
int isImageDark = ImageUtils.checkIfImageIsTooDark(bitmapRegionDecoder);
return Single.just(isImageDark)
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation());
}
public boolean checkImageGeolocationIsDifferent(String geolocationOfFileString, LatLng latLng) {
return ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng);
public Single<Integer> checkImageGeolocationIsDifferent(String geolocationOfFileString, LatLng 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
import android.app.Application
import android.content.ContentResolver
import android.content.Context
import android.content.SharedPreferences
import android.graphics.BitmapRegionDecoder
import android.net.Uri
import fr.free.nrw.commons.auth.SessionManager
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.nearby.Place
import fr.free.nrw.commons.utils.BitmapRegionDecoderWrapper
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.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
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.Mock
import org.mockito.Mockito.`when`
@ -27,6 +23,7 @@ import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import java.io.FileInputStream
import java.io.InputStream
import java.util.*
import javax.inject.Inject
import javax.inject.Named
@ -51,11 +48,9 @@ class UploadModelTest {
@Mock
internal var fileUtilsWrapper: FileUtilsWrapper? = null
@Mock
internal var imageUtilsWrapper: ImageUtilsWrapper? = null
@Mock
internal var bitmapRegionDecoderWrapper: BitmapRegionDecoderWrapper? = null
@Mock
internal var fileProcessor: FileProcessor? = null
@Mock
internal var imageProcessingService: ImageProcessingService? = null
@InjectMocks
var uploadModel: UploadModel? = null
@ -67,8 +62,6 @@ class UploadModelTest {
`when`(context!!.applicationContext)
.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()))
.thenReturn("jpg")
`when`(fileUtilsWrapper!!.getSHA1(any(InputStream::class.java)))
@ -77,12 +70,10 @@ class UploadModelTest {
.thenReturn(mock(FileInputStream::class.java))
`when`(fileUtilsWrapper!!.getGeolocationOfFile(anyString()))
.thenReturn("")
`when`(imageUtilsWrapper!!.checkIfImageIsTooDark(any(BitmapRegionDecoder::class.java)))
.thenReturn(IMAGE_OK)
`when`(imageUtilsWrapper!!.checkImageGeolocationIsDifferent(anyString(), any(LatLng::class.java)))
.thenReturn(false)
`when`(bitmapRegionDecoderWrapper!!.newInstance(any(FileInputStream::class.java), anyBoolean()))
.thenReturn(mock(BitmapRegionDecoder::class.java))
`when`(imageProcessingService!!.checkImageQuality(anyString()))
.thenReturn(Single.just(IMAGE_OK))
`when`(imageProcessingService!!.checkImageQuality(any(Place::class.java), anyString()))
.thenReturn(Single.just(IMAGE_OK))
}
@ -93,154 +84,99 @@ class UploadModelTest {
@Test
fun receive() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
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)
}
@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
fun verifyPreviousNotAvailable() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
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)
}
@Test
fun verifyNextAvailable() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
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)
}
@Test
fun isSubmitAvailable() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
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)
}
@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
fun getCurrentStep() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
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)
}
@Test
fun getStepCount() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
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)
}
@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
fun getCount() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
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)
}
}
@Test
fun getUploads() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
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)
}
@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
fun isTopCardState() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertTrue(uploadModel!!.isTopCardState)
}
@Test
fun isTopCardStateForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", mock(Place::class.java)) { _, _ -> }
uploadModel!!.preProcessImages(uriList, "image/jpeg", mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.isTopCardState)
}
@Test
fun next() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
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)
uploadModel!!.next()
assertTrue(uploadModel!!.currentStep == 2)
@ -248,10 +184,10 @@ class UploadModelTest {
@Test
fun previous() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
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)
uploadModel!!.next()
assertTrue(uploadModel!!.currentStep == 2)
@ -261,12 +197,20 @@ class UploadModelTest {
@Test
fun isShowingItem() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
val element = getElement()
val element2 = getElement()
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)
}
}
private fun getElement(): Uri {
val mock = mock(Uri::class.java)
`when`(mock.path).thenReturn(UUID.randomUUID().toString() + "/file.jpg")
return mock
}
@Test
fun buildContributions() {

View file

@ -3,12 +3,12 @@ package fr.free.nrw.commons.upload
import android.net.Uri
import fr.free.nrw.commons.mwapi.MediaWikiApi
import fr.free.nrw.commons.nearby.Place
import io.reactivex.Observable
import org.junit.Before
import org.junit.Test
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import org.mockito.*
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
class UploadPresenterTest {
@ -26,6 +26,12 @@ class UploadPresenterTest {
@Throws(Exception::class)
fun setUp() {
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
@ -33,18 +39,6 @@ class UploadPresenterTest {
val element = Mockito.mock(Uri::class.java)
val element2 = Mockito.mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadPresenter!!.receive(uriList, "image/jpeg", "external")
}
@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)) { _, _ -> }
uploadPresenter!!.receive(uriList, "image/jpeg", "external", mock(Place::class.java))
}
}

View file

@ -7,17 +7,6 @@ import org.junit.Test
import java.io.*
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
fun deleteFile() {
val file = File.createTempFile("testfile", "")