Merge remote-tracking branch 'upstream/master' into refactorNearbyClassesMVP

This commit is contained in:
neslihanturan 2019-06-13 23:09:57 +03:00
commit 22290af42f
94 changed files with 3879 additions and 2106 deletions

View file

@ -28,6 +28,7 @@ dependencies {
implementation 'com.facebook.fresco:fresco:1.13.0'
implementation 'com.drewnoakes:metadata-extractor:2.11.0'
implementation 'com.dmitrybrant:wikimedia-android-data-client:0.0.18'
implementation 'org.apache.commons:commons-lang3:3.8.1'
// UI
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
@ -64,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"

View file

@ -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" />

View file

@ -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

View file

@ -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;

View file

@ -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() {

View file

@ -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();
}
}

View file

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

View file

@ -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());

View file

@ -110,7 +110,7 @@ public class DeleteHelper {
mwApi.appendEdit(editToken, logPageString + "\n",
"Commons:Deletion_requests/" + date, summary);
mwApi.appendEdit(editToken, userPageString + "\n",
"User_Talk:" + sessionManager.getCurrentAccount().name, summary);
"User_Talk:" + media.getCreator(), summary);
} catch (Exception e) {
Timber.e(e);
return false;

View file

@ -15,6 +15,7 @@ import fr.free.nrw.commons.nearby.PlaceRenderer;
import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.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);

View file

@ -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();
}
}

View file

@ -18,6 +18,9 @@ import fr.free.nrw.commons.nearby.mvp.fragments.NearbyMapFragment;
import fr.free.nrw.commons.nearby.mvp.fragments.NearbyParentFragment;
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();
}

View file

@ -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

View file

@ -44,6 +44,7 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteHelper;
@ -56,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;
@ -367,6 +365,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@OnClick(R.id.nominateDeletion)
public void onDeleteButtonClicked(){
if(AccountUtil.getUserName(getContext()).equals(media.getCreator())){
final ArrayAdapter<String> languageAdapter = new ArrayAdapter<>(getActivity(),
R.layout.simple_spinner_dropdown_list, reasonList);
final Spinner spinner = new Spinner(getActivity());
@ -384,19 +383,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if(isDeleted) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
}
//Reviewer correct me if i have misunderstood something over here
//But how does this if (delete.getVisibility() == View.VISIBLE) {
// enableDeleteButton(true); makes sense ?
else{
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
alert.setMessage("Why should this fileckathon-2018 be deleted?");
alert.setMessage(getString(R.string.dialog_box_text_nomination,media.getDisplayTitle()));
final EditText input = new EditText(getActivity());
alert.setView(input);
input.requestFocus();
alert.setPositiveButton(R.string.ok, (dialog1, whichButton) -> {
String reason = input.getText().toString();
deleteHelper.makeDeletion(getContext(), media, reason);
enableDeleteButton(false);
onDeleteClickeddialogtext(reason);
});
alert.setNegativeButton(R.string.cancel, (dialog12, whichButton) -> {
});
@ -427,6 +426,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
d.show();
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
}
@SuppressLint("CheckResult")
private void onDeleteClicked(Spinner spinner) {
@ -445,6 +445,22 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
@SuppressLint("CheckResult")
private void onDeleteClickeddialogtext(String reason) {
Single<Boolean> resultSingletext = reasonBuilder.getReason(media, reason)
.flatMap(reasonString -> deleteHelper.makeDeletion(getContext(), media, reason));
compositeDisposable.add(resultSingletext
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
if (getActivity() != null) {
isDeleted = true;
enableDeleteButton(false);
}
}));
}
@OnClick(R.id.seeMore)
public void onSeeMoreClicked(){
if (nominatedForDeletion.getVisibility() == VISIBLE && getActivity() != null) {

View file

@ -410,7 +410,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
*/
@Nullable
@Override
public String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException {
public String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException {
Timber.d("Filename is %s", value);
CustomApiResult result = wikidataApi.action("wbcreateclaim")
.param("entity", entityId)

View file

@ -59,7 +59,7 @@ public interface MediaWikiApi {
String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
@Nullable
String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException;
String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException;
@Nullable
boolean addWikidataEditTag(String revisionId) throws IOException;

View file

@ -535,8 +535,8 @@ public class NearbyMapFragment extends DaggerFragment {
.compassGravity(Gravity.BOTTOM | Gravity.LEFT)
.compassMargins(new int[]{12, 0, 0, 24})
.styleUrl(isDarkTheme ? Style.DARK : Style.OUTDOORS)
.logoEnabled(false)
.attributionEnabled(false)
.logoEnabled(true)
.attributionEnabled(true)
.camera(new CameraPosition.Builder()
.target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()))
.zoom(ZOOM_LEVEL)

View file

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

View file

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

View file

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

View file

@ -5,11 +5,12 @@ import java.util.List;
/**
* Holds a description of an item being uploaded by {@link UploadActivity}
*/
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.

View file

@ -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);
}
}

View file

@ -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() {

View file

@ -13,12 +13,12 @@ import timber.log.Timber;
* Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation
* 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;
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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);
}

View file

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

View file

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

View file

@ -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);
}
}

View file

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

View file

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

View file

@ -75,7 +75,7 @@ public class UploadController {
/**
* Prepares the upload service.
*/
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 -> {});
}

View file

@ -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();
}
}
}

View file

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

View file

@ -1,420 +1,126 @@
package fr.free.nrw.commons.upload;
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();
}
}

View file

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

View file

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

View file

@ -15,7 +15,6 @@ public interface UploadView {
// UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
// 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();
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -79,7 +79,7 @@ public class WikidataEditService {
Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId);
Observable.fromCallable(() -> {
String propertyValue = getFileName(fileName);
return mediaWikiApi.wikidatCreateClaim(wikidataEntityId, "P18", "value", propertyValue);
return mediaWikiApi.wikidataCreateClaim(wikidataEntityId, "P18", "value", propertyValue);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())

View file

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

View file

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

View file

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

View file

@ -1,34 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,37 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<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>

View file

@ -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"

View file

@ -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"

View file

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

View file

@ -179,6 +179,7 @@
<string name="storage_permission_title">طلب إذن التخزين</string>
<string name="read_storage_permission_rationale">صلاحية مطلوبة: قراءة وحدة التخزين الخارجية، لا يمكن للتطبيق الوصول إلى معرض الصور الخاص بك بدونها.</string>
<string name="write_storage_permission_rationale">صلاحية مطلوبة: اكتب وحدة التخزين الخارجية، لا يمكن للتطبيق الوصول إلى معرض الصور/الكاميرا الخاصة بك بدونها.</string>
<string name="location_permission_title">جارٍ طلب إذن الموقع</string>
<string name="location_permission_rationale">صلاحية اختيارية: احصل على الموقع الحالي لاقتراحات التصنيفات</string>
<string name="ok">موافق</string>
<string name="title_activity_nearby">الأماكن القريبة</string>
@ -530,4 +531,5 @@
<string name="share_text">ارفع الصور لويكيميديا ​​كومنز على هاتفك قم بتنزيل تطبيق كومنز: %1$s</string>
<string name="share_via">مشاركة التطبيق عبر...</string>
<string name="image_info">معلومات الصورة</string>
<string name="dialog_box_text_nomination">لماذا يجب حذف %1$s؟</string>
</resources>

View file

@ -215,6 +215,7 @@
<string name="preference_author_name_summary">Персонализираното авторско име, което ще се използва вместо потребителското ви име при качване</string>
<string name="archived_notifications">Известия (архивирани)</string>
<string name="list_sheet">Списък</string>
<string name="submit">Изпращане</string>
<string name="desc_language_America">Америка</string>
<string name="desc_language_Europe">Европа</string>
<string name="desc_language_Africa">Африка</string>

View file

@ -15,6 +15,7 @@
<string name="preference_category_appearance">Udseende</string>
<string name="preference_category_general">Generelt</string>
<string name="preference_category_feedback">Tilbagemelding</string>
<string name="preference_category_privacy">Privatliv</string>
<string name="preference_category_location">Sted</string>
<string name="app_name">Commons</string>
<string name="bullet"></string>
@ -370,5 +371,8 @@
<string name="error_occurred_in_picking_images">Der opstod en fejl under udvælgelse af billeder</string>
<string name="please_wait">Vent venligst…</string>
<string name="skip_image">Spring over dette billede</string>
<string name="exif_tag_name_copyright">Ophavsret</string>
<string name="exif_tag_name_cameraModel">Kameramodel</string>
<string name="exif_tag_name_serialNumbers">Serienumre</string>
<string name="share_via">Del app via...</string>
</resources>

View file

@ -339,6 +339,7 @@
<string name="quiz_question_string">آیا این تصویر برای بارگذاری مناسب است؟</string>
<string name="question">پرسش</string>
<string name="result">نتیجه</string>
<string name="congratulatory_message_quiz">شما %1$s پاسخ درست دادید. آفرین!</string>
<string name="warning_for_no_answer">یکی از دو گزینه را انتخاب کنید تا به سوال پاسخ دهید</string>
<string name="user_not_logged_in">جلسه ورود به سیستم منقضی شد، لطفا دوباره وارد سیستم شوید.</string>
<string name="quiz_result_share_message">کویز خود را با دوستان خود به اشتراک بگذارید.</string>
@ -383,6 +384,11 @@
<string name="next">بعدی</string>
<string name="previous">قبلی</string>
<string name="submit">ارسال</string>
<string name="upload_title_duplicate">پرونده‌ای با نام %1$s وجود دارد. آیا اطمینان دارید که می‌خواهید ادامه دهید؟</string>
<plurals name="upload_count_title">
<item quantity="one">%1$d بارگذاری</item>
<item quantity="other">%1$d بارگذاری</item>
</plurals>
<string name="navigation_item_bookmarks">نشانک‌ها</string>
<string name="title_activity_bookmarks">نشانک‌ها</string>
<string name="title_page_bookmarks_pictures">تصویرها</string>
@ -396,6 +402,7 @@
<string name="deletion_reason_bad_for_my_privacy">فهمیدم که این برای حریم خصوصی من بد است.</string>
<string name="deletion_reason_no_longer_want_public">من تغییر عقیده دادم، نمی‌خواهم دیگر برای همه قابل‌مشاهده باشد.</string>
<string name="deletion_reason_not_interesting">با پوزش، این تصویر برای یک دانشنامه مناسب نیست</string>
<string name="uploaded_by_myself">بارگذاری‌شده توسط خودم در %1$s؛ استفاده‌شده در %2$d مقاله.</string>
<string name="no_uploads">به کامانز خوش آمدید!\n\nاولین فایلتان را با فشردن کلید اضافه بارگذاری کنید.</string>
<string name="desc_language_Worldwide">در سراسر جهان</string>
<string name="desc_language_America">آمریکا</string>
@ -423,6 +430,7 @@
<string name="nominate_for_deletion_notify_user">به کاربر در صفحه بحثش خبر بده</string>
<string name="notsure">مطمئن نیستم</string>
<string name="send_thank_success_title">ارسال تشکر: موفق</string>
<string name="send_thank_success_message">تشکر با موفقیت برای %1$s فرستاده شد</string>
<string name="send_thank_failure_message">تلاش برای فرستادن تشکر شکست خورد %1$s</string>
<string name="send_thank_failure_title">ارسال تشکر: ناموفق</string>
<string name="send_thank_send">ارسال تشکر</string>

View file

@ -189,6 +189,7 @@
<string name="storage_permission_title">Demande d\'autorisation d\'accès au stockage</string>
<string name="read_storage_permission_rationale">Autorisation nécessaire : Lire un stockage externe. Lapplication ne peut pas accéder à votre galerie sans cela.</string>
<string name="write_storage_permission_rationale">Permission obligatoire : Écriture sur stockage externe. Lapplication ne peut pas accéder à votre appareil photo/galerie sans cela.</string>
<string name="location_permission_title">Demande d\'autorisation d\'accès au stockage</string>
<string name="location_permission_rationale">Autorisation facultative : Obtenir lemplacement actuel pour des suggestions de catégorie</string>
<string name="ok">OK</string>
<string name="title_activity_nearby">Endroits à proximité</string>
@ -502,8 +503,8 @@
<string name="review_copyright_no_button_text">Semble correct</string>
<string name="review_thanks_yes_button_text">Oui, pourquoi pas</string>
<string name="review_thanks_no_button_text">Image suivante</string>
<string name="skip_image_explanation">Cliquer sur ce bouton vous fournira une autre image récemment téléversée de Wikimédia Communs.</string>
<string name="review_image_explanation">Vous pouvez revoir les images et améliorer la qualité de Wikimédia Communs.\n Les quatre paramètres à revoir sont : \n - Cette image est-elle à propos? \n - Cette image respecte-t-elle les règles de droit dauteur? \n - Cette image est-elle bien catégorisée? \n - Si tout va bien, vous pouvez aussi remercier le contributeur.</string>
<string name="skip_image_explanation">Cliquer sur ce bouton vous fournira une autre image récemment téléversée de Wikimédia Commons.</string>
<string name="review_image_explanation">Vous pouvez revoir les images et améliorer la qualité de Wikimédia Commons.\n Les quatre paramètres à revoir sont : \n - Cette image est-elle à propos? \n - Cette image respecte-t-elle les règles de droit dauteur? \n - Cette image est-elle bien catégorisée? \n - Si tout va bien, vous pouvez aussi remercier le contributeur.</string>
<plurals name="receiving_shared_content">
<item quantity="one">Réception de contenu partagé. Le traitement de l\'image peut prendre un certain temps en fonction de la taille de l\'image et de votre matériel</item>
<item quantity="other">Réception de contenu partagé. Le traitement des images peut prendre un certain temps en fonction de la taille des images et de votre matériel</item>
@ -519,7 +520,7 @@
<string name="error_occurred_in_picking_images">Erreur lors de la sélection des images</string>
<string name="image_chooser_title">Choisir les images à téléverser</string>
<string name="please_wait">Veuillez patienter…</string>
<string name="images_featured_explanation">Les images en vedette sont des images de photographes et dillustrateurs très doués que la communauté de Wikimédia Communs a choisi comme étant de la meilleure qualité pour le site.</string>
<string name="images_featured_explanation">Les images en vedette sont des images de photographes et dillustrateurs très doués que la communauté de Wikimédia Commons a choisi comme étant de la meilleure qualité pour le site.</string>
<string name="images_via_nearby_explanation">Les images téléversées par les lieux de proximité sont les images téléversées par la découverte des lieux sur la carte.</string>
<string name="thanks_received_explanation">Cette fonctionalité permet aux contributeurs d\'envoyer une notification de remerciement aux utilisateurs qui font des modifications utiles en utilisant un petit lien de remerciement sur la page historique ou sur celle du diff.</string>
<string name="previous_image_title_description">Copier le titre et la description précédente</string>
@ -529,7 +530,7 @@
<string name="skip_image">SAUTER CETTE IMAGE</string>
<string name="download_failed_we_cannot_download_the_file_without_storage_permission">Échec du téléchargement! Nous ne pouvons pas télécharger le fichier sans droit de stockage externe.</string>
<string name="manage_exif_tags">Gérer les balises EXIF</string>
<string name="manage_exif_tags_summary">Sélectionner quelles balises EXIF à conserver dans les téléchargements</string>
<string name="manage_exif_tags_summary">Sélectionner quelles balises EXIF à conserver dans les téléversements</string>
<string name="exif_tag_name_author">Auteur</string>
<string name="exif_tag_name_copyright">Droits dauteur</string>
<string name="exif_tag_name_location">Emplacement</string>
@ -537,7 +538,8 @@
<string name="exif_tag_name_lensModel">Modèle de lentille</string>
<string name="exif_tag_name_serialNumbers">Numéros de série</string>
<string name="exif_tag_name_software">Logiciel</string>
<string name="share_text">Téléverser des photos vers Wikimédia Communs, sur votre téléphone Téléchargez lapplication Communs : %1$s</string>
<string name="share_text">Téléverser des photos vers Wikimédia Commons, sur votre téléphone Téléchargez lapplication Commons : %1$s</string>
<string name="share_via">Partager lapplication via…</string>
<string name="image_info">Informations de limage</string>
<string name="dialog_box_text_nomination">Pourquoi %1$s devrait-il être supprimé?</string>
</resources>

View file

@ -9,6 +9,7 @@
<string name="preference_category_appearance">Útlit</string>
<string name="preference_category_general">Almennt</string>
<string name="preference_category_feedback">Umsagnir</string>
<string name="preference_category_privacy">Persónuvernd</string>
<string name="preference_category_location">Staðsetning</string>
<string name="app_name">Commons</string>
<string name="bullet">• \</string>
@ -167,6 +168,7 @@
<string name="storage_permission_title">Biður um aðgang að geymslurými</string>
<string name="read_storage_permission_rationale">Nauðsynlegar heimildir: Lesa ytri gagnageymslu. Forritið fær ekki aðgang að myndasafni ekki án þessa.</string>
<string name="write_storage_permission_rationale">Nauðsynlegar heimildir: Skrifa í ytri gagnageymslu. Forritið nær ekki sambandi við myndavél/myndasafn ekki án þessa.</string>
<string name="location_permission_title">Biður um aðgang að staðsetningu</string>
<string name="location_permission_rationale">Nauðsynlegar heimildir: Lesa núverandi staðsetningu til að geta stungið upp á flokkum</string>
<string name="ok">Í lagi</string>
<string name="title_activity_nearby">Staðir í nágrenninu</string>
@ -430,6 +432,8 @@
<string name="display_campaigns_explanation">Sjá yfirstandandi herferðir</string>
<string name="nominate_for_deletion_done">Lokið</string>
<string name="notsure">Ekki viss</string>
<string name="send_thank_send">Senda þakkarboð</string>
<string name="send_thank_notification_title">Sendi þakkarboð</string>
<string name="review_category">Er þetta rétt flokkað?</string>
<string name="review_spam">Kemur þetta umfjöllunarefninu við?</string>
<string name="review_c_violation_report_question">Það brýtur á móti höfundarrétti því það er</string>
@ -451,4 +455,13 @@
<string name="image_chooser_title">Veldu myndir til að senda inn</string>
<string name="please_wait">Bíddu aðeins…</string>
<string name="skip_image">SLEPPA ÞESSARI MYND</string>
<string name="manage_exif_tags">Sýsla með EXIF-merki</string>
<string name="exif_tag_name_author">Höfundur</string>
<string name="exif_tag_name_copyright">Höfundarréttur</string>
<string name="exif_tag_name_location">Staðsetning</string>
<string name="exif_tag_name_cameraModel">Tegund myndavélar</string>
<string name="exif_tag_name_lensModel">Tegund linsu</string>
<string name="exif_tag_name_serialNumbers">Raðnúmer</string>
<string name="exif_tag_name_software">Hugbúnaður</string>
<string name="image_info">Upplýsingar í mynd</string>
</resources>

View file

@ -21,6 +21,7 @@
<string name="preference_category_appearance">Aspetto</string>
<string name="preference_category_general">Generale</string>
<string name="preference_category_feedback">Commenti</string>
<string name="preference_category_privacy">Privacy</string>
<string name="preference_category_location">Posizione</string>
<string name="app_name">Commons</string>
<string name="bullet"></string>
@ -470,6 +471,10 @@
<string name="previous_button_tooltip_message">Clicca per riusare il titolo e la descrizione dell\'immagine precedente e adattarli all\'immagine attuale.</string>
<string name="skip_image">SALTA QUESTA IMMAGINE</string>
<string name="exif_tag_name_author">Autore</string>
<string name="exif_tag_name_cameraModel">Modello fotocamera</string>
<string name="exif_tag_name_serialNumbers">Numeri seriali</string>
<string name="exif_tag_name_software">Software</string>
<string name="share_via">Condividi applicazione tramite...</string>
<string name="image_info">Informazioni sull\'immagine</string>
<string name="dialog_box_text_nomination">Perché %1$s dovrebbe essere cancellato?</string>
</resources>

View file

@ -176,6 +176,7 @@
<string name="storage_permission_title">기억 장치 권한 요청 중</string>
<string name="read_storage_permission_rationale">권한 필요: 외부 저장소 읽기. 이것이 없으면 앱은 갤러리에 접근할 수 없습니다.</string>
<string name="write_storage_permission_rationale" fuzzy="true">권한 필요: 외부 저장소 쓰기. 이것이 없으면 앱은 카메라에 접근할 수 없습니다.</string>
<string name="location_permission_title">위치 권한 요청 중</string>
<string name="location_permission_rationale">선택적 권한: 분류 추천을 위해 현재 위치 정보를 가져옵니다.</string>
<string name="ok">확인</string>
<string name="title_activity_nearby">근처의 장소</string>
@ -458,6 +459,7 @@
<string name="exif_tag_name_copyright">저작권</string>
<string name="exif_tag_name_location">위치</string>
<string name="exif_tag_name_cameraModel">카메라 모델</string>
<string name="exif_tag_name_lensModel">렌즈 모델</string>
<string name="exif_tag_name_serialNumbers">일련 번호</string>
<string name="exif_tag_name_software">소프트웨어</string>
<string name="share_via">앱 공유...</string>

View file

@ -338,4 +338,9 @@
<string name="image_chooser_title">Sicht Biller eraus fir eropzelueden</string>
<string name="please_wait">Waart w.e.g. ...</string>
<string name="skip_image">DËST BILD IWWERWSPRANGEN</string>
<string name="exif_tag_name_author">Auteur</string>
<string name="exif_tag_name_copyright">Copyright</string>
<string name="exif_tag_name_location">Plaz</string>
<string name="exif_tag_name_serialNumbers">Seriennummeren</string>
<string name="exif_tag_name_software">Software</string>
</resources>

View file

@ -169,6 +169,7 @@
<string name="storage_permission_title">Се бара дозвола за складирање</string>
<string name="read_storage_permission_rationale">Потребна дозвола: Треба да се прочита од надворешен склад. Прилогот без ова нема пристап до вашата галерија.</string>
<string name="write_storage_permission_rationale">Потребна дозвола: Треба да се запише на надворешен склад. Прилогот без ова нема пристап до вашата камера/галерија.</string>
<string name="location_permission_title">Се бара дозвола за утврдување на местоположбата</string>
<string name="location_permission_rationale">Дозвола по желба: Утврдување на тековната местоположба за предлагање категории</string>
<string name="ok">ОК</string>
<string name="title_activity_nearby">Околни места</string>
@ -520,4 +521,5 @@
<string name="share_text">Подигајте слики на Ризницата од телефон. Преземете го прилогот на Ризницата: %1$s</string>
<string name="share_via">Сподели преку...</string>
<string name="image_info">Инфо за сликата</string>
<string name="dialog_box_text_nomination">Зошто сметате дека %1$s треба да се избрише?</string>
</resources>

View file

@ -9,6 +9,7 @@
<string name="preference_category_appearance">ပုံပန်းသွင်ပြင်</string>
<string name="preference_category_general">အထွေထွေ</string>
<string name="preference_category_feedback">အကြံပေးရန်</string>
<string name="preference_category_privacy">ကိုယ်ရေးမူဝါဒ</string>
<string name="preference_category_location">နေရာ</string>
<string name="app_name">ကွန်မွန်းစ်</string>
<string name="bullet"></string>
@ -129,6 +130,7 @@
<string name="detail_discussion_empty">ဆွေးနွေးချက် မရှိပါ</string>
<string name="detail_license_empty">အမည်မသိရသော လိုင်စင်</string>
<string name="menu_refresh">ပြန်လည်ဆန်းသစ်ရန်</string>
<string name="location_permission_title">တည်နေရာ ခွင့်ပြုချက် တောင်းဆိုနေသည်</string>
<string name="ok">အိုကေ</string>
<string name="title_activity_nearby">အနီးအနား နေရာများ</string>
<string name="no_nearby">အနီးအနား နေရာများ မတွေ့ပါ</string>
@ -150,7 +152,7 @@
<string name="invalid_input">မကိုက်ညီသော ထည့်သွင်းမှု</string>
<string name="maximum_limit_alert">၅၀၀ ထက်ပို၍ မပြသနိုင်ပါ</string>
<string name="enter_valid">ကိုက်ညီသောနံပါတ်တစ်ခု ရိုက်ထည့်ပါ</string>
<string name="set_limit" fuzzy="true">လတ်တလော အပ်ပလုတ်ကန့်သတ်ချက် သတ်မှတ်ရန်</string>
<string name="set_limit">လတ်တလော အပ်ပလုတ်ကန့်သတ်ချက်</string>
<string name="logout_verification">အမှန်တကယ် ထွက်သွားလိုပါသလား</string>
<string name="commons_logo">ကွန်မွန်းစ် လိုဂို</string>
<string name="commons_website">ကွန်မွန်းစ် ဝဘ်ဆိုဒ်</string>
@ -276,9 +278,24 @@
<string name="desc_language_Africa">အာဖရိက</string>
<string name="desc_language_Asia">အာရှ</string>
<string name="desc_language_Pacific">ပစိဖိတ်</string>
<string name="yes_submit">ဟုတ်ကဲ့ ထည့်သွင်းမည်</string>
<string name="no_go_back">ဟင်းအင်း၊ ပြန်သွားမည်</string>
<string name="search_this_area">ဤဧရိယာကို ရှာဖွေပါ</string>
<string name="nearby_card_permission_title">ခွင့်ပြုချက် တောင်းခံရန်</string>
<string name="never_ask_again">နောက်တခါ ထပ်မမေးပါနှင့်</string>
<string name="nominate_for_deletion_done">ပြီးပြီ</string>
<string name="notsure">မသေချာပါ</string>
<string name="send_thank_send">ကျေးဇူးတင်မှု ပို့နေသည်</string>
<string name="send_thank_notification_title">ကျေးဇူးတင်မှု ပို့နေသည်</string>
<string name="send_thank_toast">%1$ အတွက် ကျေးဇူးတင်မှု ပို့နေသည်</string>
<string name="review_thanks_no_button_text">နောက်ရုပ်ပုံ</string>
<string name="no_notification">မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ</string>
<string name="please_wait">ကျေးဇူးပြု၍ ခဏစောင့်ပါ...</string>
<string name="exif_tag_name_author">ဖန်တီးသူ</string>
<string name="exif_tag_name_copyright">မူပိုင်ခွင့်</string>
<string name="exif_tag_name_location">တည်နေရာ</string>
<string name="exif_tag_name_cameraModel">ကင်မရာ မော်ဒယ်</string>
<string name="exif_tag_name_software">ဆော့ဝဲလ်</string>
<string name="image_info">ရုပ်ပုံ အချက်အလက်</string>
<string name="dialog_box_text_nomination">%1$ ဟာ ဘာကြောင့် ဖျက်သင့်သလဲ?</string>
</resources>

View file

@ -181,6 +181,7 @@
<string name="storage_permission_title">Solicitando permissão de armazenamento</string>
<string name="read_storage_permission_rationale">Permissão necessária: leia o armazenamento externo. App não pode acessar sua galeria sem isso.</string>
<string name="write_storage_permission_rationale">Permissão necessária: escreva o armazenamento externo. App não pode acessar sua câmera/galeria sem isso.</string>
<string name="location_permission_title">Autorização para identificar localização</string>
<string name="location_permission_rationale">Permissão opcional: Obter a localização atual de sugestões de categoria</string>
<string name="ok">OK</string>
<string name="title_activity_nearby">Lugares próximos</string>
@ -532,4 +533,5 @@
<string name="share_text">Faça o carregamento de fotos para o Wikimedia Commons no seu telefone ou baixe o aplicativo Commons: %1$s</string>
<string name="share_via">Compartilhar aplicativo via...</string>
<string name="image_info">Informação da imagem</string>
<string name="dialog_box_text_nomination">Por que %1$s deve ser excluído?</string>
</resources>

View file

@ -23,6 +23,7 @@
<string name="preference_category_appearance">Aparência</string>
<string name="preference_category_general">Geral</string>
<string name="preference_category_feedback">Comentários</string>
<string name="preference_category_privacy">Privacidade</string>
<string name="preference_category_location">Localização</string>
<string name="app_name">Commons</string>
<string name="bullet"></string>
@ -181,6 +182,7 @@
<string name="storage_permission_title">A pedir permissão de armazenamento</string>
<string name="read_storage_permission_rationale">Permissão necessária: Ler a armazenagem externa. A aplicação não pode aceder à sua galeria sem isto.</string>
<string name="write_storage_permission_rationale">Permissão necessária: Escrever na armazenagem externa. A aplicação não pode aceder à sua câmara/galeria sem isto.</string>
<string name="location_permission_title">Autorização para identificar localização</string>
<string name="location_permission_rationale">Permissão opcional: Obter a localização atual para sugestões de categoria</string>
<string name="ok">OK</string>
<string name="title_activity_nearby">Locais Próximos</string>
@ -520,6 +522,15 @@
<string name="welcome_dont_upload_content_description">Exemplos de imagens que não devem ser carregadas</string>
<string name="skip_image">SALTAR ESTA IMAGEM</string>
<string name="download_failed_we_cannot_download_the_file_without_storage_permission">O descarregamento falhou! Não podemos descarregar o ficheiro sem permissão de armazenagem externa.</string>
<string name="manage_exif_tags">Gerir etiquetas EXIF</string>
<string name="manage_exif_tags_summary">Selecionar as etiquetas EXIF a manter nos carregamentos</string>
<string name="exif_tag_name_author">Autor</string>
<string name="exif_tag_name_copyright">Direitos de autor</string>
<string name="exif_tag_name_location">Localização</string>
<string name="exif_tag_name_cameraModel">Modelo da câmara</string>
<string name="exif_tag_name_lensModel">Modelo da lente</string>
<string name="exif_tag_name_serialNumbers">Números de série</string>
<string name="exif_tag_name_software">\'\'Software\'\'</string>
<string name="share_text">Carregar fotografias na wiki Wikimedia Commons, do seu telemóvel Descarregar a aplicação Commons: %1$s</string>
<string name="share_via">Partilhar aplicação por...</string>
<string name="image_info">Informação da imagem</string>

View file

@ -156,4 +156,6 @@
<string name="submit">{{Identical|Submit}}</string>
<string name="log_collection_started">\"Send log file\" is {{msg-wm|Commons-android-strings-send log file}}.</string>
<string name="nominate_for_deletion_done">{{Identical|Done}}</string>
<string name="exif_tag_name_author">{{Identical|Author}}</string>
<string name="exif_tag_name_location">{{Identical|Location}}</string>
</resources>

View file

@ -373,7 +373,7 @@
<string name="write_storage_permission_rationale_for_image_share">Avem nevoie de permisiunea dvs. pentru a accesa spațiul de stocare extern al dispozitivului dvs. pentru a încărca imagini.</string>
<string name="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>

View file

@ -187,6 +187,7 @@
<string name="storage_permission_title">Запрос разрешения по использованию внешнего носителя</string>
<string name="read_storage_permission_rationale">Требуемые разрешения: чтение с внешнего носителя. Приложение не сможет получить доступ к вашей галерее без этого разрешения.</string>
<string name="write_storage_permission_rationale">Требуемые разрешения: запись на внешнее хранилище. Приложение не сможет получить доступ к галерее/камере без этого разрешения.</string>
<string name="location_permission_title">Запрос на определение местоположения</string>
<string name="location_permission_rationale">Необязательное разрешение: получение текущего местоположения для предложения категорий</string>
<string name="ok">OK</string>
<string name="title_activity_nearby">Места поблизости</string>
@ -539,4 +540,5 @@
<string name="share_text">Чтобы загружать фото на Викисклад (Wikimedia Commons), скачайте одноимённое приложение «Викисклад» (Commons): %1$s</string>
<string name="share_via">Поделиться приложением с помощью...</string>
<string name="image_info">Информация об изображении</string>
<string name="dialog_box_text_nomination">Почему %1$s должно быть удалено?</string>
</resources>

View file

@ -174,6 +174,7 @@
<string name="storage_permission_title">Begär lagringsbehörighet</string>
<string name="read_storage_permission_rationale">Nödvändig behörighet: Läs extern lagring. Appen kan inte komma åt ditt galleri utan detta.</string>
<string name="write_storage_permission_rationale">Nödvändig behörighet: Skriv till extern lagring. Appen kan inte komma åt din kamera/galleri utan detta.</string>
<string name="location_permission_title">Begär platsbehörighet</string>
<string name="location_permission_rationale">Valfri behörighet: Hämta aktuell plats för kategoriförslag</string>
<string name="ok">OK</string>
<string name="title_activity_nearby">Platser i närheten</string>
@ -525,4 +526,5 @@
<string name="share_text">Ladda upp foton till Wikimedia Commons på din telefon Ladda ned Commons-appen: %1$s</string>
<string name="share_via">Dela appen via...</string>
<string name="image_info">Bildinfo</string>
<string name="dialog_box_text_nomination">Varför bör %1$s raderas?</string>
</resources>

View file

@ -186,6 +186,7 @@
<string name="storage_permission_title">Запит дозволу на зберігання</string>
<string name="read_storage_permission_rationale">Обов\'язковий дозвіл: читання зовнішньої пам\'яті. Без цього дозволу програма не зможе отримати доступ до вашої галереї.</string>
<string name="write_storage_permission_rationale">Обов\'язковий дозвіл: записування на зовнішнє сховище. Програма не зможе отримати доступ до камери/галереї без цього дозволу.</string>
<string name="location_permission_title">Запит на визначення місцезнаходження</string>
<string name="location_permission_rationale">Додатковий дозвіл: отримувати поточне розташування для підказок категорій</string>
<string name="ok">Гаразд</string>
<string name="title_activity_nearby">Місця поблизу</string>
@ -539,4 +540,5 @@
<string name="share_text">Вивантажуйте фото у Вікісховище зі свого телефона. Завантажте застосунок: %1$s</string>
<string name="share_via">Поділитися програмкою через…</string>
<string name="image_info">Інформація про зображення</string>
<string name="dialog_box_text_nomination">Чому %1$s має бути видалено?</string>
</resources>

View file

@ -44,7 +44,7 @@
<string name="upload_completed_notification_text">輕觸來檢視您上傳的項目</string>
<string name="upload_progress_notification_title_start">開始上傳%1$s</string>
<string name="upload_progress_notification_title_in_progress">正在上傳%1$s</string>
<string name="upload_progress_notification_title_finishing">即將完成上傳 %1$s</string>
<string name="upload_progress_notification_title_finishing">即將完成上傳%1$s</string>
<string name="upload_failed_notification_title">上傳%1$s失敗</string>
<string name="upload_failed_notification_subtitle">輕觸檢視</string>
<plurals name="uploads_pending_notification_indicator">
@ -178,6 +178,7 @@
<string name="storage_permission_title">請求存儲裝置權限</string>
<string name="read_storage_permission_rationale">必要權限:讀取外部存儲裝置。否則應用程式無法存取您的圖庫。</string>
<string name="write_storage_permission_rationale">必要權限:寫入外部存儲裝置。否則應用程式無法取用您的相機/圖庫。</string>
<string name="location_permission_title">請求位置權限</string>
<string name="location_permission_rationale">可有可無的權限:獲取目前的地理位置,以用於分類建議</string>
<string name="ok"></string>
<string name="title_activity_nearby">附近地點</string>
@ -529,4 +530,5 @@
<string name="share_text">在您的手機上更新照片到維基共享資源,下載共享資源應用程式:%1$s</string>
<string name="share_via">分享應用程式透過…</string>
<string name="image_info">圖片資訊</string>
<string name="dialog_box_text_nomination">為何應刪除%1$s</string>
</resources>

View file

@ -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>

View file

@ -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,4 +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>

View file

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

View file

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

View file

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

View file

@ -87,24 +87,6 @@ class UploadModelTest {
}
}
@Test
fun verifyPreviousNotAvailable() {
uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
assertFalse(uploadModel!!.isPreviousAvailable)
}
@Test
fun verifyNextAvailable() {
uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable)
}
@Test
fun isSubmitAvailable() {
uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
assertTrue(uploadModel!!.isNextAvailable)
}
@Test
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()

View file

@ -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()
}
}

View file

@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.android.tools.build:gradle:3.4.1'
classpath 'com.dicedmelon.gradle:jacoco-android:0.1.4'
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"

Binary file not shown.

View file

@ -25,4 +25,5 @@ DAGGER_VERSION=2.21
systemProp.http.proxyPort=0
systemProp.http.proxyHost=
android.useAndroidX=true
android.enableJetifier=true
android.enableJetifier=true
android.enableR8=false

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip