diff --git a/app/build.gradle b/app/build.gradle index dd17ee7250..c2c02d2a4f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,6 +77,8 @@ dependencies { releaseImplementation "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 implementation 'com.karumi:dexter:5.0.0' @@ -100,6 +102,8 @@ android { } testOptions { + unitTests.returnDefaultValues = true + unitTests.all { jvmArgs '-noverify' } diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index d874bae400..72518ad58c 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -119,7 +119,7 @@ public void onCreate() { ContributionUtils.emptyTemporaryDirectory(); initAcra(); - if (BuildConfig.DEBUG) { + if (BuildConfig.DEBUG && !isRoboUnitTest()) { Stetho.initializeWithDefaults(this); } @@ -162,6 +162,10 @@ private void initAcra() { Thread.setDefaultUncaughtExceptionHandler(exceptionHandler); } + public static boolean isRoboUnitTest() { + return "robolectric".equals(Build.FINGERPRINT); + } + private ThreadPoolService getFileLoggingThreadPool() { return new ThreadPoolService.Builder("file-logging-thread") .setPriority(Process.THREAD_PRIORITY_LOWEST) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java index c5b6df227e..85205f0797 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java @@ -2,37 +2,31 @@ import android.annotation.SuppressLint; import android.content.ContentResolver; -import android.content.Context; import android.content.SharedPreferences; import android.media.ExifInterface; import android.net.Uri; import android.os.Build; -import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.app.AppCompatActivity; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.util.Date; import java.util.List; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Singleton; import fr.free.nrw.commons.caching.CacheController; -import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.mwapi.CategoryApi; import io.reactivex.schedulers.Schedulers; 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 */ +@Singleton public class FileProcessor implements SimilarImageDialogFragment.onResponse { @Inject @@ -47,24 +41,23 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { private String filePath; private ContentResolver contentResolver; private GPSExtractor imageObj; - private Context context; private String decimalCoords; private ExifInterface exifInterface; - private boolean useExtStorage; private boolean haveCheckedForOtherImages = false; private GPSExtractor tempImageObj; - FileProcessor(@NonNull String filePath, ContentResolver contentResolver, Context context) { + @Inject + FileProcessor() { + } + + void initFileDetails(@NonNull String filePath, ContentResolver contentResolver) { this.filePath = filePath; this.contentResolver = contentResolver; - this.context = context; - ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this); try { - exifInterface=new ExifInterface(filePath); + exifInterface = new ExifInterface(filePath); } catch (IOException e) { Timber.e(e); } - useExtStorage = prefs.getBoolean("useExternalStorage", true); } /** @@ -85,10 +78,6 @@ GPSExtractor processFileCoordinates(SimilarImageInterface similarImageInterface) return imageObj; } - String getDecimalCoords() { - return decimalCoords; - } - /** * Find other images around the same location that were taken within the last 20 sec * @param similarImageInterface @@ -142,7 +131,7 @@ private void findOtherImages(SimilarImageInterface similarImageInterface) { * Then initiates the calls to MediaWiki API through an instance of CategoryApi. */ @SuppressLint("CheckResult") - public void useImageCoords() { + private void useImageCoords() { if (decimalCoords != null) { Timber.d("Decimal coords of image: %s", decimalCoords); Timber.d("is EXIF data present:" + imageObj.imageCoordsExists + " from findOther image"); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 9401c941eb..3b8c07eae7 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -234,8 +234,8 @@ else if ("file".equalsIgnoreCase(uri.getScheme())) { * @return The value of the _data column, which is typically a file path. */ @Nullable - public static String getDataColumn(Context context, Uri uri, String selection, - String[] selectionArgs) { + private static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { Cursor cursor = null; final String column = MediaStore.Images.ImageColumns.DATA; @@ -311,7 +311,7 @@ public static void copy(@NonNull FileInputStream source, @NonNull FileOutputStre * @param destination file path copied to * @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 { copy(new FileInputStream(source), new FileOutputStream(destination)); } @@ -415,7 +415,7 @@ public static String getFilename(Uri uri, ContentResolver contentResolver) { return result; } - public static String getFileExt(String fileName){ + static String getFileExt(String fileName){ //Default file extension String extension=".jpg"; @@ -426,7 +426,11 @@ public static String getFileExt(String fileName){ return extension; } - public static String getFileExt(Uri uri, ContentResolver contentResolver) { + 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); + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java new file mode 100644 index 0000000000..894afb9f17 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java @@ -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); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java index a6b150c42d..4559375c69 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java @@ -14,12 +14,12 @@ * 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. */ -public class GPSExtractor { +class GPSExtractor { - public static final GPSExtractor DUMMY= new GPSExtractor(); + static final GPSExtractor DUMMY= new GPSExtractor(); private double decLatitude; private double decLongitude; - public boolean imageCoordsExists; + boolean imageCoordsExists; private String latitude; private String longitude; private String latitudeRef; @@ -37,7 +37,7 @@ private GPSExtractor(){ * @param fileDescriptor the file descriptor of the image */ @RequiresApi(24) - public GPSExtractor(@NonNull FileDescriptor fileDescriptor) { + GPSExtractor(@NonNull FileDescriptor fileDescriptor) { try { ExifInterface exif = new ExifInterface(fileDescriptor); processCoords(exif); @@ -51,7 +51,7 @@ public GPSExtractor(@NonNull FileDescriptor fileDescriptor) { * @param path file path of the image * */ - public GPSExtractor(@NonNull String path) { + GPSExtractor(@NonNull String path) { try { ExifInterface exif = new ExifInterface(path); processCoords(exif); @@ -65,7 +65,7 @@ public GPSExtractor(@NonNull String path) { * @param exif exif interface of the image * */ - public GPSExtractor(@NonNull ExifInterface exif){ + GPSExtractor(@NonNull ExifInterface exif){ processCoords(exif); } @@ -89,7 +89,7 @@ private void processCoords(ExifInterface exif){ * @return coordinates as string (needs to be passed as a String in API query) */ @Nullable - public String getCoords() { + String getCoords() { if(decimalCoords!=null){ return decimalCoords; }else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) { @@ -103,11 +103,11 @@ public String getCoords() { } } - public double getDecLatitude() { + double getDecLatitude() { return decLatitude; } - public double getDecLongitude() { + double getDecLongitude() { return decLongitude; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index fd0563ab33..ca1179de48 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -51,7 +51,7 @@ public UploadController(SessionManager sessionManager, Context context, SharedPr } private boolean isUploadServiceConnected; - private ServiceConnection uploadServiceConnection = new ServiceConnection() { + public ServiceConnection uploadServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder binder) { uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService(); @@ -61,6 +61,7 @@ public void onServiceConnected(ComponentName componentName, IBinder binder) { @Override public void onServiceDisconnected(ComponentName componentName) { // this should never happen + isUploadServiceConnected = false; Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); } }; @@ -68,7 +69,7 @@ public void onServiceDisconnected(ComponentName componentName) { /** * Prepares the upload service. */ - public void prepareService() { + void prepareService() { Intent uploadServiceIntent = new Intent(context, UploadService.class); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); context.startService(uploadServiceIntent); @@ -78,7 +79,7 @@ public void prepareService() { /** * Disconnects the upload service. */ - public void cleanup() { + void cleanup() { if (isUploadServiceConnected) { context.unbindService(uploadServiceConnection); } @@ -89,7 +90,7 @@ public void cleanup() { * * @param contribution the contribution object */ - public void startUpload(Contribution contribution) { + void startUpload(Contribution contribution) { startUpload(contribution, c -> {}); } @@ -100,7 +101,7 @@ public void startUpload(Contribution contribution) { * @param onComplete the progress tracker */ @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 if (TextUtils.isEmpty(contribution.getCreator())) { Account currentAccount = sessionManager.getCurrentAccount(); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java index 46193e958c..b1e1ad820a 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -53,16 +53,20 @@ public class UploadModel { private boolean useExtStorage; private Disposable badImageSubscription; - @Inject - SessionManager sessionManager; + private SessionManager sessionManager; private Uri currentMediaUri; + private FileUtilsWrapper fileUtilsWrapper; + private FileProcessor fileProcessor; @Inject UploadModel(@Named("licenses") List licenses, @Named("default_preferences") SharedPreferences prefs, @Named("licenses_by_name") Map licensesByName, Context context, - MediaWikiApi mwApi) { + MediaWikiApi mwApi, + SessionManager sessionManager, + FileUtilsWrapper fileUtilsWrapper, + FileProcessor fileProcessor) { this.licenses = licenses; this.prefs = prefs; this.license = Prefs.Licenses.CC_BY_SA_3; @@ -70,6 +74,9 @@ public class UploadModel { this.context = context; this.mwApi = mwApi; this.contentResolver = context.getContentResolver(); + this.sessionManager = sessionManager; + this.fileUtilsWrapper = fileUtilsWrapper; + this.fileProcessor = fileProcessor; useExtStorage = this.prefs.getBoolean("useExternalStorage", false); } @@ -84,17 +91,17 @@ void receive(List mediaUri, String mimeType, String source, SimilarImageInt .map(filePath -> { long fileCreatedDate = getFileCreatedDate(currentMediaUri); Uri uri = Uri.fromFile(new File(filePath)); - FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context); - UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface), - FileUtils.getFileExt(filePath), null,fileCreatedDate); + fileProcessor.initFileDetails(filePath, context.getContentResolver()); + UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface), + fileUtilsWrapper.getFileExt(filePath), null,fileCreatedDate); Single.zip( Single.fromCallable(() -> - new FileInputStream(filePath)) - .map(FileUtils::getSHA1) + fileUtilsWrapper.getFileInputStream(filePath)) + .map(fileUtilsWrapper::getSHA1) .map(mwApi::existingFile) .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK), Single.fromCallable(() -> - new FileInputStream(filePath)) + fileUtilsWrapper.getFileInputStream(filePath)) .map(file -> BitmapRegionDecoder.newInstance(file, false)) .map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK (dupe, dark) -> dupe | dark) @@ -113,24 +120,24 @@ void receiveDirect(Uri media, String mimeType, String source, String wikidataEnt long fileCreatedDate = getFileCreatedDate(media); String filePath = this.cacheFileUpload(media); Uri uri = Uri.fromFile(new File(filePath)); - FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context); - UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface), - FileUtils.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate); + fileProcessor.initFileDetails(filePath, context.getContentResolver()); + UploadItem item = new UploadItem(uri, mimeType, source, fileProcessor.processFileCoordinates(similarImageInterface), + fileUtilsWrapper.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate); item.title.setTitleText(title); item.descriptions.get(0).setDescriptionText(desc); //TODO figure out if default descriptions in other languages exist item.descriptions.get(0).setLanguageCode("en"); Single.zip( Single.fromCallable(() -> - new FileInputStream(filePath)) - .map(FileUtils::getSHA1) + fileUtilsWrapper.getFileInputStream(filePath)) + .map(fileUtilsWrapper::getSHA1) .map(mwApi::existingFile) .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK), Single.fromCallable(() -> - new FileInputStream(filePath)) + fileUtilsWrapper.getFileInputStream(filePath)) .map(file -> BitmapRegionDecoder.newInstance(file, false)) .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.get(0).selected = true; items.get(0).first = true; @@ -239,7 +246,7 @@ public void next() { updateItemState(); } - public void setCurrentTitleAndDescriptions(Title title, List descriptions) { + void setCurrentTitleAndDescriptions(Title title, List descriptions) { setCurrentUploadTitle(title); setCurrentUploadDescriptions(descriptions); } @@ -337,9 +344,9 @@ private String cacheFileUpload(Uri media) { try { String copyPath; if (useExtStorage) - copyPath = FileUtils.createExternalCopyPathAndCopy(media, contentResolver); + copyPath = fileUtilsWrapper.createExternalCopyPathAndCopy(media, contentResolver); else - copyPath = FileUtils.createCopyPathAndCopy(media, context); + copyPath = fileUtilsWrapper.createCopyPathAndCopy(media, context); Timber.i("File path is " + copyPath); return copyPath; } catch (IOException e) { @@ -362,6 +369,9 @@ void subscribeBadPicture(Consumer consumer) { badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e); } + public List getItems() { + return items; + } @SuppressWarnings("WeakerAccess") static class UploadItem { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index e7920f3171..0873d46d8e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -88,7 +88,7 @@ private class NotificationUpdateProgressListener implements MediaWikiApi.Progres String notificationProgressTitle; 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.notificationProgressTitle = notificationProgressTitle; this.notificationFinishingTitle = notificationFinishingTitle; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java deleted file mode 100644 index a28fde5798..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java +++ /dev/null @@ -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 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"); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java b/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java deleted file mode 100644 index 438c7f77b9..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java b/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java similarity index 97% rename from app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java rename to app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java index ff100e16e8..b1dc29736f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java +++ b/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java @@ -1,4 +1,4 @@ -package fr.free.nrw.commons.upload; +package fr.free.nrw.commons.widget; import android.app.Activity; import android.content.Context; @@ -13,10 +13,7 @@ * Created by Ilgaz Er on 8/7/2018. */ public class HeightLimitedRecyclerView extends RecyclerView { - int height; - - public HeightLimitedRecyclerView(Context context) { super(context); DisplayMetrics displayMetrics = new DisplayMetrics(); diff --git a/app/src/main/res/layout/activity_upload_bottom_card.xml b/app/src/main/res/layout/activity_upload_bottom_card.xml index 273835b5d6..2eab37bfde 100644 --- a/app/src/main/res/layout/activity_upload_bottom_card.xml +++ b/app/src/main/res/layout/activity_upload_bottom_card.xml @@ -49,7 +49,7 @@ app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/ic_expand_less_black_24dp" /> - ? = null + @Mock + @field:[Inject Named("default_preferences")] + internal var prefs: SharedPreferences? = null + @Mock + @field:[Inject Named("licenses_by_name")] + internal var licensesByName: Map? = 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 = mutableListOf(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 = mutableListOf(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 = mutableListOf(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 = mutableListOf(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 = mutableListOf(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 = mutableListOf(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 = mutableListOf(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 = mutableListOf(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 = mutableListOf(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 = mutableListOf(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 = mutableListOf(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 = mutableListOf(element, element2) + uploadModel!!.receive(uriList, "image/jpeg", "external") { _, _ -> } + assertTrue(uploadModel!!.isShowingItem) + } + + @Test + fun buildContributions() { + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt new file mode 100644 index 0000000000..96dcc46446 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt @@ -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 = mutableListOf(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" + ) { _, _ -> } + } +} \ No newline at end of file