Refactor uploads (#2981)

* Feature/refractor uploads [WIP] (#2887)

* Fix duplicate param information (#2515)

* Bug fix issue #2476 (#2526)

* Added wikidataEntityID in all db versions, handled db.execSql via method runQuery

* Versioning and changelog for v2.10.2 (#2531)

* Update changelog.md

* Versioning for v2.10.2

* Update changelog.md

* Bugfix/issue 2580 (#2584)

* Corrected string placedholders in certain string files

* Corrected string placedholders in certain string files[Bug fix #2580]

* Bug Fix #2585 (#2647)

* Bug Fix #2585
* Added null checks on view in SearchImageFragment when updating views from external sources
* Disposed the disposables in SearchActivity and SearchImageFragment when no longer in active lifecycle

* use FragmentUtils to verify fragment active state

* Bug Fix issue #2648 (#2678)

* Bug Fix issue #2648
* Handled external storage permission before file download

* * Removed redudant check for permission in MediaDetailPagerFragment (Dexter already does that)
* Removed duplicate code in PermissionUtil$checkPermissionsAndPerformAction, used the existing function with conditional extra parameters

* string name typo correction

* BugFix issue #2652 (#2706)

* Addded null check on bookmark before operating on it

* BugFix issue #2711 (#2712)

* Added null checks in OkHttpJsonApiClient$searchImages MwQueryResponse

* BugFix #2718 (#2719)

* Handled null auth cookies

* Fix #2791: NPE when nominating for deletion and leaving screen (#2792)

* Bug Fix issue #2789 (#2790)

* Handled Illegal State Exception for non existent appropriate view parents in ViewUtils$showShortSnackbar

* BugFix #2720 (#2831)

BugFix deprecated licenes #2720

* ui fixes, wip, upload

* *Issue #2886, BugFix #2832[wip]
* updated UploadActivity code
* modified ui
* Updated UploadPresenterTest

* * updated interfaces names to follow names suffixed with Contract
* added test cases

* card view elevation

* view pager disabled swipe

* bug fix, duplicate image

* used existing non-swipable view pager

* Avoid image view resize with keyboard, added adjustPan and stateVisible as softinputMode for UploadActivity

* retain UploadBaseFragment instances on orientation changes

* * Added test cases for UploadMediaPresenter
* Injected io and main thread schedulers

* categories presenter test cased wip

* Added CategoriesPresenter test

* * Added the logic to show open map (with to be uploaded image's coordinates while uploading image)

* codacy suggested changes * added java docs

* Added travis_wait fot android-wait-for-emulator

* ranamed interface onResponseCallback to Callback

* * Added api to delete picture in UploadModel
* cleanUp in UploadModel. once upload has been initiated
* Removed unused methods from UploadModel and the corresponding test class

* * Added tests for UploadPresenter
* Travis suggested changes
* Addded copy previous title and description

* * Made the upload add descriptions visible when keyboard visible
* add description request focus only when user manually requests it

* Added JavaDocs, review suggested changes

* Fix dagger injection

* use DialogUtil to show info in descriptions

* use activity context for DialogUtil

* Minor changes

* Bug fix, reduced the add description edit text clickable bound (#2973)

* Bugfix/uploads (#3000)

* merged with master

* BugFix IllegalStateException
* setRetainState(true), not required with FragmentStatePagerAdapter
* Increase the ViewPager's Offscreen Limit, we want all the fragments to be active

* BugFix, clear selected categoris for previous upload session
* Clear Selected Categories
* Addded JavaDocs for CategoriesModel

* Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java

* Added class level JavaDoc UploadRemoteDataSource

* Added class level JavaDoc for UploadRepository

* Added JavaDocs for ThumbnailsAdapter

* Added JavaDocs for MediaLicensePresenter, CategoriesPresenter

* Removed null check on category query
* Show default catgeories based on image title and gps location when category text empty
* Allow search for empty category search

* Attached image scale listener to upload media image

* Bug fix, reduced the add description edit text clickable bound

* Fix memory leak (#3001)

* Bugfix/uploads (#3002)

* merged with master

* BugFix IllegalStateException
* setRetainState(true), not required with FragmentStatePagerAdapter
* Increase the ViewPager's Offscreen Limit, we want all the fragments to be active

* BugFix, clear selected categoris for previous upload session
* Clear Selected Categories
* Addded JavaDocs for CategoriesModel

* Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java

* Added class level JavaDoc UploadRemoteDataSource

* Added class level JavaDoc for UploadRepository

* Added JavaDocs for ThumbnailsAdapter

* Added JavaDocs for MediaLicensePresenter, CategoriesPresenter

* Removed null check on category query
* Show default catgeories based on image title and gps location when category text empty
* Allow search for empty category search

* Attached image scale listener to upload media image

* Bug fix, reduced the add description edit text clickable bound

* Added tooltip in Title in UploadMediaFragment

* BugFix recent categories

* Updated test methods

* Bugfix/uploads (#3011)

* merged with master

* BugFix IllegalStateException
* setRetainState(true), not required with FragmentStatePagerAdapter
* Increase the ViewPager's Offscreen Limit, we want all the fragments to be active

* BugFix, clear selected categoris for previous upload session
* Clear Selected Categories
* Addded JavaDocs for CategoriesModel

* Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java

* Added class level JavaDoc UploadRemoteDataSource

* Added class level JavaDoc for UploadRepository

* Added JavaDocs for ThumbnailsAdapter

* Added JavaDocs for MediaLicensePresenter, CategoriesPresenter

* Removed null check on category query
* Show default catgeories based on image title and gps location when category text empty
* Allow search for empty category search

* Attached image scale listener to upload media image

* Bug fix, reduced the add description edit text clickable bound

* Added tooltip in Title in UploadMediaFragment

* BugFix recent categories

* Updated test methods

* Avoid memory leak, free the adpater in MediaLicenseFragment.onDestroyView

* bugfix/uploads (#3012)

* merged with master

* BugFix IllegalStateException
* setRetainState(true), not required with FragmentStatePagerAdapter
* Increase the ViewPager's Offscreen Limit, we want all the fragments to be active

* BugFix, clear selected categoris for previous upload session
* Clear Selected Categories
* Addded JavaDocs for CategoriesModel

* Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java

* Added class level JavaDoc UploadRemoteDataSource

* Added class level JavaDoc for UploadRepository

* Added JavaDocs for ThumbnailsAdapter

* Added JavaDocs for MediaLicensePresenter, CategoriesPresenter

* Removed null check on category query
* Show default catgeories based on image title and gps location when category text empty
* Allow search for empty category search

* Attached image scale listener to upload media image

* Bug fix, reduced the add description edit text clickable bound

* Added tooltip in Title in UploadMediaFragment

* BugFix recent categories

* Updated test methods

* Avoid memory leak, free the adpater in MediaLicenseFragment.onDestroyView

* BugFix Illegal State Exception in ViewpPagerAdapter

* Remove irrelevant comment

* merge conflict with strings (#3016)
This commit is contained in:
Ashish Kumar 2019-06-14 01:09:41 +05:30 committed by Vivek Maskara
parent 04b051b37a
commit 7a5dc77057
68 changed files with 3753 additions and 2086 deletions

View file

@ -65,6 +65,8 @@ dependencies {
testImplementation 'androidx.test:core:1.2.0' testImplementation 'androidx.test:core:1.2.0'
testImplementation 'com.nhaarman:mockito-kotlin:1.5.0' testImplementation 'com.nhaarman:mockito-kotlin:1.5.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0' testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5"
testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
// Android testing // Android testing
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"

View file

@ -50,10 +50,13 @@
</activity> </activity>
<activity android:name=".WelcomeActivity" /> <activity android:name=".WelcomeActivity" />
<activity android:name=".upload.UploadActivity" <activity
android:name=".upload.UploadActivity"
android:configChanges="orientation|screenSize|keyboard"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:configChanges="orientation|screenSize|keyboard"> android:windowSoftInputMode="adjustResize"
>
<intent-filter android:label="@string/intent_share_upload_label"> <intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />

View file

@ -3,11 +3,11 @@ package fr.free.nrw.commons;
/** /**
* Base presenter, enforcing contracts to atach and detach view * Base presenter, enforcing contracts to atach and detach view
*/ */
public interface BasePresenter { public interface BasePresenter<T> {
/** /**
* Until a view is attached, it is open to listen events from the presenter * Until a view is attached, it is open to listen events from the presenter
*/ */
void onAttachView(MvpView view); void onAttachView(T view);
/** /**
* Detaching a view makes sure that the view no more receives events from the presenter * Detaching a view makes sure that the view no more receives events from the presenter

View file

@ -11,6 +11,8 @@ import android.widget.Toast;
import org.wikipedia.dataclient.WikiSite; import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.page.PageTitle; import org.wikipedia.page.PageTitle;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.ViewUtil;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -18,9 +20,7 @@ import java.util.regex.Pattern;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber; import timber.log.Timber;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;

View file

@ -28,7 +28,7 @@ import timber.log.Timber;
* success and error * success and error
*/ */
@Singleton @Singleton
public class CampaignsPresenter implements BasePresenter { public class CampaignsPresenter implements BasePresenter<ICampaignsView> {
private final OkHttpJsonApiClient okHttpJsonApiClient; private final OkHttpJsonApiClient okHttpJsonApiClient;
private ICampaignsView view; private ICampaignsView view;
@ -40,8 +40,9 @@ public class CampaignsPresenter implements BasePresenter {
this.okHttpJsonApiClient = okHttpJsonApiClient; this.okHttpJsonApiClient = okHttpJsonApiClient;
} }
@Override public void onAttachView(MvpView view) { @Override
this.view = (ICampaignsView) view; public void onAttachView(ICampaignsView view) {
this.view = view;
} }
@Override public void onDetachView() { @Override public void onDetachView() {

View file

@ -1,25 +1,25 @@
package fr.free.nrw.commons.category; package fr.free.nrw.commons.category;
import android.text.TextUtils; import android.text.TextUtils;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
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 fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable;
import timber.log.Timber; import timber.log.Timber;
public class CategoriesModel implements CategoryClickedListener { /**
* The model class for categories in upload
*/
public class CategoriesModel{
private static final int SEARCH_CATS_LIMIT = 25; private static final int SEARCH_CATS_LIMIT = 25;
private final MediaWikiApi mwApi; private final MediaWikiApi mwApi;
@ -41,13 +41,22 @@ public class CategoriesModel implements CategoryClickedListener {
this.selectedCategories = new ArrayList<>(); this.selectedCategories = new ArrayList<>();
} }
//region Misc. utility methods /**
* Sorts CategoryItem by similarity
* @param filter
* @return
*/
public Comparator<CategoryItem> sortBySimilarity(final String filter) { public Comparator<CategoryItem> sortBySimilarity(final String filter) {
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter); Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
return (firstItem, secondItem) -> stringSimilarityComparator return (firstItem, secondItem) -> stringSimilarityComparator
.compare(firstItem.getName(), secondItem.getName()); .compare(firstItem.getName(), secondItem.getName());
} }
/**
* Returns if the item contains an year
* @param item
* @return
*/
public boolean containsYear(String item) { public boolean containsYear(String item) {
//Check for current and previous year to exclude these categories from removal //Check for current and previous year to exclude these categories from removal
Calendar now = Calendar.getInstance(); Calendar now = Calendar.getInstance();
@ -67,6 +76,10 @@ public class CategoriesModel implements CategoryClickedListener {
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*"))); || (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
} }
/**
* Updates category count in category dao
* @param item
*/
public void updateCategoryCount(CategoryItem item) { public void updateCategoryCount(CategoryItem item) {
Category category = categoryDao.find(item.getName()); Category category = categoryDao.find(item.getName());
@ -78,29 +91,27 @@ public class CategoriesModel implements CategoryClickedListener {
category.incTimesUsed(); category.incTimesUsed();
categoryDao.save(category); categoryDao.save(category);
} }
//endregion
//region Category Caching
public void cacheAll(HashMap<String, ArrayList<String>> categories) {
categoriesCache.putAll(categories);
}
public HashMap<String, ArrayList<String>> getCategoriesCache() {
return categoriesCache;
}
boolean cacheContainsKey(String term) { boolean cacheContainsKey(String term) {
return categoriesCache.containsKey(term); return categoriesCache.containsKey(term);
} }
//endregion //endregion
//region Category searching /**
* Regional category search
* @param term
* @param imageTitleList
* @return
*/
public Observable<CategoryItem> searchAll(String term, List<String> imageTitleList) { public Observable<CategoryItem> searchAll(String term, List<String> imageTitleList) {
//If user hasn't typed anything in yet, get GPS and recent items //If query text is empty, show him category based on gps and title and recent searches
if (TextUtils.isEmpty(term)) { if (TextUtils.isEmpty(term)) {
return gpsCategories() Observable<CategoryItem> categoryItemObservable = gpsCategories()
.concatWith(titleCategories(imageTitleList)) .concatWith(titleCategories(imageTitleList));
.concatWith(recentCategories()); if (hasDirectCategories()) {
categoryItemObservable.concatWith(directCategories().concatWith(recentCategories()));
}
return categoryItemObservable;
} }
//if user types in something that is in cache, return cached category //if user types in something that is in cache, return cached category
@ -115,43 +126,28 @@ public class CategoriesModel implements CategoryClickedListener {
.map(name -> new CategoryItem(name, false)); .map(name -> new CategoryItem(name, false));
} }
public Observable<CategoryItem> searchCategories(String term, List<String> imageTitleList) {
//If user hasn't typed anything in yet, get GPS and recent items
if (TextUtils.isEmpty(term)) {
return gpsCategories()
.concatWith(titleCategories(imageTitleList))
.concatWith(recentCategories());
}
return mwApi
.searchCategories(term, SEARCH_CATS_LIMIT)
.map(s -> new CategoryItem(s, false));
}
/**
* Returns cached categories
* @param term
* @return
*/
private ArrayList<String> getCachedCategories(String term) { private ArrayList<String> getCachedCategories(String term) {
return categoriesCache.get(term); return categoriesCache.get(term);
} }
public Observable<CategoryItem> defaultCategories(List<String> titleList) { /**
Observable<CategoryItem> directCat = directCategories(); * Returns if we have a category in DirectKV Store
if (hasDirectCategories()) { * @return
Timber.d("Image has direct Cat"); */
return directCat
.concatWith(gpsCategories())
.concatWith(titleCategories(titleList))
.concatWith(recentCategories());
} else {
Timber.d("Image has no direct Cat");
return gpsCategories()
.concatWith(titleCategories(titleList))
.concatWith(recentCategories());
}
}
private boolean hasDirectCategories() { private boolean hasDirectCategories() {
return !directKvStore.getString("Category", "").equals(""); return !directKvStore.getString("Category", "").equals("");
} }
/**
* Returns categories in DirectKVStore
* @return
*/
private Observable<CategoryItem> directCategories() { private Observable<CategoryItem> directCategories() {
String directCategory = directKvStore.getString("Category", ""); String directCategory = directKvStore.getString("Category", "");
List<String> categoryList = new ArrayList<>(); List<String> categoryList = new ArrayList<>();
@ -164,30 +160,49 @@ public class CategoriesModel implements CategoryClickedListener {
return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false)); return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
} }
/**
* Returns GPS categories
* @return
*/
Observable<CategoryItem> gpsCategories() { Observable<CategoryItem> gpsCategories() {
return Observable.fromIterable(gpsCategoryModel.getCategoryList()) return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false)); .map(name -> new CategoryItem(name, false));
} }
/**
* Returns title based categories
* @param titleList
* @return
*/
private Observable<CategoryItem> titleCategories(List<String> titleList) { private Observable<CategoryItem> titleCategories(List<String> titleList) {
return Observable.fromIterable(titleList) return Observable.fromIterable(titleList)
.concatMap(this::getTitleCategories); .concatMap(this::getTitleCategories);
} }
/**
* Return category for single title
* @param title
* @return
*/
private Observable<CategoryItem> getTitleCategories(String title) { private Observable<CategoryItem> getTitleCategories(String title) {
return mwApi.searchTitles(title, SEARCH_CATS_LIMIT) return mwApi.searchTitles(title, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false)); .map(name -> new CategoryItem(name, false));
} }
/**
* Returns recent categories
* @return
*/
private Observable<CategoryItem> recentCategories() { private Observable<CategoryItem> recentCategories() {
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT)) return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
.map(s -> new CategoryItem(s, false)); .map(s -> new CategoryItem(s, false));
} }
//endregion
//region Category Selection /**
@Override * Handles category item selection
public void categoryClicked(CategoryItem item) { * @param item
*/
public void onCategoryItemClicked(CategoryItem item) {
if (item.isSelected()) { if (item.isSelected()) {
selectCategory(item); selectCategory(item);
updateCategoryCount(item); updateCategoryCount(item);
@ -196,22 +211,35 @@ public class CategoriesModel implements CategoryClickedListener {
} }
} }
/**
* Select's category
* @param item
*/
public void selectCategory(CategoryItem item) { public void selectCategory(CategoryItem item) {
selectedCategories.add(item); selectedCategories.add(item);
} }
/**
* Unselect Category
* @param item
*/
public void unselectCategory(CategoryItem item) { public void unselectCategory(CategoryItem item) {
selectedCategories.remove(item); selectedCategories.remove(item);
} }
public int selectedCategoriesCount() {
return selectedCategories.size();
}
/**
* Get Selected Categories
* @return
*/
public List<CategoryItem> getSelectedCategories() { public List<CategoryItem> getSelectedCategories() {
return selectedCategories; return selectedCategories;
} }
/**
* Get Categories String List
* @return
*/
public List<String> getCategoryStringList() { public List<String> getCategoryStringList() {
List<String> output = new ArrayList<>(); List<String> output = new ArrayList<>();
for (CategoryItem item : selectedCategories) { for (CategoryItem item : selectedCategories) {
@ -219,6 +247,12 @@ public class CategoriesModel implements CategoryClickedListener {
} }
return output; return output;
} }
//endregion
/**
* Cleanup the existing in memory cache's
*/
public void cleanUp() {
this.categoriesCache.clear();
this.selectedCategories.clear();
}
} }

View file

@ -19,7 +19,7 @@ public class CategoryItem implements Parcelable {
} }
}; };
CategoryItem(String name, boolean selected) { public CategoryItem(String name, boolean selected) {
this.name = name; this.name = name;
this.selected = selected; this.selected = selected;
} }

View file

@ -100,7 +100,7 @@ public class ContributionDao {
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime()); cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
} }
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength()); cv.put(Table.COLUMN_LENGTH, contribution.getDataLength());
//This was always meant to store the date created..If somehow date created is not fetched while actually saving the contribution, lets save today's date //This was always meant to store the date created..If somehow date created is not fetched while actually saving the contribution, lets saveValue today's date
cv.put(Table.COLUMN_TIMESTAMP, contribution.getDateCreated()==null?System.currentTimeMillis():contribution.getDateCreated().getTime()); cv.put(Table.COLUMN_TIMESTAMP, contribution.getDateCreated()==null?System.currentTimeMillis():contribution.getDateCreated().getTime());
cv.put(Table.COLUMN_STATE, contribution.getState()); cv.put(Table.COLUMN_STATE, contribution.getState());
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred()); cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());

View file

@ -15,6 +15,7 @@ import fr.free.nrw.commons.nearby.PlaceRenderer;
import fr.free.nrw.commons.review.ReviewController; import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.FileProcessor; import fr.free.nrw.commons.upload.FileProcessor;
import fr.free.nrw.commons.upload.UploadModule;
import fr.free.nrw.commons.widget.PicOfDayAppWidget; import fr.free.nrw.commons.widget.PicOfDayAppWidget;
@ -27,7 +28,7 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget;
ActivityBuilderModule.class, ActivityBuilderModule.class,
FragmentBuilderModule.class, FragmentBuilderModule.class,
ServiceBuilderModule.class, ServiceBuilderModule.class,
ContentProviderBuilderModule.class ContentProviderBuilderModule.class, UploadModule.class
}) })
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> { public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
void inject(CommonsApplication application); void inject(CommonsApplication application);

View file

@ -9,6 +9,11 @@ import com.google.gson.Gson;
import org.wikipedia.dataclient.WikiSite; import org.wikipedia.dataclient.WikiSite;
import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import org.wikipedia.dataclient.WikiSite;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -37,6 +42,8 @@ import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({"WeakerAccess", "unused"})
public class CommonsApplicationModule { public class CommonsApplicationModule {
private Context applicationContext; private Context applicationContext;
public static final String IO_THREAD="io_thread";
public static final String MAIN_THREAD="main_thread";
public CommonsApplicationModule(Context applicationContext) { public CommonsApplicationModule(Context applicationContext) {
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
@ -172,4 +179,16 @@ public class CommonsApplicationModule {
public boolean provideIsBetaVariant() { public boolean provideIsBetaVariant() {
return ConfigUtils.isBetaFlavour(); return ConfigUtils.isBetaFlavour();
} }
@Named(IO_THREAD)
@Provides
public Scheduler providesIoThread(){
return Schedulers.io();
}
@Named(MAIN_THREAD)
@Provides
public Scheduler providesMainThread(){
return AndroidSchedulers.mainThread();
}
} }

View file

@ -18,6 +18,9 @@ import fr.free.nrw.commons.nearby.NearbyListFragment;
import fr.free.nrw.commons.nearby.NearbyMapFragment; import fr.free.nrw.commons.nearby.NearbyMapFragment;
import fr.free.nrw.commons.review.ReviewImageFragment; import fr.free.nrw.commons.review.ReviewImageFragment;
import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
@Module @Module
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({"WeakerAccess", "unused"})
@ -71,4 +74,12 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract ReviewImageFragment bindReviewOutOfContextFragment(); abstract ReviewImageFragment bindReviewOutOfContextFragment();
@ContributesAndroidInjector
abstract UploadMediaDetailFragment bindUploadMediaDetailFragment();
@ContributesAndroidInjector
abstract UploadCategoriesFragment bindUploadCategoriesFragment();
@ContributesAndroidInjector
abstract MediaLicenseFragment bindMediaLicenseFragment();
} }

View file

@ -16,10 +16,6 @@ import butterknife.ButterKnife;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import com.jakewharton.rxbinding2.view.RxView; import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxSearchView; import com.jakewharton.rxbinding2.widget.RxSearchView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxSearchView;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment; import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
@ -33,13 +29,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import io.reactivex.disposables.Disposable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/** /**
* Represents search screen of this app * Represents search screen of this app

View file

@ -57,9 +57,6 @@ import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.util.DateUtil;
import org.wikipedia.util.StringUtil;
import timber.log.Timber; import timber.log.Timber;
import static android.view.View.GONE; import static android.view.View.GONE;

View file

@ -0,0 +1,150 @@
package fr.free.nrw.commons.repository;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.upload.UploadModel;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
/**
* The Local Data Source for UploadRepository, fetches and returns data from local db/shared prefernces
*/
@Singleton
public class UploadLocalDataSource {
private final UploadModel uploadModel;
private JsonKvStore defaultKVStore;
@Inject
public UploadLocalDataSource(
@Named("default_preferences") JsonKvStore defaultKVStore,
UploadModel uploadModel) {
this.defaultKVStore = defaultKVStore;
this.uploadModel = uploadModel;
}
/**
* Fetches and returns the string list of valid licenses
*
* @return
*/
public List<String> getLicenses() {
return uploadModel.getLicenses();
}
/**
* Returns the number of Upload Items
*
* @return
*/
public int getCount() {
return uploadModel.getCount();
}
/**
* Fetches and return the selected license for the current upload
*
* @return
*/
public String getSelectedLicense() {
return uploadModel.getSelectedLicense();
}
/**
* Set selected license for the current upload
*
* @param licenseName
*/
public void setSelectedLicense(String licenseName) {
uploadModel.setSelectedLicense(licenseName);
}
/**
* Updates the current upload item
*
* @param index
* @param uploadItem
*/
public void updateUploadItem(int index, UploadItem uploadItem) {
uploadModel.updateUploadItem(index, uploadItem);
}
/**
* upload is halted, cleanup the acquired resources
*/
public void cleanUp() {
uploadModel.cleanUp();
}
/**
* Deletes the upload item at the current index
*
* @param filePath
*/
public void deletePicture(String filePath) {
uploadModel.deletePicture(filePath);
}
/**
* Fethces and returns the previous upload item, if any, returns null otherwise
*
* @param index
* @return
*/
@Nullable
public UploadItem getPreviousUploadItem(int index) {
if (index - 1 >= 0) {
return uploadModel.getItems().get(index - 1);
}
return null; //There is no previous item to copy details
}
/**
* saves boolean value in default store
*
* @param key
* @param value
*/
public void saveValue(String key, boolean value) {
defaultKVStore.putBoolean(key, value);
}
/**
* saves string value in default store
*
* @param key
* @param value
*/
public void saveValue(String key, String value) {
defaultKVStore.putString(key, value);
}
/**
* Fetches and returns string value from the default store
*
* @param key
* @param defaultValue
* @return
*/
public String getValue(String key, String defaultValue) {
return defaultKVStore.getString(key, defaultValue);
}
/**
* Fetches and returns boolean value from the default store
*
* @param key
* @param defaultValue
* @return
*/
public boolean getValue(String key, boolean defaultValue) {
return defaultKVStore.getBoolean(key, defaultValue);
}
}

View file

@ -0,0 +1,179 @@
package fr.free.nrw.commons.repository;
import fr.free.nrw.commons.category.CategoriesModel;
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.upload.UploadModel;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.util.Comparator;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* This class would act as the data source for remote operations for UploadActivity
*/
@Singleton
public class UploadRemoteDataSource {
private UploadModel uploadModel;
private UploadController uploadController;
private CategoriesModel categoriesModel;
@Inject
public UploadRemoteDataSource(UploadModel uploadModel, UploadController uploadController,
CategoriesModel categoriesModel) {
this.uploadModel = uploadModel;
this.uploadController = uploadController;
this.categoriesModel = categoriesModel;
}
/**
* asks the UploadModel to build the contributions
*
* @return
*/
public Observable<Contribution> buildContributions() {
return uploadModel.buildContributions();
}
/**
* asks the UploadService to star the uplaod for
*
* @param contribution
*/
public void startUpload(Contribution contribution) {
uploadController.startUpload(contribution);
}
/**
* returns the list of UploadItem from the UploadModel
*
* @return
*/
public List<UploadItem> getUploads() {
return uploadModel.getUploads();
}
/**
* Prepare the UploadService for the upload
*/
public void prepareService() {
uploadController.prepareService();
}
/**
* Clean up the UploadController
*/
public void cleanup() {
uploadController.cleanup();
}
/**
* Clean up the selected categories
*/
public void clearSelectedCategories(){
//This needs further refactoring, this should not be here, right now the structure wont suppoort rhis
categoriesModel.cleanUp();
}
/**
* returnt the list of selected categories
*
* @return
*/
public List<CategoryItem> getSelectedCategories() {
return categoriesModel.getSelectedCategories();
}
/**
* all categories from MWApi
*
* @param query
* @param imageTitleList
* @return
*/
public Observable<CategoryItem> searchAll(String query, List<String> imageTitleList) {
return categoriesModel.searchAll(query, imageTitleList);
}
/**
* returns the string list of categories
*
* @return
*/
public List<String> getCategoryStringList() {
return categoriesModel.getCategoryStringList();
}
/**
* sets the selected categories in the UploadModel
*
* @param categoryStringList
*/
public void setSelectedCategories(List<String> categoryStringList) {
uploadModel.setSelectedCategories(categoryStringList);
}
/**
* handles category selection/unselection
*
* @param categoryItem
*/
public void onCategoryClicked(CategoryItem categoryItem) {
categoriesModel.onCategoryItemClicked(categoryItem);
}
/**
* returns category sorted based on similarity with query
*
* @param query
* @return
*/
public Comparator<CategoryItem> sortBySimilarity(String query) {
return categoriesModel.sortBySimilarity(query);
}
/**
* prunes the category list for irrelevant categories see #750
*
* @param name
* @return
*/
public boolean containsYear(String name) {
return categoriesModel.containsYear(name);
}
/**
* pre process the UploadableFile
*
* @param uploadableFile
* @param place
* @param source
* @param similarImageInterface
* @return
*/
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place,
String source, SimilarImageInterface similarImageInterface) {
return uploadModel.preProcessImage(uploadableFile, place, source, similarImageInterface);
}
/**
* ask the UplaodModel for the image quality of the UploadItem
*
* @param uploadItem
* @param shouldValidateTitle
* @return
*/
public Single<Integer> getImageQuality(UploadItem uploadItem, boolean shouldValidateTitle) {
return uploadModel.getImageQuality(uploadItem, shouldValidateTitle);
}
}

View file

@ -0,0 +1,265 @@
package fr.free.nrw.commons.repository;
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.util.Comparator;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* The repository class for UploadActivity
*/
@Singleton
public class UploadRepository {
private UploadLocalDataSource localDataSource;
private UploadRemoteDataSource remoteDataSource;
@Inject
public UploadRepository(UploadLocalDataSource localDataSource,
UploadRemoteDataSource remoteDataSource) {
this.localDataSource = localDataSource;
this.remoteDataSource = remoteDataSource;
}
/**
* asks the RemoteDataSource to build contributions
*
* @return
*/
public Observable<Contribution> buildContributions() {
return remoteDataSource.buildContributions();
}
/**
* asks the RemoteDataSource to start upload for the contribution
*
* @param contribution
*/
public void startUpload(Contribution contribution) {
remoteDataSource.startUpload(contribution);
}
/**
* Fetches and returns all the Upload Items
*
* @return
*/
public List<UploadItem> getUploads() {
return remoteDataSource.getUploads();
}
/**
* asks the RemoteDataSource to prepare the Upload Service
*/
public void prepareService() {
remoteDataSource.prepareService();
}
/**
*Prepare for a fresh upload
*/
public void cleanup() {
localDataSource.cleanUp();
remoteDataSource.clearSelectedCategories();
}
/**
* Fetches and returns the selected categories for the current upload
*
* @return
*/
public List<CategoryItem> getSelectedCategories() {
return remoteDataSource.getSelectedCategories();
}
/**
* all categories from MWApi
*
* @param query
* @param imageTitleList
* @return
*/
public Observable<CategoryItem> searchAll(String query, List<String> imageTitleList) {
return remoteDataSource.searchAll(query, imageTitleList);
}
/**
* returns the string list of categories
*
* @return
*/
public List<String> getCategoryStringList() {
return remoteDataSource.getCategoryStringList();
}
/**
* sets the list of selected categories for the current upload
*
* @param categoryStringList
*/
public void setSelectedCategories(List<String> categoryStringList) {
remoteDataSource.setSelectedCategories(categoryStringList);
}
/**
* handles the category selection/deselection
*
* @param categoryItem
*/
public void onCategoryClicked(CategoryItem categoryItem) {
remoteDataSource.onCategoryClicked(categoryItem);
}
/**
* returns category sorted based on similarity with query
*
* @param query
* @return
*/
public Comparator<? super CategoryItem> sortBySimilarity(String query) {
return remoteDataSource.sortBySimilarity(query);
}
/**
* prunes the category list for irrelevant categories see #750
*
* @param name
* @return
*/
public boolean containsYear(String name) {
return remoteDataSource.containsYear(name);
}
/**
* retursn the string list of available license from the LocalDataSource
*
* @return
*/
public List<String> getLicenses() {
return localDataSource.getLicenses();
}
/**
* returns the selected license for the current upload
*
* @return
*/
public String getSelectedLicense() {
return localDataSource.getSelectedLicense();
}
/**
* returns the number of Upload Items
*
* @return
*/
public int getCount() {
return localDataSource.getCount();
}
/**
* ask the RemoteDataSource to pre process the image
*
* @param uploadableFile
* @param place
* @param source
* @param similarImageInterface
* @return
*/
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place,
String source, SimilarImageInterface similarImageInterface) {
return remoteDataSource
.preProcessImage(uploadableFile, place, source, similarImageInterface);
}
/**
* query the RemoteDataSource for image quality
*
* @param uploadItem
* @param shouldValidateTitle
* @return
*/
public Single<Integer> getImageQuality(UploadItem uploadItem, boolean shouldValidateTitle) {
return remoteDataSource.getImageQuality(uploadItem, shouldValidateTitle);
}
/**
* asks the LocalDataSource to update the Upload Item
*
* @param index
* @param uploadItem
*/
public void updateUploadItem(int index, UploadItem uploadItem) {
localDataSource.updateUploadItem(index, uploadItem);
}
/**
* asks the LocalDataSource to delete the file with the given file path
*
* @param filePath
*/
public void deletePicture(String filePath) {
localDataSource.deletePicture(filePath);
}
/**
* fetches and returns the previous upload item
*
* @param index
* @return
*/
public UploadItem getPreviousUploadItem(int index) {
return localDataSource.getPreviousUploadItem(index);
}
/**
* Save boolean value locally
*
* @param key
* @param value
*/
public void saveValue(String key, boolean value) {
localDataSource.saveValue(key, value);
}
/**
* save string value locally
*
* @param key
* @param value
*/
public void saveValue(String key, String value) {
localDataSource.saveValue(key, value);
}
/**
* fetch the string value for the associated key
*
* @param key
* @param value
* @return
*/
public String getValue(String key, String value) {
return localDataSource.getValue(key, value);
}
/**
* set selected license for the current upload
*
* @param licenseName
*/
public void setSelectedLicense(String licenseName) {
localDataSource.setSelectedLicense(licenseName);
}
}

View file

@ -5,11 +5,12 @@ import java.util.List;
/** /**
* Holds a description of an item being uploaded by {@link UploadActivity} * Holds a description of an item being uploaded by {@link UploadActivity}
*/ */
class Description { public class Description {
private String languageCode; private String languageCode;
private String descriptionText; private String descriptionText;
private int selectedLanguageIndex = -1; private int selectedLanguageIndex = -1;
private boolean isManuallyAdded=false;
/** /**
* @return The language code ie. "en" or "fr" * @return The language code ie. "en" or "fr"
@ -47,6 +48,21 @@ class Description {
this.selectedLanguageIndex = selectedLanguageIndex; this.selectedLanguageIndex = selectedLanguageIndex;
} }
/**
* returns if the description was added manually (by the user, or we have added it programaticallly)
* @return
*/
public boolean isManuallyAdded() {
return isManuallyAdded;
}
/**
* sets to true if the description was manually added by the user
* @param manuallyAdded
*/
public void setManuallyAdded(boolean manuallyAdded) {
isManuallyAdded = manuallyAdded;
}
/** /**
* Formats the list of descriptions into the format Commons requires for uploads. * Formats the list of descriptions into the format Commons requires for uploads.

View file

@ -1,22 +1,22 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener; import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.EditText;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.AppCompatSpinner; import androidx.appcompat.widget.AppCompatSpinner;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView; import butterknife.BindView;
@ -24,60 +24,35 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.AbstractTextWatcher; import fr.free.nrw.commons.utils.AbstractTextWatcher;
import fr.free.nrw.commons.utils.BiMap; import fr.free.nrw.commons.utils.BiMap;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.subjects.BehaviorSubject;
import io.reactivex.subjects.Subject;
import timber.log.Timber; import timber.log.Timber;
class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> { public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {
private Title title;
private List<Description> descriptions; private List<Description> descriptions;
private Context context;
private Callback callback; private Callback callback;
private Subject<String> titleChangedSubject;
private BiMap<AdapterView, String> selectedLanguages; private BiMap<AdapterView, String> selectedLanguages;
private UploadView uploadView;
DescriptionsAdapter(UploadView uploadView) { public DescriptionsAdapter() {
title = new Title();
descriptions = new ArrayList<>(); descriptions = new ArrayList<>();
titleChangedSubject = BehaviorSubject.create();
selectedLanguages = new BiMap<>(); selectedLanguages = new BiMap<>();
this.uploadView = uploadView;
} }
void setCallback(Callback callback) { public void setCallback(Callback callback) {
this.callback = callback; this.callback = callback;
} }
void setItems(Title title, List<Description> descriptions) { public void setItems(List<Description> descriptions) {
this.descriptions = descriptions; this.descriptions = descriptions;
this.title = title;
selectedLanguages = new BiMap<>(); selectedLanguages = new BiMap<>();
notifyDataSetChanged(); notifyDataSetChanged();
} }
@Override
public int getItemViewType(int position) {
if (position == 0) return 1;
else return 2;
}
@NonNull @NonNull
@Override @Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view; return new ViewHolder(LayoutInflater.from(parent.getContext())
if (viewType == 1) { .inflate(R.layout.row_item_description, parent, false));
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.row_item_title, parent, false);
} else {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.row_item_description, parent, false);
}
context = parent.getContext();
return new ViewHolder(view);
} }
@Override @Override
@ -87,29 +62,21 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
@Override @Override
public int getItemCount() { public int getItemCount() {
return descriptions.size() + 1; return descriptions.size();
} }
/** /**
* Gets descriptions * Gets descriptions
*
* @return List of descriptions * @return List of descriptions
*/ */
List<Description> getDescriptions() { public List<Description> getDescriptions() {
return descriptions; return descriptions;
} }
void addDescription(Description description) { public void addDescription(Description description) {
this.descriptions.add(description); this.descriptions.add(description);
notifyItemInserted(descriptions.size() + 1); notifyItemInserted(descriptions.size());
}
public Title getTitle() {
return title;
}
public void setTitle(Title title) {
this.title = title;
notifyItemInserted(0);
} }
public class ViewHolder extends RecyclerView.ViewHolder { public class ViewHolder extends RecyclerView.ViewHolder {
@ -119,98 +86,53 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
AppCompatSpinner spinnerDescriptionLanguages; AppCompatSpinner spinnerDescriptionLanguages;
@BindView(R.id.description_item_edit_text) @BindView(R.id.description_item_edit_text)
EditText descItemEditText; AppCompatEditText descItemEditText;
private View view;
public ViewHolder(View itemView) { public ViewHolder(View itemView) {
super(itemView); super(itemView);
ButterKnife.bind(this, itemView); ButterKnife.bind(this, itemView);
this.view = itemView;
Timber.i("descItemEditText:" + descItemEditText); Timber.i("descItemEditText:" + descItemEditText);
} }
@SuppressLint("ClickableViewAccessibility")
public void init(int position) { public void init(int position) {
if (position == 0) { Description description = descriptions.get(position);
Timber.d("Title is " + title);
if (!title.isEmpty()) {
descItemEditText.setText(title.toString());
} else {
descItemEditText.setText("");
}
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null);
descItemEditText.addTextChangedListener(new AbstractTextWatcher(titleText ->{
title.setTitleText(titleText);
titleChangedSubject.onNext(titleText);
}));
descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
ViewUtil.hideKeyboard(v);
}
});
descItemEditText.setOnTouchListener((v, event) -> {
// Check this is a touch up event
if(event.getAction() != MotionEvent.ACTION_UP) return false;
// Check we are tapping within 15px of the info icon
int extraTapArea = 15;
Drawable info = descItemEditText.getCompoundDrawables()[2];
int infoHitboxX = descItemEditText.getWidth() - info.getBounds().width();
if (event.getX() + extraTapArea < infoHitboxX) return false;
// If the above are true, show the info dialog
callback.showAlert(R.string.media_detail_title, R.string.title_info);
return true;
});
} else {
Description description = descriptions.get(position - 1);
Timber.d("Description is " + description); Timber.d("Description is " + description);
if (!TextUtils.isEmpty(description.getDescriptionText())) { if (!TextUtils.isEmpty(description.getDescriptionText())) {
descItemEditText.setText(description.getDescriptionText()); descItemEditText.setText(description.getDescriptionText());
} else { } else {
descItemEditText.setText(""); descItemEditText.setText("");
} }
if (position == 0) {
// Show the info icon for the first description descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(),
if (position == 1) { null);
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null);
descItemEditText.setOnTouchListener((v, event) -> { descItemEditText.setOnTouchListener((v, event) -> {
// Check this is a touch up event //2 is for drawable right
if(event.getAction() != MotionEvent.ACTION_UP) return false; float twelveDpInPixels = convertDpToPixel(12, descItemEditText.getContext());
if (event.getAction() == MotionEvent.ACTION_UP && descItemEditText.getCompoundDrawables()[2].getBounds().contains((int)(descItemEditText.getWidth()-(event.getX()+twelveDpInPixels)),(int)(event.getY()-twelveDpInPixels))){
// Check we are tapping within 15px of the info icon if (getAdapterPosition() == 0) {
int extraTapArea = 15; callback.showAlert(R.string.media_detail_description,
Drawable info = descItemEditText.getCompoundDrawables()[2]; R.string.description_info);
int infoHitboxX = descItemEditText.getWidth() - info.getBounds().width(); }
if (event.getX() + extraTapArea < infoHitboxX) return false;
// If the above are true, show the info dialog
callback.showAlert(R.string.media_detail_description, R.string.description_info);
return true; return true;
});
} }
return false;
});
descItemEditText.addTextChangedListener(new AbstractTextWatcher(descriptionText->{
descriptions.get(position - 1).setDescriptionText(descriptionText);
}));
descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
ViewUtil.hideKeyboard(v);
} else { } else {
uploadView.setTopCardState(false); descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
} }
});
descItemEditText.addTextChangedListener(new AbstractTextWatcher(
descriptionText -> descriptions.get(position)
.setDescriptionText(descriptionText)));
initLanguageSpinner(position, description); initLanguageSpinner(position, description);
}
//If the description was manually added by the user, it deserves focus, if not, let the user decide
if (description.isManuallyAdded()) {
descItemEditText.requestFocus();
} else {
descItemEditText.clearFocus();
}
} }
/** /**
@ -219,48 +141,24 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
* @param description * @param description
*/ */
private void initLanguageSpinner(int position, Description description) { private void initLanguageSpinner(int position, Description description) {
SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context, SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(
spinnerDescriptionLanguages.getContext(),
R.layout.row_item_languages_spinner, selectedLanguages); R.layout.row_item_languages_spinner, selectedLanguages);
languagesAdapter.notifyDataSetChanged(); languagesAdapter.notifyDataSetChanged();
spinnerDescriptionLanguages.setAdapter(languagesAdapter); spinnerDescriptionLanguages.setAdapter(languagesAdapter);
if (description.getSelectedLanguageIndex() == -1) {
if (position == 1) {
int defaultLocaleIndex = languagesAdapter.getIndexOfUserDefaultLocale(context);
spinnerDescriptionLanguages.setSelection(defaultLocaleIndex);
} else {
// availableLangIndex gives the index of first non-selected language
int availableLangIndex = -1;
// loops over the languagesAdapter and finds the index of first non-selected language
for (int i = 0; i < languagesAdapter.getCount(); i++) {
if (!selectedLanguages.containsKey(languagesAdapter.getLanguageCode(i))) {
availableLangIndex = i;
break;
}
}
if (availableLangIndex >= 0) {
// sets the spinner value to the index of first non-selected language
spinnerDescriptionLanguages.setSelection(availableLangIndex);
selectedLanguages.put(spinnerDescriptionLanguages, languagesAdapter.getLanguageCode(position));
}
}
} else {
spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex());
selectedLanguages.put(spinnerDescriptionLanguages, description.getLanguageCode());
}
//TODO do it the butterknife way
spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() { spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, public void onItemSelected(AdapterView<?> adapterView, View view, int position,
long l) { long l) {
description.setSelectedLanguageIndex(position); description.setSelectedLanguageIndex(position);
String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter()).getLanguageCode(position); String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter())
.getLanguageCode(position);
description.setLanguageCode(languageCode); description.setLanguageCode(languageCode);
selectedLanguages.remove(adapterView); selectedLanguages.remove(adapterView);
selectedLanguages.put(adapterView, languageCode); selectedLanguages.put(adapterView, languageCode);
((SpinnerLanguagesAdapter) adapterView.getAdapter()).selectedLangCode = languageCode; ((SpinnerLanguagesAdapter) adapterView
.getAdapter()).selectedLangCode = languageCode;
} }
@Override @Override
@ -268,18 +166,43 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
} }
}); });
if (description.getSelectedLanguageIndex() == -1) {
if (position == 0) {
int defaultLocaleIndex = languagesAdapter
.getIndexOfUserDefaultLocale(spinnerDescriptionLanguages.getContext());
spinnerDescriptionLanguages.setSelection(defaultLocaleIndex, true);
} else {
spinnerDescriptionLanguages.setSelection(0);
}
} else {
spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex());
selectedLanguages.put(spinnerDescriptionLanguages, description.getLanguageCode());
}
} }
/** /**
* Extracted out the method to get the icon drawable * Extracted out the method to get the icon drawable
* @return
*/ */
private Drawable getInfoIcon() { private Drawable getInfoIcon() {
return context.getResources().getDrawable(R.drawable.mapbox_info_icon_default); return descItemEditText.getContext()
.getResources()
.getDrawable(R.drawable.mapbox_info_icon_default);
} }
} }
public interface Callback { public interface Callback {
void showAlert(int mediaDetailDescription, int descriptionInfo); void showAlert(int mediaDetailDescription, int descriptionInfo);
} }
/**
* converts dp to pixel
* @param dp
* @param context
* @return
*/
private float convertDpToPixel(float dp, Context context) {
return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
} }

View file

@ -6,6 +6,7 @@ import android.content.Context;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import fr.free.nrw.commons.upload.SimilarImageDialogFragment.Callback;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Type; import java.lang.reflect.Type;
@ -37,7 +38,7 @@ import timber.log.Timber;
* Processing of the image filePath that is about to be uploaded via ShareActivity is done here * Processing of the image filePath that is about to be uploaded via ShareActivity is done here
*/ */
@Singleton @Singleton
public class FileProcessor implements SimilarImageDialogFragment.onResponse { public class FileProcessor implements Callback {
@Inject @Inject
CacheController cacheController; CacheController cacheController;
@ -58,7 +59,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
private CompositeDisposable compositeDisposable = new CompositeDisposable(); private CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject @Inject
FileProcessor() { public FileProcessor() {
} }
public void cleanup() { public void cleanup() {

View file

@ -13,12 +13,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.
*/ */
class GPSExtractor { public class GPSExtractor {
static final GPSExtractor DUMMY= new GPSExtractor(); static final GPSExtractor DUMMY= new GPSExtractor();
private double decLatitude; private double decLatitude;
private double decLongitude; private double decLongitude;
boolean imageCoordsExists; public boolean imageCoordsExists;
private String latitude; private String latitude;
private String longitude; private String longitude;
private String latitudeRef; private String latitudeRef;
@ -96,11 +96,11 @@ class GPSExtractor {
} }
} }
double getDecLatitude() { public double getDecLatitude() {
return decLatitude; return decLatitude;
} }
double getDecLongitude() { public double getDecLongitude() {
return decLongitude; return decLongitude;
} }

View file

@ -37,17 +37,21 @@ public class SimilarImageDialogFragment extends DialogFragment {
Button positiveButton; Button positiveButton;
@BindView(R.id.negative_button) @BindView(R.id.negative_button)
Button negativeButton; Button negativeButton;
onResponse mOnResponse;//Implemented interface from shareActivity Callback callback;//Implemented interface from shareActivity
Boolean gotResponse = false; Boolean gotResponse = false;
public SimilarImageDialogFragment() { public SimilarImageDialogFragment() {
} }
public interface onResponse{ public interface Callback {
void onPositiveResponse(); void onPositiveResponse();
void onNegativeResponse(); void onNegativeResponse();
} }
public void setCallback(Callback callback) {
this.callback = callback;
}
@Override @Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false); View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false);
@ -77,7 +81,6 @@ public class SimilarImageDialogFragment extends DialogFragment {
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mOnResponse = (onResponse) getActivity();//Interface Implementation
} }
@Override @Override
@ -91,21 +94,21 @@ public class SimilarImageDialogFragment extends DialogFragment {
public void onDismiss(DialogInterface dialog) { public void onDismiss(DialogInterface dialog) {
// I user dismisses dialog by pressing outside the dialog. // I user dismisses dialog by pressing outside the dialog.
if (!gotResponse) { if (!gotResponse) {
mOnResponse.onNegativeResponse(); callback.onNegativeResponse();
} }
super.onDismiss(dialog); super.onDismiss(dialog);
} }
@OnClick(R.id.negative_button) @OnClick(R.id.negative_button)
public void onNegativeButtonClicked() { public void onNegativeButtonClicked() {
mOnResponse.onNegativeResponse(); callback.onNegativeResponse();
gotResponse = true; gotResponse = true;
dismiss(); dismiss();
} }
@OnClick(R.id.postive_button) @OnClick(R.id.postive_button)
public void onPositiveButtonClicked() { public void onPositiveButtonClicked() {
mOnResponse.onPositiveResponse(); callback.onPositiveResponse();
gotResponse = true; gotResponse = true;
dismiss(); dismiss();
} }

View file

@ -7,7 +7,6 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import java.util.ArrayList; import java.util.ArrayList;
@ -22,6 +21,10 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.BiMap; import fr.free.nrw.commons.utils.BiMap;
import fr.free.nrw.commons.utils.LangCodeUtils; import fr.free.nrw.commons.utils.LangCodeUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
public class SpinnerLanguagesAdapter extends ArrayAdapter { public class SpinnerLanguagesAdapter extends ArrayAdapter {
@ -83,27 +86,32 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter {
@Override @Override
public View getDropDownView(int position, @Nullable View convertView, public View getDropDownView(int position, @Nullable View convertView,
@NonNull ViewGroup parent) { @NonNull ViewGroup parent) {
View view = layoutInflater.inflate(resource, parent, false); if (convertView == null) {
ViewHolder holder = new ViewHolder(view); convertView = layoutInflater.inflate(resource, parent, false);
}
ViewHolder holder = new ViewHolder(convertView);
holder.init(position, true); holder.init(position, true);
return view; return convertView;
} }
@Override @Override
public @NonNull public @NonNull
View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = layoutInflater.inflate(resource, parent, false); ViewHolder holder;
ViewHolder holder = new ViewHolder(view); if (convertView == null) {
convertView = layoutInflater.inflate(resource, parent, false);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.init(position, false); holder.init(position, false);
return view; return convertView;
} }
public class ViewHolder { public class ViewHolder {
@BindView(R.id.ll_container_description_language)
LinearLayout llContainerDescriptionLanguage;
@BindView(R.id.tv_language) @BindView(R.id.tv_language)
TextView tvLanguage; TextView tvLanguage;

View file

@ -1,5 +1,7 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import fr.free.nrw.commons.filepicker.UploadableFile;
public interface ThumbnailClickedListener { public interface ThumbnailClickedListener {
void thumbnailClicked(UploadModel.UploadItem content); void thumbnailClicked(UploadableFile content);
} }

View file

@ -0,0 +1,112 @@
package fr.free.nrw.commons.upload;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.facebook.drawee.view.SimpleDraweeView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.filepicker.UploadableFile;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* The adapter class for image thumbnails to be shown while uploading.
*/
class ThumbnailsAdapter extends RecyclerView.Adapter<ThumbnailsAdapter.ViewHolder> {
List<UploadableFile> uploadableFiles;
private Callback callback;
public ThumbnailsAdapter(Callback callback) {
this.uploadableFiles = new ArrayList<>();
this.callback = callback;
}
/**
* Sets the data, the media files
* @param uploadableFiles
*/
public void setUploadableFiles(
List<UploadableFile> uploadableFiles) {
this.uploadableFiles=uploadableFiles;
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
return new ViewHolder(LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_upload_thumbnail, viewGroup, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
viewHolder.bind(position);
}
@Override
public int getItemCount() {
return uploadableFiles.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.rl_container)
RelativeLayout rlContainer;
@BindView(R.id.iv_thumbnail)
SimpleDraweeView background;
@BindView(R.id.iv_error)
ImageView ivError;
public ViewHolder(@NonNull View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
/**
* Binds a row item to the ViewHolder
* @param position
*/
public void bind(int position) {
UploadableFile uploadableFile = uploadableFiles.get(position);
Uri uri = uploadableFile.getMediaUri();
background.setImageURI(Uri.fromFile(new File(String.valueOf(uri))));
if (position == callback.getCurrentSelectedFilePosition()) {
rlContainer.setEnabled(true);
rlContainer.setClickable(true);
rlContainer.setAlpha(1.0f);
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
rlContainer.setElevation(10);
}
} else {
rlContainer.setEnabled(false);
rlContainer.setClickable(false);
rlContainer.setAlpha(0.5f);
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
rlContainer.setElevation(0);
}
}
}
}
/**
* Callback used to get the current selected file position
*/
interface Callback {
int getCurrentSelectedFilePosition();
}
}

View file

@ -31,4 +31,8 @@ public class Title{
public boolean isEmpty() { public boolean isEmpty() {
return titleText==null || titleText.isEmpty(); return titleText==null || titleText.isEmpty();
} }
public String getTitleText() {
return titleText;
}
} }

View file

@ -0,0 +1,41 @@
package fr.free.nrw.commons.upload;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
/**
* The base fragment of the fragments in upload
*/
public class UploadBaseFragment extends CommonsDaggerSupportFragment {
public Callback callback;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public void setCallback(Callback callback) {
this.callback = callback;
}
public interface Callback {
void onNextButtonClicked(int index);
void onPreviousButtonClicked(int index);
void showProgress(boolean shouldShow);
int getIndexInViewFlipper(UploadBaseFragment fragment);
int getTotalNumberOfSteps();
}
}

View file

@ -0,0 +1,40 @@
package fr.free.nrw.commons.upload;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.filepicker.UploadableFile;
import java.util.List;
/**
* The contract using which the UplaodActivity would communicate with its presenter
*/
public interface UploadContract {
public interface View {
boolean isLoggedIn();
void finish();
void askUserToLogIn();
void showProgress(boolean shouldShow);
void showMessage(int messageResourceId);
List<UploadableFile> getUploadableFiles();
void showHideTopCard(boolean shouldShow);
void onUploadMediaDeleted(int index);
void updateTopCardTitle();
}
public interface UserActionListener extends BasePresenter<View> {
void handleSubmit();
void deletePictureAtIndex(int index);
}
}

View file

@ -75,7 +75,7 @@ public class UploadController {
/** /**
* Prepares the upload service. * Prepares the upload service.
*/ */
void prepareService() { public 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);
@ -85,7 +85,7 @@ public class UploadController {
/** /**
* Disconnects the upload service. * Disconnects the upload service.
*/ */
void cleanup() { public void cleanup() {
if (isUploadServiceConnected) { if (isUploadServiceConnected) {
context.unbindService(uploadServiceConnection); context.unbindService(uploadServiceConnection);
} }
@ -96,7 +96,7 @@ public class UploadController {
* *
* @param contribution the contribution object * @param contribution the contribution object
*/ */
void startUpload(Contribution contribution) { public void startUpload(Contribution contribution) {
startUpload(contribution, c -> {}); startUpload(contribution, c -> {});
} }

View file

@ -3,16 +3,7 @@ package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
@ -25,14 +16,20 @@ import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.BehaviorSubject; import io.reactivex.subjects.BehaviorSubject;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import timber.log.Timber; import timber.log.Timber;
@Singleton
public class UploadModel { public class UploadModel {
private static UploadItem DUMMY = new UploadItem( private static UploadItem DUMMY = new UploadItem(
@ -49,15 +46,13 @@ public class UploadModel {
private String license; private String license;
private final Map<String, String> licensesByName; private final Map<String, String> licensesByName;
private List<UploadItem> items = new ArrayList<>(); private List<UploadItem> items = new ArrayList<>();
private boolean topCardState = true;
private boolean bottomCardState = true;
private boolean rightCardState = true;
private int currentStepIndex = 0; private int currentStepIndex = 0;
private CompositeDisposable compositeDisposable = new CompositeDisposable(); private CompositeDisposable compositeDisposable = new CompositeDisposable();
private SessionManager sessionManager; private SessionManager sessionManager;
private FileProcessor fileProcessor; private FileProcessor fileProcessor;
private final ImageProcessingService imageProcessingService; private final ImageProcessingService imageProcessingService;
private List<String> selectedCategories;
@Inject @Inject
UploadModel(@Named("licenses") List<String> licenses, UploadModel(@Named("licenses") List<String> licenses,
@ -77,22 +72,50 @@ public class UploadModel {
this.imageProcessingService = imageProcessingService; this.imageProcessingService = imageProcessingService;
} }
void cleanup() { /**
* cleanup the resources, I am Singleton, preparing for fresh upload
*/
public void cleanUp() {
compositeDisposable.clear(); compositeDisposable.clear();
fileProcessor.cleanup(); fileProcessor.cleanup();
this.items.clear();
if (this.selectedCategories != null) {
this.selectedCategories.clear();
}
} }
public void setSelectedCategories(List<String> selectedCategories) {
if (null == selectedCategories) {
selectedCategories = new ArrayList<>();
}
this.selectedCategories = selectedCategories;
}
/**
* pre process a list of items
*/
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
Observable<UploadItem> preProcessImages(List<UploadableFile> uploadableFiles, Observable<UploadItem> preProcessImages(List<UploadableFile> uploadableFiles,
Place place, Place place,
String source, String source,
SimilarImageInterface similarImageInterface) { SimilarImageInterface similarImageInterface) {
initDefaultValues();
return Observable.fromIterable(uploadableFiles) return Observable.fromIterable(uploadableFiles)
.map(uploadableFile -> getUploadItem(uploadableFile, place, source, similarImageInterface)); .map(uploadableFile -> getUploadItem(uploadableFile, place, source,
similarImageInterface));
} }
Single<Integer> getImageQuality(UploadItem uploadItem, boolean checkTitle) {
/**
* pre process a one item at a time
*/
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile,
Place place,
String source,
SimilarImageInterface similarImageInterface) {
return Observable.just(getUploadItem(uploadableFile, place, source, similarImageInterface));
}
public Single<Integer> getImageQuality(UploadItem uploadItem, boolean checkTitle) {
return imageProcessingService.validateImage(uploadItem, checkTitle); return imageProcessingService.validateImage(uploadItem, checkTitle);
} }
@ -100,8 +123,10 @@ public class UploadModel {
Place place, Place place,
String source, String source,
SimilarImageInterface similarImageInterface) { SimilarImageInterface similarImageInterface) {
fileProcessor.initFileDetails(Objects.requireNonNull(uploadableFile.getFilePath()), context.getContentResolver()); fileProcessor.initFileDetails(Objects.requireNonNull(uploadableFile.getFilePath()),
UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile.getFileCreatedDate(context); context.getContentResolver());
UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile
.getFileCreatedDate(context);
long fileCreatedDate = -1; long fileCreatedDate = -1;
String createdTimestampSource = ""; String createdTimestampSource = "";
if (dateTimeWithSource != null) { if (dateTimeWithSource != null) {
@ -109,52 +134,21 @@ public class UploadModel {
createdTimestampSource = dateTimeWithSource.getSource(); createdTimestampSource = dateTimeWithSource.getSource();
} }
Timber.d("File created date is %d", fileCreatedDate); Timber.d("File created date is %d", fileCreatedDate);
GPSExtractor gpsExtractor = fileProcessor.processFileCoordinates(similarImageInterface, context); GPSExtractor gpsExtractor = fileProcessor
return new UploadItem(uploadableFile.getContentUri(), Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate, createdTimestampSource); .processFileCoordinates(similarImageInterface, context);
} UploadItem uploadItem = new UploadItem(uploadableFile.getContentUri(),
Uri.parse(uploadableFile.getFilePath()),
void onItemsProcessed(Place place, List<UploadItem> uploadItems) { uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate,
items = uploadItems; createdTimestampSource);
if (items.isEmpty()) {
return;
}
UploadItem uploadItem = items.get(0);
uploadItem.selected = true;
uploadItem.first = true;
if (place != null) { if (place != null) {
uploadItem.title.setTitleText(place.getName()); uploadItem.title.setTitleText(place.name);
uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription().equals("?")?"":place.getLongDescription()); uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription());
//TODO figure out if default descriptions in other languages exist
uploadItem.descriptions.get(0).setLanguageCode("en"); uploadItem.descriptions.get(0).setLanguageCode("en");
} }
if (!items.contains(uploadItem)) {
items.add(uploadItem);
} }
return uploadItem;
private void initDefaultValues() {
currentStepIndex = 0;
topCardState = true;
bottomCardState = true;
rightCardState = true;
items = new ArrayList<>();
}
boolean isPreviousAvailable() {
return currentStepIndex > 0;
}
boolean isNextAvailable() {
return currentStepIndex < (items.size() + 1);
}
boolean isSubmitAvailable() {
int count = items.size();
boolean hasError = license == null;
for (int i = 0; i < count; i++) {
UploadItem item = items.get(i);
hasError |= item.error;
}
return !hasError;
} }
int getCurrentStep() { int getCurrentStep() {
@ -173,110 +167,20 @@ public class UploadModel {
return items; return items;
} }
boolean isTopCardState() {
return topCardState;
}
void setTopCardState(boolean topCardState) {
this.topCardState = topCardState;
}
boolean isBottomCardState() {
return bottomCardState;
}
void setRightCardState(boolean rightCardState) {
this.rightCardState = rightCardState;
}
boolean isRightCardState() {
return rightCardState;
}
void setBottomCardState(boolean bottomCardState) {
this.bottomCardState = bottomCardState;
}
@SuppressLint("CheckResult")
public void next() {
markCurrentUploadVisited();
if (currentStepIndex < items.size() + 1) {
currentStepIndex++;
}
updateItemState();
}
void setCurrentTitleAndDescriptions(Title title, List<Description> descriptions) {
setCurrentUploadTitle(title);
setCurrentUploadDescriptions(descriptions);
}
private void setCurrentUploadTitle(Title title) {
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
items.get(currentStepIndex).title = title;
}
}
private void setCurrentUploadDescriptions(List<Description> descriptions) {
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
items.get(currentStepIndex).descriptions = descriptions;
}
}
public void previous() {
cleanup();
markCurrentUploadVisited();
if (currentStepIndex > 0) {
currentStepIndex--;
}
updateItemState();
}
void jumpTo(UploadItem item) {
currentStepIndex = items.indexOf(item);
item.visited = true;
updateItemState();
}
UploadItem getCurrentItem() {
return isShowingItem() ? items.get(currentStepIndex) : DUMMY;
}
boolean isShowingItem() {
return currentStepIndex < items.size();
}
private void updateItemState() {
Timber.d("Updating item state");
int count = items.size();
for (int i = 0; i < count; i++) {
UploadItem item = items.get(i);
item.selected = (currentStepIndex >= count || i == currentStepIndex);
item.error = item.title == null || item.title.isEmpty();
}
}
private void markCurrentUploadVisited() {
Timber.d("Marking current upload visited");
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
items.get(currentStepIndex).visited = true;
}
}
public List<String> getLicenses() { public List<String> getLicenses() {
return licenses; return licenses;
} }
String getSelectedLicense() { public String getSelectedLicense() {
return license; return license;
} }
void setSelectedLicense(String licenseName) { public void setSelectedLicense(String licenseName) {
this.license = licensesByName.get(licenseName); this.license = licensesByName.get(licenseName);
store.putString(Prefs.DEFAULT_LICENSE, license); store.putString(Prefs.DEFAULT_LICENSE, license);
} }
Observable<Contribution> buildContributions(List<String> categoryStringList) { public Observable<Contribution> buildContributions() {
return Observable.fromIterable(items).map(item -> return Observable.fromIterable(items).map(item ->
{ {
Contribution contribution = new Contribution(item.mediaUri, null, Contribution contribution = new Contribution(item.mediaUri, null,
@ -287,7 +191,10 @@ public class UploadModel {
if (item.place != null) { if (item.place != null) {
contribution.setWikiDataEntityId(item.place.getWikiDataEntityId()); contribution.setWikiDataEntityId(item.place.getWikiDataEntityId());
} }
contribution.setCategories(categoryStringList); if (null == selectedCategories) {//Just a fail safe, this should never be null
selectedCategories = new ArrayList<>();
}
contribution.setCategories(selectedCategories);
contribution.setTag("mimeType", item.mimeType); contribution.setTag("mimeType", item.mimeType);
contribution.setSource(item.source); contribution.setSource(item.source);
contribution.setContentProviderUri(item.mediaUri); contribution.setContentProviderUri(item.mediaUri);
@ -304,21 +211,16 @@ public class UploadModel {
}); });
} }
void keepPicture() { public void deletePicture(String filePath) {
items.get(currentStepIndex).setImageQuality(ImageUtils.IMAGE_KEEP); Iterator<UploadItem> iterator = items.iterator();
while (iterator.hasNext()) {
if (iterator.next().mediaUri.toString().contains(filePath)) {
iterator.remove();
break;
} }
void deletePicture() {
cleanup();
updateItemState();
} }
if (items.isEmpty()) {
void subscribeBadPicture(Consumer<Integer> consumer, boolean checkTitle) { cleanUp();
if (isShowingItem()) {
compositeDisposable.add(getImageQuality(getCurrentItem(), checkTitle)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(consumer, Timber::e));
} }
} }
@ -326,8 +228,15 @@ public class UploadModel {
return items; return items;
} }
public void updateUploadItem(int index, UploadItem uploadItem) {
UploadItem uploadItem1 = items.get(index);
uploadItem1.setDescriptions(uploadItem.descriptions);
uploadItem1.setTitle(uploadItem.title);
}
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
static class UploadItem { public static class UploadItem {
private final Uri originalContentUri; private final Uri originalContentUri;
private final Uri mediaUri; private final Uri mediaUri;
private final String mimeType; private final String mimeType;
@ -426,16 +335,40 @@ public class UploadModel {
} }
public String getFileName() { public String getFileName() {
return Utils.fixExtension(title.toString(), getFileExt()); return title
!= null ? Utils.fixExtension(title.toString(), getFileExt()) : null;
} }
public Place getPlace() { public Place getPlace() {
return place; return place;
} }
public void setTitle(Title title) {
this.title = title;
}
public void setDescriptions(List<Description> descriptions) {
this.descriptions = descriptions;
}
public Uri getContentUri() { public Uri getContentUri() {
return originalContentUri; return originalContentUri;
} }
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof UploadItem)) {
return false;
}
return this.mediaUri.toString().contains(((UploadItem) (obj)).mediaUri.toString());
}
//Travis is complaining :P
@Override
public int hashCode() {
return super.hashCode();
}
} }
} }

View file

@ -0,0 +1,36 @@
package fr.free.nrw.commons.upload;
import dagger.Binds;
import dagger.Module;
import fr.free.nrw.commons.upload.categories.CategoriesContract;
import fr.free.nrw.commons.upload.categories.CategoriesPresenter;
import fr.free.nrw.commons.upload.license.MediaLicenseContract;
import fr.free.nrw.commons.upload.license.MediaLicensePresenter;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter;
/**
* The Dagger Module for upload related presenters and (some other objects maybe in future)
*/
@Module
public abstract class UploadModule {
@Binds
public abstract UploadContract.UserActionListener bindHomePresenter(UploadPresenter
presenter);
@Binds
public abstract CategoriesContract.UserActionListener bindsCategoriesPresenter(CategoriesPresenter
presenter);
@Binds
public abstract MediaLicenseContract.UserActionListener bindsMediaLicensePresenter(
MediaLicensePresenter
presenter);
@Binds
public abstract UploadMediaDetailsContract.UserActionListener bindsUploadMediaPresenter(
UploadMediaPresenter
presenter);
}

View file

@ -1,420 +1,126 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context;
import android.text.TextUtils; import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.category.CategoriesModel;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.repository.UploadRepository;
import fr.free.nrw.commons.location.LatLng; import io.reactivex.Observer;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.CustomProxy;
import fr.free.nrw.commons.utils.CustomProxy;
import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.upload.UploadModel.UploadItem; import static fr.free.nrw.commons.upload.UploadModel.UploadItem;
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
/** /**
* The MVP pattern presenter of Upload GUI * The MVP pattern presenter of Upload GUI
*/ */
@Singleton @Singleton
public class UploadPresenter { public class UploadPresenter implements UploadContract.UserActionListener {
private static final UploadView DUMMY = private static final UploadContract.View DUMMY = (UploadContract.View) Proxy.newProxyInstance(
(UploadView) CustomProxy.newInstance(UploadView.class.getClassLoader(), UploadContract.View.class.getClassLoader(),
new Class[] { UploadView.class }); new Class[]{UploadContract.View.class}, (proxy, method, methodArgs) -> null);
private final UploadRepository repository;
private UploadContract.View view = DUMMY;
private UploadView view = DUMMY; private CompositeDisposable compositeDisposable;
private static final SimilarImageInterface SIMILAR_IMAGE =
(SimilarImageInterface) CustomProxy.newInstance(
SimilarImageInterface.class.getClassLoader(),
new Class[] { SimilarImageInterface.class });
private SimilarImageInterface similarImageInterface = SIMILAR_IMAGE;
@UploadView.UploadPage
private int currentPage = UploadView.PLEASE_WAIT;
private final UploadModel uploadModel;
private final UploadController uploadController;
private final Context context;
private final JsonKvStore directKvStore;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject @Inject
UploadPresenter(UploadModel uploadModel, UploadPresenter(UploadRepository uploadRepository) {
UploadController uploadController, this.repository = uploadRepository;
Context context, compositeDisposable = new CompositeDisposable();
@Named("default_preferences") JsonKvStore directKvStore) {
this.uploadModel = uploadModel;
this.uploadController = uploadController;
this.context = context;
this.directKvStore = directKvStore;
} }
/**
* Passes the items received to {@link #uploadModel} and displays the items.
*
* @param media The Uri's of the media being uploaded.
* @param source File source from {@link Contribution.FileSource}
*/
@SuppressLint("CheckResult")
void receive(List<UploadableFile> media,
@Contribution.FileSource String source,
Place place) {
Observable<UploadItem> uploadItemObservable = uploadModel
.preProcessImages(media, place, source, similarImageInterface);
compositeDisposable.add(uploadItemObservable
.toList()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(uploadItems -> onImagesProcessed(uploadItems, place),
throwable -> Timber.e(throwable, "Error occurred in processing images")));
}
private void onImagesProcessed(List<UploadItem> uploadItems, Place place) {
uploadModel.onItemsProcessed(place, uploadItems);
updateCards();
updateLicenses();
updateContent();
uploadModel.subscribeBadPicture(this::handleBadImage, false);
}
/**
* Sets the license to parameter and updates {@link UploadActivity}
*
* @param licenseName license name
*/
void selectLicense(String licenseName) {
uploadModel.setSelectedLicense(licenseName);
view.updateLicenseSummary(uploadModel.getSelectedLicense(), uploadModel.getCount());
}
//region Wizard step management
/**
* Called by the next button in {@link UploadActivity}
*/
@SuppressLint("CheckResult")
void handleNext(Title title,
List<Description> descriptions) {
Timber.e("Inside handleNext");
view.showProgressDialog();
setTitleAndDescription(title, descriptions);
compositeDisposable.add(uploadModel.getImageQuality(uploadModel.getCurrentItem(), true)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(imageResult -> handleImage(title, descriptions, imageResult),
throwable -> Timber.e(throwable, "Error occurred while handling image")));
}
private void handleImage(Title title, List<Description> descriptions, Integer imageResult) {
view.hideProgressDialog();
if (imageResult == IMAGE_KEEP || imageResult == IMAGE_OK) {
Timber.d("Set title and desc; Show next uploaded item");
setTitleAndDescription(title, descriptions);
directKvStore.putBoolean("Picture_Has_Correct_Location", true);
nextUploadedItem();
} else {
handleBadImage(imageResult);
}
}
/**
* Called by the next button in {@link UploadActivity}
*/
@SuppressLint("CheckResult")
void handleCategoryNext(CategoriesModel categoriesModel,
boolean noCategoryWarningShown) {
if (categoriesModel.selectedCategoriesCount() < 1 && !noCategoryWarningShown) {
view.showNoCategorySelectedWarning();
} else {
nextUploadedItem();
}
}
private void handleBadImage(Integer errorCode) {
Timber.d("Handle bad picture with error code %d", errorCode);
if (errorCode >= 8) { // If location of image and nearby does not match, then set shared preferences to disable wikidata edits
directKvStore.putBoolean("Picture_Has_Correct_Location", false);
}
switch (errorCode) {
case EMPTY_TITLE:
Timber.d("Title is empty. Showing toast");
view.showErrorMessage(R.string.add_title_toast);
break;
case FILE_NAME_EXISTS:
Timber.d("Trying to show duplicate picture popup");
view.showDuplicatePicturePopup();
break;
default:
String errorMessageForResult = getErrorMessageForResult(context, errorCode);
if (TextUtils.isEmpty(errorMessageForResult)) {
return;
}
view.showBadPicturePopup(errorMessageForResult);
}
}
private void nextUploadedItem() {
Timber.d("Trying to show next uploaded item");
uploadModel.next();
updateContent();
uploadModel.subscribeBadPicture(this::handleBadImage, false);
view.dismissKeyboard();
}
private void setTitleAndDescription(Title title, List<Description> descriptions) {
Timber.d("setTitleAndDescription: Setting title and desc");
uploadModel.setCurrentTitleAndDescriptions(title, descriptions);
}
String getCurrentImageFileName() {
UploadItem currentItem = getCurrentItem();
return currentItem.getFileName();
}
/**
* Called by the previous button in {@link UploadActivity}
*/
void handlePrevious() {
uploadModel.previous();
updateContent();
uploadModel.subscribeBadPicture(this::handleBadImage, false);
view.dismissKeyboard();
}
/**
* Called when one of the pictures on the top card is clicked on in {@link UploadActivity}
*/
void thumbnailClicked(UploadItem item) {
uploadModel.jumpTo(item);
updateContent();
}
/** /**
* Called by the submit button in {@link UploadActivity} * Called by the submit button in {@link UploadActivity}
*/ */
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
void handleSubmit(CategoriesModel categoriesModel) { @Override
if (view.checkIfLoggedIn()) public void handleSubmit() {
compositeDisposable.add(uploadModel.buildContributions(categoriesModel.getCategoryStringList()) if (view.isLoggedIn()) {
view.showProgress(true);
repository.buildContributions()
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.subscribe(uploadController::startUpload)); .subscribe(new Observer<Contribution>() {
@Override
public void onSubscribe(Disposable d) {
view.showProgress(false);
view.showMessage(R.string.uploading_started);
compositeDisposable.add(d);
} }
/** @Override
* Called by the map button on the right card in {@link UploadActivity} public void onNext(Contribution contribution) {
*/ repository.startUpload(contribution);
void openCoordinateMap() {
GPSExtractor gpsObj = uploadModel.getCurrentItem().getGpsCoords();
if (gpsObj != null && gpsObj.imageCoordsExists) {
view.launchMapActivity(new LatLng(gpsObj.getDecLatitude(), gpsObj.getDecLongitude(), 0.0f));
}
} }
void keepPicture() { @Override
uploadModel.keepPicture(); public void onError(Throwable e) {
} view.showMessage(R.string.upload_failed);
repository.cleanup();
void deletePicture() {
if (uploadModel.getCount() == 1)
view.finish(); view.finish();
else {
uploadModel.deletePicture();
updateCards();
updateContent();
uploadModel.subscribeBadPicture(this::handleBadImage, false);
view.dismissKeyboard();
}
}
//endregion
//region Top Bottom and Right card state management
/**
* Toggles the top card's state between open and closed.
*/
void toggleTopCardState() {
uploadModel.setTopCardState(!uploadModel.isTopCardState());
view.setTopCardState(uploadModel.isTopCardState());
}
/**
* Toggles the bottom card's state between open and closed.
*/
void toggleBottomCardState() {
uploadModel.setBottomCardState(!uploadModel.isBottomCardState());
view.setBottomCardState(uploadModel.isBottomCardState());
}
/**
* Sets all the cards' states to closed.
*/
void closeAllCards() {
if (uploadModel.isTopCardState()) {
uploadModel.setTopCardState(false);
view.setTopCardState(false);
}
if (uploadModel.isRightCardState()) {
uploadModel.setRightCardState(false);
}
if (uploadModel.isBottomCardState()) {
uploadModel.setBottomCardState(false);
view.setBottomCardState(false);
}
}
//endregion
//region View / Lifecycle management
public void init() {
uploadController.prepareService();
}
void cleanup() {
compositeDisposable.clear(); compositeDisposable.clear();
uploadModel.cleanup(); Timber.e("failed to upload: " + e.getMessage());
uploadController.cleanup();
} }
void removeView() { @Override
this.view = DUMMY; public void onComplete() {
repository.cleanup();
view.finish();
compositeDisposable.clear();
} }
});
void addView(UploadView view) {
this.view = view;
updateCards();
updateLicenses();
updateContent();
}
/**
* Updates the cards for when there is a change to the amount of items being uploaded.
*/
private void updateCards() {
Timber.i("uploadModel.getCount():" + uploadModel.getCount());
view.updateThumbnails(uploadModel.getUploads());
view.setTopCardVisibility(uploadModel.getCount() > 1);
view.setBottomCardVisibility(uploadModel.getCount() > 0);
view.setTopCardState(uploadModel.isTopCardState());
view.setBottomCardState(uploadModel.isBottomCardState());
}
/**
* Sets the list of licences and the default license.
*/
private void updateLicenses() {
String selectedLicense = directKvStore.getString(Prefs.DEFAULT_LICENSE,
Prefs.Licenses.CC_BY_SA_4);//CC_BY_SA_4 is the default one used by the commons web app
try {//I have to make sure that the stored default license was not one of the deprecated one's
Utils.licenseNameFor(selectedLicense);
} catch (IllegalStateException exception) {
Timber.e(exception.getMessage());
selectedLicense = Prefs.Licenses.CC_BY_SA_4;
directKvStore.putString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4);
}
view.updateLicenses(uploadModel.getLicenses(), selectedLicense);
view.updateLicenseSummary(selectedLicense, uploadModel.getCount());
}
/**
* Updates the cards and the background when a new currentPage is selected.
*/
private void updateContent() {
Timber.i("Updating content for currentPage" + uploadModel.getCurrentStep());
view.setNextEnabled(uploadModel.isNextAvailable());
view.setPreviousEnabled(uploadModel.isPreviousAvailable());
view.setSubmitEnabled(uploadModel.isSubmitAvailable());
view.setBackground(uploadModel.getCurrentItem().getMediaUri());
view.updateBottomCardContent(uploadModel.getCurrentStep(),
uploadModel.getStepCount(),
uploadModel.getCurrentItem(),
uploadModel.isShowingItem());
view.updateTopCardContent();
GPSExtractor gpsObj = uploadModel.getCurrentItem().getGpsCoords();
view.updateRightCardContent(gpsObj != null && gpsObj.imageCoordsExists);
view.updateSubtitleVisibility(uploadModel.getCount());
showCorrectCards(uploadModel.getCurrentStep(), uploadModel.getCount());
}
/**
* Updates the layout to show the correct bottom card.
*
* @param currentStep the current step
* @param uploadCount how many items are being uploaded
*/
private void showCorrectCards(int currentStep, int uploadCount) {
if (uploadCount == 0) {
currentPage = UploadView.PLEASE_WAIT;
} else if (currentStep <= uploadCount) {
currentPage = UploadView.TITLE_CARD;
view.setTopCardVisibility(uploadModel.getCount() > 1);
} else if (currentStep == uploadCount + 1) {
currentPage = UploadView.CATEGORIES;
view.setTopCardVisibility(false);
view.setRightCardVisibility(false);
view.initDefaultCategories();
} else { } else {
currentPage = UploadView.LICENSE; view.askUserToLogIn();
view.setTopCardVisibility(false);
view.setRightCardVisibility(false);
} }
view.setBottomCardVisibility(currentPage, uploadCount);
} }
//endregion @Override
public void deletePictureAtIndex(int index) {
/** List<UploadableFile> uploadableFiles = view.getUploadableFiles();
* @return the item currently being displayed if (index == uploadableFiles.size() - 1) {//If the next fragment to be shown is not one of the MediaDetailsFragment, lets hide the top card
*/ view.showHideTopCard(false);
private UploadItem getCurrentItem() { }
return uploadModel.getCurrentItem(); //Ask the repository to delete the picture
repository.deletePicture(uploadableFiles.get(index).getFilePath());
if (uploadableFiles.size() == 1) {
view.showMessage(R.string.upload_cancelled);
view.finish();
return;
} else {
view.onUploadMediaDeleted(index);
}
if (uploadableFiles.size() < 2) {
view.showHideTopCard(false);
} }
List<String> getImageTitleList() { //In case lets update the number of uploadable media
List<String> titleList = new ArrayList<>(); view.updateTopCardTitle();
for (UploadItem item : uploadModel.getUploads()) {
if (item.getTitle().isSet()) {
titleList.add(item.getTitle().toString());
} }
@Override
public void onAttachView(UploadContract.View view) {
this.view = view;
repository.prepareService();
} }
return titleList;
@Override
public void onDetachView() {
this.view = DUMMY;
compositeDisposable.clear();
repository.cleanup();
} }
} }

View file

@ -1,53 +0,0 @@
package fr.free.nrw.commons.upload;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.facebook.drawee.view.SimpleDraweeView;
import com.pedrogomez.renderers.Renderer;
import java.io.File;
import fr.free.nrw.commons.R;
class UploadThumbnailRenderer extends Renderer<UploadModel.UploadItem> {
private ThumbnailClickedListener listener;
private SimpleDraweeView background;
private View space;
private ImageView error;
public UploadThumbnailRenderer(ThumbnailClickedListener listener) {
this.listener = listener;
}
@Override
protected View inflate(LayoutInflater inflater, ViewGroup parent) {
return inflater.inflate(R.layout.item_upload_thumbnail, parent, false);
}
@Override
protected void setUpView(View rootView) {
error = rootView.findViewById(R.id.error);
space = rootView.findViewById(R.id.left_space);
background = rootView.findViewById(R.id.thumbnail);
}
@Override
protected void hookListeners(View rootView) {
background.setOnClickListener(v -> listener.thumbnailClicked(getContent()));
}
@Override
public void render() {
UploadModel.UploadItem content = getContent();
Uri uri = Uri.parse(content.getMediaUri().toString());
background.setImageURI(Uri.fromFile(new File(String.valueOf(uri))));
background.setAlpha(content.isSelected() ? 1.0f : 0.5f);
space.setVisibility(content.isFirst() ? View.VISIBLE : View.GONE);
error.setVisibility(content.isVisited() && content.isError() ? View.VISIBLE : View.GONE);
}
}

View file

@ -1,24 +0,0 @@
package fr.free.nrw.commons.upload;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections;
import java.util.List;
public class UploadThumbnailsAdapterFactory {
private ThumbnailClickedListener listener;
UploadThumbnailsAdapterFactory(ThumbnailClickedListener listener) {
this.listener = listener;
}
public RVRendererAdapter<UploadModel.UploadItem> create(List<UploadModel.UploadItem> placeList) {
RendererBuilder<UploadModel.UploadItem> builder = new RendererBuilder<UploadModel.UploadItem>()
.bind(UploadModel.UploadItem.class, new UploadThumbnailRenderer(listener));
ListAdapteeCollection<UploadModel.UploadItem> collection = new ListAdapteeCollection<>(
placeList != null ? placeList : Collections.emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

@ -15,7 +15,6 @@ public interface UploadView {
// UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(), // UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
// new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null); // new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null);
List<Description> getDescriptions();
@Retention(SOURCE) @Retention(SOURCE)
@IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE}) @IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE})
@ -82,4 +81,6 @@ public interface UploadView {
void showProgressDialog(); void showProgressDialog();
void hideProgressDialog(); void hideProgressDialog();
void askUserToLogIn();
} }

View file

@ -0,0 +1,42 @@
package fr.free.nrw.commons.upload.categories;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.category.CategoryItem;
import java.util.List;
/**
* The contract with with UploadCategoriesFragment and its presenter would talk to each other
*/
public interface CategoriesContract {
public interface View {
void showProgress(boolean shouldShow);
void showError(String error);
void showError(int stringResourceId);
void setCategories(List<CategoryItem> categories);
void addCategory(CategoryItem category);
void goToNextScreen();
void showNoCategorySelected();
void setSelectedCategories(List<CategoryItem> selectedCategories);
}
public interface UserActionListener extends BasePresenter<View> {
void searchForCategories(String query);
void verifyCategories();
void onCategoryItemClicked(CategoryItem categoryItem);
}
}

View file

@ -0,0 +1,144 @@
package fr.free.nrw.commons.upload.categories;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
import android.text.TextUtils;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.repository.UploadRepository;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import timber.log.Timber;
/**
* The presenter class for UploadCategoriesFragment
*/
@Singleton
public class CategoriesPresenter implements CategoriesContract.UserActionListener {
private static final CategoriesContract.View DUMMY = (CategoriesContract.View) Proxy
.newProxyInstance(
CategoriesContract.View.class.getClassLoader(),
new Class[]{CategoriesContract.View.class},
(proxy, method, methodArgs) -> null);
private final Scheduler ioScheduler;
private final Scheduler mainThreadScheduler;
CategoriesContract.View view = DUMMY;
private UploadRepository repository;
private CompositeDisposable compositeDisposable;
@Inject
public CategoriesPresenter(UploadRepository repository, @Named(IO_THREAD) Scheduler ioScheduler,
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
this.repository = repository;
this.ioScheduler = ioScheduler;
this.mainThreadScheduler = mainThreadScheduler;
compositeDisposable = new CompositeDisposable();
}
@Override
public void onAttachView(CategoriesContract.View view) {
this.view = view;
}
@Override
public void onDetachView() {
this.view = DUMMY;
compositeDisposable.clear();
}
/**
* asks the repository to fetch categories for the query
* @param query
*
*/
@Override
public void searchForCategories(String query) {
List<CategoryItem> categoryItems = new ArrayList<>();
List<String> imageTitleList = getImageTitleList();
Observable<CategoryItem> distinctCategoriesObservable = Observable
.fromIterable(repository.getSelectedCategories())
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.doOnSubscribe(disposable -> {
view.showProgress(true);
view.showError(null);
view.setCategories(null);
})
.observeOn(ioScheduler)
.concatWith(
repository.searchAll(query, imageTitleList)
)
.filter(categoryItem -> !repository.containsYear(categoryItem.getName()))
.distinct();
if(!TextUtils.isEmpty(query)) {
distinctCategoriesObservable=distinctCategoriesObservable.sorted(repository.sortBySimilarity(query));
}
Disposable searchCategoriesDisposable = distinctCategoriesObservable
.observeOn(mainThreadScheduler)
.subscribe(
s -> categoryItems.add(s),
Timber::e,
() -> {
view.setCategories(categoryItems);
view.showProgress(false);
if (categoryItems.isEmpty()) {
view.showError(R.string.no_categories_found);
}
}
);
compositeDisposable.add(searchCategoriesDisposable);
}
/**
* Returns image title list from UploadItem
* @return
*/
private List<String> getImageTitleList() {
List<String> titleList = new ArrayList<>();
for (UploadItem item : repository.getUploads()) {
if (item.getTitle().isSet()) {
titleList.add(item.getTitle().toString());
}
}
return titleList;
}
/**
* Verifies the number of categories selected, prompts the user if none selected
*/
@Override
public void verifyCategories() {
List<CategoryItem> selectedCategories = repository.getSelectedCategories();
if (selectedCategories != null && !selectedCategories.isEmpty()) {
repository.setSelectedCategories(repository.getCategoryStringList());
view.goToNextScreen();
} else {
view.showNoCategorySelected();
}
}
/**
* ask repository to handle category clicked
*
* @param categoryItem
*/
@Override
public void onCategoryItemClicked(CategoryItem categoryItem) {
repository.onCategoryClicked(categoryItem);
}
}

View file

@ -0,0 +1,200 @@
package fr.free.nrw.commons.upload.categories;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxTextView;
import com.pedrogomez.renderers.RVRendererAdapter;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryClickedListener;
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.upload.UploadBaseFragment;
import fr.free.nrw.commons.upload.UploadCategoriesAdapterFactory;
import fr.free.nrw.commons.utils.DialogUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import timber.log.Timber;
public class UploadCategoriesFragment extends UploadBaseFragment implements CategoriesContract.View,
CategoryClickedListener {
@BindView(R.id.tv_title)
TextView tvTitle;
@BindView(R.id.til_container_search)
TextInputLayout tilContainerEtSearch;
@BindView(R.id.et_search)
TextInputEditText etSearch;
@BindView(R.id.pb_categories)
ProgressBar pbCategories;
@BindView(R.id.rv_categories)
RecyclerView rvCategories;
@Inject
CategoriesContract.UserActionListener presenter;
private RVRendererAdapter<CategoryItem> adapter;
private List<String> mediaTitleList=new ArrayList<>();
private Disposable subscribe;
private List<CategoryItem> categories;
private boolean isVisible;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public void setMediaTitleList(List<String> mediaTitleList) {
this.mediaTitleList = mediaTitleList;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.upload_categories_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
init();
}
private void init() {
tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
callback.getTotalNumberOfSteps()));
presenter.onAttachView(this);
initRecyclerView();
addTextChangeListenerToEtSearch();
//get default categories for empty query
}
@Override
public void onResume() {
super.onResume();
if (presenter != null && isVisible && (categories == null || categories.isEmpty())) {
presenter.searchForCategories(null);
}
}
private void addTextChangeListenerToEtSearch() {
subscribe = RxTextView.textChanges(etSearch)
.doOnEach(v -> tilContainerEtSearch.setError(null))
.takeUntil(RxView.detaches(etSearch))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(filter -> searchForCategory(filter.toString()), Timber::e);
}
private void searchForCategory(String query) {
presenter.searchForCategories(query);
}
private void initRecyclerView() {
adapter = new UploadCategoriesAdapterFactory(this)
.create(new ArrayList<>());
rvCategories.setLayoutManager(new LinearLayoutManager(getContext()));
rvCategories.setAdapter(adapter);
}
@Override
public void onDestroyView() {
super.onDestroyView();
presenter.onDetachView();
subscribe.dispose();
}
@Override
public void showProgress(boolean shouldShow) {
pbCategories.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
}
@Override
public void showError(String error) {
tilContainerEtSearch.setError(error);
}
@Override
public void showError(int stringResourceId) {
tilContainerEtSearch.setError(getString(stringResourceId));
}
@Override
public void setCategories(List<CategoryItem> categories) {
adapter.clear();
if (categories != null) {
this.categories = categories;
adapter.addAll(categories);
adapter.notifyDataSetChanged();
}
}
@Override
public void addCategory(CategoryItem category) {
adapter.add(category);
adapter.notifyItemInserted(adapter.getItemCount());
}
@Override
public void goToNextScreen() {
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
}
@Override
public void showNoCategorySelected() {
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.no_categories_selected),
getString(R.string.no_categories_selected_warning_desc),
getString(R.string.no_go_back),
getString(R.string.yes_submit),
null,
() -> goToNextScreen());
}
@Override
public void setSelectedCategories(List<CategoryItem> selectedCategories) {
}
@OnClick(R.id.btn_next)
public void onNextButtonClicked() {
presenter.verifyCategories();
}
@OnClick(R.id.btn_previous)
public void onPreviousButtonClicked() {
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
}
@Override
public void categoryClicked(CategoryItem item) {
presenter.onCategoryItemClicked(item);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
isVisible = isVisibleToUser;
if (presenter != null && isResumed() && (categories == null || categories.isEmpty())) {
presenter.searchForCategories(null);
}
}
}

View file

@ -0,0 +1,26 @@
package fr.free.nrw.commons.upload.license;
import fr.free.nrw.commons.BasePresenter;
import java.util.List;
/**
* The contract with with MediaLicenseFragment and its presenter would talk to each other
*/
public interface MediaLicenseContract {
interface View {
void setLicenses(List<String> licenses);
void setSelectedLicense(String license);
void updateLicenseSummary(String selectedLicense, int numberOfItems);
}
interface UserActionListener extends BasePresenter<View> {
void getLicenses();
void selectLicense(String licenseName);
}
}

View file

@ -0,0 +1,181 @@
package fr.free.nrw.commons.upload.license;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.upload.UploadBaseFragment;
import timber.log.Timber;
public class MediaLicenseFragment extends UploadBaseFragment implements MediaLicenseContract.View {
@BindView(R.id.tv_title)
TextView tvTitle;
@BindView(R.id.spinner_license_list)
Spinner spinnerLicenseList;
@BindView(R.id.tv_share_license_summary)
TextView tvShareLicenseSummary;
@Inject
MediaLicenseContract.UserActionListener presenter;
private ArrayAdapter<String> adapter;
private List<String> licenses;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_media_license, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
init();
}
private void init() {
tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
callback.getTotalNumberOfSteps()));
initPresenter();
initLicenseSpinner();
presenter.getLicenses();
}
private void initPresenter() {
presenter.onAttachView(this);
}
/**
* Initialise the license spinner
*/
private void initLicenseSpinner() {
adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item);
spinnerLicenseList.setAdapter(adapter);
spinnerLicenseList.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position,
long l) {
String licenseName = adapterView.getItemAtPosition(position).toString();
presenter.selectLicense(licenseName);
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
presenter.selectLicense(null);
}
});
}
@Override
public void setLicenses(List<String> licenses) {
adapter.clear();
this.licenses = licenses;
adapter.addAll(this.licenses);
adapter.notifyDataSetChanged();
}
@Override
public void setSelectedLicense(String license) {
int position = licenses.indexOf(getString(Utils.licenseNameFor(license)));
// Check if position is valid
if (position < 0) {
Timber.d("Invalid position: %d. Using default licenses", position);
position = licenses.size() - 1;
} else {
Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license)));
}
spinnerLicenseList.setSelection(position);
}
@Override
public void updateLicenseSummary(String licenseSummary, int numberOfItems) {
String licenseHyperLink = "<a href='" + Utils.licenseUrlFor(licenseSummary) + "'>" +
getString(Utils.licenseNameFor(licenseSummary)) + "</a><br>";
setTextViewHTML(tvShareLicenseSummary, getResources()
.getQuantityString(R.plurals.share_license_summary, numberOfItems,
licenseHyperLink));
}
private void setTextViewHTML(TextView textView, String text) {
CharSequence sequence = Html.fromHtml(text);
SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence);
URLSpan[] urls = strBuilder.getSpans(0, sequence.length(), URLSpan.class);
for (URLSpan span : urls) {
makeLinkClickable(strBuilder, span);
}
textView.setText(strBuilder);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
private void makeLinkClickable(SpannableStringBuilder strBuilder, final URLSpan span) {
int start = strBuilder.getSpanStart(span);
int end = strBuilder.getSpanEnd(span);
int flags = strBuilder.getSpanFlags(span);
ClickableSpan clickable = new ClickableSpan() {
public void onClick(View view) {
// Handle hyperlink click
String hyperLink = span.getURL();
launchBrowser(hyperLink);
}
};
strBuilder.setSpan(clickable, start, end, flags);
strBuilder.removeSpan(span);
}
private void launchBrowser(String hyperLink) {
Utils.handleWebUrl(getContext(), Uri.parse(hyperLink));
}
@Override
public void onDestroyView() {
super.onDestroyView();
presenter.onDetachView();
//Free the adapter to avoid memory leaks
adapter=null;
}
@OnClick(R.id.btn_previous)
public void onPreviousButtonClicked() {
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
}
@OnClick(R.id.btn_submit)
public void onSubmitButtonClicked() {
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
}
}

View file

@ -0,0 +1,75 @@
package fr.free.nrw.commons.upload.license;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.repository.UploadRepository;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.license.MediaLicenseContract.View;
import java.lang.reflect.Proxy;
import java.util.List;
import javax.inject.Inject;
import timber.log.Timber;
/**
* Added JavaDocs for MediaLicensePresenter
*/
public class MediaLicensePresenter implements MediaLicenseContract.UserActionListener {
private static final MediaLicenseContract.View DUMMY = (MediaLicenseContract.View) Proxy
.newProxyInstance(
MediaLicenseContract.View.class.getClassLoader(),
new Class[]{MediaLicenseContract.View.class},
(proxy, method, methodArgs) -> null);
private final UploadRepository repository;
private MediaLicenseContract.View view = DUMMY;
@Inject
public MediaLicensePresenter(UploadRepository uploadRepository) {
this.repository = uploadRepository;
}
@Override
public void onAttachView(View view) {
this.view = view;
}
@Override
public void onDetachView() {
this.view = DUMMY;
}
/**
* asks the repository for the available licenses, and informs the view on the same
*/
@Override
public void getLicenses() {
List<String> licenses = repository.getLicenses();
view.setLicenses(licenses);
String selectedLicense = repository.getValue(Prefs.DEFAULT_LICENSE,
Prefs.Licenses.CC_BY_SA_4);//CC_BY_SA_4 is the default one used by the commons web app
try {//I have to make sure that the stored default license was not one of the deprecated one's
Utils.licenseNameFor(selectedLicense);
} catch (IllegalStateException exception) {
Timber.e(exception.getMessage());
selectedLicense = Prefs.Licenses.CC_BY_SA_4;
repository.saveValue(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4);
}
view.setSelectedLicense(selectedLicense);
}
/**
* ask the repository to select a license for the current upload
*
* @param licenseName
*/
@Override
public void selectLicense(String licenseName) {
repository.setSelectedLicense(licenseName);
view.updateLicenseSummary(repository.getSelectedLicense(), repository.getCount());
}
}

View file

@ -0,0 +1,402 @@
package fr.free.nrw.commons.upload.mediaDetails;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatImageButton;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.github.chrisbanes.photoview.OnScaleChangedListener;
import com.github.chrisbanes.photoview.PhotoView;
import com.jakewharton.rxbinding2.widget.RxTextView;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.Description;
import fr.free.nrw.commons.upload.DescriptionsAdapter;
import fr.free.nrw.commons.upload.SimilarImageDialogFragment;
import fr.free.nrw.commons.upload.Title;
import fr.free.nrw.commons.upload.UploadBaseFragment;
import fr.free.nrw.commons.upload.UploadModel;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.disposables.Disposable;
import timber.log.Timber;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
public class UploadMediaDetailFragment extends UploadBaseFragment implements
UploadMediaDetailsContract.View {
@BindView(R.id.tv_title)
TextView tvTitle;
@BindView(R.id.ib_map)
AppCompatImageButton ibMap;
@BindView(R.id.ib_expand_collapse)
AppCompatImageButton ibExpandCollapse;
@BindView(R.id.ll_container_media_detail)
LinearLayout llContainerMediaDetail;
@BindView(R.id.et_title)
EditText etTitle;
@BindView(R.id.rv_descriptions)
RecyclerView rvDescriptions;
@BindView(R.id.backgroundImage)
PhotoView photoViewBackgroundImage;
@BindView(R.id.btn_next)
AppCompatButton btnNext;
@BindView(R.id.btn_previous)
AppCompatButton btnPrevious;
private DescriptionsAdapter descriptionsAdapter;
@BindView(R.id.btn_copy_prev_title_desc)
AppCompatButton btnCopyPreviousTitleDesc;
private UploadModel.UploadItem uploadItem;
private List<Description> descriptions;
@Inject
UploadMediaDetailsContract.UserActionListener presenter;
private UploadableFile uploadableFile;
private String source;
private Place place;
private Title title;
private boolean isExpanded = true;
private UploadMediaDetailFragmentCallback callback;
public void setCallback(UploadMediaDetailFragmentCallback callback) {
this.callback = callback;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public void setImageTobeUploaded(UploadableFile uploadableFile, String source, Place place) {
this.uploadableFile = uploadableFile;
this.source = source;
this.place = place;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_upload_media_detail_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
init();
}
private void init() {
tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
callback.getTotalNumberOfSteps()));
title = new Title();
initRecyclerView();
initPresenter();
Disposable disposable = RxTextView.textChanges(etTitle)
.subscribe(text -> {
if (!TextUtils.isEmpty(text)) {
btnNext.setEnabled(true);
btnNext.setClickable(true);
btnNext.setAlpha(1.0f);
title.setTitleText(text.toString());
uploadItem.setTitle(title);
} else {
btnNext.setAlpha(0.5f);
btnNext.setEnabled(false);
btnNext.setClickable(false);
}
});
compositeDisposable.add(disposable);
presenter.receiveImage(uploadableFile, source, place);
if (callback.getIndexInViewFlipper(this) == 0) {
btnPrevious.setEnabled(false);
btnPrevious.setAlpha(0.5f);
} else {
btnPrevious.setEnabled(true);
btnPrevious.setAlpha(1.0f);
}
//If this is the first media, we have nothing to copy, lets not show the button
if (callback.getIndexInViewFlipper(this) == 0) {
btnCopyPreviousTitleDesc.setVisibility(View.GONE);
} else {
btnCopyPreviousTitleDesc.setVisibility(View.VISIBLE);
}
attachImageViewScaleChangeListener();
addEtTitleTouchListener();
}
/**
* Handles the drawable click listener for Edit Text
*/
private void addEtTitleTouchListener() {
etTitle.setOnTouchListener((v, event) -> {
//2 is for drawable right
float twelveDpInPixels = convertDpToPixel(12, getContext());
if (event.getAction() == MotionEvent.ACTION_UP && etTitle.getCompoundDrawables()[2].getBounds().contains((int)(etTitle.getWidth()-(event.getX()+twelveDpInPixels)),(int)(event.getY()-twelveDpInPixels))){
showInfoAlert(R.string.media_detail_title,R.string.title_info);
return true;
}
return false;
});
}
/**
* converts dp to pixel
* @param dp
* @param context
* @return
*/
private float convertDpToPixel(float dp, Context context) {
return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
/**
* Attaches the scale change listener to the image view
*/
private void attachImageViewScaleChangeListener() {
photoViewBackgroundImage.setOnScaleChangeListener(
(scaleFactor, focusX, focusY) -> {
//Whenever the uses plays with the image, lets collapse the media detail container
expandCollapseLlMediaDetail(false);
});
}
/**
* attach the presenter with the view
*/
private void initPresenter() {
presenter.onAttachView(this);
}
/**
* init the recycler veiw
*/
private void initRecyclerView() {
descriptionsAdapter = new DescriptionsAdapter();
descriptionsAdapter.setCallback(this::showInfoAlert);
rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
rvDescriptions.setAdapter(descriptionsAdapter);
}
/**
* show dialog with info
* @param titleStringID
* @param messageStringId
*/
private void showInfoAlert(int titleStringID, int messageStringId) {
DialogUtil.showAlertDialog(getActivity(), getString(titleStringID), getString(messageStringId), getString(android.R.string.ok), null, true);
}
@OnClick(R.id.btn_next)
public void onNextButtonClicked() {
uploadItem.setDescriptions(descriptionsAdapter.getDescriptions());
presenter.verifyImageQuality(uploadItem, true);
}
@OnClick(R.id.btn_previous)
public void onPreviousButtonClicked() {
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
}
@OnClick(R.id.btn_add_description)
public void onButtonAddDescriptionClicked() {
Description description = new Description();
description.setManuallyAdded(true);//This was manually added by the user
descriptionsAdapter.addDescription(description);
}
@Override
public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) {
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
newFragment.setCallback(new SimilarImageDialogFragment.Callback() {
@Override
public void onPositiveResponse() {
Timber.d("positive response from similar image fragment");
}
@Override
public void onNegativeResponse() {
Timber.d("negative response from similar image fragment");
}
});
Bundle args = new Bundle();
args.putString("originalImagePath", originalFilePath);
args.putString("possibleImagePath", possibleFilePath);
newFragment.setArguments(args);
newFragment.show(getChildFragmentManager(), "dialog");
}
@Override
public void onImageProcessed(UploadItem uploadItem, Place place) {
this.uploadItem = uploadItem;
if (uploadItem.getTitle() != null) {
etTitle.setText(uploadItem.getTitle().toString());
}
descriptions = uploadItem.getDescriptions();
photoViewBackgroundImage.setImageURI(uploadItem.getMediaUri());
setDescriptionsInAdapter(descriptions);
}
@Override
public void showProgress(boolean shouldShow) {
callback.showProgress(shouldShow);
}
@Override
public void onImageValidationSuccess() {
presenter.setUploadItem(callback.getIndexInViewFlipper(this), uploadItem);
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
}
@Override
public void showMessage(int stringResourceId, int colorResourceId) {
ViewUtil.showLongToast(getContext(), stringResourceId);
}
@Override
public void showMessage(String message, int colorResourceId) {
ViewUtil.showLongToast(getContext(), message);
}
@Override
public void showDuplicatePicturePopup() {
String uploadTitleFormat = getString(R.string.upload_title_duplicate);
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.warning),
String.format(Locale.getDefault(),
uploadTitleFormat,
uploadItem.getFileName()),
() -> {
},
() -> {
uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP);
onNextButtonClicked();
});
}
@Override
public void showBadImagePopup(Integer errorCode) {
String errorMessageForResult = getErrorMessageForResult(getContext(), errorCode);
if (!StringUtils.isBlank(errorMessageForResult)) {
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.warning),
errorMessageForResult,
() -> deleteThisPicture(),
() -> {
uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP);
onNextButtonClicked();
});
}
//If the error message is null, we will probably not show anything
}
@Override public void showMapWithImageCoordinates(boolean shouldShow) {
ibMap.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
}
@Override
public void setTitleAndDescription(String title, List<Description> descriptions) {
etTitle.setText(title);
setDescriptionsInAdapter(descriptions);
}
private void deleteThisPicture() {
callback.deletePictureAtIndex(callback.getIndexInViewFlipper(this));
}
@Override
public void onDestroyView() {
super.onDestroyView();
presenter.onDetachView();
}
@OnClick(R.id.rl_container_title)
public void onRlContainerTitleClicked() {
expandCollapseLlMediaDetail(!isExpanded);
}
/**
* show hide media detail based on
* @param shouldExpand
*/
private void expandCollapseLlMediaDetail(boolean shouldExpand){
llContainerMediaDetail.setVisibility(shouldExpand ? View.VISIBLE : View.GONE);
isExpanded = !isExpanded;
ibExpandCollapse.setRotation(ibExpandCollapse.getRotation() + 180);
}
@OnClick(R.id.ib_map) public void onIbMapClicked() {
Utils.handleGeoCoordinates(getContext(),
new LatLng(uploadItem.getGpsCoords().getDecLatitude(),
uploadItem.getGpsCoords().getDecLongitude(), 0.0f));
}
public interface UploadMediaDetailFragmentCallback extends Callback {
void deletePictureAtIndex(int index);
}
@OnClick(R.id.btn_copy_prev_title_desc)
public void onButtonCopyPreviousTitleDesc(){
presenter.fetchPreviousTitleAndDescription(callback.getIndexInViewFlipper(this));
}
private void setDescriptionsInAdapter(List<Description> descriptions){
if(descriptions==null){
descriptions=new ArrayList<>();
}
if(descriptions.size()==0){
descriptions.add(new Description());
}
descriptionsAdapter.setItems(descriptions);
}
}

View file

@ -0,0 +1,52 @@
package fr.free.nrw.commons.upload.mediaDetails;
import java.util.List;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.Description;
import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.Title;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
/**
* The contract with with UploadMediaDetails and its presenter would talk to each other
*/
public interface UploadMediaDetailsContract {
interface View extends SimilarImageInterface {
void onImageProcessed(UploadItem uploadItem, Place place);
void showProgress(boolean shouldShow);
void onImageValidationSuccess();
void showMessage(int stringResourceId, int colorResourceId);
void showMessage(String message, int colorResourceId);
void showDuplicatePicturePopup();
void showBadImagePopup(Integer errorCode);
void showMapWithImageCoordinates(boolean shouldShow);
void setTitleAndDescription(String title, List<Description> descriptions);
}
interface UserActionListener extends BasePresenter<View> {
void receiveImage(UploadableFile uploadableFile, @Contribution.FileSource String source,
Place place);
void verifyImageQuality(UploadItem uploadItem, boolean validateTitle);
void setUploadItem(int index, UploadItem uploadItem);
void fetchPreviousTitleAndDescription(int indexInViewFlipper);
}
}

View file

@ -0,0 +1,194 @@
package fr.free.nrw.commons.upload.mediaDetails;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.repository.UploadRepository;
import fr.free.nrw.commons.upload.GPSExtractor;
import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.UserActionListener;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.View;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import java.lang.reflect.Proxy;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
public class UploadMediaPresenter implements UserActionListener, SimilarImageInterface {
private static final UploadMediaDetailsContract.View DUMMY = (UploadMediaDetailsContract.View) Proxy
.newProxyInstance(
UploadMediaDetailsContract.View.class.getClassLoader(),
new Class[]{UploadMediaDetailsContract.View.class},
(proxy, method, methodArgs) -> null);
private final UploadRepository repository;
private UploadMediaDetailsContract.View view = DUMMY;
private CompositeDisposable compositeDisposable;
private Scheduler ioScheduler;
private Scheduler mainThreadScheduler;
@Inject
public UploadMediaPresenter(UploadRepository uploadRepository,
@Named(IO_THREAD) Scheduler ioScheduler,
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
this.repository = uploadRepository;
this.ioScheduler = ioScheduler;
this.mainThreadScheduler = mainThreadScheduler;
compositeDisposable = new CompositeDisposable();
}
@Override
public void onAttachView(View view) {
this.view = view;
}
@Override
public void onDetachView() {
this.view = DUMMY;
compositeDisposable.clear();
}
/**
* Receives the corresponding uploadable file, processes it and return the view with and uplaod item
*
* @param uploadableFile
* @param source
* @param place
*/
@Override
public void receiveImage(UploadableFile uploadableFile, String source, Place place) {
view.showProgress(true);
Disposable uploadItemDisposable = repository
.preProcessImage(uploadableFile, place, source, this)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(uploadItem ->
{
view.onImageProcessed(uploadItem, place);
GPSExtractor gpsCoords = uploadItem.getGpsCoords();
view.showMapWithImageCoordinates((gpsCoords != null && gpsCoords.imageCoordsExists) ? true : false);
view.showProgress(false);
},
throwable -> Timber.e(throwable, "Error occurred in processing images"));
compositeDisposable.add(uploadItemDisposable);
}
/**
* asks the repository to verify image quality
*
* @param uploadItem
* @param validateTitle
*/
@Override
public void verifyImageQuality(UploadItem uploadItem, boolean validateTitle) {
view.showProgress(true);
Disposable imageQualityDisposable = repository
.getImageQuality(uploadItem, true)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(imageResult -> {
view.showProgress(false);
handleImageResult(imageResult);
},
throwable -> {
view.showProgress(false);
view.showMessage("" + throwable.getLocalizedMessage(),
R.color.color_error);
Timber.e(throwable, "Error occurred while handling image");
});
compositeDisposable.add(imageQualityDisposable);
}
/**
* Adds the corresponding upload item to the repository
*
* @param index
* @param uploadItem
*/
@Override
public void setUploadItem(int index, UploadItem uploadItem) {
repository.updateUploadItem(index, uploadItem);
}
/**
* Fetches and sets the title and desctiption of the previous item
*
* @param indexInViewFlipper
*/
@Override
public void fetchPreviousTitleAndDescription(int indexInViewFlipper) {
UploadItem previousUploadItem = repository.getPreviousUploadItem(indexInViewFlipper);
if (null != previousUploadItem) {
view.setTitleAndDescription(previousUploadItem.getTitle().getTitleText(), previousUploadItem.getDescriptions());
} else {
view.showMessage(R.string.previous_image_title_description_not_found, R.color.color_error);
}
}
/**
* handles image quality verifications
*
* @param imageResult
*/
public void handleImageResult(Integer imageResult) {
if (imageResult == IMAGE_KEEP || imageResult == IMAGE_OK) {
view.onImageValidationSuccess();
} else {
handleBadImage(imageResult);
}
}
/**
* Handle images, say empty title, duplicate file name, bad picture(in all other cases)
*
* @param errorCode
*/
private void handleBadImage(Integer errorCode) {
Timber.d("Handle bad picture with error code %d", errorCode);
if (errorCode
>= 8) { // If location of image and nearby does not match, then set shared preferences to disable wikidata edits
repository.saveValue("Picture_Has_Correct_Location", false);
}
switch (errorCode) {
case EMPTY_TITLE:
Timber.d("Title is empty. Showing toast");
view.showMessage(R.string.add_title_toast, R.color.color_error);
break;
case FILE_NAME_EXISTS:
Timber.d("Trying to show duplicate picture popup");
view.showDuplicatePicturePopup();
break;
default:
view.showBadImagePopup(errorCode);
}
}
/**
* notifies the user that a similar image exists
*
* @param originalFilePath
* @param possibleFilePath
*/
@Override
public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) {
view.showSimilarImageFragment(originalFilePath, possibleFilePath);
}
}

View file

@ -140,4 +140,31 @@ public class DialogUtil {
showSafely(activity, dialog); showSafely(activity, dialog);
} }
/**
* show a dialog with just a positive button
* @param activity
* @param title
* @param message
* @param positiveButtonText
* @param positiveButtonClick
* @param cancellable
*/
public static void showAlertDialog(Activity activity, String title, String message, String positiveButtonText, final Runnable positiveButtonClick, boolean cancellable) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(title);
builder.setMessage(message);
builder.setCancelable(cancellable);
builder.setPositiveButton(positiveButtonText, (dialogInterface, i) -> {
dialogInterface.dismiss();
if (positiveButtonClick != null) {
positiveButtonClick.run();
}
});
AlertDialog dialog = builder.create();
showSafely(activity, dialog);
}
} }

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/thumbnail_selected"
android:state_pressed="true"/>
<item android:drawable="@drawable/thumbnail_selected"
android:state_focused="true"/>
<item android:state_selected="true" android:drawable="@drawable/thumbnail_selected"/>
<item android:drawable="@drawable/thumbnail_not_selected"/>
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white"></solid>
</shape>

View file

@ -0,0 +1,11 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:color="@color/primaryDarkColor"
android:width="2dp"/>
<solid android:color="#F8F7F5"/>
<padding
android:bottom="2dp"
android:left="2dp"
android:right="2dp"
android:top="2dp"/>
</shape>

View file

@ -1,34 +1,79 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/upload_root_layout"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"> android:id="@+id/upload_root_layout"
android:layout_width="match_parent"
<com.github.chrisbanes.photoview.PhotoView android:layout_height="match_parent"
android:id="@+id/backgroundImage" >
<fr.free.nrw.commons.contributions.UnswipableViewPager
android:id="@+id/vp_upload"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:background="@color/commons_app_blue_dark" android:background="@color/commons_app_blue_dark"
app:actualImageScaleType="fitCenter" /> />
<androidx.cardview.widget.CardView
<ViewFlipper android:id="@+id/cv_container_top_card"
android:id="@+id/view_flipper"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:clipChildren="false" android:layout_marginBottom="8dp"
android:measureAllChildren="false"> android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:elevation="@dimen/cardview_default_elevation"
>
<LinearLayout
android:id="@+id/ll_container_top_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/standard_gap"
>
<include <RelativeLayout
layout="@layout/activity_upload_bottom_card" android:id="@+id/rl_container_title"
android:visibility="visible" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<TextView
android:id="@+id/tv_top_card_title"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginTop="@dimen/small_gap"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
tools:text="4 Uploads"
/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/ib_toggle_top_card"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginBottom="@dimen/small_gap"
android:layout_marginEnd="@dimen/small_gap"
android:layout_marginStart="@dimen/small_gap"
android:clickable="false"
android:focusable="false"
app:srcCompat="@drawable/ic_expand_less_black_24dp"
style="@style/Widget.AppCompat.Button.Borderless"
/>
<include layout="@layout/activity_upload_categories" /> </RelativeLayout>
<include layout="@layout/activity_upload_license" /> <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_thumbnails"
<include layout="@layout/activity_upload_please_wait" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
</ViewFlipper> android:layout_marginTop="@dimen/small_gap"
/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</RelativeLayout> </RelativeLayout>

View file

@ -1,199 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.cardview.widget.CardView
android:id="@+id/top_card"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:elevation="@dimen/cardview_default_elevation"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="UnusedAttribute"
tools:showIn="@layout/activity_upload">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/top_card_title"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginStart="@dimen/small_gap"
android:layout_marginTop="@dimen/small_gap"
android:layout_marginEnd="@dimen/small_gap"
android:layout_marginBottom="@dimen/small_gap"
android:gravity="center_vertical"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
tools:text="4 Uploads" />
<ImageButton
android:id="@+id/top_card_expand_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginStart="@dimen/small_gap"
android:layout_marginTop="@dimen/small_gap"
android:layout_marginEnd="@dimen/small_gap"
android:layout_marginBottom="@dimen/small_gap"
android:padding="0dp"
app:srcCompat="@drawable/ic_expand_less_black_24dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/top_card_thumbnails"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_below="@id/top_card_title"
android:layout_marginBottom="@dimen/small_gap" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/bottom_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/cardview_default_elevation"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:ignore="UnusedAttribute"
tools:showIn="@layout/activity_upload">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/small_gap">
<TextView
android:id="@+id/bottom_card_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Step 1 of 15" />
<TextView
android:id="@+id/bottom_card_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="@dimen/subtitle_text"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bottom_card_title"
tools:text="1st image" />
<ImageButton
android:id="@+id/right_card_map_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:visibility="visible"
app:layout_constraintEnd_toStartOf="@+id/bottom_card_expand_button"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_map_white_24dp" />
<ImageButton
android:id="@+id/bottom_card_expand_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="0dp"
android:rotation="180"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_expand_less_black_24dp" />
<fr.free.nrw.commons.widget.HeightLimitedRecyclerView
android:id="@+id/rv_descriptions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toTopOf="@+id/prev_title_desc"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottom_card_subtitle"
tools:visibility="gone" />
<Button
android:id="@+id/prev_title_desc"
android:layout_width="match_parent"
android:layout_height="25dp"
android:background="@color/white"
android:text="@string/previous_image_title_description"
android:textColor="@color/button_blue"
android:layout_marginBottom="15dp"
android:layout_marginEnd="3.5dp"
android:gravity="center"
style="@style/Widget.AppCompat.Button.Borderless"
app:layout_constraintBottom_toTopOf="@id/bottom_card_previous"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rv_descriptions" />
<Button
android:id="@+id/bottom_card_next"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<Button
android:id="@+id/bottom_card_previous"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:text="@string/previous"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/bottom_card_next"
app:layout_constraintRight_toLeftOf="@id/bottom_card_next" />
<Button
android:id="@+id/bottom_card_add_desc"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:text="+"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,127 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="8dp"
android:orientation="vertical"
tools:showIn="@layout/activity_upload">
<TextView
android:id="@+id/categories_title"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="@dimen/standard_gap"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:gravity="center_vertical"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
tools:text="Step 1 of 15" />
<TextView
android:id="@+id/categories_subtitle"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="@dimen/standard_gap"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginTop="@dimen/tiny_gap"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_alignParentLeft="true"
android:gravity="center_vertical"
android:textSize="@dimen/subtitle_text"
android:text="@string/upload_flow_all_images_in_set"
android:layout_below="@+id/categories_title"
android:visibility="gone" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/category_search_layout"
android:layout_marginStart="@dimen/standard_gap"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_below="@id/categories_subtitle">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/category_search_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/category_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/categories_search_text_hint"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/categoriesSearchInProgress"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/tiny_gap"
android:layout_marginRight="@dimen/tiny_gap"
android:layout_gravity="center_vertical|end"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/categories"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/standard_gap"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_above="@+id/button_divider"
android:layout_below="@id/category_search_layout" />
<View
android:id="@+id/button_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_above="@+id/category_next"
android:background="@color/divider_grey" />
<Button
android:id="@+id/category_next"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="24dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:text="@string/next" />
<Button
android:id="@+id/category_previous"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_marginBottom="24dp"
android:layout_toStartOf="@id/category_next"
android:layout_toLeftOf="@id/category_next"
android:layout_alignParentBottom="true"
android:text="@string/previous" />
</RelativeLayout>

View file

@ -1,116 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginBottom="8dp"
tools:showIn="@layout/activity_upload">
<TextView
android:id="@+id/license_title"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="@dimen/standard_gap"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:gravity="center_vertical"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
tools:text="Step 1 of 15" />
<TextView
android:id="@+id/license_subtitle"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="@dimen/standard_gap"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginTop="@dimen/tiny_gap"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_alignParentLeft="true"
android:gravity="center_vertical"
android:textSize="@dimen/subtitle_text"
android:text="@string/upload_flow_all_images_in_set"
android:layout_below="@+id/license_title"
android:visibility="gone" />
<Spinner
android:id="@+id/license_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_gap"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_below="@id/license_subtitle"
tools:visibility="gone"/>
<fr.free.nrw.commons.ui.widget.HtmlTextView
android:id="@+id/share_license_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_gap"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_below="@id/license_list"
android:text="@plurals/share_license_summary" />
<fr.free.nrw.commons.ui.widget.HtmlTextView
android:id="@+id/media_upload_policy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_gap"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_marginBottom="@dimen/standard_gap"
android:layout_above="@+id/button_divider"
android:gravity="start"
android:text="@string/media_upload_policy" />
<View
android:id="@+id/button_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_above="@+id/submit"
android:background="@color/divider_grey" />
<Button
android:id="@+id/submit"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="24dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:text="@string/submit" />
<Button
android:id="@+id/license_previous"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_marginBottom="24dp"
android:layout_toStartOf="@id/submit"
android:layout_toLeftOf="@id/submit"
android:layout_alignParentBottom="true"
android:text="@string/previous" />
</RelativeLayout>

View file

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/standard_gap"
android:layout_marginBottom="8dp"
android:gravity="center"
android:orientation="vertical">
<ProgressBar
android:id="@+id/shareInProgress"
style="?android:progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:indeterminateOnly="true" />
<TextView
android:id="@+id/please_wait_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_gap"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:gravity="center" />
</LinearLayout>

View file

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="8dp"
android:padding="@dimen/standard_gap"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/ll_container_license_desc"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:gravity="center_vertical"
android:textSize="@dimen/normal_text"
tools:text="Step 1 of 15"/>
<TextView
android:id="@+id/tv_subtitle"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginTop="@dimen/tiny_gap"
android:gravity="center_vertical"
android:text="@string/upload_flow_all_images_in_set"
android:textSize="@dimen/subtitle_text"
android:visibility="visible"
tools:visibility="visible"/>
<Spinner
android:id="@+id/spinner_license_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_gap"
android:padding="0dp"
tools:visibility="visible"/>
<TextView
android:id="@+id/tv_share_license_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_gap"
android:text="@plurals/share_license_summary"/>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_container_license_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="vertical">
<fr.free.nrw.commons.ui.widget.HtmlTextView
android:id="@+id/tv_media_upload_policy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_gap"
android:gravity="start"
android:text="@string/media_upload_policy"/>
<View
android:id="@+id/button_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:background="@color/divider_grey"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:orientation="horizontal"
>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_previous"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/previous"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_submit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/submit"
android:textColor="@android:color/white"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>

View file

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/backgroundImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:actualImageScaleType="fitXY" />
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_margin="10dp"
android:elevation="@dimen/cardview_default_elevation">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/rl_container_title"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:gravity="center_vertical"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
tools:text="Step 1 of 15" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/ib_map"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_toLeftOf="@id/ib_expand_collapse"
android:visibility="gone"
app:srcCompat="@drawable/ic_map_white_24dp"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/ib_expand_collapse"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginStart="@dimen/small_gap"
android:layout_marginEnd="@dimen/small_gap"
android:layout_marginBottom="@dimen/small_gap"
android:clickable="false"
android:focusable="false"
android:padding="12dp"
app:srcCompat="@drawable/ic_expand_less_black_24dp" />
</RelativeLayout>
<LinearLayout
android:id="@+id/ll_container_media_detail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_gap"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_container_title"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/share_title_hint"
android:imeOptions="actionNext"
android:drawableEnd="@drawable/mapbox_info_icon_default"
android:inputType="text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<fr.free.nrw.commons.widget.HeightLimitedRecyclerView
android:id="@+id/rv_descriptions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_copy_prev_title_desc"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/previous_image_title_description"
android:padding="2dp"
android:textAlignment="textEnd"
android:textColor="@color/button_blue"
android:visibility="gone"
tools:visibility="visible" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_add_description"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:text="+" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="@string/next"
android:textColor="@android:color/white" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_previous"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_toLeftOf="@+id/btn_next"
android:text="@string/previous" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.cardview.widget.CardView>
</RelativeLayout>

View file

@ -1,37 +1,28 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content" xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"> xmlns:tools="http://schemas.android.com/tools"
android:layout_width="90dp"
<LinearLayout xmlns:fresco="http://schemas.android.com/apk/res-auto" android:layout_height="90dp"
android:layout_width="wrap_content" android:id="@+id/rl_container"
android:layout_height="wrap_content" android:background="@drawable/thumbnail_not_selected"
android:orientation="horizontal"> android:orientation="horizontal">
<androidx.legacy.widget.Space
android:id="@+id/left_space"
android:layout_width="8dp"
android:layout_height="90dp" />
<com.facebook.drawee.view.SimpleDraweeView <com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/thumbnail" android:id="@+id/iv_thumbnail"
android:layout_width="90dp" android:layout_width="90dp"
android:layout_height="90dp" android:layout_height="90dp"
fresco:actualImageScaleType="fitCenter"/> fresco:actualImageScaleType="fitCenter"/>
<androidx.legacy.widget.Space
android:id="@+id/right_space"
android:layout_width="8dp"
android:layout_height="90dp" />
</LinearLayout>
<ImageView <ImageView
android:id="@+id/error" android:id="@+id/iv_error"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_gravity="end" android:layout_gravity="end"
android:visibility="gone" android:visibility="gone"
app:srcCompat="@drawable/ic_error_red_24dp" /> app:srcCompat="@drawable/ic_error_red_24dp"
</FrameLayout> tools:visibility="visible"/>
</RelativeLayout>

View file

@ -19,7 +19,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="8"> android:layout_weight="8">
<EditText <androidx.appcompat.widget.AppCompatEditText
android:id="@+id/description_item_edit_text" android:id="@+id/description_item_edit_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -7,7 +7,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:showIn="@layout/activity_upload"> tools:showIn="@layout/activity_upload">
<EditText <androidx.appcompat.widget.AppCompatEditText
android:id="@+id/description_item_edit_text" android:id="@+id/description_item_edit_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rl_container_categories"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/button_divider"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:gravity="center_vertical"
android:textSize="@dimen/normal_text"
tools:text="Step 1 of 15"/>
<TextView
android:id="@+id/tv_subtitle"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginTop="@dimen/tiny_gap"
android:gravity="center_vertical"
android:text="@string/upload_flow_all_images_in_set"
android:textSize="@dimen/subtitle_text"
android:visibility="visible"
tools:visibility="visible"/>
<FrameLayout
android:id="@+id/category_search_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_gap"
>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_container_search"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/categories_search_text_hint"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/pb_categories"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/tiny_gap"
android:layout_marginRight="@dimen/tiny_gap"
android:layout_gravity="center_vertical|end"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone"
tools:visibility="visible"/>
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_categories"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/button_divider"
android:layout_below="@id/category_search_layout"/>
</LinearLayout>
<View
android:id="@+id/button_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_above="@+id/ll_container_buttons"
android:background="@color/divider_grey"/>
<LinearLayout
android:id="@+id/ll_container_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:padding="16dp"
android:orientation="horizontal"
>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_previous"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/previous"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/next"
android:textColor="@android:color/white"/>
</LinearLayout>
</RelativeLayout>

View file

@ -373,7 +373,7 @@
<string name="write_storage_permission_rationale_for_image_share">Avem nevoie de permisiunea dvs. pentru a accesa spațiul de stocare extern al dispozitivului dvs. pentru a încărca imagini.</string> <string name="write_storage_permission_rationale_for_image_share">Avem nevoie de permisiunea dvs. pentru a accesa spațiul de stocare extern al dispozitivului dvs. pentru a încărca imagini.</string>
<string name="nearby_notification_dismiss_message">Nu veți vedea cel mai apropiat loc care are nevoie de imagini. Cu toate acestea, puteți reactiva această notificare în Setări, dacă doriți.</string> <string name="nearby_notification_dismiss_message">Nu veți vedea cel mai apropiat loc care are nevoie de imagini. Cu toate acestea, puteți reactiva această notificare în Setări, dacă doriți.</string>
<string name="step_count">Pasul %1$d din %2$d</string> <string name="step_count">Pasul %1$d din %2$d</string>
<string name="image_in_set_label">Imaginea% %1$d în set</string> <string name="image_in_set_label">Imaginea %1$d în set</string>
<string name="next">Următor</string> <string name="next">Următor</string>
<string name="previous">Precedent</string> <string name="previous">Precedent</string>
<string name="submit">Trimite</string> <string name="submit">Trimite</string>

View file

@ -67,6 +67,7 @@
<color name="black">#000000</color> <color name="black">#000000</color>
<color name="swipe_red" tools:ignore="MissingDefaultResource">#FF0000</color> <color name="swipe_red" tools:ignore="MissingDefaultResource">#FF0000</color>
<color name="color_error">#FF0000</color>
<color name="yes_button_color">#B22222</color> <color name="yes_button_color">#B22222</color>
<color name="no_button_color">#006400</color> <color name="no_button_color">#006400</color>
</resources> </resources>

View file

@ -409,7 +409,7 @@
<string name="next">Next</string> <string name="next">Next</string>
<string name="previous">Previous</string> <string name="previous">Previous</string>
<string name="submit">Submit</string> <string name="submit">Submit</string>
<string name="upload_title_duplicate">A file with the file name %1$s exists. Are you sure you want to proceed?</string> <string name="upload_title_duplicate" formatted="true">A file with the file name %1$s exists. Are you sure you want to proceed?</string>
<string name="map_application_missing">No compatible map application could be found on your device. Please install a map application to use this feature.</string> <string name="map_application_missing">No compatible map application could be found on your device. Please install a map application to use this feature.</string>
<plurals name="upload_count_title"> <plurals name="upload_count_title">
<item quantity="one">%1$d Upload</item> <item quantity="one">%1$d Upload</item>
@ -554,5 +554,8 @@ Upload your first media by tapping on the add button.</string>
<string name="share_text">Upload photos to Wikimedia Commons on your phone Download the Commons app: %1$s</string> <string name="share_text">Upload photos to Wikimedia Commons on your phone Download the Commons app: %1$s</string>
<string name="share_via">Share app via...</string> <string name="share_via">Share app via...</string>
<string name="image_info">Image Info</string> <string name="image_info">Image Info</string>
<string name="no_categories_found">No Categories found</string>
<string name="upload_cancelled">Cancelled Upload</string>
<string name="previous_image_title_description_not_found">There is no data for previous image\'s title or description</string>
<string name="dialog_box_text_nomination">Why should %1$s be deleted?</string> <string name="dialog_box_text_nomination">Why should %1$s be deleted?</string>
</resources> </resources>

View file

@ -0,0 +1,80 @@
package fr.free.nrw.commons.upload
import com.nhaarman.mockito_kotlin.verify
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.categories.CategoriesContract
import fr.free.nrw.commons.upload.categories.CategoriesPresenter
import io.reactivex.Observable
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
/**
* The class contains unit test cases for CategoriesPresenter
*/
class CategoriesPresenterTest {
@Mock
internal var repository: UploadRepository? = null
@Mock
internal var view: CategoriesContract.View? = null
var categoriesPresenter: CategoriesPresenter? = null
var testScheduler: TestScheduler? = null
val categoryItems: ArrayList<CategoryItem> = ArrayList()
@Mock
lateinit var categoryItem: CategoryItem
var testObservable: Observable<CategoryItem>? = null
private val imageTitleList = ArrayList<String>()
/**
* initial setup
*/
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler()
categoryItems.add(categoryItem)
testObservable = Observable.just(categoryItem)
categoriesPresenter = CategoriesPresenter(repository, testScheduler, testScheduler)
categoriesPresenter?.onAttachView(view)
}
/**
* unit test case for method CategoriesPresenter.searchForCategories
*/
@Test
fun searchForCategoriesTest() {
Mockito.`when`(repository?.sortBySimilarity(ArgumentMatchers.anyString())).thenReturn(Comparator<CategoryItem> { _, _ -> 1 })
Mockito.`when`(repository?.selectedCategories).thenReturn(categoryItems)
Mockito.`when`(repository?.searchAll(ArgumentMatchers.anyString(), ArgumentMatchers.anyList())).thenReturn(Observable.empty())
categoriesPresenter?.searchForCategories("test")
verify(view)?.showProgress(true)
verify(view)?.showError(null)
verify(view)?.setCategories(null)
testScheduler?.triggerActions()
verify(view)?.setCategories(categoryItems)
verify(view)?.showProgress(false)
}
/**
* unit test for method CategoriesPresenter.verifyCategories
*/
@Test
fun verifyCategoriesTest() {
Mockito.`when`(repository?.selectedCategories).thenReturn(categoryItems)
categoriesPresenter?.verifyCategories()
verify(repository)?.setSelectedCategories(ArgumentMatchers.anyList())
verify(view)?.goToNextScreen()
}
}

View file

@ -0,0 +1,66 @@
package fr.free.nrw.commons.upload
import com.nhaarman.mockito_kotlin.verify
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.license.MediaLicenseContract
import fr.free.nrw.commons.upload.license.MediaLicensePresenter
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.powermock.api.mockito.PowerMockito
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
/**
* The class contains unit test cases for MediaLicensePresenter
*/
@RunWith(PowerMockRunner::class)
@PrepareForTest(Utils::class)
class MediaLicensePresenterTest {
@Mock
internal var repository: UploadRepository? = null
@Mock
internal var view: MediaLicenseContract.View? = null
@InjectMocks
var mediaLicensePresenter: MediaLicensePresenter? = null
/**
* initial setup test environemnt
*/
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
mediaLicensePresenter?.onAttachView(view)
PowerMockito.mockStatic(Utils::class.java)
PowerMockito.`when`(Utils.licenseNameFor(ArgumentMatchers.anyString())).thenReturn(1)
}
/**
* unit test case for method MediaLicensePresenter.getLicense
*/
@Test
fun getLicenseTest() {
mediaLicensePresenter?.getLicenses()
verify(view)?.setLicenses(ArgumentMatchers.anyList())
verify(view)?.setSelectedLicense(ArgumentMatchers.any())
}
/**
* unit test case for method MediaLicensePresenter.selectLicense
*/
@Test
fun selectLicenseTest() {
mediaLicensePresenter?.selectLicense(ArgumentMatchers.anyString())
verify(view)?.updateLicenseSummary(ArgumentMatchers.any(), ArgumentMatchers.anyInt())
}
}

View file

@ -0,0 +1,110 @@
package fr.free.nrw.commons.upload
import fr.free.nrw.commons.filepicker.UploadableFile
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter
import fr.free.nrw.commons.utils.ImageUtils.*
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
/**
* The class contains unit test cases for UploadMediaPresenter
*/
class UploadMediaPresenterTest {
@Mock
internal var repository: UploadRepository? = null
@Mock
internal var view: UploadMediaDetailsContract.View? = null
private var uploadMediaPresenter: UploadMediaPresenter? = null
@Mock
private var uploadableFile: UploadableFile? = null
@Mock
private var place: Place? = null
@Mock
private var uploadItem: UploadModel.UploadItem? = null
private var testObservableUploadItem: Observable<UploadModel.UploadItem>? = null
private var testSingleImageResult: Single<Int>? = null
private var testScheduler: TestScheduler? = null
/**
* initial setup unit test environment
*/
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
testObservableUploadItem = Observable.just(uploadItem)
testSingleImageResult = Single.just(1)
testScheduler = TestScheduler()
uploadMediaPresenter = UploadMediaPresenter(repository, testScheduler, testScheduler)
uploadMediaPresenter?.onAttachView(view)
}
/**
* unit test for method UploadMediaPresenter.receiveImage
*/
@Test
fun receiveImageTest() {
Mockito.`when`(repository?.preProcessImage(ArgumentMatchers.any(UploadableFile::class.java), ArgumentMatchers.any(Place::class.java), ArgumentMatchers.anyString(), ArgumentMatchers.any(UploadMediaPresenter::class.java))).thenReturn(testObservableUploadItem)
uploadMediaPresenter?.receiveImage(uploadableFile, ArgumentMatchers.anyString(), place)
verify(view)?.showProgress(true)
testScheduler?.triggerActions()
verify(view)?.onImageProcessed(ArgumentMatchers.any(UploadModel.UploadItem::class.java), ArgumentMatchers.any(Place::class.java))
verify(view)?.showProgress(false)
}
/**
* unit test for method UploadMediaPresenter.verifyImageQuality
*/
@Test
fun verifyImageQualityTest() {
Mockito.`when`(repository?.getImageQuality(ArgumentMatchers.any(UploadModel.UploadItem::class.java), ArgumentMatchers.any(Boolean::class.java))).thenReturn(testSingleImageResult)
Mockito.`when`(uploadItem?.imageQuality).thenReturn(ArgumentMatchers.anyInt())
uploadMediaPresenter?.verifyImageQuality(uploadItem, true)
verify(view)?.showProgress(true)
testScheduler?.triggerActions()
verify(view)?.showProgress(false)
}
/**
* unit test for method UploadMediaPresenter.handleImageResult
*/
@Test
fun handleImageResult() {
//Positive case test
uploadMediaPresenter?.handleImageResult(IMAGE_KEEP)
verify(view)?.onImageValidationSuccess()
//Duplicate file name
uploadMediaPresenter?.handleImageResult(FILE_NAME_EXISTS)
verify(view)?.showDuplicatePicturePopup()
//Empty Title test
uploadMediaPresenter?.handleImageResult(EMPTY_TITLE)
verify(view)?.showMessage(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())
//Bad Picture test
//Empty Title test
uploadMediaPresenter?.handleImageResult(-7)
verify(view)?.showBadImagePopup(ArgumentMatchers.anyInt())
}
}

View file

@ -87,24 +87,6 @@ class UploadModelTest {
} }
} }
@Test
fun verifyPreviousNotAvailable() {
uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
assertFalse(uploadModel!!.isPreviousAvailable)
}
@Test
fun verifyNextAvailable() {
uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable)
}
@Test
fun isSubmitAvailable() {
uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable)
}
@Test @Test
fun getCurrentStep() { fun getCurrentStep() {
uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> } uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
@ -135,38 +117,6 @@ class UploadModelTest {
} }
} }
@Test
fun isTopCardState() {
uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.isTopCardState)
}
@Test
fun next() {
uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.currentStep == 1)
uploadModel!!.next()
assertTrue(uploadModel!!.currentStep == 2)
}
@Test
fun previous() {
uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.currentStep == 1)
uploadModel!!.next()
assertTrue(uploadModel!!.currentStep == 2)
uploadModel!!.previous()
assertTrue(uploadModel!!.currentStep == 1)
}
@Test
fun isShowingItem() {
val preProcessImages = uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
preProcessImages.doOnComplete {
assertTrue(uploadModel!!.isShowingItem)
}
}
private fun getMediaList(): List<UploadableFile> { private fun getMediaList(): List<UploadableFile> {
val element = getElement() val element = getElement()
val element2 = getElement() val element2 = getElement()

View file

@ -1,43 +1,82 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import com.nhaarman.mockito_kotlin.verify
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.filepicker.UploadableFile import fr.free.nrw.commons.filepicker.UploadableFile
import fr.free.nrw.commons.mwapi.MediaWikiApi import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.nearby.Place
import io.reactivex.Observable import io.reactivex.Observable
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.* import org.mockito.ArgumentMatchers
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations
import java.util.ArrayList
/**
* The clas contains unit test cases for UploadPresenter
*/
class UploadPresenterTest { class UploadPresenterTest {
@Mock @Mock
internal var uploadModel: UploadModel? = null internal var repository: UploadRepository? = null
@Mock @Mock
internal var uploadController: UploadController? = null internal var view: UploadContract.View? = null
@Mock @Mock
internal var mediaWikiApi: MediaWikiApi? = null var contribution: Contribution? = null
@Mock
private lateinit var uploadableFile: UploadableFile
@InjectMocks @InjectMocks
var uploadPresenter: UploadPresenter? = null var uploadPresenter: UploadPresenter? = null
private var uploadableFiles: ArrayList<UploadableFile> = ArrayList()
/**
* initial setup, test environment
*/
@Before @Before
@Throws(Exception::class) @Throws(Exception::class)
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
`when`(uploadModel!!.preProcessImages(ArgumentMatchers.anyListOf(UploadableFile::class.java), uploadPresenter?.onAttachView(view)
ArgumentMatchers.any(Place::class.java), `when`(repository?.buildContributions()).thenReturn(Observable.just(contribution))
ArgumentMatchers.anyString(), `when`(view?.isLoggedIn).thenReturn(true)
ArgumentMatchers.any(SimilarImageInterface::class.java))) uploadableFiles.add(uploadableFile)
.thenReturn(Observable.just(mock(UploadModel.UploadItem::class.java))) `when`(view?.uploadableFiles).thenReturn(uploadableFiles)
`when`(uploadableFile?.filePath).thenReturn("data://test")
} }
/**
* unit test case for method UploadPresenter.handleSubmit
*/
@Test @Test
fun receiveMultipleItems() { fun handleSubmitTest() {
val element = Mockito.mock(UploadableFile::class.java) uploadPresenter?.handleSubmit()
val element2 = Mockito.mock(UploadableFile::class.java) verify(view)?.isLoggedIn
var uriList: List<UploadableFile> = mutableListOf<UploadableFile>(element, element2) verify(view)?.showProgress(true)
uploadPresenter!!.receive(uriList, "external", mock(Place::class.java)) verify(repository)?.buildContributions()
val buildContributions = repository?.buildContributions()
buildContributions?.test()?.assertNoErrors()?.assertValue {
verify(repository)?.prepareService()
verify(view)?.showProgress(false)
verify(view)?.showMessage(ArgumentMatchers.any(Int::class.java))
verify(view)?.finish()
true
}
}
/**
* unit test for UploadMediaPresenter.deletePictureAtIndex
*/
@Test
fun deletePictureAtIndexTest() {
uploadPresenter?.deletePictureAtIndex(0)
verify(repository)?.deletePicture(ArgumentMatchers.anyString())
verify(view)?.showMessage(ArgumentMatchers.anyInt())//As there is only one while which we are asking for deletion, upload should be cancelled and this flow should be triggered
verify(view)?.finish()
} }
} }

Binary file not shown.