mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
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:
parent
04b051b37a
commit
7a5dc77057
68 changed files with 3753 additions and 2086 deletions
|
|
@ -65,6 +65,8 @@ dependencies {
|
|||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation 'com.nhaarman:mockito-kotlin:1.5.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
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
|
||||
|
|
|
|||
|
|
@ -50,10 +50,13 @@
|
|||
</activity>
|
||||
<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:label="@string/app_name"
|
||||
android:configChanges="orientation|screenSize|keyboard">
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
>
|
||||
<intent-filter android:label="@string/intent_share_upload_label">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ package fr.free.nrw.commons;
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
void onAttachView(MvpView view);
|
||||
void onAttachView(T view);
|
||||
|
||||
/**
|
||||
* Detaching a view makes sure that the view no more receives events from the presenter
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import android.widget.Toast;
|
|||
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
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.regex.Pattern;
|
||||
|
|
@ -18,9 +20,7 @@ import java.util.regex.Pattern;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import timber.log.Timber;
|
|||
* success and error
|
||||
*/
|
||||
@Singleton
|
||||
public class CampaignsPresenter implements BasePresenter {
|
||||
public class CampaignsPresenter implements BasePresenter<ICampaignsView> {
|
||||
private final OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
|
||||
private ICampaignsView view;
|
||||
|
|
@ -40,8 +40,9 @@ public class CampaignsPresenter implements BasePresenter {
|
|||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
||||
}
|
||||
|
||||
@Override public void onAttachView(MvpView view) {
|
||||
this.view = (ICampaignsView) view;
|
||||
@Override
|
||||
public void onAttachView(ICampaignsView view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
@Override public void onDetachView() {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
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.Calendar;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
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;
|
||||
|
||||
public class CategoriesModel implements CategoryClickedListener {
|
||||
/**
|
||||
* The model class for categories in upload
|
||||
*/
|
||||
public class CategoriesModel{
|
||||
private static final int SEARCH_CATS_LIMIT = 25;
|
||||
|
||||
private final MediaWikiApi mwApi;
|
||||
|
|
@ -41,13 +41,22 @@ public class CategoriesModel implements CategoryClickedListener {
|
|||
this.selectedCategories = new ArrayList<>();
|
||||
}
|
||||
|
||||
//region Misc. utility methods
|
||||
/**
|
||||
* Sorts CategoryItem by similarity
|
||||
* @param filter
|
||||
* @return
|
||||
*/
|
||||
public Comparator<CategoryItem> sortBySimilarity(final String filter) {
|
||||
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
|
||||
return (firstItem, secondItem) -> stringSimilarityComparator
|
||||
.compare(firstItem.getName(), secondItem.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the item contains an year
|
||||
* @param item
|
||||
* @return
|
||||
*/
|
||||
public boolean containsYear(String item) {
|
||||
//Check for current and previous year to exclude these categories from removal
|
||||
Calendar now = Calendar.getInstance();
|
||||
|
|
@ -67,6 +76,10 @@ public class CategoriesModel implements CategoryClickedListener {
|
|||
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates category count in category dao
|
||||
* @param item
|
||||
*/
|
||||
public void updateCategoryCount(CategoryItem item) {
|
||||
Category category = categoryDao.find(item.getName());
|
||||
|
||||
|
|
@ -78,29 +91,27 @@ public class CategoriesModel implements CategoryClickedListener {
|
|||
category.incTimesUsed();
|
||||
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) {
|
||||
return categoriesCache.containsKey(term);
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Category searching
|
||||
/**
|
||||
* Regional category search
|
||||
* @param term
|
||||
* @param imageTitleList
|
||||
* @return
|
||||
*/
|
||||
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)) {
|
||||
return gpsCategories()
|
||||
.concatWith(titleCategories(imageTitleList))
|
||||
.concatWith(recentCategories());
|
||||
Observable<CategoryItem> categoryItemObservable = gpsCategories()
|
||||
.concatWith(titleCategories(imageTitleList));
|
||||
if (hasDirectCategories()) {
|
||||
categoryItemObservable.concatWith(directCategories().concatWith(recentCategories()));
|
||||
}
|
||||
return categoryItemObservable;
|
||||
}
|
||||
|
||||
//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));
|
||||
}
|
||||
|
||||
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) {
|
||||
return categoriesCache.get(term);
|
||||
}
|
||||
|
||||
public Observable<CategoryItem> defaultCategories(List<String> titleList) {
|
||||
Observable<CategoryItem> directCat = directCategories();
|
||||
if (hasDirectCategories()) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if we have a category in DirectKV Store
|
||||
* @return
|
||||
*/
|
||||
private boolean hasDirectCategories() {
|
||||
return !directKvStore.getString("Category", "").equals("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns categories in DirectKVStore
|
||||
* @return
|
||||
*/
|
||||
private Observable<CategoryItem> directCategories() {
|
||||
String directCategory = directKvStore.getString("Category", "");
|
||||
List<String> categoryList = new ArrayList<>();
|
||||
|
|
@ -164,30 +160,49 @@ public class CategoriesModel implements CategoryClickedListener {
|
|||
return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns GPS categories
|
||||
* @return
|
||||
*/
|
||||
Observable<CategoryItem> gpsCategories() {
|
||||
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
|
||||
.map(name -> new CategoryItem(name, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns title based categories
|
||||
* @param titleList
|
||||
* @return
|
||||
*/
|
||||
private Observable<CategoryItem> titleCategories(List<String> titleList) {
|
||||
return Observable.fromIterable(titleList)
|
||||
.concatMap(this::getTitleCategories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return category for single title
|
||||
* @param title
|
||||
* @return
|
||||
*/
|
||||
private Observable<CategoryItem> getTitleCategories(String title) {
|
||||
return mwApi.searchTitles(title, SEARCH_CATS_LIMIT)
|
||||
.map(name -> new CategoryItem(name, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns recent categories
|
||||
* @return
|
||||
*/
|
||||
private Observable<CategoryItem> recentCategories() {
|
||||
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
|
||||
.map(s -> new CategoryItem(s, false));
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Category Selection
|
||||
@Override
|
||||
public void categoryClicked(CategoryItem item) {
|
||||
/**
|
||||
* Handles category item selection
|
||||
* @param item
|
||||
*/
|
||||
public void onCategoryItemClicked(CategoryItem item) {
|
||||
if (item.isSelected()) {
|
||||
selectCategory(item);
|
||||
updateCategoryCount(item);
|
||||
|
|
@ -196,22 +211,35 @@ public class CategoriesModel implements CategoryClickedListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select's category
|
||||
* @param item
|
||||
*/
|
||||
public void selectCategory(CategoryItem item) {
|
||||
selectedCategories.add(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselect Category
|
||||
* @param item
|
||||
*/
|
||||
public void unselectCategory(CategoryItem item) {
|
||||
selectedCategories.remove(item);
|
||||
}
|
||||
|
||||
public int selectedCategoriesCount() {
|
||||
return selectedCategories.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Selected Categories
|
||||
* @return
|
||||
*/
|
||||
public List<CategoryItem> getSelectedCategories() {
|
||||
return selectedCategories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Categories String List
|
||||
* @return
|
||||
*/
|
||||
public List<String> getCategoryStringList() {
|
||||
List<String> output = new ArrayList<>();
|
||||
for (CategoryItem item : selectedCategories) {
|
||||
|
|
@ -219,6 +247,12 @@ public class CategoriesModel implements CategoryClickedListener {
|
|||
}
|
||||
return output;
|
||||
}
|
||||
//endregion
|
||||
|
||||
/**
|
||||
* Cleanup the existing in memory cache's
|
||||
*/
|
||||
public void cleanUp() {
|
||||
this.categoriesCache.clear();
|
||||
this.selectedCategories.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public class CategoryItem implements Parcelable {
|
|||
}
|
||||
};
|
||||
|
||||
CategoryItem(String name, boolean selected) {
|
||||
public CategoryItem(String name, boolean selected) {
|
||||
this.name = name;
|
||||
this.selected = selected;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ public class ContributionDao {
|
|||
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
|
||||
}
|
||||
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_STATE, contribution.getState());
|
||||
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import fr.free.nrw.commons.nearby.PlaceRenderer;
|
|||
import fr.free.nrw.commons.review.ReviewController;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
import fr.free.nrw.commons.upload.FileProcessor;
|
||||
import fr.free.nrw.commons.upload.UploadModule;
|
||||
import fr.free.nrw.commons.widget.PicOfDayAppWidget;
|
||||
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget;
|
|||
ActivityBuilderModule.class,
|
||||
FragmentBuilderModule.class,
|
||||
ServiceBuilderModule.class,
|
||||
ContentProviderBuilderModule.class
|
||||
ContentProviderBuilderModule.class, UploadModule.class
|
||||
})
|
||||
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
|
||||
void inject(CommonsApplication application);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ import com.google.gson.Gson;
|
|||
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -37,6 +42,8 @@ import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
|
|||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public class CommonsApplicationModule {
|
||||
private Context applicationContext;
|
||||
public static final String IO_THREAD="io_thread";
|
||||
public static final String MAIN_THREAD="main_thread";
|
||||
|
||||
public CommonsApplicationModule(Context applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
|
|
@ -172,4 +179,16 @@ public class CommonsApplicationModule {
|
|||
public boolean provideIsBetaVariant() {
|
||||
return ConfigUtils.isBetaFlavour();
|
||||
}
|
||||
|
||||
@Named(IO_THREAD)
|
||||
@Provides
|
||||
public Scheduler providesIoThread(){
|
||||
return Schedulers.io();
|
||||
}
|
||||
|
||||
@Named(MAIN_THREAD)
|
||||
@Provides
|
||||
public Scheduler providesMainThread(){
|
||||
return AndroidSchedulers.mainThread();
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,9 @@ import fr.free.nrw.commons.nearby.NearbyListFragment;
|
|||
import fr.free.nrw.commons.nearby.NearbyMapFragment;
|
||||
import fr.free.nrw.commons.review.ReviewImageFragment;
|
||||
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
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
|
|
@ -71,4 +74,12 @@ public abstract class FragmentBuilderModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract ReviewImageFragment bindReviewOutOfContextFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract UploadMediaDetailFragment bindUploadMediaDetailFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract UploadCategoriesFragment bindUploadCategoriesFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract MediaLicenseFragment bindMediaLicenseFragment();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,6 @@ import butterknife.ButterKnife;
|
|||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
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.R;
|
||||
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.List;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -57,9 +57,6 @@ import io.reactivex.Single;
|
|||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
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 static android.view.View.GONE;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,12 @@ import java.util.List;
|
|||
/**
|
||||
* Holds a description of an item being uploaded by {@link UploadActivity}
|
||||
*/
|
||||
class Description {
|
||||
public class Description {
|
||||
|
||||
private String languageCode;
|
||||
private String descriptionText;
|
||||
private int selectedLanguageIndex = -1;
|
||||
private boolean isManuallyAdded=false;
|
||||
|
||||
/**
|
||||
* @return The language code ie. "en" or "fr"
|
||||
|
|
@ -47,6 +48,21 @@ class Description {
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemSelectedListener;
|
||||
import android.widget.EditText;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatEditText;
|
||||
import androidx.appcompat.widget.AppCompatSpinner;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import butterknife.BindView;
|
||||
|
|
@ -24,60 +24,35 @@ import butterknife.ButterKnife;
|
|||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.utils.AbstractTextWatcher;
|
||||
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;
|
||||
|
||||
class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {
|
||||
public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {
|
||||
|
||||
private Title title;
|
||||
private List<Description> descriptions;
|
||||
private Context context;
|
||||
private Callback callback;
|
||||
private Subject<String> titleChangedSubject;
|
||||
|
||||
private BiMap<AdapterView, String> selectedLanguages;
|
||||
private UploadView uploadView;
|
||||
|
||||
DescriptionsAdapter(UploadView uploadView) {
|
||||
title = new Title();
|
||||
public DescriptionsAdapter() {
|
||||
descriptions = new ArrayList<>();
|
||||
titleChangedSubject = BehaviorSubject.create();
|
||||
selectedLanguages = new BiMap<>();
|
||||
this.uploadView = uploadView;
|
||||
}
|
||||
|
||||
void setCallback(Callback callback) {
|
||||
public void setCallback(Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
void setItems(Title title, List<Description> descriptions) {
|
||||
public void setItems(List<Description> descriptions) {
|
||||
this.descriptions = descriptions;
|
||||
this.title = title;
|
||||
selectedLanguages = new BiMap<>();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == 0) return 1;
|
||||
else return 2;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view;
|
||||
if (viewType == 1) {
|
||||
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);
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.row_item_description, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -87,29 +62,21 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
|
|||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return descriptions.size() + 1;
|
||||
return descriptions.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets descriptions
|
||||
*
|
||||
* @return List of descriptions
|
||||
*/
|
||||
List<Description> getDescriptions() {
|
||||
public List<Description> getDescriptions() {
|
||||
return descriptions;
|
||||
}
|
||||
|
||||
void addDescription(Description description) {
|
||||
public void addDescription(Description description) {
|
||||
this.descriptions.add(description);
|
||||
notifyItemInserted(descriptions.size() + 1);
|
||||
}
|
||||
|
||||
public Title getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(Title title) {
|
||||
this.title = title;
|
||||
notifyItemInserted(0);
|
||||
notifyItemInserted(descriptions.size());
|
||||
}
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
|
@ -119,98 +86,53 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
|
|||
AppCompatSpinner spinnerDescriptionLanguages;
|
||||
|
||||
@BindView(R.id.description_item_edit_text)
|
||||
EditText descItemEditText;
|
||||
|
||||
private View view;
|
||||
AppCompatEditText descItemEditText;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
ButterKnife.bind(this, itemView);
|
||||
this.view = itemView;
|
||||
Timber.i("descItemEditText:" + descItemEditText);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void init(int position) {
|
||||
Description description = descriptions.get(position);
|
||||
Timber.d("Description is " + description);
|
||||
if (!TextUtils.isEmpty(description.getDescriptionText())) {
|
||||
descItemEditText.setText(description.getDescriptionText());
|
||||
} else {
|
||||
descItemEditText.setText("");
|
||||
}
|
||||
if (position == 0) {
|
||||
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.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(),
|
||||
null);
|
||||
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;
|
||||
//2 is for drawable right
|
||||
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))){
|
||||
if (getAdapterPosition() == 0) {
|
||||
callback.showAlert(R.string.media_detail_description,
|
||||
R.string.description_info);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
} else {
|
||||
Description description = descriptions.get(position - 1);
|
||||
Timber.d("Description is " + description);
|
||||
if (!TextUtils.isEmpty(description.getDescriptionText())) {
|
||||
descItemEditText.setText(description.getDescriptionText());
|
||||
} else {
|
||||
descItemEditText.setText("");
|
||||
}
|
||||
|
||||
// Show the info icon for the first description
|
||||
if (position == 1) {
|
||||
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null);
|
||||
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_description, R.string.description_info);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
descItemEditText.addTextChangedListener(new AbstractTextWatcher(descriptionText->{
|
||||
descriptions.get(position - 1).setDescriptionText(descriptionText);
|
||||
}));
|
||||
|
||||
descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
ViewUtil.hideKeyboard(v);
|
||||
} else {
|
||||
uploadView.setTopCardState(false);
|
||||
}
|
||||
});
|
||||
|
||||
initLanguageSpinner(position, description);
|
||||
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
|
||||
}
|
||||
|
||||
descItemEditText.addTextChangedListener(new AbstractTextWatcher(
|
||||
descriptionText -> descriptions.get(position)
|
||||
.setDescriptionText(descriptionText)));
|
||||
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
|
||||
*/
|
||||
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);
|
||||
languagesAdapter.notifyDataSetChanged();
|
||||
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() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int position,
|
||||
long l) {
|
||||
long l) {
|
||||
description.setSelectedLanguageIndex(position);
|
||||
String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter()).getLanguageCode(position);
|
||||
String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter())
|
||||
.getLanguageCode(position);
|
||||
description.setLanguageCode(languageCode);
|
||||
selectedLanguages.remove(adapterView);
|
||||
selectedLanguages.put(adapterView, languageCode);
|
||||
((SpinnerLanguagesAdapter) adapterView.getAdapter()).selectedLangCode = languageCode;
|
||||
((SpinnerLanguagesAdapter) adapterView
|
||||
.getAdapter()).selectedLangCode = languageCode;
|
||||
}
|
||||
|
||||
@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
|
||||
* @return
|
||||
*/
|
||||
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 {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.content.Context;
|
|||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import fr.free.nrw.commons.upload.SimilarImageDialogFragment.Callback;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
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
|
||||
*/
|
||||
@Singleton
|
||||
public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
||||
public class FileProcessor implements Callback {
|
||||
|
||||
@Inject
|
||||
CacheController cacheController;
|
||||
|
|
@ -58,7 +59,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
|||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
@Inject
|
||||
FileProcessor() {
|
||||
public FileProcessor() {
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ import timber.log.Timber;
|
|||
* Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation
|
||||
* is uploaded, extract latitude and longitude from EXIF data of image.
|
||||
*/
|
||||
class GPSExtractor {
|
||||
public class GPSExtractor {
|
||||
|
||||
static final GPSExtractor DUMMY= new GPSExtractor();
|
||||
private double decLatitude;
|
||||
private double decLongitude;
|
||||
boolean imageCoordsExists;
|
||||
public boolean imageCoordsExists;
|
||||
private String latitude;
|
||||
private String longitude;
|
||||
private String latitudeRef;
|
||||
|
|
@ -96,11 +96,11 @@ class GPSExtractor {
|
|||
}
|
||||
}
|
||||
|
||||
double getDecLatitude() {
|
||||
public double getDecLatitude() {
|
||||
return decLatitude;
|
||||
}
|
||||
|
||||
double getDecLongitude() {
|
||||
public double getDecLongitude() {
|
||||
return decLongitude;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,17 +37,21 @@ public class SimilarImageDialogFragment extends DialogFragment {
|
|||
Button positiveButton;
|
||||
@BindView(R.id.negative_button)
|
||||
Button negativeButton;
|
||||
onResponse mOnResponse;//Implemented interface from shareActivity
|
||||
Callback callback;//Implemented interface from shareActivity
|
||||
Boolean gotResponse = false;
|
||||
|
||||
public SimilarImageDialogFragment() {
|
||||
}
|
||||
public interface onResponse{
|
||||
public interface Callback {
|
||||
void onPositiveResponse();
|
||||
|
||||
void onNegativeResponse();
|
||||
}
|
||||
|
||||
public void setCallback(Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false);
|
||||
|
|
@ -77,7 +81,6 @@ public class SimilarImageDialogFragment extends DialogFragment {
|
|||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mOnResponse = (onResponse) getActivity();//Interface Implementation
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -91,21 +94,21 @@ public class SimilarImageDialogFragment extends DialogFragment {
|
|||
public void onDismiss(DialogInterface dialog) {
|
||||
// I user dismisses dialog by pressing outside the dialog.
|
||||
if (!gotResponse) {
|
||||
mOnResponse.onNegativeResponse();
|
||||
callback.onNegativeResponse();
|
||||
}
|
||||
super.onDismiss(dialog);
|
||||
}
|
||||
|
||||
@OnClick(R.id.negative_button)
|
||||
public void onNegativeButtonClicked() {
|
||||
mOnResponse.onNegativeResponse();
|
||||
callback.onNegativeResponse();
|
||||
gotResponse = true;
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@OnClick(R.id.postive_button)
|
||||
public void onPositiveButtonClicked() {
|
||||
mOnResponse.onPositiveResponse();
|
||||
callback.onPositiveResponse();
|
||||
gotResponse = true;
|
||||
dismiss();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -22,6 +21,10 @@ import butterknife.ButterKnife;
|
|||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.utils.BiMap;
|
||||
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 {
|
||||
|
||||
|
|
@ -83,27 +86,32 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter {
|
|||
@Override
|
||||
public View getDropDownView(int position, @Nullable View convertView,
|
||||
@NonNull ViewGroup parent) {
|
||||
View view = layoutInflater.inflate(resource, parent, false);
|
||||
ViewHolder holder = new ViewHolder(view);
|
||||
if (convertView == null) {
|
||||
convertView = layoutInflater.inflate(resource, parent, false);
|
||||
}
|
||||
ViewHolder holder = new ViewHolder(convertView);
|
||||
holder.init(position, true);
|
||||
return view;
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = layoutInflater.inflate(resource, parent, false);
|
||||
ViewHolder holder = new ViewHolder(view);
|
||||
ViewHolder holder;
|
||||
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);
|
||||
return view;
|
||||
return convertView;
|
||||
}
|
||||
|
||||
|
||||
public class ViewHolder {
|
||||
|
||||
@BindView(R.id.ll_container_description_language)
|
||||
LinearLayout llContainerDescriptionLanguage;
|
||||
|
||||
@BindView(R.id.tv_language)
|
||||
TextView tvLanguage;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||
|
||||
public interface ThumbnailClickedListener {
|
||||
void thumbnailClicked(UploadModel.UploadItem content);
|
||||
void thumbnailClicked(UploadableFile content);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -31,4 +31,8 @@ public class Title{
|
|||
public boolean isEmpty() {
|
||||
return titleText==null || titleText.isEmpty();
|
||||
}
|
||||
|
||||
public String getTitleText() {
|
||||
return titleText;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,162 +1,115 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
|
||||
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.MotionEvent;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ViewFlipper;
|
||||
|
||||
import com.github.chrisbanes.photoview.PhotoView;
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
import com.jakewharton.rxbinding2.widget.RxTextView;
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
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.contributions.ContributionController;
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
|
||||
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
|
||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import java.util.Collections;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_EXTERNAL;
|
||||
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
|
||||
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
||||
|
||||
public class UploadActivity extends BaseActivity implements UploadView, SimilarImageInterface {
|
||||
@Inject MediaWikiApi mwApi;
|
||||
public class UploadActivity extends BaseActivity implements UploadContract.View ,UploadBaseFragment.Callback{
|
||||
@Inject
|
||||
ContributionController contributionController;
|
||||
@Inject @Named("default_preferences") JsonKvStore directKvStore;
|
||||
@Inject UploadPresenter presenter;
|
||||
@Inject UploadContract.UserActionListener presenter;
|
||||
@Inject CategoriesModel categoriesModel;
|
||||
@Inject SessionManager sessionManager;
|
||||
|
||||
// Main GUI
|
||||
@BindView(R.id.backgroundImage) PhotoView background;
|
||||
@BindView(R.id.upload_root_layout)
|
||||
RelativeLayout rootLayout;
|
||||
@BindView(R.id.view_flipper) ViewFlipper viewFlipper;
|
||||
@BindView(R.id.cv_container_top_card)
|
||||
CardView cvContainerTopCard;
|
||||
|
||||
// Top Card
|
||||
@BindView(R.id.top_card) CardView topCard;
|
||||
@BindView(R.id.top_card_expand_button) ImageView topCardExpandButton;
|
||||
@BindView(R.id.top_card_title) TextView topCardTitle;
|
||||
@BindView(R.id.top_card_thumbnails) RecyclerView topCardThumbnails;
|
||||
@BindView(R.id.ll_container_top_card)
|
||||
LinearLayout llContainerTopCard;
|
||||
|
||||
// Bottom Card
|
||||
@BindView(R.id.bottom_card) CardView bottomCard;
|
||||
@BindView(R.id.bottom_card_expand_button) ImageView bottomCardExpandButton;
|
||||
@BindView(R.id.bottom_card_title) TextView bottomCardTitle;
|
||||
@BindView(R.id.bottom_card_subtitle) TextView bottomCardSubtitle;
|
||||
@BindView(R.id.bottom_card_next) Button next;
|
||||
@BindView(R.id.bottom_card_previous) Button previous;
|
||||
@BindView(R.id.bottom_card_add_desc) Button bottomCardAddDescription;
|
||||
@BindView(R.id.prev_title_desc) Button prevTitleDecs;
|
||||
@BindView(R.id.categories_subtitle) TextView categoriesSubtitle;
|
||||
@BindView(R.id.license_subtitle) TextView licenseSubtitle;
|
||||
@BindView(R.id.please_wait_text_view) TextView pleaseWaitTextView;
|
||||
@BindView(R.id.rl_container_title)
|
||||
RelativeLayout rlContainerTitle;
|
||||
|
||||
@BindView(R.id.tv_top_card_title)
|
||||
TextView tvTopCardTitle;
|
||||
|
||||
@BindView(R.id.right_card_map_button) View rightCardMapButton;
|
||||
@BindView(R.id.ib_toggle_top_card)
|
||||
ImageButton ibToggleTopCard;
|
||||
|
||||
// Category Search
|
||||
@BindView(R.id.categories_title) TextView categoryTitle;
|
||||
@BindView(R.id.category_next) Button categoryNext;
|
||||
@BindView(R.id.category_previous) Button categoryPrevious;
|
||||
@BindView(R.id.categoriesSearchInProgress) ProgressBar categoriesSearchInProgress;
|
||||
@BindView(R.id.category_search) EditText categoriesSearch;
|
||||
@BindView(R.id.category_search_container) TextInputLayout categoriesSearchContainer;
|
||||
@BindView(R.id.categories) RecyclerView categoriesList;
|
||||
@BindView(R.id.category_search_layout)
|
||||
FrameLayout categoryFrameLayout;
|
||||
@BindView(R.id.rv_thumbnails)
|
||||
RecyclerView rvThumbnails;
|
||||
|
||||
// Final Submission
|
||||
@BindView(R.id.license_title) TextView licenseTitle;
|
||||
@BindView(R.id.share_license_summary) HtmlTextView licenseSummary;
|
||||
@BindView(R.id.license_list) Spinner licenseSpinner;
|
||||
@BindView(R.id.submit) Button submit;
|
||||
@BindView(R.id.license_previous) Button licensePrevious;
|
||||
@BindView(R.id.rv_descriptions) RecyclerView rvDescriptions;
|
||||
@BindView(R.id.vp_upload)
|
||||
ViewPager vpUpload;
|
||||
|
||||
private DescriptionsAdapter descriptionsAdapter;
|
||||
private RVRendererAdapter<CategoryItem> categoriesAdapter;
|
||||
private boolean isTitleExpanded=true;
|
||||
|
||||
private CompositeDisposable compositeDisposable;
|
||||
private ProgressDialog progressDialog;
|
||||
private boolean multipleUpload = false, flagForSubmit = false;
|
||||
private UploadImageAdapter uploadImagesAdapter;
|
||||
private List<Fragment> fragments;
|
||||
private UploadCategoriesFragment uploadCategoriesFragment;
|
||||
private MediaLicenseFragment mediaLicenseFragment;
|
||||
private ThumbnailsAdapter thumbnailsAdapter;
|
||||
|
||||
|
||||
private String source;
|
||||
private Place place;
|
||||
private List<UploadableFile> uploadableFiles= Collections.emptyList();
|
||||
private int currentSelectedPosition=0;
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_upload);
|
||||
|
||||
ButterKnife.bind(this);
|
||||
|
||||
configureLayout();
|
||||
configureTopCard();
|
||||
configureBottomCard();
|
||||
initRecyclerView();
|
||||
configureRightCard();
|
||||
configureNavigationButtons();
|
||||
configureCategories();
|
||||
configureLicenses();
|
||||
|
||||
presenter.init();
|
||||
compositeDisposable = new CompositeDisposable();
|
||||
init();
|
||||
|
||||
PermissionUtils.checkPermissionsAndPerformAction(this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
|
|
@ -165,283 +118,150 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
|
|||
R.string.write_storage_permission_rationale_for_image_share);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkIfLoggedIn() {
|
||||
if (!sessionManager.isUserLoggedIn()) {
|
||||
Timber.d("Current account is null");
|
||||
ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in));
|
||||
Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class);
|
||||
startActivity(loginIntent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
private void init() {
|
||||
initProgressDialog();
|
||||
initViewPager();
|
||||
initThumbnailsRecyclerView();
|
||||
//And init other things you need to
|
||||
}
|
||||
|
||||
private void initProgressDialog() {
|
||||
progressDialog = new ProgressDialog(this);
|
||||
progressDialog.setMessage(getString(R.string.please_wait));
|
||||
}
|
||||
|
||||
private void initThumbnailsRecyclerView() {
|
||||
rvThumbnails.setLayoutManager(new LinearLayoutManager(this,
|
||||
LinearLayoutManager.HORIZONTAL, false));
|
||||
thumbnailsAdapter=new ThumbnailsAdapter(() -> currentSelectedPosition);
|
||||
rvThumbnails.setAdapter(thumbnailsAdapter);
|
||||
|
||||
}
|
||||
|
||||
private void initViewPager() {
|
||||
uploadImagesAdapter=new UploadImageAdapter(getSupportFragmentManager());
|
||||
vpUpload.setAdapter(uploadImagesAdapter);
|
||||
vpUpload.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset,
|
||||
int positionOffsetPixels) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
currentSelectedPosition=position;
|
||||
if (position >= uploadableFiles.size()) {
|
||||
cvContainerTopCard.setVisibility(View.GONE);
|
||||
} else {
|
||||
thumbnailsAdapter.notifyDataSetChanged();
|
||||
cvContainerTopCard.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
presenter.cleanup();
|
||||
super.onDestroy();
|
||||
public boolean isLoggedIn() {
|
||||
return sessionManager.isUserLoggedIn();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
checkIfLoggedIn();
|
||||
|
||||
presenter.onAttachView(this);
|
||||
if (!isLoggedIn()) {
|
||||
askUserToLogIn();
|
||||
}
|
||||
checkStoragePermissions();
|
||||
compositeDisposable.add(
|
||||
RxTextView.textChanges(categoriesSearch)
|
||||
.doOnEach(v -> categoriesSearchContainer.setError(null))
|
||||
.takeUntil(RxView.detaches(categoriesSearch))
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(filter -> updateCategoryList(filter.toString()), Timber::e)
|
||||
);
|
||||
}
|
||||
|
||||
private void checkStoragePermissions() {
|
||||
PermissionUtils.checkPermissionsAndPerformAction(this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
() -> presenter.addView(this),
|
||||
() -> {
|
||||
//TODO handle this
|
||||
},
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale_for_image_share);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
presenter.removeView();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateThumbnails(List<UploadModel.UploadItem> uploads) {
|
||||
int uploadCount = uploads.size();
|
||||
topCardThumbnails.setAdapter(new UploadThumbnailsAdapterFactory(presenter::thumbnailClicked).create(uploads));
|
||||
topCardTitle.setText(getResources().getQuantityString(R.plurals.upload_count_title, uploadCount, uploadCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRightCardContent(boolean gpsPresent) {
|
||||
if (gpsPresent) {
|
||||
rightCardMapButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
rightCardMapButton.setVisibility(View.GONE);
|
||||
}
|
||||
//The card should be disabled if it has no buttons.
|
||||
setRightCardVisibility(gpsPresent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBottomCardContent(int currentStep,
|
||||
int stepCount,
|
||||
UploadModel.UploadItem uploadItem,
|
||||
boolean isShowingItem) {
|
||||
boolean saveForPrevImage = false;
|
||||
int singleUploadStepCount = 3;
|
||||
|
||||
String cardTitle = getResources().getString(R.string.step_count, currentStep, stepCount);
|
||||
String cardSubTitle = getResources().getString(R.string.image_in_set_label, currentStep);
|
||||
bottomCardTitle.setText(cardTitle);
|
||||
bottomCardSubtitle.setText(cardSubTitle);
|
||||
categoryTitle.setText(cardTitle);
|
||||
licenseTitle.setText(cardTitle);
|
||||
if (currentStep == stepCount) {
|
||||
dismissKeyboard();
|
||||
}
|
||||
if (stepCount > singleUploadStepCount) {
|
||||
multipleUpload = true;
|
||||
}
|
||||
if (multipleUpload && currentStep != 1) {
|
||||
saveForPrevImage = true;
|
||||
}
|
||||
configurePrevButton(saveForPrevImage);
|
||||
if(isShowingItem) {
|
||||
descriptionsAdapter.setItems(uploadItem.getTitle(), uploadItem.getDescriptions());
|
||||
rvDescriptions.setAdapter(descriptionsAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLicenses(List<String> licenses, String selectedLicense) {
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, licenses);
|
||||
licenseSpinner.setAdapter(adapter);
|
||||
|
||||
int position = licenses.indexOf(getString(Utils.licenseNameFor(selectedLicense)));
|
||||
|
||||
// Check position is valid
|
||||
if (position < 0) {
|
||||
Timber.d("Invalid position: %d. Using default license", position);
|
||||
position = licenses.size() - 1;
|
||||
}
|
||||
|
||||
Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(selectedLicense)));
|
||||
licenseSpinner.setSelection(position);
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
@Override
|
||||
public void updateLicenseSummary(String selectedLicense, int imageCount) {
|
||||
String licenseHyperLink = "<a href='" + Utils.licenseUrlFor(selectedLicense) + "'>" +
|
||||
getString(Utils.licenseNameFor(selectedLicense)) + "</a><br>";
|
||||
licenseSummary.setHtmlText(getResources().getQuantityString(R.plurals.share_license_summary, imageCount, licenseHyperLink));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTopCardContent() {
|
||||
RecyclerView.Adapter adapter = topCardThumbnails.getAdapter();
|
||||
if (adapter != null) {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNextEnabled(boolean available) {
|
||||
next.setEnabled(available);
|
||||
categoryNext.setEnabled(available);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSubmitEnabled(boolean available) {
|
||||
submit.setEnabled(available);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPreviousEnabled(boolean available) {
|
||||
previous.setEnabled(available);
|
||||
categoryPrevious.setEnabled(available);
|
||||
licensePrevious.setEnabled(available);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTopCardState(boolean state) {
|
||||
updateCardState(state, topCardExpandButton, topCardThumbnails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTopCardVisibility(boolean visible) {
|
||||
topCard.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBottomCardVisibility(boolean visible) {
|
||||
bottomCard.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRightCardVisibility(boolean visible) {
|
||||
rightCardMapButton.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBottomCardVisibility(@UploadPage int page, int uploadCount) {
|
||||
if (page == TITLE_CARD) {
|
||||
viewFlipper.setDisplayedChild(0);
|
||||
} else if (page == CATEGORIES) {
|
||||
viewFlipper.setDisplayedChild(1);
|
||||
} else if (page == LICENSE) {
|
||||
viewFlipper.setDisplayedChild(2);
|
||||
dismissKeyboard();
|
||||
} else if (page == PLEASE_WAIT) {
|
||||
viewFlipper.setDisplayedChild(3);
|
||||
pleaseWaitTextView.setText(getResources().getQuantityText(R.plurals.receiving_shared_content, uploadCount));
|
||||
}
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Only show the subtitle ("For all images in set") if multiple images being uploaded
|
||||
* @param imageCount Number of images being uploaded
|
||||
* Show/Hide the progress dialog
|
||||
*/
|
||||
@Override
|
||||
public void updateSubtitleVisibility(int imageCount) {
|
||||
categoriesSubtitle.setVisibility(imageCount > 1 ? View.VISIBLE : View.GONE);
|
||||
licenseSubtitle.setVisibility(imageCount > 1 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBottomCardState(boolean state) {
|
||||
updateCardState(state, bottomCardExpandButton, rvDescriptions, previous, next, prevTitleDecs, bottomCardAddDescription);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setBackground(Uri mediaUri) {
|
||||
background.setImageURI(mediaUri);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void dismissKeyboard() {
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
|
||||
|
||||
// verify if the soft keyboard is open
|
||||
if (imm != null && imm.isAcceptingText() && getCurrentFocus() != null) {
|
||||
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
|
||||
public void showProgress(boolean shouldShow) {
|
||||
if (shouldShow) {
|
||||
if (!progressDialog.isShowing()) {
|
||||
progressDialog.show();
|
||||
}
|
||||
} else {
|
||||
if (progressDialog != null && !isFinishing()) {
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showBadPicturePopup(String errorMessage) {
|
||||
DialogUtil.showAlertDialog(this,
|
||||
getString(R.string.warning),
|
||||
errorMessage,
|
||||
() -> presenter.deletePicture(),
|
||||
() -> presenter.keepPicture());
|
||||
public int getIndexInViewFlipper(UploadBaseFragment fragment) {
|
||||
return fragments.indexOf(fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showDuplicatePicturePopup() {
|
||||
DialogUtil.showAlertDialog(this,
|
||||
getString(R.string.warning),
|
||||
String.format(getString(R.string.upload_title_duplicate), presenter.getCurrentImageFileName()),
|
||||
null,
|
||||
() -> {
|
||||
presenter.keepPicture();
|
||||
presenter.handleNext(descriptionsAdapter.getTitle(), getDescriptions());
|
||||
});
|
||||
}
|
||||
|
||||
public void showNoCategorySelectedWarning() {
|
||||
DialogUtil.showAlertDialog(this,
|
||||
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,
|
||||
() -> presenter.handleCategoryNext(categoriesModel, true));
|
||||
public int getTotalNumberOfSteps() {
|
||||
return fragments.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showProgressDialog() {
|
||||
if (progressDialog == null) {
|
||||
progressDialog = new ProgressDialog(this);
|
||||
}
|
||||
progressDialog.setMessage(getString(R.string.please_wait));
|
||||
progressDialog.show();
|
||||
public void showMessage(int messageResourceId) {
|
||||
ViewUtil.showLongToast(this, messageResourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideProgressDialog() {
|
||||
if (progressDialog != null && !isFinishing()) {
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
public List<UploadableFile> getUploadableFiles() {
|
||||
return uploadableFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchMapActivity(LatLng decCoords) {
|
||||
Utils.handleGeoCoordinates(this, decCoords);
|
||||
public void showHideTopCard(boolean shouldShow) {
|
||||
llContainerTopCard.setVisibility(shouldShow?View.VISIBLE:View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showErrorMessage(int resourceId) {
|
||||
ViewUtil.showShortToast(this, resourceId);
|
||||
public void onUploadMediaDeleted(int index) {
|
||||
fragments.remove(index);//Remove the corresponding fragment
|
||||
uploadableFiles.remove(index);//Remove the files from the list
|
||||
thumbnailsAdapter.notifyItemRemoved(index); //Notify the thumbnails adapter
|
||||
uploadImagesAdapter.notifyDataSetChanged(); //Notify the ViewPager
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initDefaultCategories() {
|
||||
updateCategoryList("");
|
||||
public void updateTopCardTitle() {
|
||||
tvTopCardTitle.setText(getResources()
|
||||
.getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void askUserToLogIn() {
|
||||
Timber.d("current session is null, asking user to login");
|
||||
ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in));
|
||||
Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class);
|
||||
startActivity(loginIntent);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
|
@ -450,179 +270,6 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
|
|||
}
|
||||
}
|
||||
|
||||
private void configureLicenses() {
|
||||
licenseSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
String licenseName = parent.getItemAtPosition(position).toString();
|
||||
presenter.selectLicense(licenseName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
presenter.selectLicense(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configureLayout() {
|
||||
background.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
background.setOnScaleChangeListener((scaleFactor, x, y) -> presenter.closeAllCards());
|
||||
}
|
||||
|
||||
private void configureTopCard() {
|
||||
topCardExpandButton.setOnClickListener(v -> presenter.toggleTopCardState());
|
||||
topCardThumbnails.setLayoutManager(new LinearLayoutManager(this,
|
||||
LinearLayoutManager.HORIZONTAL, false));
|
||||
}
|
||||
|
||||
private void configureBottomCard() {
|
||||
boolean flagVal = directKvStore.getBoolean("flagForSubmit");
|
||||
if(flagVal){
|
||||
prevTitleDecs.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
prevTitleDecs.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
bottomCardExpandButton.setOnClickListener(v -> presenter.toggleBottomCardState());
|
||||
bottomCard.setOnClickListener(v -> presenter.toggleBottomCardState());
|
||||
bottomCardAddDescription.setOnClickListener(v -> addNewDescription());
|
||||
}
|
||||
|
||||
private void addNewDescription() {
|
||||
descriptionsAdapter.addDescription(new Description());
|
||||
rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1);
|
||||
}
|
||||
|
||||
private void configureRightCard() {
|
||||
rightCardMapButton.setOnClickListener(v -> presenter.openCoordinateMap());
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void configurePrevButton(Boolean saveForPrevImage){
|
||||
prevTitleDecs.setCompoundDrawablesWithIntrinsicBounds(null, null, getResources().getDrawable(R.drawable.mapbox_info_icon_default), null);
|
||||
|
||||
String name = "prev_";
|
||||
if (saveForPrevImage) {
|
||||
name = name + "image_";
|
||||
} else {
|
||||
name = name + "upload_";
|
||||
}
|
||||
String title = directKvStore.getString(name + "title");
|
||||
Title t = new Title();
|
||||
t.setTitleText(title);
|
||||
|
||||
List<Description> finalDesc = new LinkedList<>();
|
||||
int descCount = directKvStore.getInt(name + "descCount");
|
||||
for (int i = 0; i < descCount; i++) {
|
||||
Description description= new Description();
|
||||
String desc = directKvStore.getString(name + "description_<" + i + ">");
|
||||
description.setDescriptionText(desc);
|
||||
finalDesc.add(description);
|
||||
int position = directKvStore.getInt(name + "spinnerPosition_<" + i + ">");
|
||||
description.setSelectedLanguageIndex(position);
|
||||
}
|
||||
prevTitleDecs.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 = prevTitleDecs.getCompoundDrawables()[2];
|
||||
int infoHintbox = prevTitleDecs.getWidth() - info.getBounds().width();
|
||||
if (event.getX() + extraTapArea < infoHintbox) return false;
|
||||
|
||||
DialogUtil.showAlertDialog(this, null, getString(R.string.previous_button_tooltip_message), "okay", null, null, null);
|
||||
|
||||
return true;
|
||||
});
|
||||
prevTitleDecs.setOnClickListener((View v) -> {
|
||||
descriptionsAdapter.setItems(t, finalDesc);
|
||||
rvDescriptions.setAdapter(descriptionsAdapter);
|
||||
});
|
||||
}
|
||||
|
||||
private void configureNavigationButtons() {
|
||||
// Navigation next / previous for each image as we're collecting title + description
|
||||
next.setOnClickListener(v -> {
|
||||
if (!NetworkUtils.isInternetConnectionEstablished(this)) {
|
||||
ViewUtil.showShortSnackbar(rootLayout, R.string.no_internet);
|
||||
return;
|
||||
}
|
||||
setTitleAndDescriptions();
|
||||
if (multipleUpload) {
|
||||
savePrevTitleDesc("prev_image_");
|
||||
}
|
||||
presenter.handleNext(descriptionsAdapter.getTitle(),
|
||||
descriptionsAdapter.getDescriptions());
|
||||
});
|
||||
previous.setOnClickListener(v -> presenter.handlePrevious());
|
||||
|
||||
// Next / previous for the category selection currentPage
|
||||
categoryNext.setOnClickListener(v -> presenter.handleCategoryNext(categoriesModel, false));
|
||||
categoryPrevious.setOnClickListener(v -> presenter.handlePrevious());
|
||||
|
||||
// Finally, the previous / submit buttons on the final currentPage of the wizard
|
||||
licensePrevious.setOnClickListener(v -> presenter.handlePrevious());
|
||||
submit.setOnClickListener(v -> {
|
||||
flagForSubmit = true;
|
||||
directKvStore.putBoolean("flagForSubmit", flagForSubmit);
|
||||
savePrevTitleDesc("prev_upload_");
|
||||
Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG).show();
|
||||
presenter.handleSubmit(categoriesModel);
|
||||
finish();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void setTitleAndDescriptions() {
|
||||
List<Description> descriptions = descriptionsAdapter.getDescriptions();
|
||||
Timber.d("Descriptions size is %d are %s", descriptions.size(), descriptions);
|
||||
}
|
||||
|
||||
private void configureCategories() {
|
||||
categoryFrameLayout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
|
||||
categoriesAdapter = new UploadCategoriesAdapterFactory(categoriesModel).create(new ArrayList<>());
|
||||
categoriesList.setLayoutManager(new LinearLayoutManager(this));
|
||||
categoriesList.setAdapter(categoriesAdapter);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void updateCategoryList(String filter) {
|
||||
List<String> imageTitleList = presenter.getImageTitleList();
|
||||
compositeDisposable.add(Observable.fromIterable(categoriesModel.getSelectedCategories())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSubscribe(disposable -> {
|
||||
categoriesSearchInProgress.setVisibility(View.VISIBLE);
|
||||
categoriesSearchContainer.setError(null);
|
||||
categoriesAdapter.clear();
|
||||
})
|
||||
.observeOn(Schedulers.io())
|
||||
.concatWith(
|
||||
categoriesModel.searchAll(filter, imageTitleList)
|
||||
.mergeWith(categoriesModel.searchCategories(filter, imageTitleList))
|
||||
.concatWith(TextUtils.isEmpty(filter)
|
||||
? categoriesModel.defaultCategories(imageTitleList) : Observable.empty())
|
||||
)
|
||||
.filter(categoryItem -> !categoriesModel.containsYear(categoryItem.getName()))
|
||||
.distinct()
|
||||
.sorted(categoriesModel.sortBySimilarity(filter))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
s -> categoriesAdapter.add(s),
|
||||
Timber::e,
|
||||
() -> {
|
||||
categoriesAdapter.notifyDataSetChanged();
|
||||
categoriesSearchInProgress.setVisibility(View.GONE);
|
||||
|
||||
if (categoriesAdapter.getItemCount() == categoriesModel.selectedCategoriesCount()
|
||||
&& !categoriesSearch.getText().toString().isEmpty()) {
|
||||
categoriesSearchContainer.setError("No categories found");
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
private void receiveSharedItems() {
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getAction();
|
||||
|
|
@ -631,21 +278,79 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
|
|||
} else if (ACTION_INTERNAL_UPLOADS.equals(action)) {
|
||||
receiveInternalSharedItems();
|
||||
}
|
||||
|
||||
if (uploadableFiles == null || uploadableFiles.isEmpty()) {
|
||||
handleNullMedia();
|
||||
} else {
|
||||
//Show thumbnails
|
||||
if (uploadableFiles.size()
|
||||
> 1) {//If there is only file, no need to show the image thumbnails
|
||||
thumbnailsAdapter.setUploadableFiles(uploadableFiles);
|
||||
} else {
|
||||
llContainerTopCard.setVisibility(View.GONE);
|
||||
}
|
||||
tvTopCardTitle.setText(getResources()
|
||||
.getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(),uploadableFiles.size()));
|
||||
|
||||
fragments = new ArrayList<>();
|
||||
for (UploadableFile uploadableFile : uploadableFiles) {
|
||||
UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
|
||||
uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, source, place);
|
||||
uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback(){
|
||||
@Override
|
||||
public void deletePictureAtIndex(int index) {
|
||||
presenter.deletePictureAtIndex(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNextButtonClicked(int index) {
|
||||
UploadActivity.this.onNextButtonClicked(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreviousButtonClicked(int index) {
|
||||
UploadActivity.this.onPreviousButtonClicked(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showProgress(boolean shouldShow) {
|
||||
UploadActivity.this.showProgress(shouldShow);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexInViewFlipper(UploadBaseFragment fragment) {
|
||||
return fragments.indexOf(fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalNumberOfSteps() {
|
||||
return fragments.size();
|
||||
}
|
||||
});
|
||||
fragments.add(uploadMediaDetailFragment);
|
||||
}
|
||||
|
||||
uploadCategoriesFragment = new UploadCategoriesFragment();
|
||||
uploadCategoriesFragment.setCallback(this);
|
||||
|
||||
mediaLicenseFragment = new MediaLicenseFragment();
|
||||
mediaLicenseFragment.setCallback(this);
|
||||
|
||||
|
||||
fragments.add(uploadCategoriesFragment);
|
||||
fragments.add(mediaLicenseFragment);
|
||||
|
||||
uploadImagesAdapter.setFragments(fragments);
|
||||
vpUpload.setOffscreenPageLimit(fragments.size());
|
||||
}
|
||||
}
|
||||
|
||||
private void receiveExternalSharedItems() {
|
||||
List<UploadableFile> uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent());
|
||||
if (uploadableFiles.isEmpty()) {
|
||||
handleNullMedia();
|
||||
return;
|
||||
}
|
||||
|
||||
presenter.receive(uploadableFiles, SOURCE_EXTERNAL, null);
|
||||
uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent());
|
||||
}
|
||||
|
||||
private void receiveInternalSharedItems() {
|
||||
Intent intent = getIntent();
|
||||
String source;
|
||||
|
||||
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
|
||||
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
|
||||
|
|
@ -658,17 +363,10 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
|
|||
intent.getAction(),
|
||||
source);
|
||||
|
||||
ArrayList<UploadableFile> uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES);
|
||||
uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES);
|
||||
Timber.i("Received multiple upload %s", uploadableFiles.size());
|
||||
|
||||
if (uploadableFiles.isEmpty()) {
|
||||
handleNullMedia();
|
||||
return;
|
||||
}
|
||||
|
||||
Place place = intent.getParcelableExtra(PLACE_OBJECT);
|
||||
presenter.receive(uploadableFiles, source, place);
|
||||
|
||||
place = intent.getParcelableExtra(PLACE_OBJECT);
|
||||
resetDirectPrefs();
|
||||
}
|
||||
|
||||
|
|
@ -685,39 +383,6 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
|
|||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the button and shows or hides the content based on the given state. Typically used
|
||||
* for collapsing or expanding {@link CardView} animation.
|
||||
*
|
||||
* @param state the expanded state of the View whose elements are to be updated. True if
|
||||
* expanded.
|
||||
* @param button the image to rotate. Typically an arrow points up when the CardView is
|
||||
* collapsed and down when it is expanded.
|
||||
* @param content the Views that should be shown or hidden based on the state.
|
||||
*/
|
||||
private void updateCardState(boolean state, ImageView button, View... content) {
|
||||
button.animate().rotation(state ? 180 : 0).start();
|
||||
if (content != null) {
|
||||
for (View view : content) {
|
||||
view.setVisibility(state ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Description> getDescriptions() {
|
||||
return descriptionsAdapter.getDescriptions();
|
||||
}
|
||||
|
||||
private void initRecyclerView() {
|
||||
descriptionsAdapter = new DescriptionsAdapter(this);
|
||||
descriptionsAdapter.setCallback(this::showInfoAlert);
|
||||
rvDescriptions.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
|
||||
rvDescriptions.setAdapter(descriptionsAdapter);
|
||||
addNewDescription();
|
||||
}
|
||||
|
||||
|
||||
private void showInfoAlert(int titleStringID, int messageStringId, String... formatArgs) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(titleStringID)
|
||||
|
|
@ -729,23 +394,66 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
|
|||
}
|
||||
|
||||
@Override
|
||||
public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) {
|
||||
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString("originalImagePath", originalFilePath);
|
||||
args.putString("possibleImagePath", possibleFilePath);
|
||||
newFragment.setArguments(args);
|
||||
newFragment.show(getSupportFragmentManager(), "dialog");
|
||||
}
|
||||
|
||||
public void savePrevTitleDesc(String name){
|
||||
|
||||
directKvStore.putString(name + "title", descriptionsAdapter.getTitle().toString());
|
||||
int n = descriptionsAdapter.getItemCount() - 1;
|
||||
directKvStore.putInt(name + "descCount", n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
directKvStore.putString(name + "description_<" + i + ">", descriptionsAdapter.getDescriptions().get(i).getDescriptionText());
|
||||
directKvStore.putInt(name + "spinnerPosition_<" + i + ">", descriptionsAdapter.getDescriptions().get(i).getSelectedLanguageIndex());
|
||||
public void onNextButtonClicked(int index) {
|
||||
if (index < fragments.size()-1) {
|
||||
vpUpload.setCurrentItem(index + 1, false);
|
||||
} else {
|
||||
presenter.handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreviousButtonClicked(int index) {
|
||||
if (index != 0) {
|
||||
vpUpload.setCurrentItem(index - 1, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The adapter used to show image upload intermediate fragments
|
||||
*/
|
||||
|
||||
private class UploadImageAdapter extends FragmentStatePagerAdapter {
|
||||
List<Fragment> fragments;
|
||||
|
||||
public UploadImageAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
this.fragments = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void setFragments(List<Fragment> fragments) {
|
||||
this.fragments = fragments;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override public Fragment getItem(int position) {
|
||||
return fragments.get(position);
|
||||
}
|
||||
|
||||
@Override public int getCount() {
|
||||
return fragments.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(Object object){
|
||||
return PagerAdapter.POSITION_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OnClick(R.id.rl_container_title)
|
||||
public void onRlContainerTitleClicked(){
|
||||
rvThumbnails.setVisibility(isTitleExpanded ? View.GONE : View.VISIBLE);
|
||||
isTitleExpanded = !isTitleExpanded;
|
||||
ibToggleTopCard.setRotation(ibToggleTopCard.getRotation() + 180);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
presenter.onDetachView();
|
||||
compositeDisposable.clear();
|
||||
mediaLicenseFragment.setCallback(null);
|
||||
uploadCategoriesFragment.setCallback(null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ public class UploadController {
|
|||
/**
|
||||
* Prepares the upload service.
|
||||
*/
|
||||
void prepareService() {
|
||||
public void prepareService() {
|
||||
Intent uploadServiceIntent = new Intent(context, UploadService.class);
|
||||
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
||||
context.startService(uploadServiceIntent);
|
||||
|
|
@ -85,7 +85,7 @@ public class UploadController {
|
|||
/**
|
||||
* Disconnects the upload service.
|
||||
*/
|
||||
void cleanup() {
|
||||
public void cleanup() {
|
||||
if (isUploadServiceConnected) {
|
||||
context.unbindService(uploadServiceConnection);
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ public class UploadController {
|
|||
*
|
||||
* @param contribution the contribution object
|
||||
*/
|
||||
void startUpload(Contribution contribution) {
|
||||
public void startUpload(Contribution contribution) {
|
||||
startUpload(contribution, c -> {});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,7 @@ package fr.free.nrw.commons.upload;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
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 androidx.annotation.Nullable;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
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 io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
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;
|
||||
|
||||
|
||||
@Singleton
|
||||
public class UploadModel {
|
||||
|
||||
private static UploadItem DUMMY = new UploadItem(
|
||||
|
|
@ -49,24 +46,22 @@ public class UploadModel {
|
|||
private String license;
|
||||
private final Map<String, String> licensesByName;
|
||||
private List<UploadItem> items = new ArrayList<>();
|
||||
private boolean topCardState = true;
|
||||
private boolean bottomCardState = true;
|
||||
private boolean rightCardState = true;
|
||||
private int currentStepIndex = 0;
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
private SessionManager sessionManager;
|
||||
private FileProcessor fileProcessor;
|
||||
private final ImageProcessingService imageProcessingService;
|
||||
private List<String> selectedCategories;
|
||||
|
||||
@Inject
|
||||
UploadModel(@Named("licenses") List<String> licenses,
|
||||
@Named("default_preferences") JsonKvStore store,
|
||||
@Named("licenses_by_name") Map<String, String> licensesByName,
|
||||
Context context,
|
||||
SessionManager sessionManager,
|
||||
FileProcessor fileProcessor,
|
||||
ImageProcessingService imageProcessingService) {
|
||||
@Named("default_preferences") JsonKvStore store,
|
||||
@Named("licenses_by_name") Map<String, String> licensesByName,
|
||||
Context context,
|
||||
SessionManager sessionManager,
|
||||
FileProcessor fileProcessor,
|
||||
ImageProcessingService imageProcessingService) {
|
||||
this.licenses = licenses;
|
||||
this.store = store;
|
||||
this.license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
|
||||
|
|
@ -77,31 +72,61 @@ public class UploadModel {
|
|||
this.imageProcessingService = imageProcessingService;
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
/**
|
||||
* cleanup the resources, I am Singleton, preparing for fresh upload
|
||||
*/
|
||||
public void cleanUp() {
|
||||
compositeDisposable.clear();
|
||||
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")
|
||||
Observable<UploadItem> preProcessImages(List<UploadableFile> uploadableFiles,
|
||||
Place place,
|
||||
String source,
|
||||
SimilarImageInterface similarImageInterface) {
|
||||
initDefaultValues();
|
||||
Place place,
|
||||
String source,
|
||||
SimilarImageInterface similarImageInterface) {
|
||||
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);
|
||||
}
|
||||
|
||||
private UploadItem getUploadItem(UploadableFile uploadableFile,
|
||||
Place place,
|
||||
String source,
|
||||
SimilarImageInterface similarImageInterface) {
|
||||
fileProcessor.initFileDetails(Objects.requireNonNull(uploadableFile.getFilePath()), context.getContentResolver());
|
||||
UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile.getFileCreatedDate(context);
|
||||
Place place,
|
||||
String source,
|
||||
SimilarImageInterface similarImageInterface) {
|
||||
fileProcessor.initFileDetails(Objects.requireNonNull(uploadableFile.getFilePath()),
|
||||
context.getContentResolver());
|
||||
UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile
|
||||
.getFileCreatedDate(context);
|
||||
long fileCreatedDate = -1;
|
||||
String createdTimestampSource = "";
|
||||
if (dateTimeWithSource != null) {
|
||||
|
|
@ -109,52 +134,21 @@ public class UploadModel {
|
|||
createdTimestampSource = dateTimeWithSource.getSource();
|
||||
}
|
||||
Timber.d("File created date is %d", fileCreatedDate);
|
||||
GPSExtractor gpsExtractor = fileProcessor.processFileCoordinates(similarImageInterface, context);
|
||||
return new UploadItem(uploadableFile.getContentUri(), Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate, createdTimestampSource);
|
||||
}
|
||||
|
||||
void onItemsProcessed(Place place, List<UploadItem> uploadItems) {
|
||||
items = uploadItems;
|
||||
if (items.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UploadItem uploadItem = items.get(0);
|
||||
uploadItem.selected = true;
|
||||
uploadItem.first = true;
|
||||
|
||||
GPSExtractor gpsExtractor = fileProcessor
|
||||
.processFileCoordinates(similarImageInterface, context);
|
||||
UploadItem uploadItem = new UploadItem(uploadableFile.getContentUri(),
|
||||
Uri.parse(uploadableFile.getFilePath()),
|
||||
uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate,
|
||||
createdTimestampSource);
|
||||
if (place != null) {
|
||||
uploadItem.title.setTitleText(place.getName());
|
||||
uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription().equals("?")?"":place.getLongDescription());
|
||||
//TODO figure out if default descriptions in other languages exist
|
||||
uploadItem.title.setTitleText(place.name);
|
||||
uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription());
|
||||
uploadItem.descriptions.get(0).setLanguageCode("en");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (!items.contains(uploadItem)) {
|
||||
items.add(uploadItem);
|
||||
}
|
||||
return !hasError;
|
||||
return uploadItem;
|
||||
}
|
||||
|
||||
int getCurrentStep() {
|
||||
|
|
@ -173,110 +167,20 @@ public class UploadModel {
|
|||
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() {
|
||||
return licenses;
|
||||
}
|
||||
|
||||
String getSelectedLicense() {
|
||||
public String getSelectedLicense() {
|
||||
return license;
|
||||
}
|
||||
|
||||
void setSelectedLicense(String licenseName) {
|
||||
public void setSelectedLicense(String licenseName) {
|
||||
this.license = licensesByName.get(licenseName);
|
||||
store.putString(Prefs.DEFAULT_LICENSE, license);
|
||||
}
|
||||
|
||||
Observable<Contribution> buildContributions(List<String> categoryStringList) {
|
||||
public Observable<Contribution> buildContributions() {
|
||||
return Observable.fromIterable(items).map(item ->
|
||||
{
|
||||
Contribution contribution = new Contribution(item.mediaUri, null,
|
||||
|
|
@ -287,7 +191,10 @@ public class UploadModel {
|
|||
if (item.place != null) {
|
||||
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.setSource(item.source);
|
||||
contribution.setContentProviderUri(item.mediaUri);
|
||||
|
|
@ -304,21 +211,16 @@ public class UploadModel {
|
|||
});
|
||||
}
|
||||
|
||||
void keepPicture() {
|
||||
items.get(currentStepIndex).setImageQuality(ImageUtils.IMAGE_KEEP);
|
||||
}
|
||||
|
||||
void deletePicture() {
|
||||
cleanup();
|
||||
updateItemState();
|
||||
}
|
||||
|
||||
void subscribeBadPicture(Consumer<Integer> consumer, boolean checkTitle) {
|
||||
if (isShowingItem()) {
|
||||
compositeDisposable.add(getImageQuality(getCurrentItem(), checkTitle)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(consumer, Timber::e));
|
||||
public void deletePicture(String filePath) {
|
||||
Iterator<UploadItem> iterator = items.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().mediaUri.toString().contains(filePath)) {
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (items.isEmpty()) {
|
||||
cleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -326,8 +228,15 @@ public class UploadModel {
|
|||
return items;
|
||||
}
|
||||
|
||||
public void updateUploadItem(int index, UploadItem uploadItem) {
|
||||
UploadItem uploadItem1 = items.get(index);
|
||||
uploadItem1.setDescriptions(uploadItem.descriptions);
|
||||
uploadItem1.setTitle(uploadItem.title);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
static class UploadItem {
|
||||
public static class UploadItem {
|
||||
|
||||
private final Uri originalContentUri;
|
||||
private final Uri mediaUri;
|
||||
private final String mimeType;
|
||||
|
|
@ -347,10 +256,10 @@ public class UploadModel {
|
|||
|
||||
@SuppressLint("CheckResult")
|
||||
UploadItem(Uri originalContentUri,
|
||||
Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords,
|
||||
Place place,
|
||||
long createdTimestamp,
|
||||
String createdTimestampSource) {
|
||||
Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords,
|
||||
Place place,
|
||||
long createdTimestamp,
|
||||
String createdTimestampSource) {
|
||||
this.originalContentUri = originalContentUri;
|
||||
this.createdTimestampSource = createdTimestampSource;
|
||||
title = new Title();
|
||||
|
|
@ -426,16 +335,40 @@ public class UploadModel {
|
|||
}
|
||||
|
||||
public String getFileName() {
|
||||
return Utils.fixExtension(title.toString(), getFileExt());
|
||||
return title
|
||||
!= null ? Utils.fixExtension(title.toString(), getFileExt()) : null;
|
||||
}
|
||||
|
||||
public Place getPlace() {
|
||||
return place;
|
||||
}
|
||||
|
||||
public void setTitle(Title title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void setDescriptions(List<Description> descriptions) {
|
||||
this.descriptions = descriptions;
|
||||
}
|
||||
|
||||
public Uri getContentUri() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -1,420 +1,126 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
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.Utils;
|
||||
import fr.free.nrw.commons.category.CategoriesModel;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
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 fr.free.nrw.commons.repository.UploadRepository;
|
||||
import io.reactivex.Observer;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
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 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
|
||||
*/
|
||||
@Singleton
|
||||
public class UploadPresenter {
|
||||
public class UploadPresenter implements UploadContract.UserActionListener {
|
||||
|
||||
private static final UploadView DUMMY =
|
||||
(UploadView) CustomProxy.newInstance(UploadView.class.getClassLoader(),
|
||||
new Class[] { UploadView.class });
|
||||
private static final UploadContract.View DUMMY = (UploadContract.View) Proxy.newProxyInstance(
|
||||
UploadContract.View.class.getClassLoader(),
|
||||
new Class[]{UploadContract.View.class}, (proxy, method, methodArgs) -> null);
|
||||
private final UploadRepository repository;
|
||||
private UploadContract.View view = DUMMY;
|
||||
|
||||
private UploadView view = DUMMY;
|
||||
|
||||
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();
|
||||
private CompositeDisposable compositeDisposable;
|
||||
|
||||
@Inject
|
||||
UploadPresenter(UploadModel uploadModel,
|
||||
UploadController uploadController,
|
||||
Context context,
|
||||
@Named("default_preferences") JsonKvStore directKvStore) {
|
||||
this.uploadModel = uploadModel;
|
||||
this.uploadController = uploadController;
|
||||
this.context = context;
|
||||
this.directKvStore = directKvStore;
|
||||
UploadPresenter(UploadRepository uploadRepository) {
|
||||
this.repository = uploadRepository;
|
||||
compositeDisposable = new CompositeDisposable();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
void handleSubmit(CategoriesModel categoriesModel) {
|
||||
if (view.checkIfLoggedIn())
|
||||
compositeDisposable.add(uploadModel.buildContributions(categoriesModel.getCategoryStringList())
|
||||
@Override
|
||||
public void handleSubmit() {
|
||||
if (view.isLoggedIn()) {
|
||||
view.showProgress(true);
|
||||
repository.buildContributions()
|
||||
.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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the map button on the right card in {@link UploadActivity}
|
||||
*/
|
||||
void openCoordinateMap() {
|
||||
GPSExtractor gpsObj = uploadModel.getCurrentItem().getGpsCoords();
|
||||
if (gpsObj != null && gpsObj.imageCoordsExists) {
|
||||
view.launchMapActivity(new LatLng(gpsObj.getDecLatitude(), gpsObj.getDecLongitude(), 0.0f));
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onNext(Contribution contribution) {
|
||||
repository.startUpload(contribution);
|
||||
}
|
||||
|
||||
void keepPicture() {
|
||||
uploadModel.keepPicture();
|
||||
}
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
view.showMessage(R.string.upload_failed);
|
||||
repository.cleanup();
|
||||
view.finish();
|
||||
compositeDisposable.clear();
|
||||
Timber.e("failed to upload: " + e.getMessage());
|
||||
}
|
||||
|
||||
void deletePicture() {
|
||||
if (uploadModel.getCount() == 1)
|
||||
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();
|
||||
uploadModel.cleanup();
|
||||
uploadController.cleanup();
|
||||
}
|
||||
|
||||
void removeView() {
|
||||
this.view = DUMMY;
|
||||
}
|
||||
|
||||
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();
|
||||
@Override
|
||||
public void onComplete() {
|
||||
repository.cleanup();
|
||||
view.finish();
|
||||
compositeDisposable.clear();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
currentPage = UploadView.LICENSE;
|
||||
view.setTopCardVisibility(false);
|
||||
view.setRightCardVisibility(false);
|
||||
view.askUserToLogIn();
|
||||
}
|
||||
view.setBottomCardVisibility(currentPage, uploadCount);
|
||||
}
|
||||
|
||||
//endregion
|
||||
@Override
|
||||
public void deletePictureAtIndex(int index) {
|
||||
List<UploadableFile> uploadableFiles = view.getUploadableFiles();
|
||||
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);
|
||||
}
|
||||
//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);
|
||||
}
|
||||
|
||||
//In case lets update the number of uploadable media
|
||||
view.updateTopCardTitle();
|
||||
|
||||
/**
|
||||
* @return the item currently being displayed
|
||||
*/
|
||||
private UploadItem getCurrentItem() {
|
||||
return uploadModel.getCurrentItem();
|
||||
}
|
||||
|
||||
List<String> getImageTitleList() {
|
||||
List<String> titleList = new ArrayList<>();
|
||||
for (UploadItem item : uploadModel.getUploads()) {
|
||||
if (item.getTitle().isSet()) {
|
||||
titleList.add(item.getTitle().toString());
|
||||
}
|
||||
}
|
||||
return titleList;
|
||||
@Override
|
||||
public void onAttachView(UploadContract.View view) {
|
||||
this.view = view;
|
||||
repository.prepareService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachView() {
|
||||
this.view = DUMMY;
|
||||
compositeDisposable.clear();
|
||||
repository.cleanup();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ public interface UploadView {
|
|||
// UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
|
||||
// new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null);
|
||||
|
||||
List<Description> getDescriptions();
|
||||
|
||||
@Retention(SOURCE)
|
||||
@IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE})
|
||||
|
|
@ -82,4 +81,6 @@ public interface UploadView {
|
|||
void showProgressDialog();
|
||||
|
||||
void hideProgressDialog();
|
||||
|
||||
void askUserToLogIn();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -140,4 +140,31 @@ public class DialogUtil {
|
|||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
9
app/src/main/res/drawable/drawable_thumbnail_image.xml
Normal file
9
app/src/main/res/drawable/drawable_thumbnail_image.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/thumbnail_not_selected.xml
Normal file
5
app/src/main/res/drawable/thumbnail_not_selected.xml
Normal 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>
|
||||
11
app/src/main/res/drawable/thumbnail_selected.xml
Normal file
11
app/src/main/res/drawable/thumbnail_selected.xml
Normal 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>
|
||||
|
|
@ -1,34 +1,79 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/upload_root_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.github.chrisbanes.photoview.PhotoView
|
||||
android:id="@+id/backgroundImage"
|
||||
android:layout_height="match_parent"
|
||||
>
|
||||
<fr.free.nrw.commons.contributions.UnswipableViewPager
|
||||
android:id="@+id/vp_upload"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/commons_app_blue_dark"
|
||||
/>
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cv_container_top_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
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="match_parent"
|
||||
android:layout_below="@id/toolbar"
|
||||
android:background="@color/commons_app_blue_dark"
|
||||
app:actualImageScaleType="fitCenter" />
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/standard_gap"
|
||||
>
|
||||
|
||||
<ViewFlipper
|
||||
android:id="@+id/view_flipper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:measureAllChildren="false">
|
||||
<RelativeLayout
|
||||
android:id="@+id/rl_container_title"
|
||||
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_bottom_card"
|
||||
android:visibility="visible" />
|
||||
</RelativeLayout>
|
||||
|
||||
<include layout="@layout/activity_upload_categories" />
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_thumbnails"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/small_gap"
|
||||
/>
|
||||
|
||||
<include layout="@layout/activity_upload_license" />
|
||||
|
||||
<include layout="@layout/activity_upload_please_wait" />
|
||||
|
||||
</ViewFlipper>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</RelativeLayout>
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
102
app/src/main/res/layout/fragment_media_license.xml
Normal file
102
app/src/main/res/layout/fragment_media_license.xml
Normal 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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -1,37 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:fresco="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="90dp"
|
||||
android:id="@+id/rl_container"
|
||||
android:background="@drawable/thumbnail_not_selected"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout xmlns:fresco="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<com.facebook.drawee.view.SimpleDraweeView
|
||||
android:id="@+id/iv_thumbnail"
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="90dp"
|
||||
fresco:actualImageScaleType="fitCenter"/>
|
||||
|
||||
<androidx.legacy.widget.Space
|
||||
android:id="@+id/left_space"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="90dp" />
|
||||
|
||||
<com.facebook.drawee.view.SimpleDraweeView
|
||||
android:id="@+id/thumbnail"
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="90dp"
|
||||
fresco:actualImageScaleType="fitCenter" />
|
||||
|
||||
<androidx.legacy.widget.Space
|
||||
android:id="@+id/right_space"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="90dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/error"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="end"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/ic_error_red_24dp" />
|
||||
</FrameLayout>
|
||||
<ImageView
|
||||
android:id="@+id/iv_error"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_gravity="end"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/ic_error_red_24dp"
|
||||
tools:visibility="visible"/>
|
||||
</RelativeLayout>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_weight="8">
|
||||
|
||||
<EditText
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/description_item_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
tools:showIn="@layout/activity_upload">
|
||||
|
||||
<EditText
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/description_item_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
|||
114
app/src/main/res/layout/upload_categories_fragment.xml
Normal file
114
app/src/main/res/layout/upload_categories_fragment.xml
Normal 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>
|
||||
|
||||
|
|
@ -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="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="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="previous">Precedent</string>
|
||||
<string name="submit">Trimite</string>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
<color name="black">#000000</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="no_button_color">#006400</color>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@
|
|||
<string name="next">Next</string>
|
||||
<string name="previous">Previous</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>
|
||||
<plurals name="upload_count_title">
|
||||
<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_via">Share app via...</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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
fun getCurrentStep() {
|
||||
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> {
|
||||
val element = getElement()
|
||||
val element2 = getElement()
|
||||
|
|
|
|||
|
|
@ -1,43 +1,82 @@
|
|||
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.mwapi.MediaWikiApi
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
import fr.free.nrw.commons.repository.UploadRepository
|
||||
import io.reactivex.Observable
|
||||
import org.junit.Before
|
||||
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.mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.util.ArrayList
|
||||
|
||||
|
||||
/**
|
||||
* The clas contains unit test cases for UploadPresenter
|
||||
*/
|
||||
class UploadPresenterTest {
|
||||
|
||||
@Mock
|
||||
internal var uploadModel: UploadModel? = null
|
||||
internal var repository: UploadRepository? = null
|
||||
@Mock
|
||||
internal var uploadController: UploadController? = null
|
||||
internal var view: UploadContract.View? = null
|
||||
@Mock
|
||||
internal var mediaWikiApi: MediaWikiApi? = null
|
||||
var contribution: Contribution? = null
|
||||
|
||||
@Mock
|
||||
private lateinit var uploadableFile: UploadableFile
|
||||
|
||||
@InjectMocks
|
||||
var uploadPresenter: UploadPresenter? = null
|
||||
|
||||
private var uploadableFiles: ArrayList<UploadableFile> = ArrayList()
|
||||
|
||||
/**
|
||||
* initial setup, test environment
|
||||
*/
|
||||
@Before
|
||||
@Throws(Exception::class)
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
`when`(uploadModel!!.preProcessImages(ArgumentMatchers.anyListOf(UploadableFile::class.java),
|
||||
ArgumentMatchers.any(Place::class.java),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.any(SimilarImageInterface::class.java)))
|
||||
.thenReturn(Observable.just(mock(UploadModel.UploadItem::class.java)))
|
||||
uploadPresenter?.onAttachView(view)
|
||||
`when`(repository?.buildContributions()).thenReturn(Observable.just(contribution))
|
||||
`when`(view?.isLoggedIn).thenReturn(true)
|
||||
uploadableFiles.add(uploadableFile)
|
||||
`when`(view?.uploadableFiles).thenReturn(uploadableFiles)
|
||||
`when`(uploadableFile?.filePath).thenReturn("data://test")
|
||||
}
|
||||
|
||||
/**
|
||||
* unit test case for method UploadPresenter.handleSubmit
|
||||
*/
|
||||
@Test
|
||||
fun receiveMultipleItems() {
|
||||
val element = Mockito.mock(UploadableFile::class.java)
|
||||
val element2 = Mockito.mock(UploadableFile::class.java)
|
||||
var uriList: List<UploadableFile> = mutableListOf<UploadableFile>(element, element2)
|
||||
uploadPresenter!!.receive(uriList, "external", mock(Place::class.java))
|
||||
fun handleSubmitTest() {
|
||||
uploadPresenter?.handleSubmit()
|
||||
verify(view)?.isLoggedIn
|
||||
verify(view)?.showProgress(true)
|
||||
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()
|
||||
}
|
||||
}
|
||||
BIN
captures/fr.free.nrw.commons_2019.04.15_22.10.li
Normal file
BIN
captures/fr.free.nrw.commons_2019.04.15_22.10.li
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue