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"
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'
}

View file

@ -119,7 +119,7 @@ public class CommonsApplication extends Application {
ContributionUtils.emptyTemporaryDirectory();
initAcra();
if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG && !isRoboUnitTest()) {
Stetho.initializeWithDefaults(this);
}
@ -162,6 +162,10 @@ public class CommonsApplication extends Application {
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)

View file

@ -2,37 +2,31 @@ package fr.free.nrw.commons.upload;
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);
} catch (IOException e) {
Timber.e(e);
}
useExtStorage = prefs.getBoolean("useExternalStorage", true);
}
/**
@ -85,10 +78,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
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 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
* 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");

View file

@ -234,7 +234,7 @@ public class FileUtils {
* @return The value of the _data column, which is typically a file path.
*/
@Nullable
public static String getDataColumn(Context context, Uri uri, String selection,
private static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
@ -311,7 +311,7 @@ public class FileUtils {
* @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 class FileUtils {
return result;
}
public static String getFileExt(String fileName){
static String getFileExt(String fileName){
//Default file extension
String extension=".jpg";
@ -426,7 +426,11 @@ public class FileUtils {
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);
}
}

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
* 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 @@ public class 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 class GPSExtractor {
* @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 class GPSExtractor {
* @param exif exif interface of the image
*
*/
public GPSExtractor(@NonNull ExifInterface exif){
GPSExtractor(@NonNull ExifInterface exif){
processCoords(exif);
}
@ -89,7 +89,7 @@ public class GPSExtractor {
* @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 class GPSExtractor {
}
}
public double getDecLatitude() {
double getDecLatitude() {
return decLatitude;
}
public double getDecLongitude() {
double getDecLongitude() {
return decLongitude;
}

View file

@ -51,7 +51,7 @@ public class UploadController {
}
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 class UploadController {
@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 class UploadController {
/**
* 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 class UploadController {
/**
* Disconnects the upload service.
*/
public void cleanup() {
void cleanup() {
if (isUploadServiceConnected) {
context.unbindService(uploadServiceConnection);
}
@ -89,7 +90,7 @@ public class UploadController {
*
* @param contribution the contribution object
*/
public void startUpload(Contribution contribution) {
void startUpload(Contribution contribution) {
startUpload(contribution, c -> {});
}
@ -100,7 +101,7 @@ public class UploadController {
* @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();

View file

@ -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<String> licenses,
@Named("default_preferences") SharedPreferences prefs,
@Named("licenses_by_name") Map<String, String> 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 @@ public class UploadModel {
.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 @@ public class UploadModel {
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 class UploadModel {
updateItemState();
}
public void setCurrentTitleAndDescriptions(Title title, List<Description> descriptions) {
void setCurrentTitleAndDescriptions(Title title, List<Description> descriptions) {
setCurrentUploadTitle(title);
setCurrentUploadDescriptions(descriptions);
}
@ -337,9 +344,9 @@ public class UploadModel {
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 @@ public class UploadModel {
badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e);
}
public List<UploadItem> getItems() {
return items;
}
@SuppressWarnings("WeakerAccess")
static class UploadItem {

View file

@ -88,7 +88,7 @@ public class UploadService extends HandlerService<Contribution> {
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;

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.content.Context;
@ -13,10 +13,7 @@ import android.view.Display;
* 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();

View file

@ -49,7 +49,7 @@
app:layout_constraintTop_toTopOf="parent"
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:layout_width="match_parent"
android:layout_height="wrap_content"

View file

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

View file

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