Upload tests (#2086)

* Add unit tests for upload flows

* Tests for upload model

* Test fixes

* Remove empty test cases

* Changes based on comments
This commit is contained in:
Vivek Maskara 2018-12-10 21:45:24 +05:30 committed by Josephine Lim
parent 1070137741
commit f3a90c004c
19 changed files with 533 additions and 252 deletions

View file

@ -77,6 +77,8 @@ dependencies {
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
androidTestImplementation "org.mockito:mockito-core:2.10.0"
//For handling runtime permissions //For handling runtime permissions
implementation 'com.karumi:dexter:5.0.0' implementation 'com.karumi:dexter:5.0.0'
@ -100,6 +102,8 @@ android {
} }
testOptions { testOptions {
unitTests.returnDefaultValues = true
unitTests.all { unitTests.all {
jvmArgs '-noverify' jvmArgs '-noverify'
} }

View file

@ -119,7 +119,7 @@ public class CommonsApplication extends Application {
ContributionUtils.emptyTemporaryDirectory(); ContributionUtils.emptyTemporaryDirectory();
initAcra(); initAcra();
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG && !isRoboUnitTest()) {
Stetho.initializeWithDefaults(this); Stetho.initializeWithDefaults(this);
} }
@ -162,6 +162,10 @@ public class CommonsApplication extends Application {
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler); Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
} }
public static boolean isRoboUnitTest() {
return "robolectric".equals(Build.FINGERPRINT);
}
private ThreadPoolService getFileLoggingThreadPool() { private ThreadPoolService getFileLoggingThreadPool() {
return new ThreadPoolService.Builder("file-logging-thread") return new ThreadPoolService.Builder("file-logging-thread")
.setPriority(Process.THREAD_PRIORITY_LOWEST) .setPriority(Process.THREAD_PRIORITY_LOWEST)

View file

@ -2,37 +2,31 @@ package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.media.ExifInterface; import android.media.ExifInterface;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.Date;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.caching.CacheController; import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.CategoryApi; import fr.free.nrw.commons.mwapi.CategoryApi;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
/** /**
* Processing of the image file that is about to be uploaded via ShareActivity is done here * Processing of the image file that is about to be uploaded via ShareActivity is done here
*/ */
@Singleton
public class FileProcessor implements SimilarImageDialogFragment.onResponse { public class FileProcessor implements SimilarImageDialogFragment.onResponse {
@Inject @Inject
@ -47,24 +41,23 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
private String filePath; private String filePath;
private ContentResolver contentResolver; private ContentResolver contentResolver;
private GPSExtractor imageObj; private GPSExtractor imageObj;
private Context context;
private String decimalCoords; private String decimalCoords;
private ExifInterface exifInterface; private ExifInterface exifInterface;
private boolean useExtStorage;
private boolean haveCheckedForOtherImages = false; private boolean haveCheckedForOtherImages = false;
private GPSExtractor tempImageObj; private GPSExtractor tempImageObj;
FileProcessor(@NonNull String filePath, ContentResolver contentResolver, Context context) { @Inject
FileProcessor() {
}
void initFileDetails(@NonNull String filePath, ContentResolver contentResolver) {
this.filePath = filePath; this.filePath = filePath;
this.contentResolver = contentResolver; this.contentResolver = contentResolver;
this.context = context;
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
try { try {
exifInterface=new ExifInterface(filePath); exifInterface = new ExifInterface(filePath);
} catch (IOException e) { } catch (IOException e) {
Timber.e(e); Timber.e(e);
} }
useExtStorage = prefs.getBoolean("useExternalStorage", true);
} }
/** /**
@ -85,10 +78,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
return imageObj; return imageObj;
} }
String getDecimalCoords() {
return decimalCoords;
}
/** /**
* Find other images around the same location that were taken within the last 20 sec * Find other images around the same location that were taken within the last 20 sec
* @param similarImageInterface * @param similarImageInterface
@ -142,7 +131,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
* Then initiates the calls to MediaWiki API through an instance of CategoryApi. * Then initiates the calls to MediaWiki API through an instance of CategoryApi.
*/ */
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
public void useImageCoords() { private void useImageCoords() {
if (decimalCoords != null) { if (decimalCoords != null) {
Timber.d("Decimal coords of image: %s", decimalCoords); Timber.d("Decimal coords of image: %s", decimalCoords);
Timber.d("is EXIF data present:" + imageObj.imageCoordsExists + " from findOther image"); Timber.d("is EXIF data present:" + imageObj.imageCoordsExists + " from findOther image");

View file

@ -234,8 +234,8 @@ public class FileUtils {
* @return The value of the _data column, which is typically a file path. * @return The value of the _data column, which is typically a file path.
*/ */
@Nullable @Nullable
public static String getDataColumn(Context context, Uri uri, String selection, private static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) { String[] selectionArgs) {
Cursor cursor = null; Cursor cursor = null;
final String column = MediaStore.Images.ImageColumns.DATA; final String column = MediaStore.Images.ImageColumns.DATA;
@ -311,7 +311,7 @@ public class FileUtils {
* @param destination file path copied to * @param destination file path copied to
* @throws IOException thrown when failing to read source or opening destination file * @throws IOException thrown when failing to read source or opening destination file
*/ */
public static void copy(@NonNull FileDescriptor source, @NonNull String destination) private static void copy(@NonNull FileDescriptor source, @NonNull String destination)
throws IOException { throws IOException {
copy(new FileInputStream(source), new FileOutputStream(destination)); copy(new FileInputStream(source), new FileOutputStream(destination));
} }
@ -415,7 +415,7 @@ public class FileUtils {
return result; return result;
} }
public static String getFileExt(String fileName){ static String getFileExt(String fileName){
//Default file extension //Default file extension
String extension=".jpg"; String extension=".jpg";
@ -426,7 +426,11 @@ public class FileUtils {
return extension; return extension;
} }
public static String getFileExt(Uri uri, ContentResolver contentResolver) { private static String getFileExt(Uri uri, ContentResolver contentResolver) {
return getFileExt(getFilename(uri, contentResolver)); return getFileExt(getFilename(uri, contentResolver));
} }
public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return new FileInputStream(filePath);
}
} }

View file

@ -0,0 +1,42 @@
package fr.free.nrw.commons.upload;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class FileUtilsWrapper {
@Inject
public FileUtilsWrapper() {
}
public String createExternalCopyPathAndCopy(Uri uri, ContentResolver contentResolver) throws IOException {
return FileUtils.createExternalCopyPathAndCopy(uri, contentResolver);
}
public String createCopyPathAndCopy(Uri uri, Context context) throws IOException {
return FileUtils.createCopyPathAndCopy(uri, context);
}
public String getFileExt(String fileName) {
return FileUtils.getFileExt(fileName);
}
public String getSHA1(InputStream is) {
return FileUtils.getSHA1(is);
}
public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return FileUtils.getFileInputStream(filePath);
}
}

View file

@ -14,12 +14,12 @@ import timber.log.Timber;
* Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation * Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation
* is uploaded, extract latitude and longitude from EXIF data of image. * is uploaded, extract latitude and longitude from EXIF data of image.
*/ */
public class GPSExtractor { class GPSExtractor {
public static final GPSExtractor DUMMY= new GPSExtractor(); static final GPSExtractor DUMMY= new GPSExtractor();
private double decLatitude; private double decLatitude;
private double decLongitude; private double decLongitude;
public boolean imageCoordsExists; boolean imageCoordsExists;
private String latitude; private String latitude;
private String longitude; private String longitude;
private String latitudeRef; private String latitudeRef;
@ -37,7 +37,7 @@ public class GPSExtractor {
* @param fileDescriptor the file descriptor of the image * @param fileDescriptor the file descriptor of the image
*/ */
@RequiresApi(24) @RequiresApi(24)
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) { GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
try { try {
ExifInterface exif = new ExifInterface(fileDescriptor); ExifInterface exif = new ExifInterface(fileDescriptor);
processCoords(exif); processCoords(exif);
@ -51,7 +51,7 @@ public class GPSExtractor {
* @param path file path of the image * @param path file path of the image
* *
*/ */
public GPSExtractor(@NonNull String path) { GPSExtractor(@NonNull String path) {
try { try {
ExifInterface exif = new ExifInterface(path); ExifInterface exif = new ExifInterface(path);
processCoords(exif); processCoords(exif);
@ -65,7 +65,7 @@ public class GPSExtractor {
* @param exif exif interface of the image * @param exif exif interface of the image
* *
*/ */
public GPSExtractor(@NonNull ExifInterface exif){ GPSExtractor(@NonNull ExifInterface exif){
processCoords(exif); processCoords(exif);
} }
@ -89,7 +89,7 @@ public class GPSExtractor {
* @return coordinates as string (needs to be passed as a String in API query) * @return coordinates as string (needs to be passed as a String in API query)
*/ */
@Nullable @Nullable
public String getCoords() { String getCoords() {
if(decimalCoords!=null){ if(decimalCoords!=null){
return decimalCoords; return decimalCoords;
}else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) { }else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
@ -103,11 +103,11 @@ public class GPSExtractor {
} }
} }
public double getDecLatitude() { double getDecLatitude() {
return decLatitude; return decLatitude;
} }
public double getDecLongitude() { double getDecLongitude() {
return decLongitude; return decLongitude;
} }

View file

@ -51,7 +51,7 @@ public class UploadController {
} }
private boolean isUploadServiceConnected; private boolean isUploadServiceConnected;
private ServiceConnection uploadServiceConnection = new ServiceConnection() { public ServiceConnection uploadServiceConnection = new ServiceConnection() {
@Override @Override
public void onServiceConnected(ComponentName componentName, IBinder binder) { public void onServiceConnected(ComponentName componentName, IBinder binder) {
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService(); uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService();
@ -61,6 +61,7 @@ public class UploadController {
@Override @Override
public void onServiceDisconnected(ComponentName componentName) { public void onServiceDisconnected(ComponentName componentName) {
// this should never happen // this should never happen
isUploadServiceConnected = false;
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
} }
}; };
@ -68,7 +69,7 @@ public class UploadController {
/** /**
* Prepares the upload service. * Prepares the upload service.
*/ */
public void prepareService() { void prepareService() {
Intent uploadServiceIntent = new Intent(context, UploadService.class); Intent uploadServiceIntent = new Intent(context, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
context.startService(uploadServiceIntent); context.startService(uploadServiceIntent);
@ -78,7 +79,7 @@ public class UploadController {
/** /**
* Disconnects the upload service. * Disconnects the upload service.
*/ */
public void cleanup() { void cleanup() {
if (isUploadServiceConnected) { if (isUploadServiceConnected) {
context.unbindService(uploadServiceConnection); context.unbindService(uploadServiceConnection);
} }
@ -89,7 +90,7 @@ public class UploadController {
* *
* @param contribution the contribution object * @param contribution the contribution object
*/ */
public void startUpload(Contribution contribution) { void startUpload(Contribution contribution) {
startUpload(contribution, c -> {}); startUpload(contribution, c -> {});
} }
@ -100,7 +101,7 @@ public class UploadController {
* @param onComplete the progress tracker * @param onComplete the progress tracker
*/ */
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) { private void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
//Set creator, desc, and license //Set creator, desc, and license
if (TextUtils.isEmpty(contribution.getCreator())) { if (TextUtils.isEmpty(contribution.getCreator())) {
Account currentAccount = sessionManager.getCurrentAccount(); Account currentAccount = sessionManager.getCurrentAccount();

View file

@ -53,16 +53,20 @@ public class UploadModel {
private boolean useExtStorage; private boolean useExtStorage;
private Disposable badImageSubscription; private Disposable badImageSubscription;
@Inject private SessionManager sessionManager;
SessionManager sessionManager;
private Uri currentMediaUri; private Uri currentMediaUri;
private FileUtilsWrapper fileUtilsWrapper;
private FileProcessor fileProcessor;
@Inject @Inject
UploadModel(@Named("licenses") List<String> licenses, UploadModel(@Named("licenses") List<String> licenses,
@Named("default_preferences") SharedPreferences prefs, @Named("default_preferences") SharedPreferences prefs,
@Named("licenses_by_name") Map<String, String> licensesByName, @Named("licenses_by_name") Map<String, String> licensesByName,
Context context, Context context,
MediaWikiApi mwApi) { MediaWikiApi mwApi,
SessionManager sessionManager,
FileUtilsWrapper fileUtilsWrapper,
FileProcessor fileProcessor) {
this.licenses = licenses; this.licenses = licenses;
this.prefs = prefs; this.prefs = prefs;
this.license = Prefs.Licenses.CC_BY_SA_3; this.license = Prefs.Licenses.CC_BY_SA_3;
@ -70,6 +74,9 @@ public class UploadModel {
this.context = context; this.context = context;
this.mwApi = mwApi; this.mwApi = mwApi;
this.contentResolver = context.getContentResolver(); this.contentResolver = context.getContentResolver();
this.sessionManager = sessionManager;
this.fileUtilsWrapper = fileUtilsWrapper;
this.fileProcessor = fileProcessor;
useExtStorage = this.prefs.getBoolean("useExternalStorage", false); useExtStorage = this.prefs.getBoolean("useExternalStorage", false);
} }
@ -84,17 +91,17 @@ public class UploadModel {
.map(filePath -> { .map(filePath -> {
long fileCreatedDate = getFileCreatedDate(currentMediaUri); long fileCreatedDate = getFileCreatedDate(currentMediaUri);
Uri uri = Uri.fromFile(new File(filePath)); Uri uri = Uri.fromFile(new File(filePath));
FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context); fileProcessor.initFileDetails(filePath, context.getContentResolver());
UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface), UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface),
FileUtils.getFileExt(filePath), null,fileCreatedDate); fileUtilsWrapper.getFileExt(filePath), null,fileCreatedDate);
Single.zip( Single.zip(
Single.fromCallable(() -> Single.fromCallable(() ->
new FileInputStream(filePath)) fileUtilsWrapper.getFileInputStream(filePath))
.map(FileUtils::getSHA1) .map(fileUtilsWrapper::getSHA1)
.map(mwApi::existingFile) .map(mwApi::existingFile)
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK), .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
Single.fromCallable(() -> Single.fromCallable(() ->
new FileInputStream(filePath)) fileUtilsWrapper.getFileInputStream(filePath))
.map(file -> BitmapRegionDecoder.newInstance(file, false)) .map(file -> BitmapRegionDecoder.newInstance(file, false))
.map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK .map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
(dupe, dark) -> dupe | dark) (dupe, dark) -> dupe | dark)
@ -113,24 +120,24 @@ public class UploadModel {
long fileCreatedDate = getFileCreatedDate(media); long fileCreatedDate = getFileCreatedDate(media);
String filePath = this.cacheFileUpload(media); String filePath = this.cacheFileUpload(media);
Uri uri = Uri.fromFile(new File(filePath)); Uri uri = Uri.fromFile(new File(filePath));
FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context); fileProcessor.initFileDetails(filePath, context.getContentResolver());
UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface), UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface),
FileUtils.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate); fileUtilsWrapper.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate);
item.title.setTitleText(title); item.title.setTitleText(title);
item.descriptions.get(0).setDescriptionText(desc); item.descriptions.get(0).setDescriptionText(desc);
//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"); item.descriptions.get(0).setLanguageCode("en");
Single.zip( Single.zip(
Single.fromCallable(() -> Single.fromCallable(() ->
new FileInputStream(filePath)) fileUtilsWrapper.getFileInputStream(filePath))
.map(FileUtils::getSHA1) .map(fileUtilsWrapper::getSHA1)
.map(mwApi::existingFile) .map(mwApi::existingFile)
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK), .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
Single.fromCallable(() -> Single.fromCallable(() ->
new FileInputStream(filePath)) fileUtilsWrapper.getFileInputStream(filePath))
.map(file -> BitmapRegionDecoder.newInstance(file, false)) .map(file -> BitmapRegionDecoder.newInstance(file, false))
.map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK .map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
(dupe, dark) -> dupe | dark).subscribe(item.imageQuality::onNext); (dupe, dark) -> dupe | dark).subscribe(item.imageQuality::onNext, Timber::e);
items.add(item); items.add(item);
items.get(0).selected = true; items.get(0).selected = true;
items.get(0).first = true; items.get(0).first = true;
@ -239,7 +246,7 @@ public class UploadModel {
updateItemState(); updateItemState();
} }
public void setCurrentTitleAndDescriptions(Title title, List<Description> descriptions) { void setCurrentTitleAndDescriptions(Title title, List<Description> descriptions) {
setCurrentUploadTitle(title); setCurrentUploadTitle(title);
setCurrentUploadDescriptions(descriptions); setCurrentUploadDescriptions(descriptions);
} }
@ -337,9 +344,9 @@ public class UploadModel {
try { try {
String copyPath; String copyPath;
if (useExtStorage) if (useExtStorage)
copyPath = FileUtils.createExternalCopyPathAndCopy(media, contentResolver); copyPath = fileUtilsWrapper.createExternalCopyPathAndCopy(media, contentResolver);
else else
copyPath = FileUtils.createCopyPathAndCopy(media, context); copyPath = fileUtilsWrapper.createCopyPathAndCopy(media, context);
Timber.i("File path is " + copyPath); Timber.i("File path is " + copyPath);
return copyPath; return copyPath;
} catch (IOException e) { } catch (IOException e) {
@ -362,6 +369,9 @@ public class UploadModel {
badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e); badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e);
} }
public List<UploadItem> getItems() {
return items;
}
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
static class UploadItem { static class UploadItem {

View file

@ -88,7 +88,7 @@ public class UploadService extends HandlerService<Contribution> {
String notificationProgressTitle; String notificationProgressTitle;
String notificationFinishingTitle; String notificationFinishingTitle;
public NotificationUpdateProgressListener(String notificationTag, String notificationProgressTitle, String notificationFinishingTitle, Contribution contribution) { NotificationUpdateProgressListener(String notificationTag, String notificationProgressTitle, String notificationFinishingTitle, Contribution contribution) {
this.notificationTag = notificationTag; this.notificationTag = notificationTag;
this.notificationProgressTitle = notificationProgressTitle; this.notificationProgressTitle = notificationProgressTitle;
this.notificationFinishingTitle = notificationFinishingTitle; this.notificationFinishingTitle = notificationFinishingTitle;

View file

@ -1,71 +0,0 @@
package fr.free.nrw.commons.upload;
import java.util.HashMap;
/**
* This is a Util class which provides the necessary token to open the Commons License
* info in the user language
*/
public class UrlLicense {
public static HashMap<String,String> urlLicense = new HashMap<>();
static {
urlLicense.put("en", "https://commons.wikimedia.org/wiki/Commons:Licensing");
urlLicense.put("ar", "https://commons.wikimedia.org/wiki/Commons:Licensing/ar");
urlLicense.put("ast", "https://commons.wikimedia.org/wiki/Commons:Licensing/ast");
urlLicense.put("az", "https://commons.wikimedia.org/wiki/Commons:Licensing/az");
urlLicense.put("be", "https://commons.wikimedia.org/wiki/Commons:Licensing/be");
urlLicense.put("bg", "https://commons.wikimedia.org/wiki/Commons:Licensing/bg");
urlLicense.put("bn", "https://commons.wikimedia.org/wiki/Commons:Licensing/bn");
urlLicense.put("ca", "https://commons.wikimedia.org/wiki/Commons:Licensing/ca");
urlLicense.put("cs", "https://commons.wikimedia.org/wiki/Commons:Licensing/cs");
urlLicense.put("da", "https://commons.wikimedia.org/wiki/Commons:Licensing/da");
urlLicense.put("de", "https://commons.wikimedia.org/wiki/Commons:Licensing/de");
urlLicense.put("el", "https://commons.wikimedia.org/wiki/Commons:Licensing/el");
urlLicense.put("eo", "https://commons.wikimedia.org/wiki/Commons:Licensing/eo");
urlLicense.put("es", "https://commons.wikimedia.org/wiki/Commons:Licensing/es");
urlLicense.put("eu", "https://commons.wikimedia.org/wiki/Commons:Licensing/eu");
urlLicense.put("fa", "https://commons.wikimedia.org/wiki/Commons:Licensing/fa");
urlLicense.put("fi", "https://commons.wikimedia.org/wiki/Commons:Licensing/fi");
urlLicense.put("fr", "https://commons.wikimedia.org/wiki/Commons:Licensing/fr");
urlLicense.put("gl", "https://commons.wikimedia.org/wiki/Commons:Licensing/gl");
urlLicense.put("gsw", "https://commons.wikimedia.org/wiki/Commons:Licensing/gsw");
urlLicense.put("he", "https://commons.wikimedia.org/wiki/Commons:Licensing/he");
urlLicense.put("hi", "https://commons.wikimedia.org/wiki/Commons:Licensing/hi");
urlLicense.put("hu", "https://commons.wikimedia.org/wiki/Commons:Licensing/hu");
urlLicense.put("id", "https://commons.wikimedia.org/wiki/Commons:Licensing/id");
urlLicense.put("is", "https://commons.wikimedia.org/wiki/Commons:Licensing/is");
urlLicense.put("it", "https://commons.wikimedia.org/wiki/Commons:Licensing/it");
urlLicense.put("ja", "https://commons.wikimedia.org/wiki/Commons:Licensing/ja");
urlLicense.put("ka", "https://commons.wikimedia.org/wiki/Commons:Licensing/ka");
urlLicense.put("km", "https://commons.wikimedia.org/wiki/Commons:Licensing/km");
urlLicense.put("ko", "https://commons.wikimedia.org/wiki/Commons:Licensing/ko");
urlLicense.put("ku", "https://commons.wikimedia.org/wiki/Commons:Licensing/ku");
urlLicense.put("mk", "https://commons.wikimedia.org/wiki/Commons:Licensing/mk");
urlLicense.put("mr", "https://commons.wikimedia.org/wiki/Commons:Licensing/mr");
urlLicense.put("ms", "https://commons.wikimedia.org/wiki/Commons:Licensing/ms");
urlLicense.put("my", "https://commons.wikimedia.org/wiki/Commons:Licensing/my");
urlLicense.put("nl", "https://commons.wikimedia.org/wiki/Commons:Licensing/nl");
urlLicense.put("oc", "https://commons.wikimedia.org/wiki/Commons:Licensing/oc");
urlLicense.put("pl", "https://commons.wikimedia.org/wiki/Commons:Licensing/pl");
urlLicense.put("pt", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt");
urlLicense.put("pt-br", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br");
urlLicense.put("ro", "https://commons.wikimedia.org/wiki/Commons:Licensing/ro");
urlLicense.put("ru", "https://commons.wikimedia.org/wiki/Commons:Licensing/ru");
urlLicense.put("scn", "https://commons.wikimedia.org/wiki/Commons:Licensing/scn");
urlLicense.put("sk", "https://commons.wikimedia.org/wiki/Commons:Licensing/sk");
urlLicense.put("sl", "https://commons.wikimedia.org/wiki/Commons:Licensing/sl");
urlLicense.put("sv", "https://commons.wikimedia.org/wiki/Commons:Licensing/sv");
urlLicense.put("tr", "https://commons.wikimedia.org/wiki/Commons:Licensing/tr");
urlLicense.put("uk", "https://commons.wikimedia.org/wiki/Commons:Licensing/uk");
urlLicense.put("ur", "https://commons.wikimedia.org/wiki/Commons:Licensing/ur");
urlLicense.put("vi", "https://commons.wikimedia.org/wiki/Commons:Licensing/vi");
urlLicense.put("zh", "https://commons.wikimedia.org/wiki/Commons:Licensing/zh");
}
public static String getLicenseUrl ( String language){
if (urlLicense.containsKey(language)) {
return urlLicense.get(language);
} else {
return urlLicense.get("en");
}
}
}

View file

@ -1,115 +0,0 @@
package fr.free.nrw.commons.upload;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v4.graphics.BitmapCompat;
import android.view.View;
import android.widget.FrameLayout;
import java.io.IOException;
import java.io.InputStream;
import timber.log.Timber;
/**
* Contains utility methods for the Zoom function in ShareActivity.
*/
public class Zoom {
private View thumbView;
private ContentResolver contentResolver;
private FrameLayout flContainer;
Zoom(View thumbView, FrameLayout flContainer, ContentResolver contentResolver) {
this.thumbView = thumbView;
this.contentResolver = contentResolver;
this.flContainer = flContainer;
}
/**
* Create a scaled bitmap to display the zoomed-in image
* @param input the input stream corresponding to the uploaded image
* @param imageUri the uploaded image's URI
* @return a zoomable bitmap
*/
Bitmap createScaledImage(InputStream input, Uri imageUri) {
Bitmap scaled = null;
BitmapRegionDecoder decoder = null;
Bitmap bitmap = null;
try {
decoder = BitmapRegionDecoder.newInstance(input, false);
bitmap = decoder.decodeRegion(new Rect(10, 10, 50, 50), null);
} catch (IOException e) {
Timber.e(e);
} catch (NullPointerException e) {
Timber.e(e);
}
try {
//Compress the Image
System.gc();
Runtime rt = Runtime.getRuntime();
long maxMemory = rt.freeMemory();
bitmap = MediaStore.Images.Media.getBitmap(contentResolver, imageUri);
int bitmapByteCount = BitmapCompat.getAllocationByteCount(bitmap);
long height = bitmap.getHeight();
long width = bitmap.getWidth();
long calHeight = (long) ((height * maxMemory) / (bitmapByteCount * 1.1));
long calWidth = (long) ((width * maxMemory) / (bitmapByteCount * 1.1));
scaled = Bitmap.createScaledBitmap(bitmap, (int) Math.min(width, calWidth), (int) Math.min(height, calHeight), true);
} catch (IOException e) {
Timber.e(e);
} catch (NullPointerException e) {
Timber.e(e);
scaled = bitmap;
}
return scaled;
}
/**
* Calculate the starting and ending bounds for the zoomed-in image.
* Also set the container view's offset as the origin for the
* bounds, since that's the origin for the positioning animation
* properties (X, Y).
* @param startBounds the global visible rectangle of the thumbnail
* @param finalBounds the global visible rectangle of the container view
* @param globalOffset the container view's offset
* @return scaled start bounds
*/
float adjustStartEndBounds(Rect startBounds, Rect finalBounds, Point globalOffset) {
thumbView.getGlobalVisibleRect(startBounds);
flContainer.getGlobalVisibleRect(finalBounds, globalOffset);
startBounds.offset(-globalOffset.x, -globalOffset.y);
finalBounds.offset(-globalOffset.x, -globalOffset.y);
// Adjust the start bounds to be the same aspect ratio as the final
// bounds using the "center crop" technique. This prevents undesirable
// stretching during the animation. Also calculate the start scaling
// factor (the end scaling factor is always 1.0).
float startScale;
if ((float) finalBounds.width() / finalBounds.height()
> (float) startBounds.width() / startBounds.height()) {
// Extend start bounds horizontally
startScale = (float) startBounds.height() / finalBounds.height();
float startWidth = startScale * finalBounds.width();
float deltaWidth = (startWidth - startBounds.width()) / 2;
startBounds.left -= deltaWidth;
startBounds.right += deltaWidth;
} else {
// Extend start bounds vertically
startScale = (float) startBounds.width() / finalBounds.width();
float startHeight = startScale * finalBounds.height();
float deltaHeight = (startHeight - startBounds.height()) / 2;
startBounds.top -= deltaHeight;
startBounds.bottom += deltaHeight;
}
return startScale;
}
}

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.widget;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
@ -13,10 +13,7 @@ import android.view.Display;
* Created by Ilgaz Er on 8/7/2018. * Created by Ilgaz Er on 8/7/2018.
*/ */
public class HeightLimitedRecyclerView extends RecyclerView { public class HeightLimitedRecyclerView extends RecyclerView {
int height; int height;
public HeightLimitedRecyclerView(Context context) { public HeightLimitedRecyclerView(Context context) {
super(context); super(context);
DisplayMetrics displayMetrics = new DisplayMetrics(); DisplayMetrics displayMetrics = new DisplayMetrics();

View file

@ -49,7 +49,7 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_expand_less_black_24dp" /> app:srcCompat="@drawable/ic_expand_less_black_24dp" />
<fr.free.nrw.commons.upload.HeightLimitedRecyclerView <fr.free.nrw.commons.widget.HeightLimitedRecyclerView
android:id="@+id/rv_descriptions" android:id="@+id/rv_descriptions"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -24,7 +24,8 @@ class TestCommonsApplication : CommonsApplication() {
override fun onCreate() { override fun onCreate() {
if (mockApplicationComponent == null) { if (mockApplicationComponent == null) {
mockApplicationComponent = DaggerCommonsApplicationComponent.builder() mockApplicationComponent = DaggerCommonsApplicationComponent.builder()
.appModule(MockCommonsApplicationModule(this)).build() .appModule(MockCommonsApplicationModule(this))
.build()
} }
super.onCreate() super.onCreate()
} }

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.mwapi package fr.free.nrw.commons.mwapi
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import android.os.Build import android.os.Build
import android.preference.PreferenceManager import android.preference.PreferenceManager
import com.google.gson.Gson import com.google.gson.Gson
@ -15,9 +16,12 @@ import org.junit.Assert.*
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import timber.log.Timber
import java.io.InputStream
import java.net.URLDecoder import java.net.URLDecoder
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*

View file

@ -0,0 +1,41 @@
package fr.free.nrw.commons.upload
import android.content.ContentResolver
import android.content.SharedPreferences
import fr.free.nrw.commons.caching.CacheController
import fr.free.nrw.commons.mwapi.CategoryApi
import org.junit.Before
import org.junit.Test
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.anyString
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import javax.inject.Inject
import javax.inject.Named
class FileProcessorTest {
@Mock
internal var cacheController: CacheController? = null
@Mock
internal var gpsCategoryModel: GpsCategoryModel? = null
@Mock
internal var apiCall: CategoryApi? = null
@Mock
@field:[Inject Named("default_preferences")]
internal var prefs: SharedPreferences? = null
@InjectMocks
var fileProcessor: FileProcessor? = null
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
@Test
fun processFileCoordinates() {
}
}

View file

@ -0,0 +1,53 @@
package fr.free.nrw.commons.upload
import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import fr.free.nrw.commons.HandlerService
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.contributions.Contribution
import org.junit.Before
import org.junit.Test
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
class UploadControllerTest {
@Mock
internal var sessionManager: SessionManager? = null
@Mock
internal var context: Context? = null
@Mock
internal var prefs: SharedPreferences? = null
@InjectMocks
var uploadController: UploadController? = null
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
val uploadService = mock(UploadService::class.java)
val binder = mock(HandlerService.HandlerServiceLocalBinder::class.java)
`when`(binder.service).thenReturn(uploadService)
uploadController!!.uploadServiceConnection.onServiceConnected(mock(ComponentName::class.java), binder)
}
@Test
fun prepareService() {
uploadController!!.prepareService()
}
@Test
fun cleanup() {
uploadController!!.cleanup()
}
@Test
fun startUpload() {
val contribution = mock(Contribution::class.java)
uploadController!!.startUpload(contribution)
}
}

View file

@ -0,0 +1,267 @@
package fr.free.nrw.commons.upload
import android.app.Application
import android.content.ContentResolver
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.MediaWikiApi
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.any
import org.mockito.ArgumentMatchers.anyString
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import java.io.FileInputStream
import java.io.InputStream
import javax.inject.Inject
import javax.inject.Named
class UploadModelTest {
@Mock
@field:[Inject Named("licenses")]
internal var licenses: List<String>? = null
@Mock
@field:[Inject Named("default_preferences")]
internal var prefs: SharedPreferences? = null
@Mock
@field:[Inject Named("licenses_by_name")]
internal var licensesByName: Map<String, String>? = null
@Mock
internal var context: Context? = null
@Mock
internal var mwApi: MediaWikiApi? = null
@Mock
internal var sessionManage: SessionManager? = null
@Mock
internal var fileUtilsWrapper: FileUtilsWrapper? = null
@Mock
internal var fileProcessor: FileProcessor? = null
@InjectMocks
var uploadModel: UploadModel? = null
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
`when`(context!!.applicationContext)
.thenReturn(mock(Application::class.java))
`when`(fileUtilsWrapper!!.createCopyPathAndCopy(any(Uri::class.java), any(Context::class.java)))
.thenReturn("file.jpg")
`when`(fileUtilsWrapper!!.createExternalCopyPathAndCopy(any(Uri::class.java), any(ContentResolver::class.java)))
.thenReturn("extFile.jpg")
`when`(fileUtilsWrapper!!.getFileExt(anyString()))
.thenReturn("jpg")
`when`(fileUtilsWrapper!!.getSHA1(any(InputStream::class.java)))
.thenReturn("sha")
`when`(fileUtilsWrapper!!.getFileInputStream(anyString()))
.thenReturn(mock(FileInputStream::class.java))
}
@After
@Throws(Exception::class)
fun tearDown() {
}
@Test
fun receive() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertTrue(uploadModel!!.items.size == 2)
}
@Test
fun receiveDirect() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", "Q1", "Test", "Test"
) { _, _ -> }
assertTrue(uploadModel!!.items.size == 1)
}
@Test
fun verifyPreviousNotAvailableForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", "Q1", "Test", "Test"
) { _, _ -> }
assertFalse(uploadModel!!.isPreviousAvailable)
}
@Test
fun verifyNextAvailableForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", "Q1", "Test", "Test"
) { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable)
}
@Test
fun verifyPreviousNotAvailable() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertFalse(uploadModel!!.isPreviousAvailable)
}
@Test
fun verifyNextAvailable() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable)
}
@Test
fun isSubmitAvailable() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable)
}
@Test
fun isSubmitAvailableForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", "Q1", "Test", "Test"
) { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable)
}
@Test
fun getCurrentStepForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", "Q1", "Test", "Test"
) { _, _ -> }
assertTrue(uploadModel!!.currentStep == 1)
}
@Test
fun getCurrentStep() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertTrue(uploadModel!!.currentStep == 1)
}
@Test
fun getStepCount() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertTrue(uploadModel!!.stepCount == 4)
}
@Test
fun getStepCountForDirectUpload() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", "Q1", "Test", "Test"
) { _, _ -> }
assertTrue(uploadModel!!.stepCount == 3)
}
@Test
fun getDirectCount() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", "Q1", "Test", "Test"
) { _, _ -> }
assertTrue(uploadModel!!.count == 1)
}
@Test
fun getCount() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertTrue(uploadModel!!.count == 2)
}
@Test
fun getUploads() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertTrue(uploadModel!!.uploads.size == 2)
}
@Test
fun getDirectUploads() {
val element = mock(Uri::class.java)
uploadModel!!.receiveDirect(element, "image/jpeg", "external", "Q1", "Test", "Test"
) { _, _ -> }
assertTrue(uploadModel!!.uploads.size == 1)
}
@Test
fun isTopCardState() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
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", "Q1", "Test", "Test"
) { _, _ -> }
assertTrue(uploadModel!!.isTopCardState)
}
@Test
fun next() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertTrue(uploadModel!!.currentStep == 1)
uploadModel!!.next()
assertTrue(uploadModel!!.currentStep == 2)
}
@Test
fun previous() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertTrue(uploadModel!!.currentStep == 1)
uploadModel!!.next()
assertTrue(uploadModel!!.currentStep == 2)
uploadModel!!.previous()
assertTrue(uploadModel!!.currentStep == 1)
}
@Test
fun isShowingItem() {
val element = mock(Uri::class.java)
val element2 = mock(Uri::class.java)
var uriList: List<Uri> = mutableListOf<Uri>(element, element2)
uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> }
assertTrue(uploadModel!!.isShowingItem)
}
@Test
fun buildContributions() {
}
}

View file

@ -0,0 +1,50 @@
package fr.free.nrw.commons.upload
import android.net.Uri
import fr.free.nrw.commons.mwapi.MediaWikiApi
import org.junit.Before
import org.junit.Test
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
class UploadPresenterTest {
@Mock
internal var uploadModel: UploadModel? = null
@Mock
internal var uploadController: UploadController? = null
@Mock
internal var mediaWikiApi: MediaWikiApi? = null
@InjectMocks
var uploadPresenter: UploadPresenter? = null
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
}
@Test
fun receiveMultipleItems() {
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", "Q1", "Test", "Test"
) { _, _ -> }
}
}