diff --git a/app/build.gradle b/app/build.gradle
index 2b32d1a3d..6118d99d5 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -31,6 +31,7 @@ dependencies {
transitive = true
}
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
+
//noinspection GradleCompatible
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
@@ -43,6 +44,7 @@ dependencies {
implementation 'com.squareup.okio:okio:1.14.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
// Because RxAndroid releases are few and far between, it is recommended you also
+
// explicitly depend on RxJava's latest version for bug fixes and new features.
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
@@ -130,7 +132,7 @@ android {
flavorDimensions 'tier'
productFlavors {
prod {
-
+
applicationId 'fr.free.nrw.commons'
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java b/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java
deleted file mode 100644
index 636d30a1b..000000000
--- a/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.net.Uri;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import fr.free.nrw.commons.BuildConfig;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertThat;
-
-@RunWith(AndroidJUnit4.class)
-public class FileUtilsTest {
- @Test
- public void isSelfOwned() throws Exception {
- Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".provider/document/1");
- boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
- assertThat(selfOwned, is(true));
- }
-
- @Test
- public void isNotSelfOwned() throws Exception {
- Uri uri = Uri.parse("content://com.android.providers.media.documents/document/1");
- boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
- assertThat(selfOwned, is(false));
- }
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6cbead479..ad76ee14d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,7 +15,7 @@
-
+
@@ -41,42 +41,35 @@
-
-
+
+
-
-
-
-
+
+
+
-
-
-
-
+
-
+
@@ -172,21 +165,18 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
-
-
-
Map arraysToMap(K[] kArray, V[] vArray){
+ if(kArray.length!=vArray.length)
+ throw new RuntimeException("arraysToMap array sizes don't match");
+ Map map=new LinkedHashMap<>();
+ for (int i=0;i create(List placeList) {
+ public CategoryRendererAdapter create(List placeList) {
RendererBuilder builder = new RendererBuilder()
.bind(CategoryItem.class, new CategoriesRenderer(listener));
ListAdapteeCollection collection = new ListAdapteeCollection<>(
placeList != null ? placeList : Collections.emptyList());
- return new RVRendererAdapter<>(builder, collection);
+ return new CategoryRendererAdapter(builder, collection);
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java
new file mode 100644
index 000000000..8375f4972
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java
@@ -0,0 +1,227 @@
+package fr.free.nrw.commons.category;
+
+import android.content.SharedPreferences;
+import android.text.TextUtils;
+
+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.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 {
+ private static final int SEARCH_CATS_LIMIT = 25;
+
+ private final MediaWikiApi mwApi;
+ private final CategoryDao categoryDao;
+ private final SharedPreferences prefs;
+ private final SharedPreferences directPrefs;
+
+ private HashMap> categoriesCache;
+ private List selectedCategories;
+
+ @Inject GpsCategoryModel gpsCategoryModel;
+ @Inject
+ public CategoriesModel(MediaWikiApi mwApi,
+ CategoryDao categoryDao,
+ @Named("default_preferences") SharedPreferences prefs,
+ @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs) {
+ this.mwApi = mwApi;
+ this.categoryDao = categoryDao;
+ this.prefs = prefs;
+ this.directPrefs = directPrefs;
+ this.categoriesCache = new HashMap<>();
+ this.selectedCategories = new ArrayList<>();
+ }
+
+ //region Misc. utility methods
+ public Comparator sortBySimilarity(final String filter) {
+ Comparator stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
+ return (firstItem, secondItem) -> stringSimilarityComparator
+ .compare(firstItem.getName(), secondItem.getName());
+ }
+
+ public boolean containsYear(String item) {
+ //Check for current and previous year to exclude these categories from removal
+ Calendar now = Calendar.getInstance();
+ int year = now.get(Calendar.YEAR);
+ String yearInString = String.valueOf(year);
+
+ int prevYear = year - 1;
+ String prevYearInString = String.valueOf(prevYear);
+ Timber.d("Previous year: %s", prevYearInString);
+
+ //Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
+ //And that item does not equal the current year or previous year
+ //And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
+ //Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
+ return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
+ || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
+ || (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
+ }
+
+ public void updateCategoryCount(CategoryItem item) {
+ Category category = categoryDao.find(item.getName());
+
+ // Newly used category...
+ if (category == null) {
+ category = new Category(null, item.getName(), new Date(), 0);
+ }
+
+ category.incTimesUsed();
+ categoryDao.save(category);
+ }
+ //endregion
+
+ //region Category Caching
+ public void cacheAll(HashMap> categories) {
+ categoriesCache.putAll(categories);
+ }
+
+ public HashMap> getCategoriesCache() {
+ return categoriesCache;
+ }
+
+ boolean cacheContainsKey(String term) {
+ return categoriesCache.containsKey(term);
+ }
+ //endregion
+
+ //region Category searching
+ public Observable searchAll(String term, List 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());
+ }
+
+ //if user types in something that is in cache, return cached category
+ if (cacheContainsKey(term)) {
+ return Observable.fromIterable(getCachedCategories(term))
+ .map(name -> new CategoryItem(name, false));
+ }
+
+ //otherwise, search API for matching categories
+ return mwApi
+ .allCategories(term, SEARCH_CATS_LIMIT)
+ .map(name -> new CategoryItem(name, false));
+ }
+
+ public Observable searchCategories(String term, List 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));
+ }
+
+ private ArrayList getCachedCategories(String term) {
+ return categoriesCache.get(term);
+ }
+
+ public Observable defaultCategories(List titleList) {
+ Observable 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());
+ }
+ }
+
+ private boolean hasDirectCategories() {
+ return !directPrefs.getString("Category", "").equals("");
+ }
+
+ private Observable directCategories() {
+ String directCategory = directPrefs.getString("Category", "");
+ List categoryList = new ArrayList<>();
+ Timber.d("Direct category found: " + directCategory);
+
+ if (!directCategory.equals("")) {
+ categoryList.add(directCategory);
+ Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList);
+ }
+ return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
+ }
+
+ Observable gpsCategories() {
+ return Observable.fromIterable(gpsCategoryModel.getCategoryList())
+ .map(name -> new CategoryItem(name, false));
+ }
+
+ private Observable titleCategories(List titleList) {
+ return Observable.fromIterable(titleList)
+ .concatMap(this::getTitleCategories);
+ }
+
+ private Observable getTitleCategories(String title) {
+ return mwApi.searchTitles(title, SEARCH_CATS_LIMIT)
+ .map(name -> new CategoryItem(name, false));
+ }
+
+ private Observable 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) {
+ if (item.isSelected()) {
+ selectCategory(item);
+ updateCategoryCount(item);
+ } else {
+ unselectCategory(item);
+ }
+ }
+
+ public void selectCategory(CategoryItem item) {
+ selectedCategories.add(item);
+ }
+
+ public void unselectCategory(CategoryItem item) {
+ selectedCategories.remove(item);
+ }
+
+ public int selectedCategoriesCount() {
+ return selectedCategories.size();
+ }
+
+ public List getSelectedCategories() {
+ return selectedCategories;
+ }
+
+ public List getCategoryStringList() {
+ List output = new ArrayList<>();
+ for (CategoryItem item : selectedCategories) {
+ output.add(item.getName());
+ }
+ return output;
+ }
+ //endregion
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java
index 81cccdb72..f9a349ccb 100644
--- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java
@@ -1,5 +1,6 @@
package fr.free.nrw.commons.category;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -11,7 +12,7 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
-class CategoriesRenderer extends Renderer {
+public class CategoriesRenderer extends Renderer {
@BindView(R.id.tvName) CheckedTextView checkedView;
private final CategoryClickedListener listener;
@@ -44,11 +45,8 @@ class CategoriesRenderer extends Renderer {
@Override
public void render() {
CategoryItem item = getContent();
+ Log.e("Commons", "Rendering: "+item);
checkedView.setChecked(item.isSelected());
checkedView.setText(item.getName());
}
-
- interface CategoryClickedListener {
- void categoryClicked(CategoryItem item);
- }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java
deleted file mode 100644
index c53c29379..000000000
--- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java
+++ /dev/null
@@ -1,421 +0,0 @@
-package fr.free.nrw.commons.category;
-
-
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.support.v7.app.AlertDialog;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.EditText;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-import com.jakewharton.rxbinding2.view.RxView;
-import com.jakewharton.rxbinding2.widget.RxTextView;
-import com.pedrogomez.renderers.RVRendererAdapter;
-
-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 java.util.concurrent.TimeUnit;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
-import fr.free.nrw.commons.mwapi.MediaWikiApi;
-import fr.free.nrw.commons.upload.GpsCategoryModel;
-import fr.free.nrw.commons.utils.StringSortingUtils;
-import fr.free.nrw.commons.utils.ViewUtil;
-import io.reactivex.Observable;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.schedulers.Schedulers;
-import timber.log.Timber;
-
-import static android.view.KeyEvent.ACTION_UP;
-import static android.view.KeyEvent.KEYCODE_BACK;
-
-/**
- * Displays the category suggestion and selection screen. Category search is initiated here.
- */
-public class CategorizationFragment extends CommonsDaggerSupportFragment {
-
- public static final int SEARCH_CATS_LIMIT = 25;
-
- @BindView(R.id.categoriesListBox)
- RecyclerView categoriesList;
- @BindView(R.id.categoriesSearchBox)
- EditText categoriesFilter;
- @BindView(R.id.categoriesSearchInProgress)
- ProgressBar categoriesSearchInProgress;
- @BindView(R.id.categoriesNotFound)
- TextView categoriesNotFoundView;
- @BindView(R.id.categoriesExplanation)
- TextView categoriesSkip;
-
- @Inject MediaWikiApi mwApi;
- @Inject @Named("default_preferences") SharedPreferences prefs;
- @Inject @Named("prefs") SharedPreferences prefsPrefs;
- @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
- @Inject CategoryDao categoryDao;
- @Inject GpsCategoryModel gpsCategoryModel;
-
- private RVRendererAdapter categoriesAdapter;
- private OnCategoriesSaveHandler onCategoriesSaveHandler;
- private HashMap> categoriesCache;
- private List selectedCategories = new ArrayList<>();
- private TitleTextWatcher textWatcher = new TitleTextWatcher();
- private boolean hasDirectCategories = false;
-
- private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
- if (item.isSelected()) {
- selectedCategories.add(item);
- updateCategoryCount(item);
- } else {
- selectedCategories.remove(item);
- }
- });
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_categorization, container, false);
- ButterKnife.bind(this, rootView);
-
- categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
-
- ArrayList items = new ArrayList<>();
- categoriesCache = new HashMap<>();
- if (savedInstanceState != null) {
- items.addAll(savedInstanceState.getParcelableArrayList("currentCategories"));
- //noinspection unchecked
- categoriesCache.putAll((HashMap>) savedInstanceState
- .getSerializable("categoriesCache"));
- }
-
- categoriesAdapter = adapterFactory.create(items);
- categoriesList.setAdapter(categoriesAdapter);
-
-
- categoriesFilter.addTextChangedListener(textWatcher);
-
- categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> {
- if (!hasFocus) {
- ViewUtil.hideKeyboard(v);
- }
- });
-
- RxTextView.textChanges(categoriesFilter)
- .takeUntil(RxView.detaches(categoriesFilter))
- .debounce(500, TimeUnit.MILLISECONDS)
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(filter -> updateCategoryList(filter.toString()));
- return rootView;
- }
-
- @Override
- public void onDestroyView() {
- categoriesFilter.removeTextChangedListener(textWatcher);
- super.onDestroyView();
- }
-
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- menu.clear();
- inflater.inflate(R.menu.fragment_categorization, menu);
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- View rootView = getView();
- if (rootView != null) {
- rootView.setFocusableInTouchMode(true);
- rootView.requestFocus();
- rootView.setOnKeyListener((v, keyCode, event) -> {
- if (event.getAction() == ACTION_UP && keyCode == KEYCODE_BACK) {
- showBackButtonDialog();
- return true;
- }
- return false;
- });
- }
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- int itemCount = categoriesAdapter.getItemCount();
- ArrayList items = new ArrayList<>(itemCount);
- for (int i = 0; i < itemCount; i++) {
- items.add(categoriesAdapter.getItem(i));
- }
- outState.putParcelableArrayList("currentCategories", items);
- outState.putSerializable("categoriesCache", categoriesCache);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem menuItem) {
- switch (menuItem.getItemId()) {
- case R.id.menu_save_categories:
- if (selectedCategories.size() > 0) {
- //Some categories selected, proceed to submission
- onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories));
- } else {
- //No categories selected, prompt the user to select some
- showConfirmationDialog();
- }
- return true;
- default:
- return super.onOptionsItemSelected(menuItem);
- }
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- setHasOptionsMenu(true);
- onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
- getActivity().setTitle(R.string.categories_activity_title);
- }
-
- private void updateCategoryList(String filter) {
- Observable.fromIterable(selectedCategories)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .doOnSubscribe(disposable -> {
- categoriesSearchInProgress.setVisibility(View.VISIBLE);
- categoriesNotFoundView.setVisibility(View.GONE);
- categoriesSkip.setVisibility(View.GONE);
- categoriesAdapter.clear();
- })
- .observeOn(Schedulers.io())
- .concatWith(
- searchAll(filter)
- .mergeWith(searchCategories(filter))
- .concatWith(TextUtils.isEmpty(filter)
- ? defaultCategories() : Observable.empty())
- )
- .filter(categoryItem -> !containsYear(categoryItem.getName()))
- .distinct()
- .sorted(sortBySimilarity(filter))
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(
- s -> categoriesAdapter.add(s),
- Timber::e,
- () -> {
- categoriesAdapter.notifyDataSetChanged();
- categoriesSearchInProgress.setVisibility(View.GONE);
-
- if (categoriesAdapter.getItemCount() == selectedCategories.size()) {
- // There are no suggestions
- if (TextUtils.isEmpty(filter)) {
- // Allow to send image with no categories
- categoriesSkip.setVisibility(View.VISIBLE);
- } else {
- // Inform the user that the searched term matches no category
- categoriesNotFoundView.setText(getString(R.string.categories_not_found, filter));
- categoriesNotFoundView.setVisibility(View.VISIBLE);
- }
- }
- }
- );
- }
-
- private Comparator sortBySimilarity(final String filter) {
- Comparator stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
- return (firstItem, secondItem) -> stringSimilarityComparator
- .compare(firstItem.getName(), secondItem.getName());
- }
-
- private List getStringList(List input) {
- List output = new ArrayList<>();
- for (CategoryItem item : input) {
- output.add(item.getName());
- }
- return output;
- }
-
- private Observable defaultCategories() {
- Observable directCat = directCategories();
- if (hasDirectCategories) {
- Timber.d("Image has direct Cat");
- return directCat
- .concatWith(gpsCategories())
- .concatWith(titleCategories())
- .concatWith(recentCategories());
- }
- else {
- Timber.d("Image has no direct Cat");
- return gpsCategories()
- .concatWith(titleCategories())
- .concatWith(recentCategories());
- }
- }
-
- private Observable directCategories() {
- String directCategory = directPrefs.getString("Category", "");
- // Strip newlines to prevent blank categories, and to tidy existing categories
- directCategory = directCategory.replace("\n", "");
-
- List categoryList = new ArrayList<>();
- Timber.d("Direct category found: " + "'" + directCategory + "'");
-
- if (!directCategory.equals("")) {
- hasDirectCategories = true;
- categoryList.add(directCategory);
- Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList);
- }
- return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
- }
-
- private Observable gpsCategories() {
- return Observable.fromIterable(gpsCategoryModel.getCategoryList())
- .map(name -> new CategoryItem(name, false));
- }
-
- private Observable titleCategories() {
- //Retrieve the title that was saved when user tapped submit icon
- String title = prefs.getString("Title", "");
-
- return mwApi
- .searchTitles(title, SEARCH_CATS_LIMIT)
- .map(name -> new CategoryItem(name, false));
- }
-
- private Observable recentCategories() {
- return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
- .map(s -> new CategoryItem(s, false));
- }
-
- private Observable searchAll(String term) {
- //If user hasn't typed anything in yet, get GPS and recent items
- if (TextUtils.isEmpty(term)) {
- return Observable.empty();
- }
-
- //if user types in something that is in cache, return cached category
- if (categoriesCache.containsKey(term)) {
- return Observable.fromIterable(categoriesCache.get(term))
- .map(name -> new CategoryItem(name, false));
- }
-
- //otherwise, search API for matching categories
- return mwApi
- .allCategories(term, SEARCH_CATS_LIMIT)
- .map(name -> new CategoryItem(name, false));
- }
-
- private Observable searchCategories(String term) {
- //If user hasn't typed anything in yet, get GPS and recent items
- if (TextUtils.isEmpty(term)) {
- return Observable.empty();
- }
-
- return mwApi
- .searchCategories(term, SEARCH_CATS_LIMIT)
- .map(s -> new CategoryItem(s, false));
- }
-
- private boolean containsYear(String item) {
- //Check for current and previous year to exclude these categories from removal
- Calendar now = Calendar.getInstance();
- int year = now.get(Calendar.YEAR);
- String yearInString = String.valueOf(year);
-
- int prevYear = year - 1;
- String prevYearInString = String.valueOf(prevYear);
- Timber.d("Previous year: %s", prevYearInString);
-
- //Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
- //And that item does not equal the current year or previous year
- //And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
- //Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
- return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
- || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
- || (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
- }
-
- private void updateCategoryCount(CategoryItem item) {
- Category category = categoryDao.find(item.getName());
-
- // Newly used category...
- if (category == null) {
- category = new Category(null, item.getName(), new Date(), 0);
- }
-
- category.incTimesUsed();
- categoryDao.save(category);
- }
-
- public int getCurrentSelectedCount() {
- return selectedCategories.size();
- }
-
- /**
- * Show dialog asking for confirmation to leave without saving categories.
- */
- public void showBackButtonDialog() {
- new AlertDialog.Builder(getActivity())
- .setMessage("Are you sure you want to go back? The image will not "
- + "have any categories saved.")
- .setTitle("Warning")
- .setPositiveButton(android.R.string.no, (dialog, id) -> {
- //No need to do anything, user remains on categorization screen
- })
- .setNegativeButton(android.R.string.yes, (dialog, id) -> getActivity().finish())
- .create()
- .show();
- }
-
- private void showConfirmationDialog() {
- new AlertDialog.Builder(getActivity())
- .setMessage("Images without categories are rarely usable. "
- + "Are you sure you want to submit without selecting "
- + "categories?")
- .setTitle("No Categories Selected")
- .setPositiveButton(android.R.string.no, (dialog, id) -> {
- //Exit menuItem so user can select their categories
- })
- .setNegativeButton(android.R.string.yes, (dialog, id) -> {
- //Proceed to submission
- onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories));
- })
- .create()
- .show();
- }
-
- private class TitleTextWatcher implements TextWatcher {
- @Override
- public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
- }
-
- @Override
- public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
- }
-
- @Override
- public void afterTextChanged(Editable editable) {
- if (getActivity() != null) {
- getActivity().invalidateOptionsMenu();
- }
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryClickedListener.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryClickedListener.java
new file mode 100644
index 000000000..df99b4060
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClickedListener.java
@@ -0,0 +1,5 @@
+package fr.free.nrw.commons.category;
+
+public interface CategoryClickedListener {
+ void categoryClicked(CategoryItem item);
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java
index f6bacfb51..f3ade09d8 100644
--- a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java
@@ -3,7 +3,7 @@ package fr.free.nrw.commons.category;
import android.os.Parcel;
import android.os.Parcelable;
-class CategoryItem implements Parcelable {
+public class CategoryItem implements Parcelable {
private final String name;
private boolean selected;
@@ -71,4 +71,9 @@ class CategoryItem implements Parcelable {
public int hashCode() {
return name.hashCode();
}
+
+ @Override
+ public String toString() {
+ return "CategoryItem: '" + name + '\'';
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryRendererAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryRendererAdapter.java
new file mode 100644
index 000000000..887ad595f
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryRendererAdapter.java
@@ -0,0 +1,22 @@
+package fr.free.nrw.commons.category;
+
+import com.pedrogomez.renderers.AdapteeCollection;
+import com.pedrogomez.renderers.RVRendererAdapter;
+import com.pedrogomez.renderers.RendererBuilder;
+
+import java.util.ArrayList;
+
+public class CategoryRendererAdapter extends RVRendererAdapter {
+ CategoryRendererAdapter(RendererBuilder rendererBuilder, AdapteeCollection collection) {
+ super(rendererBuilder, collection);
+ }
+
+ protected ArrayList allItems() {
+ int itemCount = getItemCount();
+ ArrayList items = new ArrayList<>(itemCount);
+ for (int i = 0; i < itemCount; i++) {
+ items.add(getItem(i));
+ }
+ return items;
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java
index 9f5084e6a..cdbc8c28d 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java
@@ -2,8 +2,11 @@ package fr.free.nrw.commons.contributions;
import android.net.Uri;
import android.os.Parcel;
+import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
+import android.support.annotation.StringDef;
+import java.lang.annotation.Retention;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
@@ -13,6 +16,8 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.settings.Prefs;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
public class Contribution extends Media {
public static Creator CREATOR = new Creator() {
@@ -33,6 +38,10 @@ public class Contribution extends Media {
public static final int STATE_QUEUED = 2;
public static final int STATE_IN_PROGRESS = 3;
+ @Retention(SOURCE)
+ @StringDef({SOURCE_CAMERA, SOURCE_GALLERY, SOURCE_EXTERNAL})
+ public @interface FileSource {}
+
public static final String SOURCE_CAMERA = "camera";
public static final String SOURCE_GALLERY = "gallery";
public static final String SOURCE_EXTERNAL = "external";
@@ -40,7 +49,6 @@ public class Contribution extends Media {
private Uri contentUri;
private String source;
private String editSummary;
- private Date timestamp;
private int state;
private long transferred;
private String decimalCoords;
@@ -48,14 +56,13 @@ public class Contribution extends Media {
private String wikiDataEntityId;
private Uri contentProviderUri;
- public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
+ public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated,
int state, long dataLength, Date dateUploaded, long transferred,
String source, String description, String creator, boolean isMultiple,
int width, int height, String license) {
- super(localUri, imageUrl, filename, description, dataLength, timestamp, dateUploaded, creator);
+ super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.contentUri = contentUri;
this.state = state;
- this.timestamp = timestamp;
this.transferred = transferred;
this.source = source;
this.isMultiple = isMultiple;
@@ -69,14 +76,12 @@ public class Contribution extends Media {
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.decimalCoords = decimalCoords;
this.editSummary = editSummary;
- timestamp = new Date(System.currentTimeMillis());
}
public Contribution(Parcel in) {
super(in);
contentUri = in.readParcelable(Uri.class.getClassLoader());
source = in.readString();
- timestamp = (Date) in.readSerializable();
state = in.readInt();
transferred = in.readLong();
isMultiple = in.readInt() == 1;
@@ -87,12 +92,13 @@ public class Contribution extends Media {
super.writeToParcel(parcel, flags);
parcel.writeParcelable(contentUri, flags);
parcel.writeString(source);
- parcel.writeSerializable(timestamp);
parcel.writeInt(state);
parcel.writeLong(transferred);
parcel.writeInt(isMultiple ? 1 : 0);
}
+
+
public boolean getMultiple() {
return isMultiple;
}
@@ -121,14 +127,6 @@ public class Contribution extends Media {
this.contentUri = contentUri;
}
- public Date getTimestamp() {
- return timestamp;
- }
-
- public void setTimestamp(Date timestamp) {
- this.timestamp = timestamp;
- }
-
public int getState() {
return state;
}
@@ -141,10 +139,6 @@ public class Contribution extends Media {
this.dateUploaded = date;
}
- public String getTrackingTemplates() {
- return "{{subst:unc}}"; // Remove when we have categorization
- }
-
public String getPageContents() {
StringBuilder buffer = new StringBuilder();
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
@@ -169,8 +163,15 @@ public class Contribution extends Media {
buffer.append("== {{int:license-header}} ==\n")
.append(licenseTemplateFor(getLicense())).append("\n\n")
- .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n")
- .append(getTrackingTemplates());
+ .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n");
+ if(categories!=null&&categories.size()!=0) {
+ for (int i = 0; i < categories.size(); i++) {
+ String category = categories.get(i);
+ buffer.append("\n[[Category:").append(category).append("]]");
+ }
+ }
+ else
+ buffer.append("{{subst:unc}}");
return buffer.toString();
}
@@ -184,7 +185,7 @@ public class Contribution extends Media {
}
public Contribution() {
- timestamp = new Date(System.currentTimeMillis());
+
}
public String getSource() {
@@ -232,7 +233,7 @@ public class Contribution extends Media {
/**
* When the corresponding wikidata entity is known as in case of nearby uploads, it can be set
* using the setter method
- * @param wikiDataEntityId
+ * @param wikiDataEntityId wikiDataEntityId
*/
public void setWikiDataEntityId(String wikiDataEntityId) {
this.wikiDataEntityId = wikiDataEntityId;
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java
index 3aacb6155..c3a0f329b 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java
@@ -5,22 +5,26 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.FileProvider;
import java.io.File;
+import java.util.ArrayList;
import java.util.Date;
import java.util.List;
-import fr.free.nrw.commons.upload.ShareActivity;
+import fr.free.nrw.commons.upload.UploadActivity;
import timber.log.Timber;
import static android.content.Intent.ACTION_GET_CONTENT;
import static android.content.Intent.ACTION_SEND;
+import static android.content.Intent.ACTION_SEND_MULTIPLE;
import static android.content.Intent.EXTRA_STREAM;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY;
@@ -31,6 +35,7 @@ public class ContributionController {
public static final int SELECT_FROM_GALLERY = 1;
public static final int SELECT_FROM_CAMERA = 2;
+ public static final int PICK_IMAGE_MULTIPLE = 3;
private Fragment fragment;
@@ -79,6 +84,14 @@ public class ContributionController {
}
public void startGalleryPick() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ startMultipleGalleryPick();
+ } else {
+ startSingleGalleryPick();
+ }
+ }
+
+ public void startSingleGalleryPick() {
//FIXME: Starts gallery (opens Google Photos)
Intent pickImageIntent = new Intent(ACTION_GET_CONTENT);
pickImageIntent.setType("image/*");
@@ -87,15 +100,41 @@ public class ContributionController {
Timber.d("Fragment is not added, startActivityForResult cannot be called");
return;
}
- Timber.d("startGalleryPick() called with pickImageIntent");
+ Timber.d("startSingleGalleryPick() called with pickImageIntent");
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
}
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
+ public void startMultipleGalleryPick() {
+ Intent pickImageIntent = new Intent(ACTION_GET_CONTENT);
+ pickImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+ pickImageIntent.setType("image/*");
+ if (!fragment.isAdded()) {
+ Timber.d("Fragment is not added, startActivityForResult cannot be called");
+ return;
+ }
+ Timber.d("startMultipleGalleryPick() called with pickImageIntent");
+
+ fragment.startActivityForResult(pickImageIntent, PICK_IMAGE_MULTIPLE);
+ }
+
+ public void handleImagesPicked(int requestCode, @Nullable ArrayList uri) {
+ FragmentActivity activity = fragment.getActivity();
+ Intent shareIntent = new Intent(activity, UploadActivity.class);
+ shareIntent.setAction(ACTION_SEND_MULTIPLE);
+ shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
+ shareIntent.putExtra(EXTRA_STREAM, uri);
+ shareIntent.setType("image/jpeg");
+ if (activity != null) {
+ activity.startActivity(shareIntent);
+ }
+ }
+
public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId) {
FragmentActivity activity = fragment.getActivity();
Timber.d("handleImagePicked() called with onActivityResult(). Boolean isDirectUpload: " + isDirectUpload + "String wikiDataEntityId: " + wikiDataEntityId);
- Intent shareIntent = new Intent(activity, ShareActivity.class);
+ Intent shareIntent = new Intent(activity, UploadActivity.class);
shareIntent.setAction(ACTION_SEND);
switch (requestCode) {
case SELECT_FROM_GALLERY:
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
index 6d290b1a5..4c83af393 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
@@ -98,7 +98,8 @@ public class ContributionDao {
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
}
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength());
- cv.put(Table.COLUMN_TIMESTAMP, contribution.getTimestamp().getTime());
+ //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
+ 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());
cv.put(Table.COLUMN_SOURCE, contribution.getSource());
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java
index 7a4294863..b238c8b8b 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java
@@ -1,9 +1,11 @@
package fr.free.nrw.commons.contributions;
+import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
@@ -11,6 +13,7 @@ import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -20,9 +23,9 @@ import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListAdapter;
import android.widget.ProgressBar;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import android.widget.TextView;
+import java.util.ArrayList;
import java.util.Arrays;
import javax.inject.Inject;
@@ -39,7 +42,9 @@ import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.app.Activity.RESULT_OK;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.view.View.GONE;
+import static fr.free.nrw.commons.contributions.ContributionController.SELECT_FROM_GALLERY;
/**
* Created by root on 01.06.2018.
@@ -168,7 +173,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
fabGalery.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- //Gallery crashes before reach ShareActivity screen so must implement permissions check here
+ //Gallery crashes before reach ShareActivity screen so must implement permissions check here
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Here, thisActivity is the current activity
@@ -251,6 +256,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
// If coming from camera, pass null as uri. Because camera photos get saved to a
// fixed directory
controller.handleImagePicked(requestCode, null, false, null);
+ } else if (requestCode == ContributionController.PICK_IMAGE_MULTIPLE) {
+ handleMultipleImages(requestCode, data);
} else if (requestCode == ContributionController.SELECT_FROM_GALLERY){
controller.handleImagePicked(requestCode, data.getData(), false, null);
}
@@ -294,6 +301,28 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
}
}
+ private void handleMultipleImages(int requestCode, Intent data) {
+ if (getContext() == null) {
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
+ && data.getClipData() != null) {
+ ClipData mClipData = data.getClipData();
+ ArrayList mArrayUri = new ArrayList();
+ for (int i = 0; i < mClipData.getItemCount(); i++) {
+
+ ClipData.Item item = mClipData.getItemAt(i);
+ Uri uri = item.getUri();
+ mArrayUri.add(uri);
+ }
+ Log.v("LOG_TAG", "Selected Images" + mArrayUri.size());
+ controller.handleImagesPicked(requestCode, mArrayUri);
+ } else if(data.getData() != null) {
+ controller.handleImagePicked(SELECT_FROM_GALLERY, data.getData(), false, null);
+ }
+ }
+
/**
* Responsible to set progress bar invisible and visible
diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
index 19cb09dc6..6ae133fa3 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
@@ -15,8 +15,7 @@ import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
-import fr.free.nrw.commons.upload.MultipleShareActivity;
-import fr.free.nrw.commons.upload.ShareActivity;
+import fr.free.nrw.commons.upload.UploadActivity;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
@@ -28,12 +27,6 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract WelcomeActivity bindWelcomeActivity();
- @ContributesAndroidInjector
- abstract ShareActivity bindShareActivity();
-
- @ContributesAndroidInjector
- abstract MultipleShareActivity bindMultipleShareActivity();
-
@ContributesAndroidInjector
abstract MainActivity bindContributionsActivity();
@@ -52,6 +45,9 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract CategoryImagesActivity bindFeaturedImagesActivity();
+ @ContributesAndroidInjector
+ abstract UploadActivity bindUploadActivity();
+
@ContributesAndroidInjector
abstract SearchActivity bindSearchActivity();
diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
index d54b556c4..8c019da5d 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
@@ -1,10 +1,17 @@
package fr.free.nrw.commons.di;
+import android.app.Activity;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v4.util.LruCache;
+import android.view.inputmethod.InputMethodManager;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import javax.inject.Named;
import javax.inject.Singleton;
@@ -12,12 +19,14 @@ import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
+import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
+import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
@@ -38,6 +47,35 @@ public class CommonsApplicationModule {
return this.applicationContext;
}
+ @Provides
+ public InputMethodManager provideInputMethodManager() {
+ return (InputMethodManager) applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE);
+ }
+
+ @Provides
+ @Named("licenses")
+ public List provideLicenses(Context context) {
+ List licenseItems = new ArrayList<>();
+ licenseItems.add(context.getString(R.string.license_name_cc0));
+ licenseItems.add(context.getString(R.string.license_name_cc_by));
+ licenseItems.add(context.getString(R.string.license_name_cc_by_sa));
+ licenseItems.add(context.getString(R.string.license_name_cc_by_four));
+ licenseItems.add(context.getString(R.string.license_name_cc_by_sa_four));
+ return licenseItems;
+ }
+
+ @Provides
+ @Named("licenses_by_name")
+ public Map provideLicensesByName(Context context) {
+ Map byName = new HashMap<>();
+ byName.put(context.getString(R.string.license_name_cc0), Prefs.Licenses.CC0);
+ byName.put(context.getString(R.string.license_name_cc_by), Prefs.Licenses.CC_BY_3);
+ byName.put(context.getString(R.string.license_name_cc_by_sa), Prefs.Licenses.CC_BY_SA_3);
+ byName.put(context.getString(R.string.license_name_cc_by_four), Prefs.Licenses.CC_BY_4);
+ byName.put(context.getString(R.string.license_name_cc_by_sa_four), Prefs.Licenses.CC_BY_SA_4);
+ return byName;
+ }
+
@Provides
public AccountUtil providesAccountUtil(Context context) {
return new AccountUtil(context);
diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
index 04804dab0..b664d49e1 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
@@ -4,7 +4,6 @@ import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
-import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.category.CategoryImagesListFragment;
import fr.free.nrw.commons.category.SubCategoryListFragment;
import fr.free.nrw.commons.contributions.ContributionsFragment;
@@ -19,16 +18,11 @@ import fr.free.nrw.commons.nearby.NearbyListFragment;
import fr.free.nrw.commons.nearby.NearbyMapFragment;
import fr.free.nrw.commons.nearby.NoPermissionsFragment;
import fr.free.nrw.commons.settings.SettingsFragment;
-import fr.free.nrw.commons.upload.MultipleUploadListFragment;
-import fr.free.nrw.commons.upload.SingleUploadFragment;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class FragmentBuilderModule {
- @ContributesAndroidInjector
- abstract CategorizationFragment bindCategorizationFragment();
-
@ContributesAndroidInjector
abstract ContributionsListFragment bindContributionsListFragment();
@@ -50,12 +44,6 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract SettingsFragment bindSettingsFragment();
- @ContributesAndroidInjector
- abstract MultipleUploadListFragment bindMultipleUploadListFragment();
-
- @ContributesAndroidInjector
- abstract SingleUploadFragment bindSingleUploadFragment();
-
@ContributesAndroidInjector
abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java
index 43fcf4460..8b28f50d5 100644
--- a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java
@@ -1,6 +1,7 @@
package fr.free.nrw.commons.explore.images;
+import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
@@ -123,6 +124,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
* Checks for internet connection and then initializes the recycler view with 25 images of the searched query
* Clearing imageAdapter every time new keyword is searched so that user can see only new results
*/
+ @SuppressLint("CheckResult")
public void updateImageList(String query) {
this.query = query;
if (imagesNotFoundView != null) {
@@ -146,6 +148,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
/**
* Adds more results to existing search results
*/
+ @SuppressLint("CheckResult")
public void addImagesToList(String query) {
this.query = query;
progressBar.setVisibility(View.VISIBLE);
@@ -163,13 +166,11 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
*/
private void handlePaginationSuccess(List mediaList) {
progressBar.setVisibility(View.GONE);
- if (mediaList.size()!=0){
- if (!queryList.get(queryList.size()-1).getFilename().equals(mediaList.get(mediaList.size()-1).getFilename())) {
- queryList.addAll(mediaList);
- imagesAdapter.addAll(mediaList);
- imagesAdapter.notifyDataSetChanged();
- ((SearchActivity)getContext()).viewPagerNotifyDataSetChanged();
- }
+ if (mediaList.size() != 0 || !queryList.get(queryList.size() - 1).getFilename().equals(mediaList.get(mediaList.size() - 1).getFilename())) {
+ queryList.addAll(mediaList);
+ imagesAdapter.addAll(mediaList);
+ imagesAdapter.notifyDataSetChanged();
+ ((SearchActivity) getContext()).viewPagerNotifyDataSetChanged();
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java
index e7ccd97d5..5d82b5492 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java
@@ -47,11 +47,11 @@ class DirectUpload {
fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP);
}
} else {
- controller.startGalleryPick();
+ controller.startSingleGalleryPick();
}
}
else {
- controller.startGalleryPick();
+ controller.startSingleGalleryPick();
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java
index 8ef8e84a4..9668f11ee 100644
--- a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java
@@ -8,7 +8,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
- boolean currentTheme;
+ protected boolean currentTheme;
@Override
protected void onCreate(Bundle savedInstanceState) {
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Description.java b/app/src/main/java/fr/free/nrw/commons/upload/Description.java
index b19992574..ae18d4adb 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/Description.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/Description.java
@@ -1,56 +1,72 @@
package fr.free.nrw.commons.upload;
-import android.text.TextUtils;
+import java.util.List;
+/**
+ * Holds a description of an item being uploaded by {@link UploadActivity}
+ */
class Description {
- private String languageId;
- private String languageDisplayText;
+ private String languageCode;
private String descriptionText;
- private boolean set;
private int selectedLanguageIndex = -1;
- public String getLanguageId() {
- return languageId;
+ /**
+ * @return The language code ie. "en" or "fr"
+ */
+ String getLanguageCode() {
+ return languageCode;
}
- public void setLanguageId(String languageId) {
- this.languageId = languageId;
+ /**
+ * @param languageCode The language code ie. "en" or "fr"
+ */
+ void setLanguageCode(String languageCode) {
+ this.languageCode = languageCode;
}
- public String getLanguageDisplayText() {
- return languageDisplayText;
- }
-
- public void setLanguageDisplayText(String languageDisplayText) {
- this.languageDisplayText = languageDisplayText;
- }
-
- public String getDescriptionText() {
+ String getDescriptionText() {
return descriptionText;
}
- public void setDescriptionText(String descriptionText) {
+ void setDescriptionText(String descriptionText) {
this.descriptionText = descriptionText;
-
- if (!TextUtils.isEmpty(descriptionText)) {
- set = true;
- }
}
- public boolean isSet() {
- return set;
- }
-
- public void setSet(boolean set) {
- this.set = set;
- }
-
- public int getSelectedLanguageIndex() {
+ /**
+ * @return the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter}
+ */
+ int getSelectedLanguageIndex() {
return selectedLanguageIndex;
}
- public void setSelectedLanguageIndex(int selectedLanguageIndex) {
+ /**
+ * @param selectedLanguageIndex the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter}
+ */
+ void setSelectedLanguageIndex(int selectedLanguageIndex) {
this.selectedLanguageIndex = selectedLanguageIndex;
}
+
+
+ /**
+ * Formats the list of descriptions into the format Commons requires for uploads.
+ *
+ * @param descriptions the list of descriptions, description is ignored if text is null.
+ * @return a string with the pattern of {{en|1=descriptionText}}
+ */
+ static String formatList(List descriptions) {
+ StringBuilder descListString = new StringBuilder();
+ for (Description description : descriptions) {
+ if (!description.isEmpty()) {
+ String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageCode(),
+ description.getDescriptionText());
+ descListString.append(individualDescription);
+ }
+ }
+ return descListString.toString();
+ }
+
+ public boolean isEmpty() {
+ return descriptionText == null || descriptionText.isEmpty();
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java
index 5dfd02cae..8c2432ee1 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java
@@ -2,12 +2,12 @@ package fr.free.nrw.commons.upload;
import android.content.Context;
import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.AppCompatSpinner;
import android.support.v7.widget.RecyclerView;
-import android.text.Editable;
import android.text.TextUtils;
-import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@@ -17,182 +17,241 @@ import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.EditText;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
-import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnTouch;
+import butterknife.Optional;
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;
import static android.view.MotionEvent.ACTION_UP;
class DescriptionsAdapter extends RecyclerView.Adapter {
- List descriptions;
- List languages;
+ private Title title;
+ private List descriptions;
private Context context;
private Callback callback;
+ private Subject titleChangedSubject;
- public DescriptionsAdapter() {
+ private BiMap selectedLanguages;
+ private UploadView uploadView;
+
+ DescriptionsAdapter(UploadView uploadView) {
+ title = new Title();
descriptions = new ArrayList<>();
- descriptions.add(new Description());
- languages = new ArrayList<>();
+ titleChangedSubject = BehaviorSubject.create();
+ selectedLanguages = new BiMap<>();
+ this.uploadView = uploadView;
}
- public void setCallback(Callback callback) {
+ void setCallback(Callback callback) {
this.callback = callback;
}
- public void setDescriptions(List descriptions) {
+ void setItems(Title title, List descriptions) {
this.descriptions = descriptions;
+ this.title = title;
+ selectedLanguages = new BiMap<>();
notifyDataSetChanged();
}
- public void setLanguages(List languages) {
- this.languages = languages;
+ @Override
+ public int getItemViewType(int position) {
+ if (position == 0) return 1;
+ else return 2;
}
+ @NonNull
@Override
- public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View view = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.row_item_description, parent, false);
+ 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();
- ViewHolder viewHolder = new ViewHolder(view);
- return viewHolder;
+ return new ViewHolder(view);
}
@Override
- public void onBindViewHolder(ViewHolder holder, int position) {
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.init(position);
}
@Override
public int getItemCount() {
- return descriptions.size();
+ return descriptions.size() + 1;
}
- public List getDescriptions() {
+ List getDescriptions() {
return descriptions;
}
- public void addDescription(Description description) {
+ void addDescription(Description description) {
this.descriptions.add(description);
- notifyItemInserted(descriptions.size() - 1);
+ notifyItemInserted(descriptions.size() + 1);
}
+ public Title getTitle() {
+ return title;
+ }
+
+ public void setTitle(Title title) {
+ this.title = title;
+ notifyItemInserted(0);
+ }
public class ViewHolder extends RecyclerView.ViewHolder {
+ @Nullable
@BindView(R.id.spinner_description_languages)
AppCompatSpinner spinnerDescriptionLanguages;
- @BindView(R.id.et_description_text)
- EditText etDescriptionText;
- private View view;
+ @BindView(R.id.description_item_edit_text)
+ EditText descItemEditText;
+
+ private View view;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
this.view = itemView;
+ Timber.i("descItemEditText:" + descItemEditText);
}
public void init(int position) {
- Description description = descriptions.get(position);
- if (!TextUtils.isEmpty(description.getDescriptionText())) {
- etDescriptionText.setText(description.getDescriptionText());
- } else {
- etDescriptionText.setText("");
- }
- Drawable drawableRight = context.getResources()
- .getDrawable(R.drawable.mapbox_info_icon_default);
- if (position != 0) {
- etDescriptionText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
- } else {
- etDescriptionText.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableRight, null);
- }
-
- etDescriptionText.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-
- }
-
- @Override
- public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-
- }
-
- @Override
- public void afterTextChanged(Editable editable) {
- description.setDescriptionText(editable.toString());
- }
- });
-
- etDescriptionText.setOnFocusChangeListener((v, hasFocus) -> {
- if (!hasFocus) {
- ViewUtil.hideKeyboard(v);
- }
- });
-
- SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context,
- R.layout.row_item_languages_spinner);
- Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayLanguage()
- .compareTo(t1.getLocale().getDisplayLanguage().toString()));
- languagesAdapter.setLanguages(languages);
- languagesAdapter.notifyDataSetChanged();
- spinnerDescriptionLanguages.setAdapter(languagesAdapter);
-
- if (description.getSelectedLanguageIndex() == -1) {
- if (position == 0) {
- int defaultLocaleIndex = getIndexOfUserDefaultLocale();
- spinnerDescriptionLanguages.setSelection(defaultLocaleIndex);
+ if (position == 0) {
+ Timber.d("Title is " + title);
+ if (!title.isEmpty()) {
+ descItemEditText.setText(title.toString());
} else {
- spinnerDescriptionLanguages.setSelection(0);
+ 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);
+ } else {
+ uploadView.setTopCardState(false);
+ }
+ });
+
} else {
- spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex());
+ Description description = descriptions.get(position - 1);
+ Timber.d("Description is " + description);
+ if (!TextUtils.isEmpty(description.getDescriptionText())) {
+ descItemEditText.setText(description.getDescriptionText());
+ } else {
+ descItemEditText.setText("");
+ }
+ if (position == 1) {
+ descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null);
+ } else {
+ descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+ }
+
+ descItemEditText.addTextChangedListener(new AbstractTextWatcher(descriptionText -> {
+ description.setDescriptionText(descriptionText);
+ }));
+ descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
+ if (!hasFocus) {
+ ViewUtil.hideKeyboard(v);
+ } else {
+ uploadView.setTopCardState(false);
+ }
+ });
+
+ SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context,
+ 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 {
+ spinnerDescriptionLanguages.setSelection(0);
+ }
+ } 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) {
+ description.setSelectedLanguageIndex(position);
+ String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter()).getLanguageCode(position);
+ description.setLanguageCode(languageCode);
+ selectedLanguages.remove(adapterView);
+ selectedLanguages.put(adapterView, languageCode);
+ ((SpinnerLanguagesAdapter) adapterView.getAdapter()).selectedLangCode = languageCode;
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> adapterView) {
+
+ }
+ });
}
- languages.get(spinnerDescriptionLanguages.getSelectedItemPosition()).setSet(true);
-
- //TODO do it the butterknife way
- spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView> adapterView, View view, int position,
- long l) {
- //TODO handle case when user tries to select an already selected language
- updateDescriptionBasedOnSelectedLanguageIndex(description, position);
- }
-
- @Override
- public void onNothingSelected(AdapterView> adapterView) {
-
- }
- });
-
-
}
- @OnTouch(R.id.et_description_text)
+ @Optional
+ @OnTouch(R.id.description_item_edit_text)
boolean descriptionInfo(View view, MotionEvent motionEvent) {
-
+ //Title info is visible only for the title
if (getAdapterPosition() == 0) {
- //Description info is visible only for the first item
+ if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+ final int value = view.getRight() - descItemEditText
+ .getCompoundDrawables()[2]
+ .getBounds().width();
+ if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
+ callback.showAlert(R.string.media_detail_title, R.string.title_info);
+ return true;
+ }
+ } else {
+ final int value = descItemEditText.getLeft() + descItemEditText
+ .getCompoundDrawables()[0]
+ .getBounds().width();
+ if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
+ callback.showAlert(R.string.media_detail_title, R.string.title_info);
+ return true;
+ }
+ }
+ //Description info is visible only for the first description
+ } else if (getAdapterPosition() == 1) {
final int value;
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
- value = etDescriptionText.getRight() - etDescriptionText
- .getCompoundDrawables()[2]
- .getBounds().width() - etDescriptionText.getPaddingRight();
- if (motionEvent.getAction() == ACTION_UP && motionEvent.getX() >= value) {
+ value = view.getRight() - descItemEditText.getCompoundDrawables()[2].getBounds().width();
+ if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
callback.showAlert(R.string.media_detail_description,
R.string.description_info);
return true;
}
} else {
- value = etDescriptionText.getLeft() + etDescriptionText
+ value = descItemEditText.getLeft() + descItemEditText
.getCompoundDrawables()[0]
.getBounds().width();
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
@@ -206,27 +265,12 @@ class DescriptionsAdapter extends RecyclerView.AdapterResponsible for checking if the picture that the user is trying to upload is useful or not. Will attempt to filter
- * away completely black,fuzzy/blurry pictures(for now).
- *
- *
todo: Detect selfies?
- */
-
-public class DetectUnwantedPicturesAsync extends AsyncTask {
-
- private final String imageMediaFilePath;
- public final WeakReference activityWeakReference;
-
- DetectUnwantedPicturesAsync(WeakReference activityWeakReference, String imageMediaFilePath) {
- //this.callback = callback;
- this.imageMediaFilePath = imageMediaFilePath;
- this.activityWeakReference = activityWeakReference;
- }
-
- @Override
- protected ImageUtils.Result doInBackground(Void... voids) {
- try {
- Timber.d("FilePath: " + imageMediaFilePath);
- if (imageMediaFilePath == null) {
- return ImageUtils.Result.IMAGE_OK;
- }
-
- BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(imageMediaFilePath,false);
-
- return ImageUtils.checkIfImageIsTooDark(decoder);
- } catch (IOException ioe) {
- Timber.e(ioe, "IO Exception");
- return ImageUtils.Result.IMAGE_OK;
- }
- }
-
- @Override
- protected void onPostExecute(ImageUtils.Result result) {
- super.onPostExecute(result);
- Activity activity = activityWeakReference.get();
-
- if (result != ImageUtils.Result.IMAGE_OK) {
- //show appropriate error message
- String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? activity.getString(R.string.upload_image_too_dark) : activity.getString(R.string.upload_image_blurry);
- AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(activity);
- errorDialogBuilder.setMessage(errorMessage);
- errorDialogBuilder.setTitle(activity.getString(R.string.warning));
- errorDialogBuilder.setPositiveButton(activity.getString(R.string.no), (dialogInterface, i) -> {
- //user does not wish to upload the picture, take them back to MainActivity
- Intent intent = new Intent(activity, MainActivity.class);
- dialogInterface.dismiss();
- activity.startActivity(intent);
- });
- errorDialogBuilder.setNegativeButton(activity.getString(R.string.yes), (dialogInterface, i) -> {
- //user wishes to go ahead with the upload of this picture, just dismiss this dialog
- dialogInterface.dismiss();
- });
-
- AlertDialog errorDialog = errorDialogBuilder.create();
- if (!activity.isFinishing()) {
- errorDialog.show();
- }
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java b/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java
new file mode 100644
index 000000000..64f483572
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java
@@ -0,0 +1,153 @@
+package fr.free.nrw.commons.upload;
+
+import android.app.Activity;
+
+import com.karumi.dexter.Dexter;
+import com.karumi.dexter.DexterBuilder;
+import com.karumi.dexter.listener.PermissionDeniedResponse;
+import com.karumi.dexter.listener.PermissionGrantedResponse;
+import com.karumi.dexter.listener.single.BasePermissionListener;
+
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.utils.DialogUtil;
+import fr.free.nrw.commons.utils.ExternalStorageUtils;
+import fr.free.nrw.commons.utils.PermissionUtils;
+import io.reactivex.Completable;
+import io.reactivex.subjects.CompletableSubject;
+import timber.log.Timber;
+
+public class DexterPermissionObtainer {
+ private final String requestedPermission;
+ private android.app.AlertDialog storagePermissionInfoDialog;
+ private DexterBuilder dexterStoragePermissionBuilder;
+
+ private PermissionDeniedResponse permissionDeniedResponse;
+
+ private boolean storagePromptInProgress;
+
+ private final String rationaleTitle;
+ private final String rationaleText;
+
+ private Activity activity;
+
+ private CompletableSubject storagePromptObservable;
+
+ /**
+ * @param activity The activity that is requesting the permission
+ * @param requestedPermission The permission being requested in the form of Manifest.permission.*
+ * @param rationaleTitle The title of the rationale dialog
+ * @param rationaleText The text inside the rationale dialog
+ */
+ DexterPermissionObtainer(Activity activity, String requestedPermission, String rationaleTitle, String rationaleText) {
+ this.activity = activity;
+ this.rationaleTitle = rationaleTitle;
+ this.rationaleText = rationaleText;
+ this.requestedPermission = requestedPermission;
+ this.storagePromptObservable = CompletableSubject.create();
+ initPermissionsRationaleDialog();
+ }
+
+ /**
+ * Checks if storage permissions are obtained, prompts the users to grant storage permissions if necessary.
+ * When storage permission is present, onPermissionObtained is called.
+ */
+ Completable confirmStoragePermissions() {
+ if (ExternalStorageUtils.isStoragePermissionGranted(activity)) {
+ Timber.i("Storage permissions already granted.");
+ storagePromptObservable.onComplete();
+ } else if (!storagePromptInProgress) {
+ if (storagePromptObservable.hasComplete()) {
+ storagePromptObservable = CompletableSubject.create();
+ }
+ //If permission is not there, ask for it
+ storagePromptInProgress = true;
+ askDexterToHandleExternalStoragePermission();
+ }
+ return storagePromptObservable;
+ }
+
+
+ /**
+ * To be called when the user returns to the original activity after manually enabling storage permissions.
+ */
+ void onManualPermissionReturned() {
+ //OnActivity result, no matter what the result is, our function can handle that.
+ askDexterToHandleExternalStoragePermission();
+ }
+
+ /**
+ * This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised
+ * only once, otherwise we would'nt know on which instance of it, the user is working on. And after the builder is initialised, it checks
+ * for the required permission and then handles the permission status, thanks to Dexter's appropriate callbacks.
+ */
+ private void askDexterToHandleExternalStoragePermission() {
+ Timber.d("External storage permission is being requested");
+ if (null == dexterStoragePermissionBuilder) {
+ dexterStoragePermissionBuilder = Dexter.withActivity(activity)
+ .withPermission(requestedPermission)
+ .withListener(new BasePermissionListener() {
+ @Override
+ public void onPermissionGranted(PermissionGrantedResponse response) {
+ Timber.d("User has granted us the permission for writing the external storage");
+ //If permission is granted, well and good
+ storagePromptInProgress = false;
+ storagePromptObservable.onComplete();
+ //onPermissionObtained.run();
+ }
+
+ @Override
+ public void onPermissionDenied(PermissionDeniedResponse response) {
+ Timber.d("User has granted us the permission for writing the external storage");
+ //If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission
+ permissionDeniedResponse = response;
+ if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog
+ .isShowing()) {
+ storagePermissionInfoDialog.show();
+ }
+ }
+ });
+ }
+ dexterStoragePermissionBuilder.check();
+ }
+
+ /**
+ * We have agreed to show a dialog showing why we need a particular permission.
+ * This method is used to initialise the dialog which is going to show the permission's rationale.
+ * The dialog is initialised along with a callback for positive and negative user actions.
+ */
+ private void initPermissionsRationaleDialog() {
+ if (storagePermissionInfoDialog == null) {
+ storagePermissionInfoDialog = DialogUtil
+ .getAlertDialogWithPositiveAndNegativeCallbacks(
+ activity,
+ rationaleTitle, rationaleText,
+ R.drawable.ic_launcher, new DialogUtil.Callback() {
+ @Override
+ public void onPositiveButtonClicked() {
+ //If the user is willing to give us the permission
+ //But had somehow previously choose never ask again, we take him to app settings to manually enable permission
+ if (null == permissionDeniedResponse) {
+ //Dexter returned null, lets see if this ever happens
+ Timber.w("Dexter returned null as permissionDeniedResponse");
+ } else if (permissionDeniedResponse.isPermanentlyDenied()) {
+ PermissionUtils.askUserToManuallyEnablePermissionFromSettings(activity);
+ Timber.i("Permission permanently denied.");
+ } else {
+ //or if we still have chance to show runtime permission dialog, we show him that.
+ askDexterToHandleExternalStoragePermission();
+ Timber.d("Asking via Dexter for permission.");
+ }
+ }
+
+ @Override
+ public void onNegativeButtonClicked() {
+ //This was the behaviour as of now, I was planning to maybe snack him with some message
+ //and then call finish after some time, or may be it could be associated with some action
+ // on the snack. If the user does not want us to give the permission, even after showing
+ // rationale dialog, lets not trouble him any more.
+ activity.finish();
+ }
+ });
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java
deleted file mode 100644
index 08669ed9e..000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.support.v7.app.AlertDialog;
-
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.contributions.MainActivity;
-import fr.free.nrw.commons.mwapi.MediaWikiApi;
-import timber.log.Timber;
-
-/**
- * Sends asynchronous queries to the Commons MediaWiki API to check that file doesn't already exist
- * Displays a warning to the user if the file already exists on Commons
- */
-public class ExistingFileAsync extends AsyncTask {
-
- interface Callback {
- void onResult(Result result);
- }
-
- public enum Result {
- NO_DUPLICATE,
- DUPLICATE_PROCEED,
- DUPLICATE_CANCELLED
- }
-
- private final WeakReference activity;
- private final MediaWikiApi api;
- private final String fileSha1;
- private final WeakReference context;
- private final Callback callback;
-
- public ExistingFileAsync(WeakReference activity, String fileSha1, WeakReference context, Callback callback, MediaWikiApi mwApi) {
- this.activity = activity;
- this.fileSha1 = fileSha1;
- this.context = context;
- this.callback = callback;
- this.api = mwApi;
- }
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- }
-
- @Override
- protected Boolean doInBackground(Void... voids) {
-
- // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba
- boolean fileExists;
- try {
- String fileSha1 = this.fileSha1;
- fileExists = api.existingFile(fileSha1);
- } catch (IOException e) {
- Timber.e(e, "IO Exception: ");
- return false;
- }
-
- Timber.d("File already exists in Commons: %s", fileExists);
- return fileExists;
- }
-
- @Override
- protected void onPostExecute(Boolean fileExists) {
- super.onPostExecute(fileExists);
-
- // If file exists, display warning to user.
- // Use soft warning for now (user able to choose to proceed) until have determined that implementation works without bugs
- if (fileExists) {
- AlertDialog.Builder builder = new AlertDialog.Builder(context.get());
- builder.setMessage(R.string.file_exists)
- .setTitle(R.string.warning);
- builder.setPositiveButton(R.string.no, (dialog, id) -> {
- //Go back to MainActivity
- Intent intent = new Intent(context.get(), MainActivity.class);
- context.get().startActivity(intent);
- callback.onResult(Result.DUPLICATE_CANCELLED);
- });
- builder.setNegativeButton(R.string.yes, (dialog, id) -> callback.onResult(Result.DUPLICATE_PROCEED));
-
- AlertDialog dialog = builder.create();
- if (!activity.get().isFinishing()) {
- dialog.show();
- }
- } else {
- callback.onResult(Result.NO_DUPLICATE);
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java
index b29d686f5..c5b6df227 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java
@@ -1,21 +1,21 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
-import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
+import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
-import java.lang.ref.WeakReference;
import java.util.Date;
import java.util.List;
@@ -44,89 +44,44 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
@Inject
@Named("default_preferences")
SharedPreferences prefs;
- private Uri mediaUri;
+ private String filePath;
private ContentResolver contentResolver;
private GPSExtractor imageObj;
private Context context;
private String decimalCoords;
- private boolean haveCheckedForOtherImages = false;
- private String filePath;
+ private ExifInterface exifInterface;
private boolean useExtStorage;
- private boolean cacheFound;
+ private boolean haveCheckedForOtherImages = false;
private GPSExtractor tempImageObj;
- FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) {
- this.mediaUri = mediaUri;
+ FileProcessor(@NonNull String filePath, ContentResolver contentResolver, Context context) {
+ this.filePath = filePath;
this.contentResolver = contentResolver;
this.context = context;
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
+ try {
+ exifInterface=new ExifInterface(filePath);
+ } catch (IOException e) {
+ Timber.e(e);
+ }
useExtStorage = prefs.getBoolean("useExternalStorage", true);
}
- /**
- * Gets file path from media URI.
- * In older devices getPath() may fail depending on the source URI, creating and using a copy of the file seems to work instead.
- *
- * @return file path of media
- */
- @Nullable
- private String getPathOfMediaOrCopy() {
- filePath = FileUtils.getPath(context, mediaUri);
- Timber.d("Filepath: " + filePath);
- if (filePath == null) {
- String copyPath = null;
- try {
- ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
- if (descriptor != null) {
- if (useExtStorage) {
- copyPath = FileUtils.createCopyPath(descriptor);
- return copyPath;
- }
- copyPath = getApplicationContext().getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + ".jpg";
- FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
- Timber.d("Filepath (copied): %s", copyPath);
- return copyPath;
- }
- } catch (IOException e) {
- Timber.w(e, "Error in file " + copyPath);
- return null;
- }
- }
- return filePath;
- }
-
/**
* Processes file coordinates, either from EXIF data or user location
- *
- * @param gpsEnabled if true use GPS
*/
- GPSExtractor processFileCoordinates(boolean gpsEnabled) {
+ GPSExtractor processFileCoordinates(SimilarImageInterface similarImageInterface) {
Timber.d("Calling GPSExtractor");
- try {
- ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- if (descriptor != null) {
- imageObj = new GPSExtractor(descriptor.getFileDescriptor());
- }
- } else {
- String filePath = getPathOfMediaOrCopy();
- if (filePath != null) {
- imageObj = new GPSExtractor(filePath);
- }
- }
-
- decimalCoords = imageObj.getCoords();
- if (decimalCoords == null || !imageObj.imageCoordsExists) {
- //Find other photos taken around the same time which has gps coordinates
- if (!haveCheckedForOtherImages)
- findOtherImages();// Do not do repeat the process
- } else {
- useImageCoords();
- }
-
- } catch (FileNotFoundException e) {
- Timber.w("File not found: " + mediaUri, e);
+ imageObj = new GPSExtractor(exifInterface);
+ decimalCoords = imageObj.getCoords();
+ if (decimalCoords == null || !imageObj.imageCoordsExists) {
+ //Find other photos taken around the same time which has gps coordinates
+ if (!haveCheckedForOtherImages)
+ findOtherImages(similarImageInterface);// Do not do repeat the process
+ } else {
+ useImageCoords();
}
+
return imageObj;
}
@@ -136,10 +91,10 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
/**
* Find other images around the same location that were taken within the last 20 sec
- *
+ * @param similarImageInterface
*/
- private void findOtherImages() {
- Timber.d("filePath" + getPathOfMediaOrCopy());
+ private void findOtherImages(SimilarImageInterface similarImageInterface) {
+ Timber.d("filePath" + filePath);
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
File folder = new File(filePath.substring(0, filePath.lastIndexOf('/')));
@@ -154,7 +109,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
ParcelFileDescriptor descriptor = null;
try {
- descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r");
+ descriptor = contentResolver.openFileDescriptor(Uri.fromFile(file), "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
@@ -173,12 +128,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
if (tempImageObj.getCoords() != null && tempImageObj.imageCoordsExists) {
// Current image has gps coordinates and it's not current gps locaiton
Timber.d("This file has image coords:" + file.getAbsolutePath());
- SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
- Bundle args = new Bundle();
- args.putString("originalImagePath", filePath);
- args.putString("possibleImagePath", file.getAbsolutePath());
- newFragment.setArguments(args);
- newFragment.show(((AppCompatActivity) context).getSupportFragmentManager(), "dialog");
+ similarImageInterface.showSimilarImageFragment(filePath, file.getAbsolutePath());
break;
}
}
@@ -210,7 +160,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
// If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories
if (catListEmpty) {
- cacheFound = false;
apiCall.request(decimalCoords)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
@@ -223,7 +172,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
);
Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList);
} else {
- cacheFound = true;
Timber.d("Cache found, setting categoryList in model to %s", displayCatList);
gpsCategoryModel.setCategoryList(displayCatList);
}
@@ -232,20 +180,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
}
}
- boolean isCacheFound() {
- return cacheFound;
- }
-
- /**
- * Calls the async task that detects if image is fuzzy, too dark, etc
- */
- void detectUnwantedPictures() {
- String imageMediaFilePath = FileUtils.getPath(context, mediaUri);
- DetectUnwantedPicturesAsync detectUnwantedPicturesAsync
- = new DetectUnwantedPicturesAsync(new WeakReference((Activity) context), imageMediaFilePath);
- detectUnwantedPicturesAsync.execute();
- }
-
@Override
public void onPositiveResponse() {
imageObj = tempImageObj;
@@ -259,4 +193,4 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
Timber.d("EXIF from imageObj");
useImageCoords();
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java
index 2536909b0..9401c941e 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java
@@ -1,6 +1,7 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
+import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.SharedPreferences;
@@ -12,6 +13,7 @@ import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
+import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -33,6 +35,8 @@ import java.util.Date;
import timber.log.Timber;
+import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
+
public class FileUtils {
/**
@@ -76,21 +80,32 @@ public class FileUtils {
/**
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
+ *
* @return path of copy
*/
- @Nullable
- static String createCopyPath(ParcelFileDescriptor descriptor) {
- try {
- String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + ".jpg";
- File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
- newFile.mkdir();
- FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
- Timber.d("Filepath (copied): %s", copyPath);
- return copyPath;
- } catch (IOException e) {
- Timber.e(e);
- return null;
- }
+ @NonNull
+ static String createExternalCopyPathAndCopy(Uri uri, ContentResolver contentResolver) throws IOException {
+ FileDescriptor fileDescriptor = contentResolver.openFileDescriptor(uri, "r").getFileDescriptor();
+ String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + "." + getFileExt(uri, contentResolver);
+ File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
+ newFile.mkdir();
+ FileUtils.copy(fileDescriptor, copyPath);
+ Timber.d("Filepath (copied): %s", copyPath);
+ return copyPath;
+ }
+
+ /**
+ * In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
+ *
+ * @return path of copy
+ */
+ @NonNull
+ static String createCopyPathAndCopy(Uri uri, Context context) throws IOException {
+ FileDescriptor fileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r").getFileDescriptor();
+ String copyPath = context.getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + "." + getFileExt(uri, context.getContentResolver());
+ FileUtils.copy(fileDescriptor, copyPath);
+ Timber.d("Filepath (copied): %s", copyPath);
+ return copyPath;
}
/**
@@ -121,13 +136,13 @@ public class FileUtils {
if ("primary".equalsIgnoreCase(type)) {
returnPath = Environment.getExternalStorageDirectory() + "/" + split[1];
}
- } else if (isDownloadsDocument(uri)) { // DownloadsProvider
+ } else if (isDownloadsDocument(uri)) { // DownloadsProvider
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/document"), Long.valueOf(id));
- returnPath = getDataColumn(context, contentUri, null, null);
+ returnPath = getDataColumn(context, contentUri, null, null);
} else if (isMediaDocument(uri)) { // MediaProvider
final String docId = DocumentsContract.getDocumentId(uri);
@@ -304,6 +319,7 @@ public class FileUtils {
/**
* Read and return the content of a resource file as string.
+ *
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
* @return the content of the file
*/
@@ -330,6 +346,7 @@ public class FileUtils {
/**
* Deletes files.
+ *
* @param file context
*/
public static boolean deleteFile(File file) {
@@ -355,7 +372,7 @@ public class FileUtils {
commonsAppDirectory.mkdir();
}
- File logsFile = new File(commonsAppDirectory,"logs.txt");
+ File logsFile = new File(commonsAppDirectory, "logs.txt");
if (logsFile.exists()) {
//old logs file is useless
logsFile.delete();
@@ -377,4 +394,39 @@ public class FileUtils {
}
}
-}
+ public static String getFilename(Uri uri, ContentResolver contentResolver) {
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN)
+ return "";
+ String result = null;
+ if (uri.getScheme().equals("content")) {
+ try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
+ }
+ }
+ }
+ if (result == null) {
+ result = uri.getPath();
+ int cut = result.lastIndexOf('/');
+ if (cut != -1) {
+ result = result.substring(cut + 1);
+ }
+ }
+ return result;
+ }
+
+ public static String getFileExt(String fileName){
+ //Default file extension
+ String extension=".jpg";
+
+ int i = fileName.lastIndexOf('.');
+ if (i > 0) {
+ extension = fileName.substring(i+1);
+ }
+ return extension;
+ }
+
+ public static String getFileExt(Uri uri, ContentResolver contentResolver) {
+ return getFileExt(getFilename(uri, contentResolver));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java
index e45b31f05..a6b150c42 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java
@@ -16,11 +16,22 @@ import timber.log.Timber;
*/
public class GPSExtractor {
- private ExifInterface exif;
+ public static final GPSExtractor DUMMY= new GPSExtractor();
private double decLatitude;
private double decLongitude;
public boolean imageCoordsExists;
+ private String latitude;
+ private String longitude;
+ private String latitudeRef;
+ private String longitudeRef;
+ private String decimalCoords;
+ /**
+ * Dummy constructor.
+ */
+ private GPSExtractor(){
+
+ }
/**
* Construct from the file descriptor of the image (only for API 24 or newer).
* @param fileDescriptor the file descriptor of the image
@@ -28,7 +39,8 @@ public class GPSExtractor {
@RequiresApi(24)
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
try {
- exif = new ExifInterface(fileDescriptor);
+ ExifInterface exif = new ExifInterface(fileDescriptor);
+ processCoords(exif);
} catch (IOException | IllegalArgumentException e) {
Timber.w(e);
}
@@ -41,47 +53,53 @@ public class GPSExtractor {
*/
public GPSExtractor(@NonNull String path) {
try {
- exif = new ExifInterface(path);
+ ExifInterface exif = new ExifInterface(path);
+ processCoords(exif);
} catch (IOException | IllegalArgumentException e) {
Timber.w(e);
}
}
+ /**
+ * Construct from the file path of the image.
+ * @param exif exif interface of the image
+ *
+ */
+ public GPSExtractor(@NonNull ExifInterface exif){
+ processCoords(exif);
+ }
+
+ private void processCoords(ExifInterface exif){
+ //If image has no EXIF data and user has enabled GPS setting, get user's location
+ //Always return null as a temporary fix for #1599
+ if (exif != null && exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) != null) {
+ //If image has EXIF data, extract image coords
+ imageCoordsExists = true;
+ Timber.d("EXIF data has location info");
+
+ latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
+ latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
+ longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
+ longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
+ }
+ }
+
/**
* Extracts geolocation (either of image from EXIF data, or of user)
* @return coordinates as string (needs to be passed as a String in API query)
*/
@Nullable
public String getCoords() {
- String latitude;
- String longitude;
- String latitudeRef;
- String longitudeRef;
- String decimalCoords;
+ if(decimalCoords!=null){
+ return decimalCoords;
+ }else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
+ Timber.d("Latitude: %s %s", latitude, latitudeRef);
+ Timber.d("Longitude: %s %s", longitude, longitudeRef);
- //If image has no EXIF data and user has enabled GPS setting, get user's location
- //TODO: Always return null as a temporary fix for #1599
- if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
- return null;
+ decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef);
+ return decimalCoords;
} else {
- //If image has EXIF data, extract image coords
- imageCoordsExists = true;
- Timber.d("EXIF data has location info");
-
- latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
- latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
- longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
- longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
-
- if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
- Timber.d("Latitude: %s %s", latitude, latitudeRef);
- Timber.d("Longitude: %s %s", longitude, longitudeRef);
-
- decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef);
- return decimalCoords;
- } else {
- return null;
- }
+ return null;
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java b/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java
new file mode 100644
index 000000000..ff100e16e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java
@@ -0,0 +1,52 @@
+package fr.free.nrw.commons.upload;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Point;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.Display;
+
+/**
+ * Created by Ilgaz Er on 8/7/2018.
+ */
+public class HeightLimitedRecyclerView extends RecyclerView {
+
+ int height;
+
+
+ public HeightLimitedRecyclerView(Context context) {
+ super(context);
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ ((Activity) getContext()).getWindowManager()
+ .getDefaultDisplay()
+ .getMetrics(displayMetrics);
+ height=displayMetrics.heightPixels;
+ }
+
+ public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ ((Activity) getContext()).getWindowManager()
+ .getDefaultDisplay()
+ .getMetrics(displayMetrics);
+ height=displayMetrics.heightPixels;
+ }
+
+ public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ ((Activity) getContext()).getWindowManager()
+ .getDefaultDisplay()
+ .getMetrics(displayMetrics);
+ height=displayMetrics.heightPixels;
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ heightSpec = MeasureSpec.makeMeasureSpec((int) (height*0.3), MeasureSpec.AT_MOST);
+ super.onMeasure(widthSpec, heightSpec);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Language.java b/app/src/main/java/fr/free/nrw/commons/upload/Language.java
index 8d4b27239..ab03a4db7 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/Language.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/Language.java
@@ -3,7 +3,6 @@ package fr.free.nrw.commons.upload;
import java.util.Locale;
class Language {
-
private Locale locale;
private boolean isSet = false;
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java
deleted file mode 100644
index 703e26657..000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java
+++ /dev/null
@@ -1,493 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.Manifest;
-import android.Manifest.permission;
-import android.app.AlertDialog;
-import android.app.ProgressDialog;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.database.DataSetObserver;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
-import android.support.annotation.Nullable;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.content.ContextCompat;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.AdapterView;
-import android.widget.Toast;
-
-import com.karumi.dexter.Dexter;
-import com.karumi.dexter.DexterBuilder;
-import com.karumi.dexter.listener.PermissionDeniedResponse;
-import com.karumi.dexter.listener.PermissionGrantedResponse;
-import com.karumi.dexter.listener.single.BasePermissionListener;
-
-import java.io.FileNotFoundException;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-import butterknife.ButterKnife;
-import fr.free.nrw.commons.BuildConfig;
-import fr.free.nrw.commons.CommonsApplication;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.AuthenticatedActivity;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.category.CategorizationFragment;
-import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
-import fr.free.nrw.commons.contributions.Contribution;
-import fr.free.nrw.commons.media.MediaDetailPagerFragment;
-import fr.free.nrw.commons.modifications.CategoryModifier;
-import fr.free.nrw.commons.modifications.ModifierSequence;
-import fr.free.nrw.commons.modifications.ModifierSequenceDao;
-import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
-import fr.free.nrw.commons.mwapi.MediaWikiApi;
-import fr.free.nrw.commons.utils.ContributionUtils;
-import fr.free.nrw.commons.utils.DialogUtil;
-import fr.free.nrw.commons.utils.DialogUtil.Callback;
-import fr.free.nrw.commons.utils.ExternalStorageUtils;
-import fr.free.nrw.commons.utils.PermissionUtils;
-import timber.log.Timber;
-
-//TODO: We should use this class to see how multiple uploads are handled, and then REMOVE it.
-
-public class MultipleShareActivity extends AuthenticatedActivity
- implements MediaDetailPagerFragment.MediaDetailProvider,
- AdapterView.OnItemClickListener,
- FragmentManager.OnBackStackChangedListener,
- MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
- OnCategoriesSaveHandler,
- ActivityCompat.OnRequestPermissionsResultCallback{
-
- @Inject
- MediaWikiApi mwApi;
- @Inject
- SessionManager sessionManager;
- @Inject
- UploadController uploadController;
- @Inject
- ModifierSequenceDao modifierSequenceDao;
- @Inject
- @Named("default_preferences")
- SharedPreferences prefs;
-
- private ArrayList photosList = null;
-
- private MultipleUploadListFragment uploadsList;
- private MediaDetailPagerFragment mediaDetails;
- private CategorizationFragment categorizationFragment;
-
- private boolean locationPermitted = false;
- private boolean isMultipleUploadsPrepared = false;
- private boolean isMultipleUploadsFinalised = false; // Checks is user clicked to upload button or regret before this phase
- private final String TAG="#MultipleShareActivity#";
- private AlertDialog storagePermissionInfoDialog;
- private DexterBuilder dexterStoragePermissionBuilder;
-
- private PermissionDeniedResponse permissionDeniedResponse;
-
- @Override
- public Media getMediaAtPosition(int i) {
- return photosList.get(i);
- }
-
- @Override
- public int getTotalMediaCount() {
- if (photosList == null) {
- return 0;
- }
- return photosList.size();
- }
-
- @Override
- public void notifyDatasetChanged() {
- if (uploadsList != null) {
- uploadsList.notifyDatasetChanged();
- }
- }
-
- @Override
- public void registerDataSetObserver(DataSetObserver observer) {
- // fixme implement me if needed
- }
-
- @Override
- public void unregisterDataSetObserver(DataSetObserver observer) {
- // fixme implement me if needed
- }
-
- @Override
- public void onItemClick(AdapterView> adapterView, View view, int index, long item) {
- showDetail(index);
- }
-
- @Override
- public void OnMultipleUploadInitiated() {
- // No need to request external permission here, because if user can reach this point, then she permission granted
- Timber.d("OnMultipleUploadInitiated");
- multipleUploadBegins();
- }
-
- private void multipleUploadBegins() {
-
- Timber.d("Multiple upload begins");
- final ProgressDialog dialog = new ProgressDialog(this);
- dialog.setIndeterminate(false);
- dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
- dialog.setMax(photosList.size());
- dialog.setTitle(getResources().getQuantityString(R.plurals.starting_multiple_uploads, photosList.size(), photosList.size()));
- dialog.show();
-
- for (int i = 0; i < photosList.size(); i++) {
- Contribution up = photosList.get(i);
- final int uploadCount = i + 1; // Goddamn Java
-
- uploadController.startUpload(up, contribution -> {
- dialog.setProgress(uploadCount);
- if (uploadCount == photosList.size()) {
- dialog.dismiss();
- Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
- startingToast.show();
- }
- });
- }
-
- uploadsList.setImageOnlyMode(true);
-
- categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
- if (categorizationFragment == null) {
- categorizationFragment = new CategorizationFragment();
- }
- // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
- View target = getCurrentFocus();
- if (target != null) {
- InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- if (imm != null)
- imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
- }
- getSupportFragmentManager().beginTransaction()
- .add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization")
- .commitAllowingStateLoss();
- isMultipleUploadsFinalised = true;
- //See http://stackoverflow.com/questions/7469082/getting-exception-illegalstateexception-can-not-perform-this-action-after-onsa
- }
-
- @Override
- public void onCategoriesSave(List categories) {
- if (categories.size() > 0) {
- for (Contribution contribution : photosList) {
- ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
-
- categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
- categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
-
- modifierSequenceDao.save(categoriesSequence);
- }
- }
- // FIXME: Make sure that the content provider is up
- // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
- ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default!
- finish();
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- if (mediaDetails.isVisible()) {
- getSupportFragmentManager().popBackStack();
- }
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.activity_multiple_uploads);
- ButterKnife.bind(this);
- initDrawer();
- initPermissionsRationaleDialog();
-
- if (savedInstanceState != null) {
- photosList = savedInstanceState.getParcelableArrayList("uploadsList");
- }
-
- getSupportFragmentManager().addOnBackStackChangedListener(this);
- requestAuthToken();
-
- //TODO: 15/10/17 should location permission be explicitly requested if not provided?
- //check if location permission is enabled
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
- {
- locationPermitted = true;
- }
- }
- }
-
-
- /**
- * We have agreed to show a dialog showing why we need a particular permission.
- * This method is used to initialise the dialog which is going to show the permission's rationale.
- * The dialog is initialised along with a callback for positive and negative user actions.
- */
- private void initPermissionsRationaleDialog() {
- if (storagePermissionInfoDialog == null) {
- storagePermissionInfoDialog = DialogUtil
- .getAlertDialogWithPositiveAndNegativeCallbacks(
- MultipleShareActivity.this,
- getString(R.string.storage_permission), getString(
- R.string.write_storage_permission_rationale_for_image_share),
- R.drawable.ic_launcher, new Callback() {
- @Override
- public void onPositiveButtonClicked() {
- //If the user is willing to give us the permission
- //But had somehow previously choose never ask again, we take him to app settings to manually enable permission
- if (null== permissionDeniedResponse){
- //Dexter returned null, lets see if this ever happens
- return;
- }
- else if (permissionDeniedResponse.isPermanentlyDenied()) {
- PermissionUtils.askUserToManuallyEnablePermissionFromSettings(MultipleShareActivity.this);
- } else {
- //or if we still have chance to show runtime permission dialog, we show him that.
- askDexterToHandleExternalStoragePermission();
- }
- }
-
- @Override
- public void onNegativeButtonClicked() {
- //This was the behaviour as of now, I was planning to maybe snack him with some message
- //and then call finish after some time, or may be it could be associated with some action on the snack
- //If the user does not want us to give the permission, even after showing rationale dialog, lets not trouble him anymore
- finish();
- }
- });
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- getSupportFragmentManager().removeOnBackStackChangedListener(this);
- uploadController.cleanup();
- }
-
- private void showDetail(int i) {
- if (mediaDetails == null || !mediaDetails.isVisible()) {
- mediaDetails = new MediaDetailPagerFragment(true, false);
- getSupportFragmentManager()
- .beginTransaction()
- .replace(R.id.uploadsFragmentContainer, mediaDetails)
- .addToBackStack(null)
- .commit();
- getSupportFragmentManager().executePendingTransactions();
- }
- mediaDetails.showImage(i);
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- /* This will be true if permission request is granted before we request. Otherwise we will
- * explicitly call operations under this method again.
- */
- if (isMultipleUploadsPrepared) {
- super.onSaveInstanceState(outState);
- Timber.d("onSaveInstanceState multiple uploads is prepared, permission granted");
- outState.putParcelableArrayList("uploadsList", photosList);
- } else {
- Timber.d("onSaveInstanceState multiple uploads is not prepared, permission not granted");
- return;
- }
- }
-
- @Override
- protected void onAuthCookieAcquired(String authCookie) {
- // Multiple uploads prepared boolean is used to decide when to call multipleUploadsBegin()
- isMultipleUploadsFinalised = false;
- isMultipleUploadsPrepared = false;
- mwApi.setAuthCookie(authCookie);
- if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
- //If permission is not there, handle the negative cases
- askDexterToHandleExternalStoragePermission();
- isMultipleUploadsPrepared = false;
- return; // Postpone operation to do after gettion permission
- } else {
- isMultipleUploadsPrepared = true;
- prepareMultipleUploadList();
- }
- }
-
- /**
- * This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised
- * only once, otherwise we would'nt know on which instance of it, the user is working on. And after the builder is initialised, it checks for the required
- * permission and then handles the permission status, thanks to Dexter's appropriate callbacks.
- */
- private void askDexterToHandleExternalStoragePermission() {
- Timber.d(TAG, "External storage permission is being requested");
- if (null == dexterStoragePermissionBuilder) {
- dexterStoragePermissionBuilder = Dexter.withActivity(this)
- .withPermission(permission.WRITE_EXTERNAL_STORAGE)
- .withListener(new BasePermissionListener() {
- @Override
- public void onPermissionGranted(PermissionGrantedResponse response) {
- Timber.d(TAG,"User has granted us the permission for writing the external storage");
- //If permission is granted, well and good
- prepareMultipleUploadList();
- }
-
- @Override
- public void onPermissionDenied(PermissionDeniedResponse response) {
- Timber.d(TAG,"User has granted us the permission for writing the external storage");
- //If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission
- permissionDeniedResponse=response;
- if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog
- .isShowing()) {
- storagePermissionInfoDialog.show();
- }
- }
- });
- }
- dexterStoragePermissionBuilder.check();
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) {
- //OnActivity result, no matter what the result is, our function can handle that.
- askDexterToHandleExternalStoragePermission();
- }
- }
-
- /**
- * Prepares a list from files will be uploaded. Saves these files temporarily to external
- * storage. Adds them to uploads list
- */
- private void prepareMultipleUploadList() {
- Intent intent = getIntent();
-
- if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
- if (photosList == null) {
- photosList = new ArrayList<>();
- ArrayList urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- for (int i = 0; i < urisList.size(); i++) {
- Contribution up = new Contribution();
- Uri uri = urisList.get(i);
- // Use temporarily saved file Uri instead
- uri = ContributionUtils.saveFileBeingUploadedTemporarily(this, uri);
- up.setLocalUri(uri);
- up.setTag("mimeType", intent.getType());
- up.setTag("sequence", i);
- up.setSource(Contribution.SOURCE_EXTERNAL);
- up.setMultiple(true);
- String imageGpsCoordinates = extractImageGpsData(uri);
- if (imageGpsCoordinates != null) {
- Timber.d("GPS data for image found!");
- up.setDecimalCoords(imageGpsCoordinates);
- }
- photosList.add(up);
- }
- }
-
- uploadsList = (MultipleUploadListFragment) getSupportFragmentManager().findFragmentByTag("uploadsList");
- if (uploadsList == null) {
- uploadsList = new MultipleUploadListFragment();
- getSupportFragmentManager()
- .beginTransaction()
- .add(R.id.uploadsFragmentContainer, uploadsList, "uploadsList")
- .commit();
- }
- setTitle(getResources().getQuantityString(R.plurals.multiple_uploads_title, photosList.size(), photosList.size()));
- uploadController.prepareService();
- }
- }
-
- @Override
- protected void onAuthFailure() {
- Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
- failureToast.show();
- finish();
- }
-
- @Override
- public void onBackStackChanged() {
- getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible());
- }
-
- /**
- * Will attempt to extract the gps coordinates using exif data or by using the current
- * location if available for the image who's imageUri has been provided.
- * @param imageUri The uri of the image who's GPS coordinates data we wish to extract
- * @return GPS coordinates as a String as is returned by {@link GPSExtractor}
- */
- @Nullable
- private String extractImageGpsData(Uri imageUri) {
- Timber.d("Entering extractImagesGpsData");
-
- if (imageUri == null) {
- //now why would you do that???
- return null;
- }
-
- GPSExtractor gpsExtractor = null;
-
- try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r");
- if (fd != null) {
- gpsExtractor = new GPSExtractor(fd.getFileDescriptor());
- }
- } else {
- String filePath = FileUtils.getPath(this,imageUri);
- if (filePath != null) {
- gpsExtractor = new GPSExtractor(filePath);
- }
- }
-
- if (gpsExtractor != null) {
- //get image coordinates from exif data or user location
- return gpsExtractor.getCoords();
- }
-
- } catch (FileNotFoundException fnfe) {
- Timber.w(fnfe);
- return null;
- }
-
- return null;
- }
-
- // If on back pressed before sharing
- @Override
- public void onBackPressed() {
- super.onBackPressed();
- }
-
- @Override
- protected void onStop() {
- // Remove saved files if activity is stopped before upload operation, ie user changed mind
- if (!isMultipleUploadsFinalised) {
- if (photosList != null) {
- for (Contribution contribution : photosList) {
- Timber.d("User changed mind, didn't click to upload button, deleted file: "+contribution.getLocalUri());
- ContributionUtils.removeTemporaryFile(contribution.getLocalUri());
- }
- }
- }
- super.onStop();
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java
deleted file mode 100644
index 0dfdf5589..000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java
+++ /dev/null
@@ -1,254 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.content.Context;
-import android.graphics.Point;
-import android.net.Uri;
-import android.os.Bundle;
-import android.support.graphics.drawable.VectorDrawableCompat;
-import android.support.v4.app.Fragment;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.util.DisplayMetrics;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.BaseAdapter;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-import android.widget.GridView;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
-import com.facebook.drawee.view.SimpleDraweeView;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import dagger.android.support.AndroidSupportInjection;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.contributions.Contribution;
-import fr.free.nrw.commons.media.MediaDetailPagerFragment;
-import fr.free.nrw.commons.utils.ViewUtil;
-
-public class MultipleUploadListFragment extends Fragment {
-
- public interface OnMultipleUploadInitiatedHandler {
- void OnMultipleUploadInitiated();
- }
-
- @BindView(R.id.multipleShareBackground)
- GridView photosGrid;
-
- @BindView(R.id.multipleBaseTitle)
- EditText baseTitle;
-
- private PhotoDisplayAdapter photosAdapter;
- private TitleTextWatcher textWatcher = new TitleTextWatcher();
-
- private Point photoSize;
- private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
- private OnMultipleUploadInitiatedHandler multipleUploadInitiatedHandler;
-
- private boolean imageOnlyMode;
-
- private static class UploadHolderView {
- private Uri imageUri;
- private SimpleDraweeView image;
- private TextView title;
- private RelativeLayout overlay;
- }
-
- @Override
- public void onAttach(Context context) {
- AndroidSupportInjection.inject(this);
- super.onAttach(context);
- }
-
- private class PhotoDisplayAdapter extends BaseAdapter {
-
- @Override
- public int getCount() {
- return detailProvider.getTotalMediaCount();
- }
-
- @Override
- public Object getItem(int i) {
- return detailProvider.getMediaAtPosition(i);
- }
-
- @Override
- public long getItemId(int i) {
- return i;
- }
-
- @Override
- public View getView(int i, View view, ViewGroup viewGroup) {
- UploadHolderView holder;
-
- if (view == null) {
- view = LayoutInflater.from(getContext()).inflate(R.layout.layout_upload_item, viewGroup, false);
- holder = new UploadHolderView();
- holder.image = view.findViewById(R.id.uploadImage);
- holder.title = view.findViewById(R.id.uploadTitle);
- holder.overlay = view.findViewById(R.id.uploadOverlay);
-
- holder.image.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, photoSize.y));
- holder.image.setHierarchy(GenericDraweeHierarchyBuilder
- .newInstance(getResources())
- .setPlaceholderImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_image_black_24dp, getContext().getTheme()))
- .setFailureImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
- .build());
- view.setTag(holder);
- } else {
- holder = (UploadHolderView) view.getTag();
- }
-
- Contribution up = (Contribution) this.getItem(i);
-
- if (holder.imageUri == null || !holder.imageUri.equals(up.getLocalUri())) {
- holder.image.setImageURI(up.getLocalUri().toString());
- holder.imageUri = up.getLocalUri();
- }
-
- if (!imageOnlyMode) {
- holder.overlay.setVisibility(View.VISIBLE);
- holder.title.setText(up.getFilename());
- } else {
- holder.overlay.setVisibility(View.GONE);
- }
-
- return view;
- }
- }
-
- @Override
- public void onStop() {
- super.onStop();
-
- // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
- View target = getActivity().getCurrentFocus();
- ViewUtil.hideKeyboard(target);
- }
-
- // FIXME: Wrong result type
- private Point calculatePicDimension(int count) {
- DisplayMetrics screenMetrics = getResources().getDisplayMetrics();
- int screenWidth = screenMetrics.widthPixels;
- int screenHeight = screenMetrics.heightPixels;
-
- int picWidth = Math.min((int) Math.sqrt(screenWidth * screenHeight / count), screenWidth);
- picWidth = Math.min((int) (192 * screenMetrics.density), Math.max((int) (120 * screenMetrics.density), picWidth / 48 * 48));
- int picHeight = Math.min(picWidth, (int) (192 * screenMetrics.density)); // Max Height is same as Contributions list
-
- return new Point(picWidth, picHeight);
- }
-
- public void notifyDatasetChanged() {
- if (photosAdapter != null) {
- photosAdapter.notifyDataSetChanged();
- }
- }
-
- public void setImageOnlyMode(boolean mode) {
- imageOnlyMode = mode;
- if (imageOnlyMode) {
- baseTitle.setVisibility(View.GONE);
- } else {
- baseTitle.setVisibility(View.VISIBLE);
- }
- photosAdapter.notifyDataSetChanged();
- photosGrid.setEnabled(!mode);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false);
- ButterKnife.bind(this,view);
- photosAdapter = new PhotoDisplayAdapter();
- photosGrid.setAdapter(photosAdapter);
- photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
- photoSize = calculatePicDimension(detailProvider.getTotalMediaCount());
- photosGrid.setColumnWidth(photoSize.x);
-
- baseTitle.addTextChangedListener(textWatcher);
-
- baseTitle.setOnFocusChangeListener((v, hasFocus) -> {
- if (!hasFocus) {
- ViewUtil.hideKeyboard(v);
- }
- });
-
- return view;
- }
-
- @Override
- public void onDestroyView() {
- baseTitle.removeTextChangedListener(textWatcher);
- super.onDestroyView();
- }
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- super.onCreateOptionsMenu(menu, inflater);
- menu.clear();
- inflater.inflate(R.menu.fragment_multiple_upload_list, menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_upload_multiple:
- if (baseTitle.getText().toString().trim().isEmpty()) {
- Toast.makeText(getContext(), R.string.add_set_name_toast, Toast.LENGTH_LONG).show();
- return false;
- }
- multipleUploadInitiatedHandler.OnMultipleUploadInitiated();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity();
- multipleUploadInitiatedHandler = (OnMultipleUploadInitiatedHandler) getActivity();
-
- setHasOptionsMenu(true);
- }
-
- private class TitleTextWatcher implements TextWatcher {
- @Override
- public void beforeTextChanged(CharSequence charSequence, int i1, int i2, int i3) {
- }
-
- @Override
- public void onTextChanged(CharSequence charSequence, int i1, int i2, int i3) {
- for (int i = 0; i < detailProvider.getTotalMediaCount(); i++) {
- Contribution up = (Contribution) detailProvider.getMediaAtPosition(i);
- Boolean isDirty = (Boolean) up.getTag("isDirty");
- if (isDirty == null || !isDirty) {
- if (!TextUtils.isEmpty(charSequence)) {
- up.setFilename(charSequence.toString() + " - " + ((Integer) up.getTag("sequence") + 1));
- } else {
- up.setFilename("");
- }
- }
- }
- detailProvider.notifyDatasetChanged();
- }
-
- @Override
- public void afterTextChanged(Editable editable) {
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java
deleted file mode 100644
index 9da820a7e..000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java
+++ /dev/null
@@ -1,674 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.Manifest;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.graphics.Bitmap;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Environment;
-import android.support.annotation.NonNull;
-import android.support.annotation.RequiresApi;
-import android.support.design.widget.FloatingActionButton;
-import android.support.graphics.drawable.VectorDrawableCompat;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.content.ContextCompat;
-import android.view.KeyEvent;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.FrameLayout;
-import android.widget.Toast;
-
-import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
-import com.facebook.drawee.view.SimpleDraweeView;
-import com.github.chrisbanes.photoview.PhotoView;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.ref.WeakReference;
-import java.util.List;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import butterknife.OnClick;
-import fr.free.nrw.commons.BuildConfig;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.AuthenticatedActivity;
-import fr.free.nrw.commons.auth.LoginActivity;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.caching.CacheController;
-import fr.free.nrw.commons.category.CategorizationFragment;
-import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
-import fr.free.nrw.commons.contributions.Contribution;
-import fr.free.nrw.commons.modifications.CategoryModifier;
-import fr.free.nrw.commons.modifications.ModifierSequence;
-import fr.free.nrw.commons.modifications.ModifierSequenceDao;
-import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
-import fr.free.nrw.commons.mwapi.CategoryApi;
-import fr.free.nrw.commons.mwapi.MediaWikiApi;
-import fr.free.nrw.commons.utils.ContributionUtils;
-import fr.free.nrw.commons.utils.ExternalStorageUtils;
-import fr.free.nrw.commons.utils.ViewUtil;
-import timber.log.Timber;
-
-import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED;
-import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE;
-import static fr.free.nrw.commons.upload.FileUtils.getSHA1;
-import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
-
-/**
- * Activity for the title/desc screen after image is selected. Also starts processing image
- * GPS coordinates or user location (if enabled in Settings) for category suggestions.
- */
-public class ShareActivity
- extends AuthenticatedActivity
- implements SingleUploadFragment.OnUploadActionInitiated,
- OnCategoriesSaveHandler,
- ActivityCompat.OnRequestPermissionsResultCallback {
-
- private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4;
- //Had to make them class variables, to extract out the click listeners, also I see no harm in this
- final Rect startBounds = new Rect();
- final Rect finalBounds = new Rect();
- final Point globalOffset = new Point();
- @Inject
- MediaWikiApi mwApi;
- @Inject
- CacheController cacheController;
- @Inject
- SessionManager sessionManager;
- @Inject
- UploadController uploadController;
- @Inject
- ModifierSequenceDao modifierSequenceDao;
- @Inject
- CategoryApi apiCall;
- @Inject @Named("application_preferences") SharedPreferences applicationPrefs;
- @Inject
- @Named("default_preferences")
- SharedPreferences prefs;
- @Inject
- GpsCategoryModel gpsCategoryModel;
-
- @BindView(R.id.container)
- FrameLayout flContainer;
- @BindView(R.id.backgroundImage)
- SimpleDraweeView backgroundImageView;
- @BindView(R.id.media_map)
- FloatingActionButton mapButton;
- @BindView(R.id.media_upload_zoom_in)
- FloatingActionButton zoomInButton;
- @BindView(R.id.media_upload_zoom_out)
- FloatingActionButton zoomOutButton;
- @BindView(R.id.main_fab)
- FloatingActionButton mainFab;
- @BindView(R.id.expanded_image)
- PhotoView expandedImageView;
-
- private String source;
- private String mimeType;
- private CategorizationFragment categorizationFragment;
- private Uri mediaUri;
- private Uri contentProviderUri;
- private Contribution contribution;
- private GPSExtractor gpsObj;
- private String decimalCoords;
- private FileProcessor fileObj;
- private boolean useNewPermissions = false;
- private boolean storagePermitted = false;
- private boolean locationPermitted = false;
- private String title;
- private String description;
- private String wikiDataEntityId;
- private boolean duplicateCheckPassed = false;
- private boolean isNearbyUpload = false;
- private Animator CurrentAnimator;
- private long ShortAnimationDuration;
- private boolean isFABOpen = false;
- private float startScaleFinal;
- private Bundle savedInstanceState;
- private boolean isUploadFinalised = false; // Checks is user clicked to upload button or regret before this phase
- private boolean isZoom = false;
-
-
-
- /**
- * Called when user taps the submit button.
- * Requests Storage permission, if needed.
- */
-
- @Override
- public void uploadActionInitiated(String title, String description) {
-
- this.title = title;
- this.description = description;
-
-
- if (sessionManager.getCurrentAccount() != null) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- // Check for Storage permission that is required for upload.
- // Do not allow user to proceed without permission, otherwise will crash
- if (needsToRequestStoragePermission()) {
- requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
- REQUEST_PERM_ON_SUBMIT_STORAGE);
- } else {
- uploadBegins();
- }
- } else {
- uploadBegins();
- }
- }
- else //Send user to login activity
- {
- Toast.makeText(this, "You need to login first!", Toast.LENGTH_SHORT).show();
- Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class);
- startActivity(loginIntent);
- }
- }
-
- /**
- * Checks whether storage permissions need to be requested.
- * Permissions are needed if the file is not owned by this application, (e.g. shared from the Gallery)
- *
- * @return true if file is not owned by this application and permission hasn't been granted beforehand
- */
- @RequiresApi(16)
- private boolean needsToRequestStoragePermission() {
- return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri)
- && (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
- != PackageManager.PERMISSION_GRANTED);
- //return false;
- }
-
-
- /**
- * Called after permission checks are done.
- * Gets file metadata for category suggestions, displays toast, caches categories found, calls uploadController
- */
-
- private void uploadBegins() {
- fileObj.processFileCoordinates(locationPermitted);
-
- Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
- startingToast.show();
-
- if (!fileObj.isCacheFound()) {
- //Has to be called after apiCall.request()
- cacheController.cacheCategory();
- Timber.d("Cache the categories found");
- }
-
- uploadController.startUpload(title, contentProviderUri, mediaUri, description, mimeType, source, decimalCoords, wikiDataEntityId, c -> {
- ShareActivity.this.contribution = c;
- showPostUpload();
- });
- isUploadFinalised = true;
- }
-
- /**
- * Starts CategorizationFragment after uploadBegins.
- */
-
- private void showPostUpload() {
- if (categorizationFragment == null) {
- categorizationFragment = new CategorizationFragment();
- }
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.single_upload_fragment_container, categorizationFragment, "categorization")
- .commit();
- }
-
- /**
- * Send categories to modifications queue after they are selected
- *
- * @param categories categories selected
- */
- @Override
- public void onCategoriesSave(List categories) {
- if (categories.size() > 0) {
- ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
-
- categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
- categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
- modifierSequenceDao.save(categoriesSequence);
- }
-
- // FIXME: Make sure that the content provider is up
- // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
- ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default!
-
- finish();
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- if (contribution != null) {
- outState.putParcelable("contribution", contribution);
- }
- }
-
- @Override
- protected void onAuthCookieAcquired(String authCookie) {
- mwApi.setAuthCookie(authCookie);
- }
-
- @Override
- protected void onAuthFailure() {
- Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
- failureToast.show();
- finish();
- }
-
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- isUploadFinalised = false;
- setContentView(R.layout.activity_share);
- ButterKnife.bind(this);
- initBack();
- backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder
- .newInstance(getResources())
- .setPlaceholderImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_image_black_24dp, getTheme()))
- .setFailureImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_error_outline_black_24dp, getTheme()))
- .build());
- if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
- this.savedInstanceState = savedInstanceState;
- ExternalStorageUtils.requestExternalStoragePermission(this);
- return; // Postpone operation to do after getting permission
- } else {
- receiveImageIntent();
- createContributionWithReceivedIntent(savedInstanceState);
- }
- }
-
- @Override
- protected void onStop() {
- // If upload is not finalised with failure or success, but contribution is created,
- // we have to remove temp file, to prevent using unnecessary memory
- if (!isUploadFinalised) {
- if (mediaUri != null) {
- ContributionUtils.removeTemporaryFile(mediaUri);
- }
- }
- super.onStop();
- }
-
- private void createContributionWithReceivedIntent(Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- contribution = savedInstanceState.getParcelable("contribution");
- }
-
- requestAuthToken();
- Timber.d("Uri: %s", mediaUri.toString());
- Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory());
-
- SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
- categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
- if (shareView == null && categorizationFragment == null) {
- shareView = new SingleUploadFragment();
- getSupportFragmentManager()
- .beginTransaction()
- .add(R.id.single_upload_fragment_container, shareView, "shareView")
- .commitAllowingStateLoss();
- }
- uploadController.prepareService();
-
- ContentResolver contentResolver = this.getContentResolver();
- fileObj = new FileProcessor(mediaUri, contentResolver, this);
- checkIfFileExists();
- gpsObj = fileObj.processFileCoordinates(locationPermitted);
- decimalCoords = fileObj.getDecimalCoords();
- if (sessionManager.getCurrentAccount() == null) {
- Toast.makeText(this, getString(R.string.login_alert_message), Toast.LENGTH_SHORT).show();
- applicationPrefs.edit().putBoolean("login_skipped", false).apply();
- Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class);
- startActivity(loginIntent);
- }
- }
-
- /**
- * Receive intent from ContributionController.java when user selects picture to upload
- */
- private void receiveImageIntent() {
- Intent intent = getIntent();
-
- if (Intent.ACTION_SEND.equals(intent.getAction())) {
- mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
- contentProviderUri = mediaUri;
- mediaUri = ContributionUtils.saveFileBeingUploadedTemporarily(this, mediaUri);
-
- if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
- source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
- } else {
- source = Contribution.SOURCE_EXTERNAL;
- }
-
- boolean isDirectUpload = intent.getBooleanExtra("isDirectUpload", false);
-
- if (isDirectUpload) {
- Timber.d("This was initiated by a direct upload from Nearby");
- isNearbyUpload = true;
- wikiDataEntityId = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF);
- Timber.d("Received wikiDataEntityId from contribution controller %s", wikiDataEntityId);
- }
- mimeType = intent.getType();
- }
-
- if (mediaUri != null) {
- backgroundImageView.setImageURI(mediaUri);
- }
- }
-
- /**
- * Function to display the zoom and map FAB
- */
- private void showFABMenu() {
- isFABOpen = true;
-
- if (gpsObj != null && gpsObj.imageCoordsExists)
- mapButton.setVisibility(View.VISIBLE);
- zoomInButton.setVisibility(View.VISIBLE);
-
- mainFab.animate().rotationBy(180);
- mapButton.animate().translationY(-getResources().getDimension(R.dimen.second_fab));
- zoomInButton.animate().translationY(-getResources().getDimension(R.dimen.first_fab));
- }
-
- /**
- * Function to close the zoom and map FAB
- */
- private void closeFABMenu() {
- isFABOpen = false;
- mainFab.animate().rotationBy(-180);
- mapButton.animate().translationY(0);
- zoomInButton.animate().translationY(0).setListener(new Animator.AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animator) {
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- if (!isFABOpen) {
- mapButton.setVisibility(View.GONE);
- zoomInButton.setVisibility(View.GONE);
- }
- }
-
- @Override
- public void onAnimationCancel(Animator animator) {
- }
-
- @Override
- public void onAnimationRepeat(Animator animator) {
- }
- });
- }
-
- /**
- * Checks if upload was initiated via Nearby
- *
- * @return true if upload was initiated via Nearby
- */
- protected boolean isNearbyUpload() {
- return isNearbyUpload;
- }
-
- /**
- * Handles submit button permission request (for storage)
- *
- * @param requestCode type of request
- * @param permissions permissions requested
- * @param grantResults grant results
- */
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- Timber.d("onRequestPermissionsResult external storage permission granted");
- // You can receive image intent and save image to a temp file only if ext storage permission is granted
- receiveImageIntent();
- createContributionWithReceivedIntent(savedInstanceState);
-
- if (requestCode == REQUEST_PERM_ON_SUBMIT_STORAGE) {
- checkIfFileExists();
- //Uploading only begins if storage permission granted from arrow icon
- uploadBegins();
- }
-
- } else {
- finish();
- }
- }
-
- /**
- * Check if file user wants to upload already exists on Commons
- */
- private void checkIfFileExists() {
- if (!useNewPermissions || storagePermitted) {
- if (!duplicateCheckPassed) {
- //Test SHA1 of image to see if it matches SHA1 of a file on Commons
- try {
- InputStream inputStream = getContentResolver().openInputStream(mediaUri);
- String fileSHA1 = getSHA1(inputStream);
- Timber.d("Input stream created from %s", mediaUri.toString());
- Timber.d("File SHA1 is: %s", fileSHA1);
-
- ExistingFileAsync fileAsyncTask =
- new ExistingFileAsync(new WeakReference(this), fileSHA1, new WeakReference(this), result -> {
- Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
- duplicateCheckPassed = (result == DUPLICATE_PROCEED || result == NO_DUPLICATE);
- if (duplicateCheckPassed) {
- //image is not a duplicate, so now check if its a unwanted picture or not
- fileObj.detectUnwantedPictures();
- }
- }, mwApi);
- fileAsyncTask.execute();
- } catch (IOException e) {
- Timber.e(e, "IO Exception: ");
- }
- }
- } else {
- Timber.w("not ready for preprocessing: useNewPermissions=%s storage=%s location=%s",
- useNewPermissions, storagePermitted, locationPermitted);
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- uploadController.cleanup();
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- if (categorizationFragment != null && categorizationFragment.isVisible()) {
- categorizationFragment.showBackButtonDialog();
- } else {
- onBackPressed();
- }
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- /**
- * Allows zooming in to the image about to be uploaded. Called when zoom FAB is tapped
- */
- private void zoomImageFromThumb(final View thumbView, Uri imageuri) {
- // If there's an animation in progress, cancel it immediately and proceed with this one.
- if (CurrentAnimator != null) {
- CurrentAnimator.cancel();
- }
- isZoom = true;
- ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit));
- closeFABMenu();
- mainFab.setVisibility(View.GONE);
-
- InputStream input = null;
- try {
- input = this.getContentResolver().openInputStream(imageuri);
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- }
-
- Zoom zoomObj = new Zoom(thumbView, flContainer, this.getContentResolver());
- Bitmap scaledImage = zoomObj.createScaledImage(input, imageuri);
-
- // Load the high-resolution "zoomed-in" image.
- expandedImageView.setImageBitmap(scaledImage);
- float startScale = zoomObj.adjustStartEndBounds(startBounds, finalBounds, globalOffset);
-
- // Hide the thumbnail and show the zoomed-in view. When the animation
- // begins, it will position the zoomed-in view in the place of the
- // thumbnail.
- thumbView.setAlpha(0f);
- expandedImageView.setVisibility(View.VISIBLE);
- zoomOutButton.setVisibility(View.VISIBLE);
- zoomInButton.setVisibility(View.GONE);
-
- // Set the pivot point for SCALE_X and SCALE_Y transformations
- // to the top-left corner of the zoomed-in view (the default
- // is the center of the view).
- expandedImageView.setPivotX(0f);
- expandedImageView.setPivotY(0f);
-
- // Construct and run the parallel animation of the four translation and
- // scale properties (X, Y, SCALE_X, and SCALE_Y).
- AnimatorSet set = new AnimatorSet();
- set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds.top))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f));
- set.setDuration(ShortAnimationDuration);
- set.setInterpolator(new DecelerateInterpolator());
- set.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- CurrentAnimator = null;
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- CurrentAnimator = null;
- }
- });
- set.start();
- CurrentAnimator = set;
-
- // Upon clicking the zoomed-in image, it should zoom back down
- // to the original bounds and show the thumbnail instead of
- // the expanded image.
- startScaleFinal = startScale;
- }
-
- /**
- * Called when user taps the ^ FAB button, expands to show Zoom and Map
- */
- @OnClick(R.id.main_fab)
- public void onMainFabClicked() {
- if (!isFABOpen) {
- showFABMenu();
- } else {
- closeFABMenu();
- }
- }
-
- @OnClick(R.id.media_upload_zoom_in)
- public void onZoomInFabClicked() {
- try {
- zoomImageFromThumb(backgroundImageView, mediaUri);
- } catch (Exception e) {
- Timber.e(e);
- }
- }
-
- @OnClick(R.id.media_upload_zoom_out)
- public void onZoomOutFabClicked() {
- if (CurrentAnimator != null) {
- CurrentAnimator.cancel();
- }
- isZoom = false;
- zoomOutButton.setVisibility(View.GONE);
- mainFab.setVisibility(View.VISIBLE);
-
- // Animate the four positioning/sizing properties in parallel,
- // back to their original values.
- AnimatorSet set = new AnimatorSet();
- set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScaleFinal))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal));
-
- set.setDuration(ShortAnimationDuration);
- set.setInterpolator(new DecelerateInterpolator());
- set.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- //background image view is thumbView
- backgroundImageView.setAlpha(1f);
- expandedImageView.setVisibility(View.GONE);
- CurrentAnimator = null;
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- //background image view is thumbView
- backgroundImageView.setAlpha(1f);
- expandedImageView.setVisibility(View.GONE);
- CurrentAnimator = null;
- }
- });
- set.start();
- CurrentAnimator = set;
- }
-
- @OnClick(R.id.media_map)
- public void onFabShowMapsClicked() {
- if (gpsObj != null && gpsObj.imageCoordsExists) {
- Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude());
- Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
- mapIntent.setPackage("com.google.android.apps.maps");
- startActivity(mapIntent);
- }
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_BACK:
- if (isZoom) {
- onZoomOutFabClicked();
- return true;
- }
- }
- return super.onKeyDown(keyCode,event);
-
- }
-}
-
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java
new file mode 100644
index 000000000..6436f621b
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java
@@ -0,0 +1,5 @@
+package fr.free.nrw.commons.upload;
+
+public interface SimilarImageInterface {
+ void showSimilarImageFragment(String originalFilePath, String possibleFilePath);
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java
deleted file mode 100644
index 9ea2f23f4..000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java
+++ /dev/null
@@ -1,389 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.annotation.SuppressLint;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.support.v4.view.ViewCompat;
-import android.support.v7.app.AlertDialog;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.text.Editable;
-import android.text.Html;
-import android.text.TextWatcher;
-import android.text.method.LinkMovementMethod;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.Spinner;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-
-import java.lang.reflect.Type;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import butterknife.OnClick;
-import butterknife.OnItemSelected;
-import butterknife.OnTouch;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.Utils;
-import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
-import fr.free.nrw.commons.settings.Prefs;
-import fr.free.nrw.commons.utils.ViewUtil;
-import timber.log.Timber;
-
-import static android.view.MotionEvent.ACTION_UP;
-
-public class SingleUploadFragment extends CommonsDaggerSupportFragment {
-
- @BindView(R.id.titleEdit) EditText titleEdit;
- @BindView(R.id.rv_descriptions) RecyclerView rvDescriptions;
- @BindView(R.id.titleDescButton) Button titleDescButton;
- @BindView(R.id.share_license_summary) TextView licenseSummaryView;
- @BindView(R.id.licenseSpinner) Spinner licenseSpinner;
-
-
- @Inject @Named("default_preferences") SharedPreferences prefs;
- @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
-
- private String license;
- private OnUploadActionInitiated uploadActionInitiatedHandler;
- private TitleTextWatcher textWatcher = new TitleTextWatcher();
- private DescriptionsAdapter descriptionsAdapter;
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- inflater.inflate(R.menu.activity_share, menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- //What happens when the 'submit' icon is tapped
- case R.id.menu_upload_single:
-
- if (titleEdit.getText().toString().trim().isEmpty()) {
- Toast.makeText(getContext(), R.string.add_title_toast, Toast.LENGTH_LONG).show();
- return false;
- }
-
- String title = titleEdit.getText().toString();
- String descriptionsInVariousLanguages = getDescriptionsInAppropriateFormat();
-
- //Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these
- prefs.edit()
- .putString("Title", title)
- .putString("Desc", new Gson().toJson(descriptionsAdapter
- .getDescriptions()))//Description, now is not just a string, its a list of description objects
- .apply();
-
- uploadActionInitiatedHandler
- .uploadActionInitiated(title, descriptionsInVariousLanguages);
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- private String getDescriptionsInAppropriateFormat() {
- List descriptions = descriptionsAdapter.getDescriptions();
- StringBuilder descriptionsInAppropriateFormat = new StringBuilder();
- for (Description description : descriptions) {
- String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageId(),
- description.getDescriptionText());
- descriptionsInAppropriateFormat.append(individualDescription);
- }
- return descriptionsInAppropriateFormat.toString();
-
- }
-
- private List getDescriptions() {
- List descriptions = descriptionsAdapter.getDescriptions();
- return descriptions;
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false);
- ButterKnife.bind(this, rootView);
-
- initRecyclerView();
-
- Intent activityIntent = getActivity().getIntent();
- if (activityIntent.hasExtra("title")) {
- titleEdit.setText(activityIntent.getStringExtra("title"));
- }
- if (activityIntent.hasExtra("description") && descriptionsAdapter.getDescriptions() != null
- && descriptionsAdapter.getDescriptions().size() > 0) {
- descriptionsAdapter.getDescriptions().get(0)
- .setDescriptionText(activityIntent.getStringExtra("description"));
- descriptionsAdapter.notifyItemChanged(0);
- }
-
-
- ArrayList licenseItems = new ArrayList<>();
- licenseItems.add(getString(R.string.license_name_cc0));
- licenseItems.add(getString(R.string.license_name_cc_by));
- licenseItems.add(getString(R.string.license_name_cc_by_sa));
- licenseItems.add(getString(R.string.license_name_cc_by_four));
- licenseItems.add(getString(R.string.license_name_cc_by_sa_four));
-
- license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
-
- // If this is a direct upload from Nearby, autofill title and desc fields with the Place's values
- boolean isNearbyUpload = ((ShareActivity) getActivity()).isNearbyUpload();
-
- if (isNearbyUpload) {
- String imageTitle = directPrefs.getString("Title", "");
- String imageDesc = directPrefs.getString("Desc", "");
- String imageCats = directPrefs.getString("Category", "");
- Timber.d("Image title: " + imageTitle + ", image desc: " + imageDesc + ", image categories: " + imageCats);
- titleEdit.setText(imageTitle);
- if (descriptionsAdapter.getDescriptions() != null
- && descriptionsAdapter.getDescriptions().size() > 0) {
- descriptionsAdapter.getDescriptions().get(0).setDescriptionText(imageDesc);
- descriptionsAdapter.notifyItemChanged(0);
- }
- }
-
- // check if this is the first time we have uploaded
- if (prefs.getString("Title", "").trim().length() == 0
- && prefs.getString("Desc", "").trim().length() == 0) {
- titleDescButton.setVisibility(View.GONE);
- }
-
- Timber.d(license);
-
- ArrayAdapter adapter;
- if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme", false)) {
- // dark theme
- adapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_dropdown_item, licenseItems);
- } else {
- // light theme
- adapter = new ArrayAdapter<>(getActivity(), R.layout.light_simple_spinner_dropdown_item, licenseItems);
- }
-
- licenseSpinner.setAdapter(adapter);
-
- int position = licenseItems.indexOf(getString(Utils.licenseNameFor(license)));
-
- // Check position is valid
- if (position < 0) {
- Timber.d("Invalid position: %d. Using default license", position);
- position = 4;
- }
-
- Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license)));
- licenseSpinner.setSelection(position);
-
- titleEdit.addTextChangedListener(textWatcher);
-
- titleEdit.setOnFocusChangeListener((v, hasFocus) -> {
- if (!hasFocus) {
- ViewUtil.hideKeyboard(v);
- }
- });
-
- setLicenseSummary(license);
-
- return rootView;
- }
-
- private void initRecyclerView() {
- descriptionsAdapter = new DescriptionsAdapter();
- descriptionsAdapter.setCallback(this::showInfoAlert);
- descriptionsAdapter.setLanguages(getLocaleSupportedByDevice());
- rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
- rvDescriptions.setAdapter(descriptionsAdapter);
- }
-
- private List getLocaleSupportedByDevice() {
- List languages = new ArrayList<>();
- Locale[] localesArray = Locale.getAvailableLocales();
- List locales = Arrays.asList(localesArray);
- for (Locale locale : locales) {
- languages.add(new Language(locale));
- }
- return languages;
- }
-
- @Override
- public void onDestroyView() {
- titleEdit.removeTextChangedListener(textWatcher);
- super.onDestroyView();
- }
-
- @OnItemSelected(R.id.licenseSpinner)
- void onLicenseSelected(AdapterView> parent, View view, int position, long id) {
- String licenseName = parent.getItemAtPosition(position).toString();
-
- // Set selected color to white because it should be readable on random images.
- TextView selectedText = (TextView) licenseSpinner.getChildAt(0);
- if (selectedText != null) {
- selectedText.setTextColor(Color.WHITE);
- selectedText.setBackgroundColor(Color.TRANSPARENT);
- }
-
- String license;
- if (getString(R.string.license_name_cc0).equals(licenseName)) {
- license = Prefs.Licenses.CC0;
- } else if (getString(R.string.license_name_cc_by).equals(licenseName)) {
- license = Prefs.Licenses.CC_BY_3;
- } else if (getString(R.string.license_name_cc_by_sa).equals(licenseName)) {
- license = Prefs.Licenses.CC_BY_SA_3;
- } else if (getString(R.string.license_name_cc_by_four).equals(licenseName)) {
- license = Prefs.Licenses.CC_BY_4;
- } else if (getString(R.string.license_name_cc_by_sa_four).equals(licenseName)) {
- license = Prefs.Licenses.CC_BY_SA_4;
- } else {
- throw new IllegalStateException("Unknown licenseName: " + licenseName);
- }
-
- setLicenseSummary(license);
- prefs.edit()
- .putString(Prefs.DEFAULT_LICENSE, license)
- .apply();
- }
-
-
- @OnClick(R.id.titleDescButton)
- void setTitleDescButton() {
- //Retrieve last title and desc entered
- String title = prefs.getString("Title", "");
- String descriptionJson = prefs.getString("Desc", "");
- Timber.d("Title: %s, Desc: %s", title, descriptionJson);
-
- titleEdit.setText(title);
- Type typeOfDest = new TypeToken>() {
- }.getType();
-
- List descriptions = new Gson().fromJson(descriptionJson, typeOfDest);
- descriptionsAdapter.setDescriptions(descriptions);
-
- }
-
- /**
- * Copied from https://stackoverflow.com/a/26269435/8065933
- */
- @OnTouch(R.id.titleEdit)
- boolean titleInfo(View view, MotionEvent motionEvent) {
- final int value;
- if (ViewCompat.getLayoutDirection(getView()) == ViewCompat.LAYOUT_DIRECTION_LTR) {
- value = titleEdit.getRight() - titleEdit.getCompoundDrawables()[2].getBounds().width();
- if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
- showInfoAlert(R.string.media_detail_title, R.string.title_info);
- return true;
- }
- }
- else {
- value = titleEdit.getLeft() + titleEdit.getCompoundDrawables()[0].getBounds().width();
- if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
- showInfoAlert(R.string.media_detail_title, R.string.title_info);
- return true;
- }
- }
- return false;
- }
-
- @SuppressLint("StringFormatInvalid")
- private void setLicenseSummary(String license) {
- String licenseHyperLink = ""+ getString(Utils.licenseNameFor(license)) + " ";
- licenseSummaryView.setMovementMethod(LinkMovementMethod.getInstance());
- licenseSummaryView.setText(Html.fromHtml(getString(R.string.share_license_summary, licenseHyperLink)));
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- setHasOptionsMenu(true);
- uploadActionInitiatedHandler = (OnUploadActionInitiated) getActivity();
- }
-
- @Override
- public void onStop() {
- super.onStop();
-
- // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
- View target = getActivity().getCurrentFocus();
- ViewUtil.hideKeyboard(target);
- }
-
- @NonNull
- private String licenseUrlFor(String license) {
- switch (license) {
- case Prefs.Licenses.CC_BY_3:
- return "https://creativecommons.org/licenses/by/3.0/";
- case Prefs.Licenses.CC_BY_4:
- return "https://creativecommons.org/licenses/by/4.0/";
- case Prefs.Licenses.CC_BY_SA_3:
- return "https://creativecommons.org/licenses/by-sa/3.0/";
- case Prefs.Licenses.CC_BY_SA_4:
- return "https://creativecommons.org/licenses/by-sa/4.0/";
- case Prefs.Licenses.CC0:
- return "https://creativecommons.org/publicdomain/zero/1.0/";
- }
- throw new RuntimeException("Unrecognized license value: " + license);
- }
-
- public interface OnUploadActionInitiated {
-
- void uploadActionInitiated(String title, String description);
- }
-
- private class TitleTextWatcher implements TextWatcher {
-
- @Override
- public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
- }
-
- @Override
- public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
- }
-
- @Override
- public void afterTextChanged(Editable editable) {
- if (getActivity() != null) {
- getActivity().invalidateOptionsMenu();
- }
- }
- }
-
-
- private void showInfoAlert (int titleStringID, int messageStringID){
- new AlertDialog.Builder(getContext())
- .setTitle(titleStringID)
- .setMessage(messageStringID)
- .setCancelable(true)
- .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
- .create()
- .show();
- }
-
- @OnClick(R.id.ll_add_description)
- public void onLLAddDescriptionClicked() {
- descriptionsAdapter.addDescription(new Description());
- rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1);
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java
index eebe6b4fa..97908fa67 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java
@@ -1,48 +1,88 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
+import android.graphics.Color;
+import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
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;
+import java.util.Collections;
import java.util.List;
+import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.utils.BiMap;
public class SpinnerLanguagesAdapter extends ArrayAdapter {
private final int resource;
private final LayoutInflater layoutInflater;
- List languages;
+ private List languageNamesList;
+ private List languageCodesList;
+ private final BiMap selectedLanguages;
+ public String selectedLangCode="";
+
+
public SpinnerLanguagesAdapter(@NonNull Context context,
- int resource) {
+ int resource, BiMap selectedLanguages) {
super(context, resource);
this.resource = resource;
this.layoutInflater = LayoutInflater.from(context);
- languages = new ArrayList<>();
+ languageNamesList = new ArrayList<>();
+ languageCodesList = new ArrayList<>();
+ prepareLanguages();
+ this.selectedLanguages = selectedLanguages;
}
- public void setLanguages(List languages) {
- this.languages = languages;
+ private void prepareLanguages() {
+ List languages = getLocaleSupportedByDevice();
+
+ for(Language language: languages) {
+ if(!languageCodesList.contains(language.getLocale().getLanguage())) {
+ languageNamesList.add(language.getLocale().getDisplayName());
+ languageCodesList.add(language.getLocale().getLanguage());
+ }
+ }
+ }
+
+ private List getLocaleSupportedByDevice() {
+ List languages = new ArrayList<>();
+ Locale[] localesArray = Locale.getAvailableLocales();
+ for (Locale locale : localesArray) {
+ languages.add(new Language(locale));
+ }
+
+ Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayName()
+ .compareTo(t1.getLocale().getDisplayName()));
+ return languages;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return !languageCodesList.get(position).isEmpty()&&
+ (!selectedLanguages.containsKey(languageCodesList.get(position)) ||
+ languageCodesList.get(position).equals(selectedLangCode));
}
@Override
public int getCount() {
- return languages.size();
+ return languageNamesList.size();
}
@Override
public View getDropDownView(int position, @Nullable View convertView,
- @NonNull ViewGroup parent) {
+ @NonNull ViewGroup parent) {
View view = layoutInflater.inflate(resource, parent, false);
ViewHolder holder = new ViewHolder(view);
holder.init(position, true);
@@ -75,19 +115,40 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter {
}
public void init(int position, boolean isDropDownView) {
- Language language = languages.get(position);
if (!isDropDownView) {
view.setVisibility(View.GONE);
- tvLanguage.setText(
- language.getLocale().getLanguage());
+ if(languageCodesList.get(position).length()>2)
+ tvLanguage.setText(languageCodesList.get(position).subSequence(0,2));
+ else
+ tvLanguage.setText(languageCodesList.get(position));
+
} else {
view.setVisibility(View.VISIBLE);
- tvLanguage.setText(
- String.format("%s [%s]", language.getLocale().getDisplayName(),
- language.getLocale().getLanguage()));
+ if (languageCodesList.get(position).isEmpty()) {
+ tvLanguage.setText(languageNamesList.get(position));
+ tvLanguage.setTextColor(Color.GRAY);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ tvLanguage.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
+ }
+ } else {
+ tvLanguage.setText(
+ String.format("%s [%s]", languageNamesList.get(position), languageCodesList.get(position)));
+ if(selectedLanguages.containsKey(languageCodesList.get(position))&&
+ !languageCodesList.get(position).equals(selectedLangCode))
+ tvLanguage.setTextColor(Color.GRAY);
+ else
+ tvLanguage.setTextColor(Color.BLACK);
+ }
}
-
}
}
+ String getLanguageCode(int position) {
+ return languageCodesList.get(position);
+ }
+
+ int getIndexOfUserDefaultLocale(Context context) {
+ return languageCodesList.indexOf(context.getResources().getConfiguration().locale.getLanguage());
+ }
+
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java
new file mode 100644
index 000000000..8963d0e25
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java
@@ -0,0 +1,5 @@
+package fr.free.nrw.commons.upload;
+
+public interface ThumbnailClickedListener {
+ void thumbnailClicked(UploadModel.UploadItem content);
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Title.java b/app/src/main/java/fr/free/nrw/commons/upload/Title.java
new file mode 100644
index 000000000..e0781ce67
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/Title.java
@@ -0,0 +1,37 @@
+package fr.free.nrw.commons.upload;
+
+import android.text.TextUtils;
+
+import io.reactivex.subjects.BehaviorSubject;
+import timber.log.Timber;
+
+class Title{
+
+ private String titleText;
+ private boolean set;
+
+ @Override
+ public String toString() {
+ return titleText;
+ }
+
+ public void setTitleText(String titleText) {
+ this.titleText = titleText;
+
+ if (!TextUtils.isEmpty(titleText)) {
+ set = true;
+ }
+ }
+
+ public boolean isSet() {
+ return set;
+ }
+
+ public void setSet(boolean set) {
+ this.set = set;
+ }
+
+ public boolean isEmpty() {
+ return titleText==null || titleText.isEmpty();
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
new file mode 100644
index 000000000..2ea89e8e6
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
@@ -0,0 +1,607 @@
+package fr.free.nrw.commons.upload;
+
+import android.Manifest;
+import android.animation.LayoutTransition;
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.constraint.ConstraintLayout;
+import android.support.design.widget.TextInputLayout;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+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.ImageView;
+import android.widget.ProgressBar;
+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.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import fr.free.nrw.commons.CommonsApplication;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.Utils;
+import fr.free.nrw.commons.auth.AuthenticatedActivity;
+import fr.free.nrw.commons.auth.LoginActivity;
+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.mwapi.MediaWikiApi;
+import fr.free.nrw.commons.utils.DialogUtil;
+import fr.free.nrw.commons.utils.StringUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.schedulers.Schedulers;
+import timber.log.Timber;
+
+import static fr.free.nrw.commons.utils.ImageUtils.Result;
+import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
+import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
+
+public class UploadActivity extends AuthenticatedActivity implements UploadView, SimilarImageInterface {
+ @Inject InputMethodManager inputMethodManager;
+ @Inject MediaWikiApi mwApi;
+ @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
+ @Inject UploadPresenter presenter;
+ @Inject CategoriesModel categoriesModel;
+
+ // Main GUI
+ @BindView(R.id.backgroundImage) PhotoView background;
+ @BindView(R.id.activity_upload_cards) ConstraintLayout cardLayout;
+ @BindView(R.id.view_flipper) ViewFlipper viewFlipper;
+
+ // 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;
+
+ // 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;
+
+ //Right Card
+ @BindView(R.id.right_card) CardView rightCard;
+ @BindView(R.id.right_card_expand_button) ImageView rightCardExpandButton;
+ @BindView(R.id.right_card_map_button) View rightCardMapButton;
+
+ // 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;
+
+ // Final Submission
+ @BindView(R.id.license_title) TextView licenseTitle;
+ @BindView(R.id.share_license_summary) TextView licenseSummary;
+ @BindView(R.id.media_upload_policy) TextView licensePolicy;
+ @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;
+
+ private DescriptionsAdapter descriptionsAdapter;
+ private RVRendererAdapter categoriesAdapter;
+ private CompositeDisposable compositeDisposable;
+
+ DexterPermissionObtainer dexterPermissionObtainer;
+
+
+ @SuppressLint("CheckResult")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_upload);
+ ButterKnife.bind(this);
+ compositeDisposable = new CompositeDisposable();
+
+ configureLayout();
+ configureTopCard();
+ configureBottomCard();
+ initRecyclerView();
+ configureRightCard();
+ configureNavigationButtons();
+ configureCategories();
+ configureLicenses();
+
+ presenter.init();
+
+ dexterPermissionObtainer = new DexterPermissionObtainer(this,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ getString(R.string.storage_permission),
+ getString(R.string.write_storage_permission_rationale_for_image_share));
+
+ dexterPermissionObtainer.confirmStoragePermissions().subscribe(this::receiveSharedItems);
+ }
+
+ @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;
+ }
+
+ @Override
+ protected void onDestroy() {
+ presenter.cleanup();
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ checkIfLoggedIn();
+ compositeDisposable.add(
+ dexterPermissionObtainer.confirmStoragePermissions()
+ .subscribe(() -> presenter.addView(this)));
+ 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)
+ );
+ }
+
+ @Override
+ protected void onPause() {
+ presenter.removeView();
+ compositeDisposable.dispose();
+ compositeDisposable = new CompositeDisposable();
+ super.onPause();
+ }
+
+ @Override
+ public void updateThumbnails(List 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) {
+ 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(isShowingItem) {
+ descriptionsAdapter.setItems(uploadItem.title, uploadItem.descriptions);
+ rvDescriptions.setAdapter(descriptionsAdapter);
+ }
+ }
+
+ @Override
+ public void updateLicenses(List licenses, String selectedLicense) {
+ ArrayAdapter 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) {
+ String licenseHyperLink = "" +
+ getString(Utils.licenseNameFor(selectedLicense)) + " ";
+ licenseSummary.setMovementMethod(LinkMovementMethod.getInstance());
+ licenseSummary.setText(
+ Html.fromHtml(
+ getString(R.string.share_license_summary, 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) {
+ rightCard.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void setBottomCardVisibility(@UploadPage int page) {
+ 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);
+ }
+ }
+
+ @Override
+ public void setBottomCardState(boolean state) {
+ updateCardState(state, bottomCardExpandButton, rvDescriptions, previous, next, bottomCardAddDescription);
+ }
+
+ @Override
+ public void setRightCardState(boolean state) {
+ rightCardExpandButton.animate().rotation(rightCardExpandButton.getRotation() + (state ? -180 : 180)).start();
+ //Add all items in rightCard here
+ rightCardMapButton.setVisibility(state ? View.VISIBLE : View.GONE);
+ }
+
+ @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);
+ }
+ }
+
+ @Override
+ public void showBadPicturePopup(@Result int result) {
+ String errorMessageForResult = getErrorMessageForResult(this, result);
+ if (StringUtils.isNullOrWhiteSpace(errorMessageForResult)) {
+ return;
+ }
+
+ DialogUtil.showAlertDialog(this,
+ getString(R.string.warning),
+ errorMessageForResult,
+ () -> presenter.deletePicture(),
+ () -> presenter.keepPicture());
+ }
+
+ @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));
+ }
+
+ @Override
+ public void launchMapActivity(String decCoords) {
+ Utils.handleGeoCoordinates(this, decCoords);
+ }
+
+ @Override
+ public void showErrorMessage(int resourceId) {
+ ViewUtil.showShortToast(this, resourceId);
+ }
+
+ @Override
+ public void initDefaultCategories() {
+ updateCategoryList("");
+ }
+
+ @Override
+ protected void onAuthCookieAcquired(String authCookie) {
+ mwApi.setAuthCookie(authCookie);
+ }
+
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) {
+ dexterPermissionObtainer.onManualPermissionReturned();
+ }
+ }
+
+
+ @Override
+ protected void onAuthFailure() {
+ Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG).show();
+ finish();
+ }
+
+ 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() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ cardLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
+ }
+ 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() {
+ bottomCardExpandButton.setOnClickListener(v -> presenter.toggleBottomCardState());
+ bottomCardAddDescription.setOnClickListener(v -> addNewDescription());
+ }
+
+ private void addNewDescription() {
+ descriptionsAdapter.addDescription(new Description());
+ rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1);
+ }
+
+ private void configureRightCard() {
+ rightCardExpandButton.setOnClickListener(v -> presenter.toggleRightCardState());
+ rightCardMapButton.setOnClickListener(v -> presenter.openCoordinateMap());
+ }
+
+ private void configureNavigationButtons() {
+ // Navigation next / previous for each image as we're collecting title + description
+ next.setOnClickListener(v -> {
+ setTitleAndDescriptions();
+ 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 -> {
+ Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG).show();
+ presenter.handleSubmit(categoriesModel);
+ finish();
+ });
+
+ }
+
+ private void setTitleAndDescriptions() {
+ List descriptions = descriptionsAdapter.getDescriptions();
+ Timber.d("Descriptions size is %d are %s", descriptions.size(), descriptions);
+ }
+
+ private void configureCategories() {
+ categoriesAdapter = new UploadCategoriesAdapterFactory(categoriesModel).create(new ArrayList<>());
+ categoriesList.setLayoutManager(new LinearLayoutManager(this));
+ categoriesList.setAdapter(categoriesAdapter);
+ }
+
+ @SuppressLint("CheckResult")
+ private void updateCategoryList(String filter) {
+ List imageTitleList = presenter.getImageTitleList();
+ 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 mimeType = intent.getType();
+ String source;
+
+ if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
+ source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
+ } else {
+ source = Contribution.SOURCE_EXTERNAL;
+ }
+
+ if (Intent.ACTION_SEND.equals(intent.getAction())) {
+ Uri mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (intent.getBooleanExtra("isDirectUpload", false)) {
+ String imageTitle = directPrefs.getString("Title", "");
+ String imageDesc = directPrefs.getString("Desc", "");
+ Timber.i("Received direct upload with title %s and description %s", imageTitle, imageDesc);
+ String wikidataEntityIdPref = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF);
+ presenter.receiveDirect(mediaUri, mimeType, source, wikidataEntityIdPref, imageTitle, imageDesc);
+ } else {
+ Timber.i("Received single upload");
+ presenter.receive(mediaUri, mimeType, source);
+ }
+ } else if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
+ ArrayList urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ Timber.i("Received multiple upload %s", urisList.size());
+ presenter.receive(urisList, mimeType, source);
+ }
+ }
+
+ private void updateCardState(boolean state, ImageView button, View... content) {
+ button.animate().rotation(button.getRotation() + (state ? 180 : -180)).start();
+ if (content != null) {
+ for (View view : content) {
+ view.setVisibility(state ? View.VISIBLE : View.GONE);
+ }
+ }
+ }
+
+ @Override
+ public List 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)
+ .setMessage(getString(messageStringId, (Object[]) formatArgs))
+ .setCancelable(true)
+ .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
+ .create()
+ .show();
+ }
+
+ @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");
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java
new file mode 100644
index 000000000..1797cbe80
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java
@@ -0,0 +1,27 @@
+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;
+
+import fr.free.nrw.commons.category.CategoryClickedListener;
+import fr.free.nrw.commons.category.CategoryItem;
+
+public class UploadCategoriesAdapterFactory {
+ private final CategoryClickedListener listener;
+
+ public UploadCategoriesAdapterFactory(CategoryClickedListener listener) {
+ this.listener = listener;
+ }
+
+ public RVRendererAdapter create(List placeList) {
+ RendererBuilder builder = new RendererBuilder()
+ .bind(CategoryItem.class, new UploadCategoriesRenderer(listener));
+ ListAdapteeCollection collection = new ListAdapteeCollection<>(
+ placeList != null ? placeList : Collections.emptyList());
+ return new RVRendererAdapter<>(builder, collection);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java
new file mode 100644
index 000000000..d0862b964
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java
@@ -0,0 +1,52 @@
+package fr.free.nrw.commons.upload;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+
+import com.pedrogomez.renderers.Renderer;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.category.CategoryClickedListener;
+import fr.free.nrw.commons.category.CategoryItem;
+
+public class UploadCategoriesRenderer extends Renderer {
+ @BindView(R.id.tvName) CheckBox checkedView;
+ private final CategoryClickedListener listener;
+
+ UploadCategoriesRenderer(CategoryClickedListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
+ return layoutInflater.inflate(R.layout.layout_upload_categories_item, viewGroup, false);
+ }
+
+ @Override
+ protected void setUpView(View view) {
+ ButterKnife.bind(this, view);
+ }
+
+ @Override
+ protected void hookListeners(View view) {
+ view.setOnClickListener(v -> {
+ CategoryItem item = getContent();
+ item.setSelected(!item.isSelected());
+ checkedView.setChecked(item.isSelected());
+ if (listener != null) {
+ listener.categoryClicked(item);
+ }
+ });
+ }
+
+ @Override
+ public void render() {
+ CategoryItem item = getContent();
+ checkedView.setChecked(item.isSelected());
+ checkedView.setText(item.getName());
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java
index ce69110cc..fd0563ab3 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java
@@ -23,7 +23,6 @@ import java.io.InputStream;
import java.util.Date;
import java.util.concurrent.Executors;
-import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
@@ -87,49 +86,11 @@ public class UploadController {
/**
* Starts a new upload task.
- * @param title the title of the contribution
- * @param mediaUri the media URI of the contribution
- * @param description the description of the contribution
- * @param mimeType the MIME type of the contribution
- * @param source the source of the contribution
- * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615")
- * @param wikiDataEntityId
- * @param onComplete the progress tracker
+ *
+ * @param contribution the contribution object
*/
- public void startUpload(String title, Uri contentProviderUri, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) {
- Contribution contribution;
-
-
- //TODO: Modify this to include coords
- contribution = new Contribution(mediaUri, null, title, description, -1,
- null, null, sessionManager.getCurrentAccount().name,
- CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords);
-
-
- contribution.setTag("mimeType", mimeType);
- contribution.setSource(source);
-
- Timber.d("Wikidata entity ID received from Share activity is %s", wikiDataEntityId);
- //TODO: Modify this to include coords
- Account currentAccount = sessionManager.getCurrentAccount();
- if (currentAccount == null) {
- Timber.d("Current account is null");
- ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
- sessionManager.forceLogin(context);
- return;
- }
- contribution = new Contribution(mediaUri, null, title, description, -1,
- null, null, currentAccount.name,
- CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords);
-
-
- contribution.setTag("mimeType", mimeType);
- contribution.setSource(source);
- contribution.setWikiDataEntityId(wikiDataEntityId);
- contribution.setContentProviderUri(contentProviderUri);
-
- //Calls the next overloaded method
- startUpload(contribution, onComplete);
+ public void startUpload(Contribution contribution) {
+ startUpload(contribution, c -> {});
}
/**
@@ -142,7 +103,14 @@ public class UploadController {
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
//Set creator, desc, and license
if (TextUtils.isEmpty(contribution.getCreator())) {
- contribution.setCreator(sessionManager.getCurrentAccount().name);
+ Account currentAccount = sessionManager.getCurrentAccount();
+ if (currentAccount == null) {
+ Timber.d("Current account is null");
+ ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
+ sessionManager.forceLogin(context);
+ return;
+ }
+ contribution.setCreator(currentAccount.name);
}
if (contribution.getDescription() == null) {
@@ -163,8 +131,6 @@ public class UploadController {
long length;
ContentResolver contentResolver = context.getContentResolver();
try {
-
- //TODO: understand do we really need this code
if (contribution.getDataLength() <= 0) {
Timber.d("UploadController/doInBackground, contribution.getLocalUri():" + contribution.getLocalUri());
AssetFileDescriptor assetFileDescriptor = contentResolver
@@ -218,7 +184,7 @@ public class UploadController {
contribution.setDateCreated(new Date());
}
}
- return contribution;
+ return contribution;
}
@Override
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java
new file mode 100644
index 000000000..46193e958
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java
@@ -0,0 +1,400 @@
+package fr.free.nrw.commons.upload;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import fr.free.nrw.commons.CommonsApplication;
+import fr.free.nrw.commons.auth.SessionManager;
+import fr.free.nrw.commons.contributions.Contribution;
+import fr.free.nrw.commons.mwapi.MediaWikiApi;
+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.disposables.Disposable;
+import io.reactivex.functions.Consumer;
+import io.reactivex.schedulers.Schedulers;
+import io.reactivex.subjects.BehaviorSubject;
+import timber.log.Timber;
+
+public class UploadModel {
+
+ private MediaWikiApi mwApi;
+ private static UploadItem DUMMY = new UploadItem(Uri.EMPTY, "", "", GPSExtractor.DUMMY, "", null,-1l) {
+ };
+ private final SharedPreferences prefs;
+ private final List licenses;
+ private String license;
+ private final Map licensesByName;
+ private List items = new ArrayList<>();
+ private boolean topCardState = true;
+ private boolean bottomCardState = true;
+ private boolean rightCardState = true;
+ private int currentStepIndex = 0;
+ private Context context;
+ private ContentResolver contentResolver;
+ private boolean useExtStorage;
+ private Disposable badImageSubscription;
+
+ @Inject
+ SessionManager sessionManager;
+ private Uri currentMediaUri;
+
+ @Inject
+ UploadModel(@Named("licenses") List licenses,
+ @Named("default_preferences") SharedPreferences prefs,
+ @Named("licenses_by_name") Map licensesByName,
+ Context context,
+ MediaWikiApi mwApi) {
+ this.licenses = licenses;
+ this.prefs = prefs;
+ this.license = Prefs.Licenses.CC_BY_SA_3;
+ this.licensesByName = licensesByName;
+ this.context = context;
+ this.mwApi = mwApi;
+ this.contentResolver = context.getContentResolver();
+ useExtStorage = this.prefs.getBoolean("useExternalStorage", false);
+ }
+
+ @SuppressLint("CheckResult")
+ void receive(List mediaUri, String mimeType, String source, SimilarImageInterface similarImageInterface) {
+ initDefaultValues();
+ Observable itemObservable = Observable.fromIterable(mediaUri)
+ .map(media -> {
+ currentMediaUri=media;
+ return cacheFileUpload(media);
+ })
+ .map(filePath -> {
+ long fileCreatedDate = getFileCreatedDate(currentMediaUri);
+ Uri uri = Uri.fromFile(new File(filePath));
+ FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
+ UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
+ FileUtils.getFileExt(filePath), null,fileCreatedDate);
+ Single.zip(
+ Single.fromCallable(() ->
+ new FileInputStream(filePath))
+ .map(FileUtils::getSHA1)
+ .map(mwApi::existingFile)
+ .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
+ Single.fromCallable(() ->
+ new FileInputStream(filePath))
+ .map(file -> BitmapRegionDecoder.newInstance(file, false))
+ .map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
+ (dupe, dark) -> dupe | dark)
+ .observeOn(Schedulers.io())
+ .subscribe(item.imageQuality::onNext, Timber::e);
+ return item;
+ });
+ items = itemObservable.toList().blockingGet();
+ items.get(0).selected = true;
+ items.get(0).first = true;
+ }
+
+ @SuppressLint("CheckResult")
+ void receiveDirect(Uri media, String mimeType, String source, String wikidataEntityIdPref, String title, String desc, SimilarImageInterface similarImageInterface) {
+ initDefaultValues();
+ long fileCreatedDate = getFileCreatedDate(media);
+ String filePath = this.cacheFileUpload(media);
+ Uri uri = Uri.fromFile(new File(filePath));
+ FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
+ UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
+ FileUtils.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate);
+ item.title.setTitleText(title);
+ item.descriptions.get(0).setDescriptionText(desc);
+ //TODO figure out if default descriptions in other languages exist
+ item.descriptions.get(0).setLanguageCode("en");
+ Single.zip(
+ Single.fromCallable(() ->
+ new FileInputStream(filePath))
+ .map(FileUtils::getSHA1)
+ .map(mwApi::existingFile)
+ .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
+ Single.fromCallable(() ->
+ new FileInputStream(filePath))
+ .map(file -> BitmapRegionDecoder.newInstance(file, false))
+ .map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
+ (dupe, dark) -> dupe | dark).subscribe(item.imageQuality::onNext);
+ items.add(item);
+ items.get(0).selected = true;
+ items.get(0).first = true;
+ }
+
+ private void initDefaultValues() {
+ currentStepIndex = 0;
+ topCardState = true;
+ bottomCardState = true;
+ rightCardState = true;
+ items = new ArrayList<>();
+ }
+
+ /**
+ * Get file creation date from uri from all possible content providers
+ * @param media
+ * @return
+ */
+ private long getFileCreatedDate(Uri media) {
+ try {
+ Cursor cursor = contentResolver.query(media, null, null, null, null);
+ if (cursor == null) {
+ return -1;//Could not fetch last_modified
+ }
+ //Content provider contracts for opening gallery from the app and that by sharing from gallery from outside are different and we need to handle both the cases
+ int lastModifiedColumnIndex = cursor.getColumnIndex("last_modified");//If gallery is opened from in app
+ if(lastModifiedColumnIndex==-1){
+ lastModifiedColumnIndex=cursor.getColumnIndex("datetaken");
+ }
+ //If both the content providers do not give the data, lets leave it to Jesus
+ if(lastModifiedColumnIndex==-1){
+ return -1l;
+ }
+ cursor.moveToFirst();
+ return cursor.getLong(lastModifiedColumnIndex);
+ } catch (Exception e) {
+ return -1;////Could not fetch last_modified
+ }
+ }
+
+ boolean isPreviousAvailable() {
+ return currentStepIndex > 0;
+ }
+
+ boolean isNextAvailable() {
+ return currentStepIndex < (items.size() + 1);
+ }
+
+ boolean isSubmitAvailable() {
+ int count = items.size();
+ boolean hasError = license == null;
+ for (int i = 0; i < count; i++) {
+ UploadItem item = items.get(i);
+ hasError |= item.error;
+ }
+ return !hasError;
+ }
+
+ int getCurrentStep() {
+ return currentStepIndex + 1;
+ }
+
+ int getStepCount() {
+ return items.size() + 2;
+ }
+
+ public int getCount() {
+ return items.size();
+ }
+
+ public List getUploads() {
+ 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;
+ }
+
+ public void next() {
+ if (badImageSubscription != null)
+ badImageSubscription.dispose();
+ markCurrentUploadVisited();
+ if (currentStepIndex < items.size() + 1) {
+ currentStepIndex++;
+ }
+ updateItemState();
+ }
+
+ public void setCurrentTitleAndDescriptions(Title title, List 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 descriptions) {
+ if (currentStepIndex < items.size() && currentStepIndex >= 0) {
+ items.get(currentStepIndex).descriptions = descriptions;
+ }
+ }
+
+ public void previous() {
+ if (badImageSubscription != null)
+ badImageSubscription.dispose();
+ 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() {
+ 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() {
+ if (currentStepIndex < items.size() && currentStepIndex >= 0) {
+ items.get(currentStepIndex).visited = true;
+ }
+ }
+
+ public List getLicenses() {
+ return licenses;
+ }
+
+ String getSelectedLicense() {
+ return license;
+ }
+
+ void setSelectedLicense(String licenseName) {
+ this.license = licensesByName.get(licenseName);
+ }
+
+ Observable buildContributions(List categoryStringList) {
+ return Observable.fromIterable(items).map(item ->
+ {
+ Contribution contribution = new Contribution(item.mediaUri, null, item.title + "." + item.fileExt,
+ Description.formatList(item.descriptions), -1,
+ null, null, sessionManager.getUserName(),
+ CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getCoords());
+ contribution.setWikiDataEntityId(item.wikidataEntityId);
+ contribution.setCategories(categoryStringList);
+ contribution.setTag("mimeType", item.mimeType);
+ contribution.setSource(item.source);
+ contribution.setContentProviderUri(item.mediaUri);
+ if (item.createdTimestamp != -1l) {
+ contribution.setDateCreated(new Date(item.createdTimestamp));
+ //Set the date only if you have it, else the upload service is gonna try it the other way
+ }
+ return contribution;
+ });
+ }
+
+ /**
+ * Copy files into local storage and return file path
+ *
+ * @param media Uri of the file
+ * @return path of the enw file
+ */
+ private String cacheFileUpload(Uri media) {
+ try {
+ String copyPath;
+ if (useExtStorage)
+ copyPath = FileUtils.createExternalCopyPathAndCopy(media, contentResolver);
+ else
+ copyPath = FileUtils.createCopyPathAndCopy(media, context);
+ Timber.i("File path is " + copyPath);
+ return copyPath;
+ } catch (IOException e) {
+ Timber.w(e, "Error in copying URI " + media.getPath());
+ return null;
+ }
+ }
+
+ void keepPicture() {
+ items.get(currentStepIndex).imageQuality.onNext(ImageUtils.IMAGE_KEEP);
+ }
+
+ void deletePicture() {
+ badImageSubscription.dispose();
+ items.remove(currentStepIndex).imageQuality.onComplete();
+ updateItemState();
+ }
+
+ void subscribeBadPicture(Consumer consumer) {
+ badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e);
+ }
+
+
+ @SuppressWarnings("WeakerAccess")
+ static class UploadItem {
+ public final Uri mediaUri;
+ public final String mimeType;
+ public final String source;
+ public final GPSExtractor gpsCoords;
+
+ public boolean selected = false;
+ public boolean first = false;
+ public String fileExt;
+ public BehaviorSubject imageQuality;
+ Title title;
+ List descriptions;
+ public String wikidataEntityId;
+ public boolean visited;
+ public boolean error;
+ public long createdTimestamp;
+
+ @SuppressLint("CheckResult")
+ UploadItem(Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, String fileExt, @Nullable String wikidataEntityId, long createdTimestamp) {
+ title = new Title();
+ descriptions = new ArrayList<>();
+ descriptions.add(new Description());
+ this.wikidataEntityId = wikidataEntityId;
+ this.mediaUri = mediaUri;
+ this.mimeType = mimeType;
+ this.source = source;
+ this.gpsCoords = gpsCoords;
+ this.fileExt = fileExt;
+ imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
+ this.createdTimestamp=createdTimestamp;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java
new file mode 100644
index 000000000..74e3192bd
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java
@@ -0,0 +1,430 @@
+package fr.free.nrw.commons.upload;
+
+import android.annotation.SuppressLint;
+import android.net.Uri;
+
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.category.CategoriesModel;
+import fr.free.nrw.commons.contributions.Contribution;
+import fr.free.nrw.commons.mwapi.MediaWikiApi;
+import fr.free.nrw.commons.utils.ImageUtils;
+import io.reactivex.Completable;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+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;
+
+/**
+ * The MVP pattern presenter of Upload GUI
+ */
+@Singleton
+public class UploadPresenter {
+
+ private final UploadModel uploadModel;
+ private final UploadController uploadController;
+ private final MediaWikiApi mediaWikiApi;
+
+ private static final UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
+ new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null);
+ private UploadView view = DUMMY;
+
+ private static final SimilarImageInterface SIMILAR_IMAGE = (SimilarImageInterface) Proxy.newProxyInstance(SimilarImageInterface.class.getClassLoader(),
+ new Class[]{SimilarImageInterface.class}, (proxy, method, methodArgs) -> null);
+ private SimilarImageInterface similarImageInterface = SIMILAR_IMAGE;
+
+ @UploadView.UploadPage
+ private int currentPage = UploadView.PLEASE_WAIT;
+
+
+ @Inject
+ UploadPresenter(UploadModel uploadModel,
+ UploadController uploadController,
+ MediaWikiApi mediaWikiApi) {
+ this.uploadModel = uploadModel;
+ this.uploadController = uploadController;
+ this.mediaWikiApi = mediaWikiApi;
+ }
+
+ void receive(Uri mediaUri, String mimeType, String source) {
+ receive(Collections.singletonList(mediaUri), mimeType, source);
+ }
+
+ /**
+ * Passes the items received to {@link #uploadModel} and displays the items.
+ *
+ * @param media The Uri's of the media being uploaded.
+ * @param mimeType the mimeType of the files.
+ * @param source File source from {@link Contribution.FileSource}
+ */
+ @SuppressLint("CheckResult")
+ void receive(List media, String mimeType, @Contribution.FileSource String source) {
+ Completable.fromRunnable(() -> uploadModel.receive(media, mimeType, source, similarImageInterface))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ updateCards();
+ updateLicenses();
+ updateContent();
+ if (uploadModel.isShowingItem())
+ uploadModel.subscribeBadPicture(this::handleBadPicture);
+ }, Timber::e);
+ }
+
+ /**
+ * Passes the direct upload item received to {@link #uploadModel} and displays the items.
+ *
+ * @param media The Uri's of the media being uploaded.
+ * @param mimeType the mimeType of the files.
+ * @param source File source from {@link Contribution.FileSource}
+ */
+ @SuppressLint("CheckResult")
+ void receiveDirect(Uri media, String mimeType, @Contribution.FileSource String source, String wikidataEntityIdPref, String title, String desc) {
+ Completable.fromRunnable(() -> uploadModel.receiveDirect(media, mimeType, source, wikidataEntityIdPref, title, desc, similarImageInterface))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ updateCards();
+ updateLicenses();
+ updateContent();
+ if (uploadModel.isShowingItem())
+ uploadModel.subscribeBadPicture(this::handleBadPicture);
+ }, Timber::e);
+ }
+ /**
+ * Sets the license to parameter and updates {@link UploadActivity}
+ *
+ * @param licenseName license name
+ */
+ void selectLicense(String licenseName) {
+ uploadModel.setSelectedLicense(licenseName);
+ view.updateLicenseSummary(uploadModel.getSelectedLicense());
+ }
+
+ //region Wizard step management
+
+ /**
+ * Called by the next button in {@link UploadActivity}
+ */
+ @SuppressLint("CheckResult")
+ void handleNext(Title title,
+ List descriptions) {
+ validateCurrentItemTitle()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(errorCode -> handleImage(errorCode, title, descriptions));
+ }
+
+ /**
+ * 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 handleImage(Integer errorCode, Title title, List descriptions) {
+ switch (errorCode) {
+ case EMPTY_TITLE:
+ view.showErrorMessage(R.string.add_title_toast);
+ break;
+ case FILE_NAME_EXISTS:
+ if(getCurrentItem().imageQuality.getValue().equals(IMAGE_KEEP)) {
+ setTitleAndDescription(title, descriptions);
+ nextUploadedItem();
+ } else {
+ view.showDuplicatePicturePopup();
+ }
+ break;
+ case IMAGE_OK:
+ default:
+ setTitleAndDescription(title, descriptions);
+ nextUploadedItem();
+ }
+ }
+
+ private void nextUploadedItem() {
+ uploadModel.next();
+ updateContent();
+ if (uploadModel.isShowingItem()) {
+ uploadModel.subscribeBadPicture(this::handleBadPicture);
+ }
+ view.dismissKeyboard();
+ }
+
+ private void setTitleAndDescription(Title title, List descriptions) {
+ uploadModel.setCurrentTitleAndDescriptions(title, descriptions);
+ }
+
+ private Title getCurrentImageTitle() {
+ return getCurrentItem().title;
+ }
+
+ String getCurrentImageFileName() {
+ UploadItem currentItem = getCurrentItem();
+ return currentItem.title + "." + uploadModel.getCurrentItem().fileExt;
+ }
+
+ @SuppressLint("CheckResult")
+ private Observable validateCurrentItemTitle() {
+ Title title = getCurrentImageTitle();
+ if (title.isEmpty()) {
+ view.showErrorMessage(R.string.add_title_toast);
+ return Observable.just(EMPTY_TITLE);
+ }
+
+ return Observable.fromCallable(() -> mediaWikiApi.fileExistsWithName(getCurrentImageFileName()))
+ .subscribeOn(Schedulers.io())
+ .map(doesFileExist -> {
+ if (doesFileExist) {
+ return FILE_NAME_EXISTS;
+ }
+ return IMAGE_OK;
+ });
+ }
+
+ /**
+ * Called by the previous button in {@link UploadActivity}
+ */
+ void handlePrevious() {
+ uploadModel.previous();
+ updateContent();
+ if (uploadModel.isShowingItem()) {
+ uploadModel.subscribeBadPicture(this::handleBadPicture);
+ }
+ 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())
+ uploadModel.buildContributions(categoriesModel.getCategoryStringList())
+ .observeOn(Schedulers.io())
+ .subscribe(uploadController::startUpload);
+ }
+
+ /**
+ * Called by the map button on the right card in {@link UploadActivity}
+ */
+ void openCoordinateMap() {
+ GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords;
+ if (gpsObj != null && gpsObj.imageCoordsExists) {
+ view.launchMapActivity(gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude());
+ }
+ }
+
+
+ /**
+ * Called by the image processors when a result is obtained.
+ *
+ * @param result the result returned by the image procesors.
+ */
+ private void handleBadPicture(@ImageUtils.Result int result) {
+ view.showBadPicturePopup(result);
+ }
+
+ void keepPicture() {
+ uploadModel.keepPicture();
+ }
+
+ void deletePicture() {
+ if (uploadModel.getCount() == 1)
+ view.finish();
+ else {
+ uploadModel.deletePicture();
+ updateCards();
+ updateContent();
+ if (uploadModel.isShowingItem())
+ uploadModel.subscribeBadPicture(this::handleBadPicture);
+ 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());
+ }
+
+ /**
+ * Toggles the right card's state between open and closed.
+ */
+ void toggleRightCardState() {
+ uploadModel.setRightCardState(!uploadModel.isRightCardState());
+ view.setRightCardState(uploadModel.isRightCardState());
+ }
+
+ /**
+ * Sets all the cards' states to closed.
+ */
+ void closeAllCards() {
+ if (uploadModel.isTopCardState()) {
+ uploadModel.setTopCardState(false);
+ view.setTopCardState(false);
+ }
+ if (uploadModel.isRightCardState()) {
+ uploadModel.setRightCardState(false);
+ view.setRightCardState(false);
+ }
+ if (uploadModel.isBottomCardState()) {
+ uploadModel.setBottomCardState(false);
+ view.setBottomCardState(false);
+ }
+ }
+ //endregion
+
+ //region View / Lifecycle management
+ public void init() {
+ uploadController.prepareService();
+ }
+
+ void 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 = uploadModel.getSelectedLicense();
+ view.updateLicenses(uploadModel.getLicenses(), selectedLicense);
+ view.updateLicenseSummary(selectedLicense);
+ }
+
+ /**
+ * 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().mediaUri);
+
+ view.updateBottomCardContent(uploadModel.getCurrentStep(),
+ uploadModel.getStepCount(),
+ uploadModel.getCurrentItem(),
+ uploadModel.isShowingItem());
+
+ view.updateTopCardContent();
+
+ GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords;
+ view.updateRightCardContent(gpsObj != null && gpsObj.imageCoordsExists);
+
+ showCorrectCards(uploadModel.getCurrentStep(), uploadModel.getCount());
+ }
+
+ /**
+ * Updates the layout to show the correct bottom card.
+ *
+ * @param currentStep the current step
+ * @param uploadCount how many items are being uploaded
+ */
+ private void showCorrectCards(int currentStep, int uploadCount) {
+ if (uploadCount == 0) {
+ currentPage = UploadView.PLEASE_WAIT;
+ } else if (currentStep <= uploadCount) {
+ currentPage = UploadView.TITLE_CARD;
+ view.setTopCardVisibility(uploadModel.getCount() > 1);
+ } else if (currentStep == uploadCount + 1) {
+ currentPage = UploadView.CATEGORIES;
+ view.setTopCardVisibility(false);
+ view.setRightCardVisibility(false);
+ view.initDefaultCategories();
+ } else {
+ currentPage = UploadView.LICENSE;
+ view.setTopCardVisibility(false);
+ view.setRightCardVisibility(false);
+ }
+ view.setBottomCardVisibility(currentPage);
+ }
+
+ //endregion
+
+ /**
+ * @return the item currently being displayed
+ */
+ private UploadItem getCurrentItem() {
+ return uploadModel.getCurrentItem();
+ }
+
+ List getImageTitleList() {
+ List titleList = new ArrayList<>();
+ for (UploadItem item : uploadModel.getUploads()) {
+ if (item.title.isSet()) {
+ titleList.add(item.title.toString());
+ }
+ }
+ return titleList;
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java
index 823a4b91f..e7920f317 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java
@@ -62,7 +62,9 @@ public class UploadService extends HandlerService {
private NotificationCompat.Builder curProgressNotification;
private int toUpload;
- // The file names of unfinished uploads, used to prevent overwriting
+ /**
+ * The file names of unfinished uploads, used to prevent overwriting
+ */
private Set unfinishedUploads = new HashSet<>();
// DO NOT HAVE NOTIFICATION ID OF 0 FOR ANYTHING
@@ -314,6 +316,7 @@ public class UploadService extends HandlerService {
}
@SuppressLint("StringFormatInvalid")
+ @SuppressWarnings("deprecation")
private void showFailedNotification(Contribution contribution) {
Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java
new file mode 100644
index 000000000..64b873da0
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java
@@ -0,0 +1,49 @@
+package fr.free.nrw.commons.upload;
+
+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 fr.free.nrw.commons.R;
+
+class UploadThumbnailRenderer extends Renderer {
+ 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();
+ background.setImageURI(content.mediaUri);
+ background.setAlpha(content.selected ? 1.0f : 0.5f);
+ space.setVisibility(content.first ? View.VISIBLE : View.GONE);
+ error.setVisibility(content.visited && content.error ? View.VISIBLE : View.GONE);
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java
new file mode 100644
index 000000000..bc0a79c80
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java
@@ -0,0 +1,26 @@
+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;
+
+import javax.inject.Inject;
+
+public class UploadThumbnailsAdapterFactory {
+ private ThumbnailClickedListener listener;
+
+ UploadThumbnailsAdapterFactory(ThumbnailClickedListener listener) {
+ this.listener = listener;
+ }
+
+ public RVRendererAdapter create(List placeList) {
+ RendererBuilder builder = new RendererBuilder()
+ .bind(UploadModel.UploadItem.class, new UploadThumbnailRenderer(listener));
+ ListAdapteeCollection collection = new ListAdapteeCollection<>(
+ placeList != null ? placeList : Collections.emptyList());
+ return new RVRendererAdapter<>(builder, collection);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java
new file mode 100644
index 000000000..410914446
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java
@@ -0,0 +1,82 @@
+package fr.free.nrw.commons.upload;
+
+import android.net.Uri;
+import android.support.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.util.List;
+
+import fr.free.nrw.commons.utils.ImageUtils;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+public interface UploadView {
+ // Dummy implementation of the view interface to allow us to have a 'null object pattern'
+ // in the presenter and avoid constant NULL checking.
+// UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
+// new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null);
+
+ List getDescriptions();
+
+ @Retention(SOURCE)
+ @IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE})
+ @interface UploadPage {}
+
+ int PLEASE_WAIT = 0;
+
+ int TITLE_CARD = 1;
+ int CATEGORIES = 2;
+ int LICENSE = 3;
+
+ boolean checkIfLoggedIn();
+
+ void updateThumbnails(List uploads);
+
+ void setNextEnabled(boolean available);
+
+ void setSubmitEnabled(boolean available);
+
+ void setPreviousEnabled(boolean available);
+
+ void setTopCardState(boolean state);
+
+ void setRightCardVisibility(boolean visible);
+
+ void setBottomCardState(boolean state);
+
+ void setRightCardState(boolean bottomCardState);
+
+ void setBackground(Uri mediaUri);
+
+ void setTopCardVisibility(boolean visible);
+
+ void setBottomCardVisibility(boolean visible);
+
+ void setBottomCardVisibility(@UploadPage int page);
+
+ void updateRightCardContent(boolean gpsPresent);
+
+ void updateBottomCardContent(int currentStep, int stepCount, UploadModel.UploadItem uploadItem, boolean isShowingItem);
+
+ void updateLicenses(List licenses, String selectedLicense);
+
+ void updateLicenseSummary(String selectedLicense);
+
+ void updateTopCardContent();
+
+ void dismissKeyboard();
+
+ void showBadPicturePopup(@ImageUtils.Result int errorMessage);
+
+ void showDuplicatePicturePopup();
+
+ void finish();
+
+ void launchMapActivity(String decCoords);
+
+ void showErrorMessage(int resourceId);
+
+ void initDefaultCategories();
+
+ void showNoCategorySelectedWarning();
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java
index 53aaaa106..a28fde579 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java
@@ -7,61 +7,61 @@ import java.util.HashMap;
* info in the user language
*/
public class UrlLicense {
- HashMap urlLicense = new HashMap();
- public void initialize(){
- urlLicense.put("en","https://commons.wikimedia.org/wiki/Commons:Licensing");
- urlLicense.put("ar","https://commons.wikimedia.org/wiki/Commons:Licensing/ar");
- urlLicense.put("ast","https://commons.wikimedia.org/wiki/Commons:Licensing/ast");
- urlLicense.put("az","https://commons.wikimedia.org/wiki/Commons:Licensing/az");
- urlLicense.put("be","https://commons.wikimedia.org/wiki/Commons:Licensing/be");
- urlLicense.put("bg","https://commons.wikimedia.org/wiki/Commons:Licensing/bg");
- urlLicense.put("bn","https://commons.wikimedia.org/wiki/Commons:Licensing/bn");
- urlLicense.put("ca","https://commons.wikimedia.org/wiki/Commons:Licensing/ca");
- urlLicense.put("cs","https://commons.wikimedia.org/wiki/Commons:Licensing/cs");
- urlLicense.put("da","https://commons.wikimedia.org/wiki/Commons:Licensing/da");
- urlLicense.put("de","https://commons.wikimedia.org/wiki/Commons:Licensing/de");
- urlLicense.put("el","https://commons.wikimedia.org/wiki/Commons:Licensing/el");
- urlLicense.put("eo","https://commons.wikimedia.org/wiki/Commons:Licensing/eo");
- urlLicense.put("es","https://commons.wikimedia.org/wiki/Commons:Licensing/es");
- urlLicense.put("eu","https://commons.wikimedia.org/wiki/Commons:Licensing/eu");
- urlLicense.put("fa","https://commons.wikimedia.org/wiki/Commons:Licensing/fa");
- urlLicense.put("fi","https://commons.wikimedia.org/wiki/Commons:Licensing/fi");
- urlLicense.put("fr","https://commons.wikimedia.org/wiki/Commons:Licensing/fr");
- urlLicense.put("gl","https://commons.wikimedia.org/wiki/Commons:Licensing/gl");
- urlLicense.put("gsw","https://commons.wikimedia.org/wiki/Commons:Licensing/gsw");
- urlLicense.put("he","https://commons.wikimedia.org/wiki/Commons:Licensing/he");
- urlLicense.put("hi","https://commons.wikimedia.org/wiki/Commons:Licensing/hi");
- urlLicense.put("hu","https://commons.wikimedia.org/wiki/Commons:Licensing/hu");
- urlLicense.put("id","https://commons.wikimedia.org/wiki/Commons:Licensing/id");
- urlLicense.put("is","https://commons.wikimedia.org/wiki/Commons:Licensing/is");
- urlLicense.put("it","https://commons.wikimedia.org/wiki/Commons:Licensing/it");
- urlLicense.put("ja","https://commons.wikimedia.org/wiki/Commons:Licensing/ja");
- urlLicense.put("ka","https://commons.wikimedia.org/wiki/Commons:Licensing/ka");
- urlLicense.put("km","https://commons.wikimedia.org/wiki/Commons:Licensing/km");
- urlLicense.put("ko","https://commons.wikimedia.org/wiki/Commons:Licensing/ko");
- urlLicense.put("ku","https://commons.wikimedia.org/wiki/Commons:Licensing/ku");
- urlLicense.put("mk","https://commons.wikimedia.org/wiki/Commons:Licensing/mk");
- urlLicense.put("mr","https://commons.wikimedia.org/wiki/Commons:Licensing/mr");
- urlLicense.put("ms","https://commons.wikimedia.org/wiki/Commons:Licensing/ms");
- urlLicense.put("my","https://commons.wikimedia.org/wiki/Commons:Licensing/my");
- urlLicense.put("nl","https://commons.wikimedia.org/wiki/Commons:Licensing/nl");
- urlLicense.put("oc","https://commons.wikimedia.org/wiki/Commons:Licensing/oc");
- urlLicense.put("pl","https://commons.wikimedia.org/wiki/Commons:Licensing/pl");
- urlLicense.put("pt","https://commons.wikimedia.org/wiki/Commons:Licensing/pt");
- urlLicense.put("pt-br","https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br");
- urlLicense.put("ro","https://commons.wikimedia.org/wiki/Commons:Licensing/ro");
- urlLicense.put("ru","https://commons.wikimedia.org/wiki/Commons:Licensing/ru");
- urlLicense.put("scn","https://commons.wikimedia.org/wiki/Commons:Licensing/scn");
- urlLicense.put("sk","https://commons.wikimedia.org/wiki/Commons:Licensing/sk");
- urlLicense.put("sl","https://commons.wikimedia.org/wiki/Commons:Licensing/sl");
- urlLicense.put("sv","https://commons.wikimedia.org/wiki/Commons:Licensing/sv");
- urlLicense.put("tr","https://commons.wikimedia.org/wiki/Commons:Licensing/tr");
- urlLicense.put("uk","https://commons.wikimedia.org/wiki/Commons:Licensing/uk");
- urlLicense.put("ur","https://commons.wikimedia.org/wiki/Commons:Licensing/ur");
- urlLicense.put("vi","https://commons.wikimedia.org/wiki/Commons:Licensing/vi");
- urlLicense.put("zh","https://commons.wikimedia.org/wiki/Commons:Licensing/zh");
+ public static HashMap urlLicense = new HashMap<>();
+ static {
+ urlLicense.put("en", "https://commons.wikimedia.org/wiki/Commons:Licensing");
+ urlLicense.put("ar", "https://commons.wikimedia.org/wiki/Commons:Licensing/ar");
+ urlLicense.put("ast", "https://commons.wikimedia.org/wiki/Commons:Licensing/ast");
+ urlLicense.put("az", "https://commons.wikimedia.org/wiki/Commons:Licensing/az");
+ urlLicense.put("be", "https://commons.wikimedia.org/wiki/Commons:Licensing/be");
+ urlLicense.put("bg", "https://commons.wikimedia.org/wiki/Commons:Licensing/bg");
+ urlLicense.put("bn", "https://commons.wikimedia.org/wiki/Commons:Licensing/bn");
+ urlLicense.put("ca", "https://commons.wikimedia.org/wiki/Commons:Licensing/ca");
+ urlLicense.put("cs", "https://commons.wikimedia.org/wiki/Commons:Licensing/cs");
+ urlLicense.put("da", "https://commons.wikimedia.org/wiki/Commons:Licensing/da");
+ urlLicense.put("de", "https://commons.wikimedia.org/wiki/Commons:Licensing/de");
+ urlLicense.put("el", "https://commons.wikimedia.org/wiki/Commons:Licensing/el");
+ urlLicense.put("eo", "https://commons.wikimedia.org/wiki/Commons:Licensing/eo");
+ urlLicense.put("es", "https://commons.wikimedia.org/wiki/Commons:Licensing/es");
+ urlLicense.put("eu", "https://commons.wikimedia.org/wiki/Commons:Licensing/eu");
+ urlLicense.put("fa", "https://commons.wikimedia.org/wiki/Commons:Licensing/fa");
+ urlLicense.put("fi", "https://commons.wikimedia.org/wiki/Commons:Licensing/fi");
+ urlLicense.put("fr", "https://commons.wikimedia.org/wiki/Commons:Licensing/fr");
+ urlLicense.put("gl", "https://commons.wikimedia.org/wiki/Commons:Licensing/gl");
+ urlLicense.put("gsw", "https://commons.wikimedia.org/wiki/Commons:Licensing/gsw");
+ urlLicense.put("he", "https://commons.wikimedia.org/wiki/Commons:Licensing/he");
+ urlLicense.put("hi", "https://commons.wikimedia.org/wiki/Commons:Licensing/hi");
+ urlLicense.put("hu", "https://commons.wikimedia.org/wiki/Commons:Licensing/hu");
+ urlLicense.put("id", "https://commons.wikimedia.org/wiki/Commons:Licensing/id");
+ urlLicense.put("is", "https://commons.wikimedia.org/wiki/Commons:Licensing/is");
+ urlLicense.put("it", "https://commons.wikimedia.org/wiki/Commons:Licensing/it");
+ urlLicense.put("ja", "https://commons.wikimedia.org/wiki/Commons:Licensing/ja");
+ urlLicense.put("ka", "https://commons.wikimedia.org/wiki/Commons:Licensing/ka");
+ urlLicense.put("km", "https://commons.wikimedia.org/wiki/Commons:Licensing/km");
+ urlLicense.put("ko", "https://commons.wikimedia.org/wiki/Commons:Licensing/ko");
+ urlLicense.put("ku", "https://commons.wikimedia.org/wiki/Commons:Licensing/ku");
+ urlLicense.put("mk", "https://commons.wikimedia.org/wiki/Commons:Licensing/mk");
+ urlLicense.put("mr", "https://commons.wikimedia.org/wiki/Commons:Licensing/mr");
+ urlLicense.put("ms", "https://commons.wikimedia.org/wiki/Commons:Licensing/ms");
+ urlLicense.put("my", "https://commons.wikimedia.org/wiki/Commons:Licensing/my");
+ urlLicense.put("nl", "https://commons.wikimedia.org/wiki/Commons:Licensing/nl");
+ urlLicense.put("oc", "https://commons.wikimedia.org/wiki/Commons:Licensing/oc");
+ urlLicense.put("pl", "https://commons.wikimedia.org/wiki/Commons:Licensing/pl");
+ urlLicense.put("pt", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt");
+ urlLicense.put("pt-br", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br");
+ urlLicense.put("ro", "https://commons.wikimedia.org/wiki/Commons:Licensing/ro");
+ urlLicense.put("ru", "https://commons.wikimedia.org/wiki/Commons:Licensing/ru");
+ urlLicense.put("scn", "https://commons.wikimedia.org/wiki/Commons:Licensing/scn");
+ urlLicense.put("sk", "https://commons.wikimedia.org/wiki/Commons:Licensing/sk");
+ urlLicense.put("sl", "https://commons.wikimedia.org/wiki/Commons:Licensing/sl");
+ urlLicense.put("sv", "https://commons.wikimedia.org/wiki/Commons:Licensing/sv");
+ urlLicense.put("tr", "https://commons.wikimedia.org/wiki/Commons:Licensing/tr");
+ urlLicense.put("uk", "https://commons.wikimedia.org/wiki/Commons:Licensing/uk");
+ urlLicense.put("ur", "https://commons.wikimedia.org/wiki/Commons:Licensing/ur");
+ urlLicense.put("vi", "https://commons.wikimedia.org/wiki/Commons:Licensing/vi");
+ urlLicense.put("zh", "https://commons.wikimedia.org/wiki/Commons:Licensing/zh");
}
- public String getLicenseUrl ( String language){
+ public static String getLicenseUrl ( String language){
if (urlLicense.containsKey(language)) {
return urlLicense.get(language);
} else {
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java
new file mode 100644
index 000000000..92f9f1935
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java
@@ -0,0 +1,30 @@
+package fr.free.nrw.commons.utils;
+
+import android.support.annotation.NonNull;
+import android.text.Editable;
+import android.text.TextWatcher;
+
+public class AbstractTextWatcher implements TextWatcher {
+ private final TextChange textChange;
+
+ public AbstractTextWatcher(@NonNull TextChange textChange) {
+ this.textChange = textChange;
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ textChange.onTextChanged(s.toString());
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+
+ public interface TextChange {
+ void onTextChanged(String value);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java b/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java
new file mode 100644
index 000000000..227f5f024
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java
@@ -0,0 +1,41 @@
+package fr.free.nrw.commons.utils;
+
+import java.util.HashMap;
+import java.util.Set;
+
+/**
+ * HashMap that can be searched in both the forward and reverse directions.
+ */
+public class BiMap {
+
+ private HashMap map = new HashMap();
+ private HashMap inversedMap = new HashMap();
+
+ public void put(K k, V v) {
+ map.put(k, v);
+ inversedMap.put(v, k);
+ }
+
+ public V get(K k) {
+ return map.get(k);
+ }
+
+ public K getKey(V v) {
+ return inversedMap.get(v);
+ }
+
+ public Set getEntrySet(){
+ return inversedMap.keySet();
+ }
+
+ public void remove(K k){
+ inversedMap.remove(map.remove(k));
+ }
+
+
+ public boolean containsKey(V v){
+ return inversedMap.containsKey(v);
+ }
+
+}
+
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java
index 2e4592e40..f68037488 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java
@@ -5,6 +5,7 @@ import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.content.Context;
+import android.content.DialogInterface;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
@@ -114,7 +115,49 @@ public class DialogUtil {
.setIcon(iconResourceId).create();
return alertDialog;
+ }
+ public static void showAlertDialog(Activity activity,
+ String title,
+ String message,
+ final Runnable onPositiveBtnClick,
+ final Runnable onNegativeBtnClick) {
+ showAlertDialog(activity,
+ title,
+ message,
+ activity.getString(R.string.no),
+ activity.getString(R.string.yes),
+ onPositiveBtnClick,
+ onNegativeBtnClick);
+ }
+
+ public static void showAlertDialog(Activity activity,
+ String title,
+ String message,
+ String positiveButtonText,
+ String negativeButtonText,
+ final Runnable onPositiveBtnClick,
+ final Runnable onNegativeBtnClick) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(title);
+ builder.setMessage(message);
+
+ builder.setPositiveButton(positiveButtonText, (dialogInterface, i) -> {
+ dialogInterface.dismiss();
+ if (onPositiveBtnClick != null) {
+ onPositiveBtnClick.run();
+ }
+ });
+
+ builder.setNegativeButton(negativeButtonText, (DialogInterface dialogInterface, int i) -> {
+ dialogInterface.dismiss();
+ if (onNegativeBtnClick != null) {
+ onNegativeBtnClick.run();
+ }
+ });
+
+ AlertDialog dialog = builder.create();
+ showSafely(activity, dialog);
}
public interface Callback {
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
index 460046bab..e6cc2fc5d 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
@@ -7,6 +7,7 @@ import android.graphics.BitmapRegionDecoder;
import android.graphics.Color;
import android.graphics.Rect;
import android.net.Uri;
+import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.facebook.common.executors.CallerThreadExecutor;
@@ -20,6 +21,8 @@ import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import fr.free.nrw.commons.R;
import timber.log.Timber;
@@ -30,20 +33,44 @@ import timber.log.Timber;
public class ImageUtils {
- public enum Result {
- IMAGE_DARK,
- IMAGE_OK
+ public static final int IMAGE_DARK = 1;
+ public static final int IMAGE_BLURRY = 1 << 1;
+ public static final int IMAGE_DUPLICATE = 1 << 2;
+ public static final int IMAGE_OK = 0;
+ public static final int IMAGE_KEEP = -1;
+ public static final int IMAGE_WAIT = -2;
+ public static final int EMPTY_TITLE = -3;
+ public static final int FILE_NAME_EXISTS = -4;
+ public static final int NO_CATEGORY_SELECTED = -5;
+
+ @IntDef(
+ flag = true,
+ value = {
+ IMAGE_DARK,
+ IMAGE_BLURRY,
+ IMAGE_DUPLICATE,
+ IMAGE_OK,
+ IMAGE_KEEP,
+ IMAGE_WAIT,
+ EMPTY_TITLE,
+ FILE_NAME_EXISTS,
+ NO_CATEGORY_SELECTED
+ }
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Result {
}
/**
* @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process
- * @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
- * Result.IMAGE_DARK if image is too dark
+ * @return IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
+ * IMAGE_DARK if image is too dark
*/
- public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
+ public static @Result
+ int checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
if (bitmapRegionDecoder == null) {
Timber.e("Expected bitmapRegionDecoder was null");
- return Result.IMAGE_OK;
+ return IMAGE_OK;
}
int loadImageHeight = bitmapRegionDecoder.getHeight();
@@ -59,10 +86,10 @@ public class ImageUtils {
Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null);
if (checkIfImageIsDark(processBitmap)) {
- return Result.IMAGE_DARK;
+ return IMAGE_DARK;
}
- return Result.IMAGE_OK;
+ return IMAGE_OK;
}
/**
@@ -132,8 +159,9 @@ public class ImageUtils {
/**
* Downloads the image from the URL and sets it as the phone's wallpaper
* Fails silently if download or setting wallpaper fails.
- * @param context
- * @param imageUrl
+ *
+ * @param context context
+ * @param imageUrl Url of the image
*/
public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) {
Timber.d("Trying to set wallpaper from url %s", imageUrl.toString());
@@ -150,7 +178,7 @@ public class ImageUtils {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
- if (dataSource.isFinished() && bitmap != null){
+ if (dataSource.isFinished() && bitmap != null) {
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
setWallpaper(context, Bitmap.createBitmap(bitmap));
dataSource.close();
@@ -173,7 +201,29 @@ public class ImageUtils {
wallpaperManager.setBitmap(bitmap);
ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully));
} catch (IOException e) {
- Timber.e(e,"Error setting wallpaper");
+ Timber.e(e, "Error setting wallpaper");
}
}
+
+ public static String getErrorMessageForResult(Context context, @Result int result) {
+ String errorMessage;
+ if (result == ImageUtils.IMAGE_DARK)
+ errorMessage = context.getString(R.string.upload_image_problem_dark);
+ else if (result == ImageUtils.IMAGE_BLURRY)
+ errorMessage = context.getString(R.string.upload_image_problem_blurry);
+ else if (result == ImageUtils.IMAGE_DUPLICATE)
+ errorMessage = context.getString(R.string.upload_image_problem_duplicate);
+ else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY))
+ errorMessage = context.getString(R.string.upload_image_problem_dark_blurry);
+ else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_DUPLICATE))
+ errorMessage = context.getString(R.string.upload_image_problem_dark_duplicate);
+ else if (result == (ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
+ errorMessage = context.getString(R.string.upload_image_problem_blurry_duplicate);
+ else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
+ errorMessage = context.getString(R.string.upload_image_problem_dark_blurry_duplicate);
+ else
+ return "";
+
+ return errorMessage;
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java
index 8595634d5..d76e2ff5f 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java
@@ -9,6 +9,7 @@ import android.support.v4.content.ContextCompat;
import fr.free.nrw.commons.CommonsApplication;
+
public class PermissionUtils {
public static final int CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST = 100;
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java
index 0eb8216e4..0f93e65ef 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java
@@ -12,4 +12,8 @@ public class StringUtils {
return Html.fromHtml(source).toString();
}
}
+
+ public static boolean isNullOrWhiteSpace(String value) {
+ return value == null || value.trim().isEmpty();
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java
index a2c25c948..aef3dddb0 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java
@@ -2,6 +2,7 @@ package fr.free.nrw.commons.utils;
import android.app.Activity;
import android.content.Context;
+import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.view.Display;
import android.view.View;
@@ -32,6 +33,30 @@ public class ViewUtil {
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show());
}
+ public static void showLongToast(Context context, @StringRes int stringResourceId) {
+ if (context == null) {
+ return;
+ }
+
+ ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show());
+ }
+
+ public static void showShortToast(Context context, String text) {
+ if (context == null) {
+ return;
+ }
+
+ ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_SHORT).show());
+ }
+
+ public static void showShortToast(Context context, @StringRes int stringResourceId) {
+ if (context == null) {
+ return;
+ }
+
+ ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show());
+ }
+
public static boolean isPortrait(Context context) {
Display orientation = ((Activity)context).getWindowManager().getDefaultDisplay();
if (orientation.getWidth() < orientation.getHeight()){
diff --git a/app/src/main/res/drawable/ic_error_red_24dp.xml b/app/src/main/res/drawable/ic_error_red_24dp.xml
new file mode 100644
index 000000000..e1569395b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_error_red_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_less_black_24dp.xml b/app/src/main/res/drawable/ic_expand_less_black_24dp.xml
new file mode 100644
index 000000000..3afdf9682
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_less_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_less_white_24dp.xml b/app/src/main/res/drawable/ic_expand_less_white_24dp.xml
new file mode 100644
index 000000000..d58421a2f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_less_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_more_black_24dp.xml b/app/src/main/res/drawable/ic_expand_more_black_24dp.xml
new file mode 100644
index 000000000..8d57dbc10
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_more_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_more_white_24dp.xml b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml
new file mode 100644
index 000000000..fd3ce4a46
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_upload.xml b/app/src/main/res/layout/activity_upload.xml
new file mode 100644
index 000000000..01414ff9f
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_upload_bottom_card.xml b/app/src/main/res/layout/activity_upload_bottom_card.xml
new file mode 100644
index 000000000..273835b5d
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_bottom_card.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_categories.xml b/app/src/main/res/layout/activity_upload_categories.xml
new file mode 100644
index 000000000..b3ae7c8a2
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_categories.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_license.xml b/app/src/main/res/layout/activity_upload_license.xml
new file mode 100644
index 000000000..0f3391128
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_license.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_please_wait.xml b/app/src/main/res/layout/activity_upload_please_wait.xml
new file mode 100644
index 000000000..e74d576e4
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_please_wait.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_right_card.xml b/app/src/main/res/layout/activity_upload_right_card.xml
new file mode 100644
index 000000000..56fb96880
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_right_card.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_upload_top_card.xml b/app/src/main/res/layout/activity_upload_top_card.xml
new file mode 100644
index 000000000..4e93d2a31
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_top_card.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_categorization.xml b/app/src/main/res/layout/fragment_categorization.xml
deleted file mode 100644
index 58a768094..000000000
--- a/app/src/main/res/layout/fragment_categorization.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_single_upload.xml b/app/src/main/res/layout/fragment_single_upload.xml
index 196760bb0..d5cba971d 100644
--- a/app/src/main/res/layout/fragment_single_upload.xml
+++ b/app/src/main/res/layout/fragment_single_upload.xml
@@ -6,13 +6,13 @@
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
+ android:nestedScrollingEnabled="false"
android:paddingBottom="@dimen/small_gap"
android:paddingEnd="@dimen/standard_gap"
android:paddingLeft="@dimen/standard_gap"
android:paddingRight="@dimen/standard_gap"
android:paddingStart="@dimen/standard_gap"
android:paddingTop="@dimen/small_gap"
- android:nestedScrollingEnabled="false"
android:theme="@style/DarkAppTheme">
-
-
-
-
+ android:orientation="vertical">
+
+
+
+
+
+
+
+
+ android:gravity="center"
+ android:text="@string/share_license_summary"
+ android:textColorLink="@color/button_blue" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_categories_item.xml b/app/src/main/res/layout/layout_categories_item.xml
index d301e9098..6fd154344 100644
--- a/app/src/main/res/layout/layout_categories_item.xml
+++ b/app/src/main/res/layout/layout_categories_item.xml
@@ -7,6 +7,6 @@
android:checked="false"
android:gravity="center_vertical"
android:padding="@dimen/tiny_gap"
- android:theme="@style/DarkAppTheme">
+ android:textColor="@color/primaryDarkColor">
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_upload_categories_item.xml b/app/src/main/res/layout/layout_upload_categories_item.xml
new file mode 100644
index 000000000..7379fb199
--- /dev/null
+++ b/app/src/main/res/layout/layout_upload_categories_item.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/row_item_description.xml b/app/src/main/res/layout/row_item_description.xml
index 8b49f0902..f626abcfa 100644
--- a/app/src/main/res/layout/row_item_description.xml
+++ b/app/src/main/res/layout/row_item_description.xml
@@ -1,36 +1,35 @@
-
-
-
+ android:orientation="horizontal"
+ android:weightSum="8">
-
+
-
-
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/row_item_title.xml b/app/src/main/res/layout/row_item_title.xml
new file mode 100644
index 000000000..3a2262067
--- /dev/null
+++ b/app/src/main/res/layout/row_item_title.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 17febe602..7a12bd3e9 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -240,8 +240,8 @@
خطأ أثناء تخزين الصورعنوان وصفي فريد للملف، والذي سيكون بمثابة اسم الملف، يمكنك استخدام لغة واضحة مع مسافات، لا تقم بتضمين امتداد الملفيُرجَى وصف الوسائط قدر الإمكان: أين تم التقاطها؟ ما تظهر؟ ما هو السياق؟ يُرجَى وصف الأشياء أو الأشخاص، اكشف المعلومات التي لا يمكن تخمينها بسهولة، على سبيل المثال الوقت في اليوم إذا كان منظرا طبيعيا، إذا أظهرت الوسائط شيئا غير عادي، فيُرجَى توضيح ما يجعله غير عادي.
- هذه الصورة مظلمة للغاية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.
- هذه الصورة ضبابية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.
+ هذه الصورة مظلمة للغاية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.
+ هذه الصورة ضبابية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.إعطاء السماحاستخدم تخزينا خارجيااحفظ الصور الملتقطة بالكاميرا داخل التطبيق على جهازك
diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml
index 38526b387..d8d36226c 100644
--- a/app/src/main/res/values-ast/strings.xml
+++ b/app/src/main/res/values-ast/strings.xml
@@ -168,7 +168,7 @@
Llugares cercanosNun s\'alcontraron llugares cercanosAvisu
- Esti ficheru yá esiste\'n Commons. ¿Confirmes que quies siguir?
+ Esti ficheru yá esiste\'n Commons. ¿Confirmes que quies siguir?SíNonTítulu
@@ -230,8 +230,8 @@
Error al poner les fotos na cachéUn títulu descriptivu únicu pal ficheru, que sirvirá para da-y nome al mesmu. Puede usase llinguaxe normal con espacios. Nun incluyas la estensión del ficheruPor favor, describi l\'elementu multimedia tantu como sía posible: ¿ónde se tomó?, ¿qué amuesa?, ¿cuál ye\'l contestu? Por favor, describi los oxetos o persones. Revela la información que nun pueda aldovinase de mou cenciellu, por casu el momentu del día si ye un paisaxe. Si\'l mediu amuesa daqué desacostumao, esplica qué lo fai raro.
- Esta imaxe ye escura enforma, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.
- Esta imaxe ta borrosa, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.
+ Esta imaxe ye escura enforma, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.
+ Esta imaxe ta borrosa, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.Dar permisuUsar almacenamientu esternuGuardar nel preséu les imaxes tomaes cola cámara de la app
diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml
index cd36cebd4..83bdd49f9 100644
--- a/app/src/main/res/values-b+sr+Latn/strings.xml
+++ b/app/src/main/res/values-b+sr+Latn/strings.xml
@@ -169,7 +169,7 @@
Mesta u bliziniNisu pronađena obližnja mestaUpozorenje
- Ova datoteka je već dostupna na Ostavi. Da li ste sigurni da želite da nastavite?
+ Ova datoteka je već dostupna na Ostavi. Da li ste sigurni da želite da nastavite?DaNeNaslov
diff --git a/app/src/main/res/values-ba/strings.xml b/app/src/main/res/values-ba/strings.xml
index 53916bbe2..58ae9cd4b 100644
--- a/app/src/main/res/values-ba/strings.xml
+++ b/app/src/main/res/values-ba/strings.xml
@@ -157,7 +157,7 @@
Мотлаҡ булмаған рөхсәт: категория тәҡдиме өсөн ошо урынды алыуЯҡындағы урындарЯҡындағы урындар табылманы
- Был файл Викискладта бар. Дауам итергә ризаһыңмы?
+ Был файл Викискладта бар. Дауам итергә ризаһыңмы?АтамаМәғлүмәт йөрөтөүсенең атамаһыМәғлүмәт йөрөтөүсене һүрәтләү ошонда яҙыла.Уның ярайһы уҡ оҙон, хатта бер-нисә юлға һуҙылып китеүе лә бар. Шулай булһа ла ул бик матур күренер тип уйлайбыҙ.
@@ -204,8 +204,8 @@
Рәсемде кэшлағандағы хатаФайлдың исеме булараҡ һаҡланасаҡ үҙенсәлекле һәртәләү. Тәбиғи телегеҙҙе, һүҙҙәр араһын айырып, ҡулланырға була. Зинһар, файл киңәйтеүҙәрен күрһәтмәгеҙ.Зинһар, тейәләсәк файлды тәфсирләп һүрәтлә:ҡайҙа төшөрөлгән? нимә һәрәтләнә? һүрәт нимәне аңлата? Рәсемдәге кешеләр йәки объекттарҙы ла һүрәтлә. Һүрәткә ҡарап ҡына белеп булмаған мәғлүмәттәрҙе өҫтә: мәҫәлән, тәүлектең ниндәй мәлендә, ҡасан төшөрөлгән был файл. Әгәр ғәҙәти булмаған әйбер төшөрөлһә, уның нимәһе шаҡ ҡатырғанын аңлат.
- Был рәсем бик ҡараңғы күренә. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.
- Был рәсем асыҡ түгел. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.
+ Был рәсем бик ҡараңғы күренә. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.
+ Был рәсем асыҡ түгел. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.Рөхсәт бирәмТышҡы һаҡлағысты ҡулланҠулайламаның камераһы ярҙамында төшөрөлгән һүрәттәрҙе һаҡлау
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 0c8b40906..b6295448b 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -76,7 +76,7 @@
ОбновяванеДобреПредупреждение
- Този файл вече съществува в Общомедия. Наистина ли искате да продължите?
+ Този файл вече съществува в Общомедия. Наистина ли искате да продължите?ДаНеЗаглавие
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
index 1b402196d..76bdae2c6 100644
--- a/app/src/main/res/values-bn/strings.xml
+++ b/app/src/main/res/values-bn/strings.xml
@@ -178,7 +178,7 @@
কাছাকাছি স্থানকাছাকাছি কোন স্থান পাওয়া যায়নিসতর্কীকরণ
- এই ফাইলটি ইতিমধ্যে কমন্সে বিদ্যমান। আপনি কি নিশ্চিত আপনি সামনে এগুতে চান?
+ এই ফাইলটি ইতিমধ্যে কমন্সে বিদ্যমান। আপনি কি নিশ্চিত আপনি সামনে এগুতে চান?হ্যাঁনাশিরোনাম
@@ -239,8 +239,8 @@
ছবি আনার সময় ত্রুটিফাইলের একটি স্বতন্ত্র বর্ণনামূলক নাম যা ফাইলের নাম হিসাবে কাজ করবে। অাপনি সাধারণ ভাষা ব্যবহার করতে পারেন শূন্যস্থানসহ। ফাইলের এক্সটেনশন যুক্ত করবেন না।যতটা সম্ভব মিডিয়াটি বর্ণনা করুন: এটি কোথায় ধারণ করা হয়েছিল? এটি কি প্রদর্শন করে? এটির প্রসঙ্গ কি? ধারণকৃত বস্তু অথবা ব্যক্তির বর্ণনা করুন। সহজে অনুমান করা যায়না সেরকম তথ্য উদঘাটন করুন, উদাহরণস্বরূপ, যদি ল্যান্ডস্কেপ হয় তাহলে দিবসকালের সময় দিন।
- এই ছবিটি খুবই অন্ধকারময়, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।
- এই ছবিটি অস্পষ্ট, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।
+ এই ছবিটি খুবই অন্ধকারময়, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।
+ এই ছবিটি অস্পষ্ট, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।অনুমতি দিনবাহ্যিক সঞ্চয়স্থান ব্যবহার করুনঅাপনার ডিভাইসের নিজস্ব ক্যামেরায় ধারণকৃত ছবি সংরক্ষণ করুন
diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml
index bee0df5c9..5135a7236 100644
--- a/app/src/main/res/values-br/strings.xml
+++ b/app/src/main/res/values-br/strings.xml
@@ -166,7 +166,7 @@
Lec\'hioù nesN\'eus bet kavet netra tostikDiwallit
- Emañ ar restr-mañ war Commons c\'hoazh. Ha sur oc\'h e fell deoc\'h kenderc\'hel ?
+ Emañ ar restr-mañ war Commons c\'hoazh. Ha sur oc\'h e fell deoc\'h kenderc\'hel ?YaKetTitl
diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml
index 1a8253fa3..1c456a749 100644
--- a/app/src/main/res/values-bs/strings.xml
+++ b/app/src/main/res/values-bs/strings.xml
@@ -142,7 +142,7 @@
Mjesta u bliziniNema okolnih mjestaUpozorenje
- Ova datoteka već postoji na Commonsu. Jeste li sigurni da želite nastaviti?
+ Ova datoteka već postoji na Commonsu. Jeste li sigurni da želite nastaviti?DaNeNaslov
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index d0027df81..951fbce09 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -139,7 +139,7 @@
Llocs propersNo s\'han trobat llocs propersAvís
- El fitxer ja existeix a Commons. Segur que voleu procedir?
+ El fitxer ja existeix a Commons. Segur que voleu procedir?SíNoTítol
diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml
index 5f892b17f..36697a1a1 100644
--- a/app/src/main/res/values-ckb/strings.xml
+++ b/app/src/main/res/values-ckb/strings.xml
@@ -41,7 +41,7 @@
باشەشوێنە نزیکەکانئاگاداری
- ئەم پەڕگەیە لەسەر کۆمنز ھەیە. دڵنیایت کە دەتەوێت بەردەوام بیت؟
+ ئەم پەڕگەیە لەسەر کۆمنز ھەیە. دڵنیایت کە دەتەوێت بەردەوام بیت؟بەڵێنەخێرناونیشان
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 267a93eaf..cecfce340 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -181,7 +181,7 @@
Místa v okolíPoblíž nebylo nic nalezenoUpozornění
- Tento soubor již na Commons existuje. Jste si jist, že chcete pokračovat?
+ Tento soubor již na Commons existuje. Jste si jist, že chcete pokračovat?AnoNeNázev
@@ -243,8 +243,8 @@
Chyba při meziukládání obrázkůUnikátní a popisný název pro daný soubor, který bude sloužit jako název souboru. Můžete použít běžný psaný jazyk s mezerami; nezahrnujte koncovku souboru.Popište prosím obrázek, jak jen to je možné: Kde byl pořízen? Co znázorňuje? Jaký je kontext obrázku? Popisujte prosím významné předměty nebo osoby na obrázku a nezapomeňte na informace, které není možné snadno odhadnout ze samotného obrázku, jako je například denní doba, pokud jde o krajinu. Pokud je na obrázku něco neobvyklého, popište, co to dělá neobvyklým.
- Tento obrázek je příliš tmavý, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.
- Tento obrázek je rozmazaný, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.
+ Tento obrázek je příliš tmavý, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.
+ Tento obrázek je rozmazaný, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.Dát povoleníPoužít externí úložištěUložit obrázky pořízené fotoaparátem, jenž je součástí této aplikace
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index a065b878f..af29073ab 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -174,7 +174,7 @@
Steder i nærhedenIngen steder i nærheden fundetAdvarsel
- Denne fil findes allerede på Commons. Er du sikker på, at du ønsker at fortsætte?
+ Denne fil findes allerede på Commons. Er du sikker på, at du ønsker at fortsætte?JaNejTitel
@@ -235,8 +235,8 @@
Fejl under mellemlagring af billederEn unik beskrivelse for filen, som vil fungere som et filnavn. Du kan bruge normalt sprog med mellemrum. Udelad filendelsen.Beskriv mediet så godt som muligt: Hvor blev det taget? Hvad viser det? Hvad er konteksten? Beskriv objekterne eller personerne. Giv information som ikke nemt kan gættes, for eksempel hvornår på dagen billedet blev taget, om det er et landskabsbillede. Om billedet viser noget usædvanligt, forklar hvad som gør det usædvanlig.
- Billedet er for mørkt. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder encyklopædisk værdi.
- Dette billede er sløret. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder med encyklopædisk værdi.
+ Billedet er for mørkt. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder encyklopædisk værdi.
+ Dette billede er sløret. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder med encyklopædisk værdi.Giv tilladelseBrug eksternt lagerGem billeder taget med din enheds program på kameraet
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index f4f078e31..77220b744 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -173,7 +173,7 @@
Orte in der NäheKeine Orte in der Nähe gefundenWarnung
- Diese Datei ist bereits auf Commons vorhanden. Bist du sicher, dass du fortfahren möchtest?
+ Diese Datei ist bereits auf Commons vorhanden. Bist du sicher, dass du fortfahren möchtest?JaNeinTitel
@@ -235,8 +235,8 @@
Fehler beim Zwischenspeichern der BilderEin eindeutiger beschreibender Titel für die Datei, der als Dateiname dient. Du kannst Klartext mit Leerzeichen verwenden. Gib nicht die Dateierweiterung mit an.Bitte beschreibe das Medium so gut wie möglich: Wo wurde es aufgenommen? Was zeigt es? Was ist der Kontext? Bitte beschreibe die Objekte oder Personen. Zeige Informationen auf, die nicht einfach erraten werden können, zum Beispiel die Tageszeit, falls es eine Landschaft ist. Falls das Medium etwas Ungewöhnliches zeigt, erkläre bitte, was es ungewöhnlich macht.
- Dieses Bild ist zu dunkel. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.
- Dieses Bild ist verschwommen. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.
+ Dieses Bild ist zu dunkel. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.
+ Dieses Bild ist verschwommen. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.Berechtigung gebenExternen Speicher verwendenMit der In-App-Kamera aufgenommene Bilder auf deinem Gerät speichern
diff --git a/app/src/main/res/values-dty/strings.xml b/app/src/main/res/values-dty/strings.xml
index 09c0426ab..94f6fa993 100644
--- a/app/src/main/res/values-dty/strings.xml
+++ b/app/src/main/res/values-dty/strings.xml
@@ -46,7 +46,7 @@
हुन्छनज्यूकाऽ ठउरअनचेतावनी
- यो फाइल कमन्स मी पैली बठेइ छ। तम पक्का छऽ कि तम ऐतिर जान चाहन्छऽ?
+ यो फाइल कमन्स मी पैली बठेइ छ। तम पक्का छऽ कि तम ऐतिर जान चाहन्छऽ?होनाइँशीर्षक
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 8928400b3..6228fff6b 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -176,7 +176,7 @@
Κοντινοί ΤόποιΔεν βρέθηκαν τόποι εδώ κοντάΠροειδοποίηση
- Αυτό το αρχείο υπάρχει ήδη στα Commons. Είστε σίγουρος ότι θέλετε να συνεχίσετε;
+ Αυτό το αρχείο υπάρχει ήδη στα Commons. Είστε σίγουρος ότι θέλετε να συνεχίσετε;ΝαιΌχιΤίτλος
@@ -238,8 +238,8 @@
Υπήρξε σφάλμα κατά την σκίαση εικόνωνΈνας μοναδικός τίτλος περιγραφής του φακέλλου, που θα χρησιμεύσει ως όνομα φακέλλου. Μπορείτε να χρησιμοποιήσετε τις ήδη υπάρχουσες γλώσσες με διαστήματα. Μην συμπεριλάβετε την επέκταση φακέλλου\nΠαρακαλώ περιγράψετε τα μέσα το δυνατό περισσότερο : Πού οδηγήθηκε αυτό; Τι δείχνει; Ποιο είναι το περιεχόμενο του; Παρακαλώ περιγράψετε τα αντικείμενα ή τα πρόσωπα. Αποκαλύψετε πληροφορίες που δεν μπορούν εύκολο να μαντέψει κανείς, για παράδειγμα την ώρα εντός της ημέρας αν πρόκειται για τοπίο. Αν τα μέσα δείξουν κάτι ασύνηθες, παρακαλώ εξηγήστε τι το καθιστά μη συνηθισμένα.
- Αυτή η εικόνα είναι πολύ σκοτεινή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.
- Αυτή η εικόνα είναι θολή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.
+ Αυτή η εικόνα είναι πολύ σκοτεινή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.
+ Αυτή η εικόνα είναι θολή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.Χορηγήστε άδειαΧρησιμοποιήσετε την εξωτερική αποθήκευσηΑποθηκεύσετε εικόνες που παίρνονται στην κάμερα εφαρμογής στην συσκευή σας
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 0929b0c58..d3d4388a1 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -181,7 +181,7 @@
Lugares cercanosNo se encontraron lugares cercanosAtención
- Este archivo ya existe en Commons. ¿Confirmas que quieres continuar?
+ Este archivo ya existe en Commons. ¿Confirmas que quieres continuar?SíNoTítulo
@@ -243,8 +243,8 @@
Error al almacenar imágenes en la antememoriaUn título único descriptivo para el archivo, que servirá como un nombre de archivo. Puede usar un lenguaje claro con espacios. No incluya la extensión del archivo.Por favor, describa el elemento multimedia tanto como sea posible: ¿dónde fue tomado?, ¿qué muestra?, ¿cuál es el contexto? Por favor, describa los objetos o personas. Ofrezca la información que no puede ser inferida tan fácilmente, por ejemplo el momento del día si es un paisaje. Si el medio muestra algo inusual, explique qué lo hace insual.
- Esta imagen es demasiado oscura. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.
- Esta imagen se ve borrosa. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.
+ Esta imagen es demasiado oscura. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.
+ Esta imagen se ve borrosa. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.Otorgar permisoUtilizar almacenamiento externoGuardar en el dispositivo imágenes capturadas con la cámara de la aplicación
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 1f7fb5347..eb964e1a2 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -172,7 +172,7 @@
Gertuko lekuakEz da hurbileko lekurik aurkituOharra
- Fifxategia dagoeneko Commonsen existitzen da. Ziur zaude jarraitu nahi duzula?
+ Fifxategia dagoeneko Commonsen existitzen da. Ziur zaude jarraitu nahi duzula?BaiEzIzenburua
@@ -229,8 +229,8 @@
Argazkiak hartzerakoan sortutako akatsaFitxategi izenburu deskribatzaile bakarra, fitxategi-izen gisa balioko duena. Hizkuntza arrunta erabil dezakezu espazioekin. Ez sartu fitxategiaren luzapena.Mesedez, deskribatu multimedia elementua ahal duzun gehien: non hartu zen? zer erakusten du? zein da bere testuingurua? Mesedez, objektuak eta pertsonak deskribatu. Eman asmatzeko erraza ez den informazioa, adibidez, paisaia bat izatekotan, eguneko zein orudtan hartu den. Multimediak zerbait berezia erakusten badu, mesedez azaldu zerk egiten duen berezia.
- Argazkia ilunegia da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.
- Argazkia lausoa da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.
+ Argazkia ilunegia da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.
+ Argazkia lausoa da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.Baimena emanKanpo-biltegia erabiliAplikazioaren kamerarekin ateratako argazkiak zure gailuan gorde
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 504575cec..62313d209 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -174,7 +174,7 @@
مکانهای اطرافمکانی در نزدیکی یافت نشدهشدار
- پرونده در ویکیانبار موجود است. آیا مطمئنید که میخواهید ادامه دهید؟
+ پرونده در ویکیانبار موجود است. آیا مطمئنید که میخواهید ادامه دهید؟بلهخیرعنوان
@@ -236,8 +236,8 @@
خطا در زمان دریافت تصاویرعنوانی توصیفی و یکتا برای پرونده که به عنوان نام پرونده در نظر گرفته خواهد شد. ترجیحاً به زبان ساده باشد، میتوانید فاصله هم به کار ببرید. پسوند پرونده را ننویسید.لطفاً تصویر را تا حد توان شرح دهید. کجا گرفته شدهاست؟ شامل چه چیزی میشود؟ لطفاً اشیا یا افراد را شرح دهید. اطلاعاتی که به راحتی قابل مشاهده هستند را صرفهنظر کنید. اگر چیزی در تصویر غیر طبیعی به نظر میرسد آن را شرح دهید.
- این تصویر خیلی تیره است آیا مطمئنید که میخواهید آن را بارگذاری کنید؟ ویکیانبار فقط برای نگهداری از تصاویری که ارزش دانشنامهای داشته باشند، است.
- این تصویر خیلی تار است آیا مطمئنید که میخواهید آن را بارگذاری کنید؟ ویکیانبار فقط برای نگهداری از تصاویری که ارزش دانشنامهای داشته باشند، است.
+ این تصویر خیلی تیره است آیا مطمئنید که میخواهید آن را بارگذاری کنید؟ ویکیانبار فقط برای نگهداری از تصاویری که ارزش دانشنامهای داشته باشند، است.
+ این تصویر خیلی تار است آیا مطمئنید که میخواهید آن را بارگذاری کنید؟ ویکیانبار فقط برای نگهداری از تصاویری که ارزش دانشنامهای داشته باشند، است.اجازه بدهاستفاده از حافظهٔ خارجیذخیرهٔ تصویرهای گرفته شده توسط دوربین درونکار اپلیکیشن بر روی دستگاه شما
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 80699eb99..5e4fc6f45 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -176,7 +176,7 @@
Lähellä olevat paikatLähistöltä ei löytynyt paikkojaVaroitus
- Tämä tiedosto on jo Wikimedia Commonsissa. Haluatko varmasti jatkaa?
+ Tämä tiedosto on jo Wikimedia Commonsissa. Haluatko varmasti jatkaa?KylläEiOtsikko
@@ -237,8 +237,8 @@
Virhe varastoidessa kuviaTiedoston yksilöllinen ja kuvaava otsikko, jota käytetään tiedostonimenä. Voit käyttää tavallista kieltä välilyönnein. Älä sisällytä tiedoston päätettä.Kuvaile mediaa niin paljon kuin mahdollista: Missä se otettiin? Mitä se esittää? Mikä on asiayhteys? Kuvaile esineitä tai henkilöitä. Tuo ilmi tietoja, joita ei ole helppo arvailla, esimerkiksi vuorokaudenaika, jos se on maisema. Jos media esittää jotain epätavallista, selitä, mikä tekee siitä epätavallisen.
- Tämä kuva on liian tumma, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.
- Tämä kuva on epäselvä, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.
+ Tämä kuva on liian tumma, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.
+ Tämä kuva on epäselvä, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.Anna lupaKäytä ulkoista tallennustilaaTallenna sovelluksen sisäisen kameran kanssa otetut kuvat laitteellesi
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index a451c48db..cb127276c 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -185,7 +185,7 @@
Endroits à proximitéRien trouvé dans le voisinageAvertissement
- Ce fichier existe déjà sur Commons. Êtes-vous sûr de vouloir continuer ?
+ Ce fichier existe déjà sur Commons. Êtes-vous sûr de vouloir continuer ?OuiNonTitre
@@ -247,8 +247,8 @@
Erreur en mettant les images en cacheUn titre descriptif unique pour le fichier, qui servira de nom de fichier. Vous pouvez utiliser un langage simple avec des espaces. N’incluez pas l’extension du fichierVeuillez décrire le média autant que possible : Où a-t-il été enregistré ? Que montre-t-il ? Quel est le contexte ? Veuillez décrire les objets ou les personnes. Révélez les informations qui ne peuvent pas être devinées facilement, par exemple l’heure de la journée si c’est un paysage. Si le média montre quelque chose d’inhabituel, veuillez expliquer ce qui le rend exceptionnel.
- Cette image est trop sombre, êtes-vous sûr de vouloir la télécharger ? Wikimédia Communs n’est que pour les images avec une valeur encyclopédique.
- Cette image est floue, êtes-vous sûr de vouloir la télécharger ? Wikimédia Communs n’est que pour les images ayant une valeur encyclopédique.
+ Cette image est trop sombre, êtes-vous sûr de vouloir la télécharger ? Wikimédia Communs n’est que pour les images avec une valeur encyclopédique.
+ Cette image est floue, êtes-vous sûr de vouloir la télécharger ? Wikimédia Communs n’est que pour les images ayant une valeur encyclopédique.Accorder le droitUtiliser le stockage externeEnregistrer les images prises avec l’appareil photo de votre appareil
diff --git a/app/src/main/res/values-frr/strings.xml b/app/src/main/res/values-frr/strings.xml
index 3c00cdb3d..2aa1c3049 100644
--- a/app/src/main/res/values-frr/strings.xml
+++ b/app/src/main/res/values-frr/strings.xml
@@ -141,7 +141,7 @@
Steeden naibiNian steeden uun a naite fünjenWäärnang
- Detdiar datei jaft det al üüb Commons. Beest dü seeker, dat dü widjer maage wel?
+ Detdiar datei jaft det al üüb Commons. Beest dü seeker, dat dü widjer maage wel?JaNaanTiitel
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index c5ede2e05..5b30894de 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -175,7 +175,7 @@
Lugares próximosNon se atoparon lugares pretoAviso
- Este ficheiro xa existe en Commons. Está seguro de que quere continuar?
+ Este ficheiro xa existe en Commons. Está seguro de que quere continuar?SiNonTítulo
@@ -237,8 +237,8 @@
Erro mentras se gardaban as imaxes na cachéUn título único descritivo para o ficheiro, que servirá como un nome de ficheiro. Pode usar unha linguaxe clara con espazos. Non inclúa a extensión do ficheiroPor favor, describa o ficheiro todo o posibleː Onde se gravou? Cal é o contexto? Por favor, describa os obxectos ou persoas. Indique información que non pode ser adiviñada de forma doada, por exemplo, a hora do día se é unha paisaxe. Se o ficheiro amosa algo pouco habitual, por favor, explique que é o que o fai excepcional.
- Esta imaxe é demasiado escura. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.
- Esta imaxe está borrosa. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.
+ Esta imaxe é demasiado escura. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.
+ Esta imaxe está borrosa. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.Outorgar permisoUsar o almacenamento externoGardar as imaxes capturadas coa cámara do seu dispositivo
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 582e03b73..6bed1f583 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -154,7 +154,7 @@
आसपास के स्थानपास के कोई भी स्थान नहीं मिलेचेतावनी
- यह फ़ाइल कॉमन्स पर पहले से है। क्या आप फिर भी आगे बढ़ना चाहते हैं?
+ यह फ़ाइल कॉमन्स पर पहले से है। क्या आप फिर भी आगे बढ़ना चाहते हैं?हाँनहींशीर्षक
@@ -211,7 +211,7 @@
चित्र कैशिंग करते समय त्रुटिफ़ाइल के लिए एक अद्वितीय वर्णनात्मक शीर्षक, जो एक फ़ाइल नाम के रूप में काम करेगा। आप रिक्त स्थान के साथ सादे भाषा का उपयोग कर सकते हैं। फ़ाइल विस्तार शामिल न करेंकृपया मीडिया जितना संभव हो उतना बताएं: यह कहां लिया गया? यह क्या दिखाता है? संदर्भ क्या है? कृपया वस्तुओं या व्यक्तियों का वर्णन करें। ऐसी जानकारी का खुलासा करें जिसे आसानी से अनुमानित नहीं किया जा सकता, उदाहरण के लिए दिन का समय यदि यह परिदृश्य है। अगर मीडिया कुछ असामान्य दिखाता है, तो कृपया बताएं कि इसे क्या असामान्य बनाता है।
- यह चित्र बहुत गहरा है, क्या आप वाकई इसे अपलोड करना चाहते हैं? विकिमीडिया कॉमन्स केवल विश्वकोषीय मूल्य वाले चित्रों के लिए है।
+ यह चित्र बहुत गहरा है, क्या आप वाकई इसे अपलोड करना चाहते हैं? विकिमीडिया कॉमन्स केवल विश्वकोषीय मूल्य वाले चित्रों के लिए है।अनुमति देंबाहरी स्टॉरज का पृयोग करे।आप अपने डिवाइस के इन-ऐप कैमरा से ली गई तस्वीरों को सहेजें।
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 10e6ebc67..709bdaa87 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -162,7 +162,7 @@
Mjesta u bliziniNisu pronađena mjesta u bliziniUpozorenje
- Ova datoteka već postoji na Zajedničkom poslužitelju. Jeste li sigurni da želite nastaviti?
+ Ova datoteka već postoji na Zajedničkom poslužitelju. Jeste li sigurni da želite nastaviti?DaNeNaslov
@@ -222,8 +222,8 @@
Pogrješka predmemoriranja slikaJedinstveni naziv datoteke koji će služiti kao njeno ime. Možete koristiti uobičajeni jezik s razmacima. Ne uključuje datotečni nastavak.Opišite medij što je više moguće: gdje je napravljen, što prikazuje,... Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično.
- Slika je pretamna, želite ili je ipak ostaviti? Zajednički poslužitelj je namijenjen slikama od enciklopedijske vrijednosti.
- Slika je mutna, želite ili je ipak ostaviti? Zajednički poslužitelj je namijenjen slikama od enciklopedijske vrijednosti.
+ Slika je pretamna, želite ili je ipak ostaviti? Zajednički poslužitelj je namijenjen slikama od enciklopedijske vrijednosti.
+ Slika je mutna, želite ili je ipak ostaviti? Zajednički poslužitelj je namijenjen slikama od enciklopedijske vrijednosti.Daj dopuštenjeRabi vanjsku pohranuSpremite slike načinjene kamerom Vašeg uređaja
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index ac751f6b5..e9ebefb50 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -172,7 +172,7 @@
Közeli helyekNem találhatók közeli helyekFigyelmeztetés
- Ez a fájl már létezik a Commons-on. Biztos, hogy folytatni akarod?
+ Ez a fájl már létezik a Commons-on. Biztos, hogy folytatni akarod?IgenNemCím
@@ -229,8 +229,8 @@
Hiba a képek gyorsítótárazásakorEgy egyedi, leíró cím a fájlnak, ami fájlnévként fog szolgálni. Egyszerű nyelvezetet használhatsz szóközökkel. Ne tedd bele a kiterjesztést.Kérlek a lehető legteljesebb módon írd le a fájlt: hol készült, mit ábrázol, mi a kontextus? Kérlek add meg az objektumokat vagy személyeket a képen, valamint a nehezen kitalálható információkat (például a kép készítésének dátumát, ha az egy tájkép). Amennyiben a média valami szokatlant ábrázol, kérlek fejtsd ki, hogy mi teszi szokatlanná.
- Ez a fénykép túl sötét, biztos fel akarod tölteni? A Wikimédia Commons csak enciklopédikus értékkel bíró képeket tart meg.
- Ez a fénykép homályos, biztos fel akarod tölteni? A Wikimédia Commons csak enciklopédikus értékkel bíró képeket tart meg.
+ Ez a fénykép túl sötét, biztos fel akarod tölteni? A Wikimédia Commons csak enciklopédikus értékkel bíró képeket tart meg.
+ Ez a fénykép homályos, biztos fel akarod tölteni? A Wikimédia Commons csak enciklopédikus értékkel bíró képeket tart meg.Engedély adásaKülső tárhely használataAz alkalmazáson belüli kamerával készült képek mentése az eszközre
diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml
index 809cc4446..d9c375a40 100644
--- a/app/src/main/res/values-is/strings.xml
+++ b/app/src/main/res/values-is/strings.xml
@@ -161,7 +161,7 @@
Staðir í nágrenninuEngir staðir fundust í nágrenninuAðvörun
- Þessi skrá er þegar fyrirliggjandi á Commons. Ertu viss um að þú viljir halda áfram?
+ Þessi skrá er þegar fyrirliggjandi á Commons. Ertu viss um að þú viljir halda áfram?JáNeiTitill
@@ -221,8 +221,8 @@
Villa kom upp í skyndiminni myndaEinstakur og lýsandi titill, sem mun verða skráarheiti. Þú mátt nota einfaldan texta með bilum. Ekki hafa með neina skráarendinguLýstu gögnunum eins vel og auðið er: Hvar er myndin tekin? Hvað sýnir hún? Hvert er samhengið? Lýstu fólki og fyrirbærum. Gefðu upp þær upplýsingar sem ekki er auðvelt að giska á, til dæmis á hvaða tíma dags myndin er tekin ef hún sýnir landslag. Ef gögnin sýna eitthvað óvenjulegt, útskýrðu þá hvað það er sem sé sérstakt.
- Þessi mynd er of dökk, ertu viss um að þú viljir senda hana inn? Wikimedia Commons er aðeins fyrir myndir sem hafa eitthvað fræðslugildi.
- Þessi mynd er ekki skörp, ertu viss um að þú viljir senda hana inn? Wikimedia Commons er aðeins fyrir myndir sem hafa eitthvað fræðslugildi.
+ Þessi mynd er of dökk, ertu viss um að þú viljir senda hana inn? Wikimedia Commons er aðeins fyrir myndir sem hafa eitthvað fræðslugildi.
+ Þessi mynd er ekki skörp, ertu viss um að þú viljir senda hana inn? Wikimedia Commons er aðeins fyrir myndir sem hafa eitthvað fræðslugildi.Gefa heimildNota ytri gagnageymsluVistaðu myndir sem teknar hafa verið með innbyggðu myndavélinni í tækinu þínu
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index d6b1c78ad..8518dc82f 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -166,7 +166,7 @@
Luoghi nelle vicinanzeNessun luogo trovato nelle vicinanzeAttenzione
- Questo file esiste già su Commons. Sei sicuro di voler continuare?
+ Questo file esiste già su Commons. Sei sicuro di voler continuare?SìNoTitolo
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index 18a2c0723..a20b81fc5 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -177,7 +177,7 @@
מקומות בסביבהלא נמצאו מקומות בסביבהאזהרה
- הקובץ הזה כבר קיים בוויקישיתוף. האם להמשיך?
+ הקובץ הזה כבר קיים בוויקישיתוף. האם להמשיך?כןלאכותרת
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 3ed4883fa..c02ba7258 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -172,7 +172,7 @@
近くの場所付近の場所が見つかりません警告
- このファイルは既にコモンズにあります。本当にアップロードしますか?
+ このファイルは既にコモンズにあります。本当にアップロードしますか?はいいいえタイトル
@@ -232,8 +232,8 @@
画像をキャッシュする際のエラーファイル固有の説明的な表題。ファイル名として使われます。平易な言葉を使い、空白を入れることができます。拡張子は含めないでください。可能な限りメディアを説明してください: 撮影地はどこですか? それは何を示していますか? どんな文脈がありますか? 被写体の物や人を説明してください。容易に推測できない情報、例えば風景であれば時刻を明示します。特筆すべき物事が映っている場合は、何が珍しいのかを説明してください。
- この画像は暗すぎますがアップロードしますか? ウィキメディア・コモンズは百科事典に適した画像のみ受け付けます。
- ピントが合っていませんが、アップロードしますか? ウィキメディア・コモンズは百科事典に適した画像のみ受け付けます。
+ この画像は暗すぎますがアップロードしますか? ウィキメディア・コモンズは百科事典に適した画像のみ受け付けます。
+ ピントが合っていませんが、アップロードしますか? ウィキメディア・コモンズは百科事典に適した画像のみ受け付けます。権限を付与外部ストレージを使用アプリ内のカメラで撮影した写真を端末に保存する
diff --git a/app/src/main/res/values-jv/strings.xml b/app/src/main/res/values-jv/strings.xml
index f523e01f7..28f68394b 100644
--- a/app/src/main/res/values-jv/strings.xml
+++ b/app/src/main/res/values-jv/strings.xml
@@ -145,7 +145,7 @@
Papan Cedhak KénéOra ana papan cedhak kénéPélik
- Barkasé wis ana ing Commons. Panjenengan yakin arep mbacutaké?
+ Barkasé wis ana ing Commons. Panjenengan yakin arep mbacutaké?IyaOraSesirah
diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml
index 32a623e2f..d72208b1c 100644
--- a/app/src/main/res/values-kab/strings.xml
+++ b/app/src/main/res/values-kab/strings.xml
@@ -143,7 +143,7 @@
Idigen iqeṛbenUlac ayen yettwafen s lqerbƔur-k
- Afaylu yella yakan di Commons. Tebɣiḍ ad tedduḍ ar zdat?
+ Afaylu yella yakan di Commons. Tebɣiḍ ad tedduḍ ar zdat?IhUhuAzwel
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index d87f0ebda..c2ef5622a 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -176,7 +176,7 @@
근처의 장소근처에 장소가 없습니다경고
- 이 파일은 이미 공용에 존재합니다. 계속하시겠습니까?
+ 이 파일은 이미 공용에 존재합니다. 계속하시겠습니까?예아니오제목
@@ -238,8 +238,8 @@
그림 캐시 처리 오류이 파일을 설명할 수 있는 제목으로, 파일 이름으로 사용됩니다. 띄어쓰기를 포함한 일반적인 단어를 사용할 수 있습니다. 파일 확장자는 포함하지 마세요미디어에 대해 가능한 많이 설명하십시오: 어디서 촬영한 것인가? 무엇을 보여주는가? 무슨 문맥을 가지는가? 물건이나 사람에 대해 설명하십시오. 풍경에서 시간을 알려주는 것처럼 쉽게 추측할 수 없는 정보를 제공합니다. 미디어가 평범하지 않다면 무엇이 이를 평범하지 않게 만들었는지 설명하십시오.
- 사진이 너무 어둡습니다. 정말 업로드하겠습니까? 위키미디어 공용은 사전적인 가치가 있는 사진을 위한 공간입니다.
- 사진이 흐릿합니다. 정말 업로드하겠습니까? 위키미디어 공용은 사전적인 가치가 있는 사진을 위한 공간입니다.
+ 사진이 너무 어둡습니다. 정말 업로드하겠습니까? 위키미디어 공용은 사전적인 가치가 있는 사진을 위한 공간입니다.
+ 사진이 흐릿합니다. 정말 업로드하겠습니까? 위키미디어 공용은 사전적인 가치가 있는 사진을 위한 공간입니다.권한 부여외부 저장소 사용하기장치의 인앱 카메라로 찍은 사진 저장하기
diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml
index a40f6ae9a..22b823d38 100644
--- a/app/src/main/res/values-lb/strings.xml
+++ b/app/src/main/res/values-lb/strings.xml
@@ -161,7 +161,7 @@
Plazen nobäiKeng Plazen nobäi fonntWarnung
- Dëse Fichier gëtt et schonn op Commons. Sidd Dir sécher datt Dir virufuere wëllt?
+ Dëse Fichier gëtt et schonn op Commons. Sidd Dir sécher datt Dir virufuere wëllt?JoNeenTitel
diff --git a/app/src/main/res/values-li/strings.xml b/app/src/main/res/values-li/strings.xml
index 48733bd97..5f7f48100 100644
--- a/app/src/main/res/values-li/strings.xml
+++ b/app/src/main/res/values-li/strings.xml
@@ -159,7 +159,7 @@
Plaatse in de buurtGein plaatse in de buurt gevónjeWaorsjoewing
- Dit bestandj besteit al op Commons. Wèts se zeker det se door wils gaon?
+ Dit bestandj besteit al op Commons. Wèts se zeker det se door wils gaon?JaoNaeTitel
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index 08397cb0b..34b7d7ce8 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -139,7 +139,7 @@
GeraiNetoliese Esančios VietosĮspėjimas
- Šis failas jau egzistuoja Commons. Ar tikrai norite tęsti?
+ Šis failas jau egzistuoja Commons. Ar tikrai norite tęsti?TaipNePavadinimas
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index 116ca2004..a0019b1fa 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -168,7 +168,7 @@
Околни местаНе најдов околни местаПредупредување
- Податотекава веќе постои на Ризницата. Дали сигурно сакате да продолжите?
+ Податотекава веќе постои на Ризницата. Дали сигурно сакате да продолжите?ДаНеНаслов
@@ -230,8 +230,8 @@
Грешка при меѓускладирање на сликитеКраток и единствен наслов на податотеката, кој ќе служи како нејзин назив. Можете да користите прост јазик со меѓупростор, но не пишувајте ја податотечната наставкаОбјаснете ја податотеката што подобро можете: Каде е направена? Што е прикажано на неа? Кој е контекстот? Опишете ги предметите, објектите и личностите. Дајте сознанија што не можат лесно да се погодат, како на пр. време од денот ако се работи за природен предел. Ако на неа е претставено нешто необично, објаснете зошто прикажаното е необично.
- Сликата ви е претемна. Дали сигурно сакате да ја подигнете? Ризницата е посветена само на слики со енциклопедиска вредност.
- Сликата ви е матна. Дали сигурно сакате да ја подигнете? Ризницата е посветена само на слики со енциклопедиска вредност.
+ Сликата ви е претемна. Дали сигурно сакате да ја подигнете? Ризницата е посветена само на слики со енциклопедиска вредност.
+ Сликата ви е матна. Дали сигурно сакате да ја подигнете? Ризницата е посветена само на слики со енциклопедиска вредност.Дај дозволаКористи надворешен складЗачувување на направените слики во прилогот со камерата на вашиот уред
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index f5ca39042..dbb598dd6 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -133,7 +133,7 @@
ശരിസമീപ സ്ഥലങ്ങൾമുന്നറിയിപ്പ്
- ഈ പ്രമാണം കോമൺസിൽ നിലവിലുണ്ട്. തുടരണം എന്ന് താങ്കൾക്കുറപ്പാണോ?
+ ഈ പ്രമാണം കോമൺസിൽ നിലവിലുണ്ട്. തുടരണം എന്ന് താങ്കൾക്കുറപ്പാണോ?അതെഅല്ലശീർഷകം
diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml
index 318259796..18c081d38 100644
--- a/app/src/main/res/values-mr/strings.xml
+++ b/app/src/main/res/values-mr/strings.xml
@@ -164,7 +164,7 @@
जवळपासची स्थानेआसपास काहीही सापडले नाहीइशारा
- ही संचिका पूर्वीच कॉमन्सवर उपलब्ध आहे. आपणास पुढे जायचे याची निश्चिती करता काय?
+ ही संचिका पूर्वीच कॉमन्सवर उपलब्ध आहे. आपणास पुढे जायचे याची निश्चिती करता काय?होयनाहीशीर्षक
@@ -220,8 +220,8 @@
चित्र दाखवताना त्रुटी आढळलीफ़ाईलला असे नाव द्या जे सुयोग्य असेल,चित्राविषयी त्यामधून माहिती कळेल अशी साधी भाषा वापरा,नावामध्ये फ़ाईलचे एक्सटेन्शन लिहू नका.माध्यमांची शक्य तितकी जास्त माहिती द्या:छायाचित्र/चलचित्र/ध्वनिमुद्रण कोठे घेतले आहे? त्यात काय दाखवले आहे? त्या माध्यमांचा संदर्भ काय? त्यातील वस्तू,व्यक्ति व इतर माहिती द्या? अशी सगळी माहिती जी त्या माध्यमाकडे पाहून लक्षात येणार नाही पण ती महत्वाचे आहे ती सर्व पुरवा. जर त्या माध्यमामध्ये काहीही वेगळे दिसत असेल तर ते वेगळे का आहे याची माहिती पुरवा.
- हे छायाचित्र खुप अंधारलेले आहे, तुम्ही खरच हे छायाचित्र येथे चढवू इच्छीता का? विकीमिडीया कॉमन्सवर नेहमी विश्वकोषात महत्त्वाची असलेलीच चित्रे चढवली जाऊ शकतात.
- हे छायाचित्र खुप फ़िकट आलेले आहे, तुम्ही खरच हे छायाचित्र येथे चढवू इच्छीता का? विकीमिडीया कॉमन्सवर नेहमी विश्वकोषात महत्त्वाची असलेलीच चित्रे चढवली जाऊ शकतात.
+ हे छायाचित्र खुप अंधारलेले आहे, तुम्ही खरच हे छायाचित्र येथे चढवू इच्छीता का? विकीमिडीया कॉमन्सवर नेहमी विश्वकोषात महत्त्वाची असलेलीच चित्रे चढवली जाऊ शकतात.
+ हे छायाचित्र खुप फ़िकट आलेले आहे, तुम्ही खरच हे छायाचित्र येथे चढवू इच्छीता का? विकीमिडीया कॉमन्सवर नेहमी विश्वकोषात महत्त्वाची असलेलीच चित्रे चढवली जाऊ शकतात.परवानगी द्याबाहेरील स्टोरेज वापराआपल्या डिव्हाइसवरील इन-अॅप्स कॅमेर्यासह घेतलेली चित्रे जतन करा
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index 25cf64f9d..ba3d826b3 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -175,7 +175,7 @@
Plasser i nærhetenIngen steder i nærheten funnetAdvarsel
- Denne filen eksisterer allerede på Commons. Er du sikker på at du vil fortsette?
+ Denne filen eksisterer allerede på Commons. Er du sikker på at du vil fortsette?JaNeiTittel
@@ -237,8 +237,8 @@
Feil under mellomlagring av bilderEn unik beskrivende tittel for fila, som vil fungere som filnavn. Du kan bruke vanlig språk med mellomrom. Ikke ta med filendelsenBeskriv bidraget så mye som mulig: Hvor ble det tatt? Hva viser det? Hva er konteksten? Beskriv objektene eller personene. Gi informasjon som ikke kan gjettes lett, for eksempel når på dagen bildet ble tatt om det er et landskapsbilde. Om bildet viser noe uvanlig, forklar hva som gjør det uvanlig.
- Dette bildet er for mørkt, er du sikker på at du ønsker å laste det opp? Wikimedia Commons er kun for bilder med ensyklopedisk verdi.
- Dette bildet er uklart, er du sikker på at du ønsker å laste det opp? Wikimedia Commons er bare for bilder med ensyklopedisk verdi.
+ Dette bildet er for mørkt, er du sikker på at du ønsker å laste det opp? Wikimedia Commons er kun for bilder med ensyklopedisk verdi.
+ Dette bildet er uklart, er du sikker på at du ønsker å laste det opp? Wikimedia Commons er bare for bilder med ensyklopedisk verdi.Gi tillatelseBruk ekstern lagringLagre bilder som er tatt med kameraet i appen på enheten din
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 6544ebd71..0a0d101d7 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -144,7 +144,7 @@
OKPlaatsen in de buurtWaarschuwing
- Dit bestand bestaat al op Commons. Weet u zeker dat u wilt doorgaan?
+ Dit bestand bestaat al op Commons. Weet u zeker dat u wilt doorgaan?JaNeeTitel
diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml
index 0f70b4a42..146576998 100644
--- a/app/src/main/res/values-pa/strings.xml
+++ b/app/src/main/res/values-pa/strings.xml
@@ -140,7 +140,7 @@
ਨਜ਼ਦੀਕੀ ਥਾਵਾਂਕੋਈ ਨਜ਼ਦੀਕੀ ਥਾਵਾਂ ਨਹੀਂ ਮਿਲੀਆਂਖ਼ਬਰਦਾਰ
- ਇਹ ਤਸਵੀਰ ਕਾਮਨਜ਼ \'ਤੇ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ। ਕੀ ਤੁਸੀਂ ਫਿਰ ਵੀ ਚਾਹੋਂਗੇ?
+ ਇਹ ਤਸਵੀਰ ਕਾਮਨਜ਼ \'ਤੇ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ। ਕੀ ਤੁਸੀਂ ਫਿਰ ਵੀ ਚਾਹੋਂਗੇ?ਹਾਂਨਹੀਂਸਿਰਲੇਖ
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index e18896e89..7b8feceac 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -167,7 +167,7 @@
Pobliskie miejscaNie znaleziono niczego w pobliżuOstrzeżenie
- Ten plik już istnieje na Commons. Jesteś pewien, że chcesz kontynuować?
+ Ten plik już istnieje na Commons. Jesteś pewien, że chcesz kontynuować?TakNieTytuł
diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml
index b17871297..b19a231f5 100644
--- a/app/src/main/res/values-pms/strings.xml
+++ b/app/src/main/res/values-pms/strings.xml
@@ -168,7 +168,7 @@
Pòst davzinTrovà gnente ant ij parageAvertensa
- S\'archivi a esist già su Comun. É-lo sigur ëd vorèj andé anans?
+ S\'archivi a esist già su Comun. É-lo sigur ëd vorèj andé anans?BòNòTìtol
@@ -230,8 +230,8 @@
Eror antramentre ch\'as butavo le plance an memòria localUn tìtol dëscritiv ùnich për l\'archivi, che a servirà com nòm d\'archivi. A peul dovré un lengagi sempi con djë spassi. Ch\'a ancluda pa l\'estension dl\'archiviPër piasì, ch\'a descriva ël mojen mej ch\'a peul: Andoa a l\'é stàit fàit? Për piasì, ch\'a descriva j\'oget o le përson-e. Ch\'a arvela j\'anformassion ch\'a l\'é nen belfé andviné, për esempi l\'ora dël dì, s\'a l\'é un panorama. Si ël mojen a smon cheicòs ëd foravìa, për piasì ch\'a spiega lòn ch\'a lo rend foravìa.
- Sa plancia a l\'é tròp sombra, e-lo sigur ëd vorèj cariela? Wikipedia Comun a l\'é mach për plance con un valor enciclopédich.
- Sa plancia a l\'é tërbola, e-lo sigur ëd vorèj cariela? Wikipedia Comun a l\'é mach për plance con un valor enciclopédich.
+ Sa plancia a l\'é tròp sombra, e-lo sigur ëd vorèj cariela? Wikipedia Comun a l\'é mach për plance con un valor enciclopédich.
+ Sa plancia a l\'é tërbola, e-lo sigur ëd vorèj cariela? Wikipedia Comun a l\'é mach për plance con un valor enciclopédich.Dé ël përmessDovré n\'anmagasinament esternArgistré le plance pijà con la màchina fòto ëd sò angign
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index f6a3a8714..2b9926b33 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -180,7 +180,7 @@
Lugares próximosNão fora encontrado locais próximos.Alerta
- Este arquivo já existe no Commons. Tem certeza de que deseja continuar?
+ Este arquivo já existe no Commons. Tem certeza de que deseja continuar?SimNãoTítulo
@@ -242,8 +242,8 @@
Erro durante o cache de imagensUm título descritivo exclusivo para o arquivo, que servirá como um nome de arquivo. Você pode usar linguagem simples com espaços. Não inclua a extensão do arquivoPor favor, descreva a mídia tanto quanto possível: onde foi tomada? O que isso mostra? Qual é o contexto? Descreva os objetos ou pessoas. Revelar informações que não podem ser facilmente adivinhadas, por exemplo, a hora do dia, se for uma paisagem. Se a mídia mostrar algo incomum, explique o que torna incomum.
- Esta foto está muito escura, você tem certeza de que deseja enviá-la? O Wikimedia Commons é apenas para imagens com valor enciclopédico.
- Esta foto está embaçada, tem certeza de que deseja enviá-la? O Wikimedia Commons é apenas para imagens com valor enciclopédico.
+ Esta foto está muito escura, você tem certeza de que deseja enviá-la? O Wikimedia Commons é apenas para imagens com valor enciclopédico.
+ Esta foto está embaçada, tem certeza de que deseja enviá-la? O Wikimedia Commons é apenas para imagens com valor enciclopédico.Dar permissãoUsar o armazenamento externoSalvar as fotos tiradas com a câmera no aplicativo no seu dispositivo
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 330e1e6d7..ae9e19fe3 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -179,7 +179,7 @@
Locais PróximosNão foram encontrados locais próximos.Aviso
- Este ficheiro já existe na wiki Commons. Tem a certeza de que deseja continuar?
+ Este ficheiro já existe na wiki Commons. Tem a certeza de que deseja continuar?SimNãoTítulo
@@ -241,8 +241,8 @@
Erro ao colocar imagens na cacheUm título descritivo exclusivo para o ficheiro, que servirá como um nome de ficheiro. Pode utilizar uma linguagem simples com espaços. Não inclua a extensão do ficheiroPor favor, descreva o ficheiro da melhor forma possível: Onde foi tirado? O que isso mostra? Qual é o contexto? Por favor, descreva os objetos ou as pessoas. Indique as informações que não podem ser facilmente adivinhadas, por exemplo, a hora do dia, se for uma paisagem. Se o ficheiro mostrar algo incomum, explique o que torna incomum.
- Esta imagem está demasiada escura; tem a certeza de que deseja carregá-la? A wiki Wikimedia Commons só aceita as imagens de valor enciclopédico.
- Esta imagem está desfocada; tem a certeza de que deseja carregá-la? A wiki Wikimedia Commons serve apenas para imagens de valor enciclopédico.
+ Esta imagem está demasiada escura; tem a certeza de que deseja carregá-la? A wiki Wikimedia Commons só aceita as imagens de valor enciclopédico.
+ Esta imagem está desfocada; tem a certeza de que deseja carregá-la? A wiki Wikimedia Commons serve apenas para imagens de valor enciclopédico.PermitirUtilizar a armazenagem externaGravar as fotografias tiradas com a câmara da aplicação no seu dispositivo
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 7b05363ab..26e0285ba 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -186,7 +186,7 @@
Места поблизостиМеста поблизости не найденыВнимание
- Этот файл уже существует на Викискладе. Вы уверены, что хотите продолжить?
+ Этот файл уже существует на Викискладе. Вы уверены, что хотите продолжить?ДаНетНазвание
@@ -248,8 +248,8 @@
Ошибка при кэшировании картинокУникальное описание, которое будет сохранено как имя файла. Вы можете использовать естественный язык, разделяя слова пробелами. Пожалуйста, не указывайте расширение файла.Пожалуйста, подробно опишите загружаемый файл: где он был снят? что на нём изображено? каков его контекст? Пожалуйста опишите изображённых персон или объекты. Добавьте информацию, о которой нельзя легко догадаться, например, время суток, когда снимался файл. Если снято что-то необычное, постарайтесь пояснить, что именно в этом необычного.
- Это изображение слишком тёмное. Вы уверены, что хотите его загрузить? Викисклад подходит только для фотографий, имеющих энциклопедическую ценность.
- Это изображение размыто. Вы уверены, что хотите его загрузить? Викисклад подходит только для фотографий, имеющих энциклопедическую ценность.
+ Это изображение слишком тёмное. Вы уверены, что хотите его загрузить? Викисклад подходит только для фотографий, имеющих энциклопедическую ценность.
+ Это изображение размыто. Вы уверены, что хотите его загрузить? Викисклад подходит только для фотографий, имеющих энциклопедическую ценность.РазрешитьИспользовать внешнее хранилищеСохранять изображения, сделанные с помощью встроенной камеры на устройстве
diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml
index 5b07e722c..5660a7c89 100644
--- a/app/src/main/res/values-sd/strings.xml
+++ b/app/src/main/res/values-sd/strings.xml
@@ -161,7 +161,7 @@
ويجھڙائيءَ ۾ جڳھونآپاس وارو جڳهه نه لڌوچتاءُ
- فائل اڳ ئي العام ۾ موجود آھي. ڇا توھان کي پڪ آھي تہ توھان اڳتي وڌڻ ٿا چاھيو؟
+ فائل اڳ ئي العام ۾ موجود آھي. ڇا توھان کي پڪ آھي تہ توھان اڳتي وڌڻ ٿا چاھيو؟هانہعنوان
diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml
index 6fb06002a..3b2fde0fb 100644
--- a/app/src/main/res/values-si/strings.xml
+++ b/app/src/main/res/values-si/strings.xml
@@ -131,7 +131,7 @@
අවට ස්ථානළඟපාත තැන් කිසිවක් සොයා ගැනීමට නොහැකි වියඅනතුරු හැඟවීමයි
- මෙම ගොනුව දැනටමත් කොමන්ස් හි ඇත. ඔබට ඉදිරියට යෑම ගැන විශ්වාසද?
+ මෙම ගොනුව දැනටමත් කොමන්ස් හි ඇත. ඔබට ඉදිරියට යෑම ගැන විශ්වාසද?ඔව්නැතමාතෘකාව
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index ae91a8d25..2b7190fa7 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -139,7 +139,7 @@
Miesta v okolíV okolí sa nenašli žiadne miestaUpozornenie
- Tento súbor už na Commons existuje. Ste si istí, že chcete pokračovať?
+ Tento súbor už na Commons existuje. Ste si istí, že chcete pokračovať?ÁnoNieNázov
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index 1964ed2e3..6e3c5811f 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -176,7 +176,7 @@
Места у близиниНису пронађена оближња местаУпозорење
- Ова датотека је већ доступна на Остави. Да ли сте сигурни да желите да наставите?
+ Ова датотека је већ доступна на Остави. Да ли сте сигурни да желите да наставите?ДаНеНаслов
@@ -238,8 +238,8 @@
Грешка при кеширању сликаЈединствен описни наслов за датотеку, који ће бити име датотеке. Можете да користите обични језик са размацима. Не треба уносити екстензију датотекеМолимо да опишете датотеку колико је то могуће: Где је направљена? Шта приказује? Шта је контекст? Опишите објекте и/или особе. Откријте информације које се не могу лако погодити, на пример доба дана ако је у питању пејзаж. Ако датотека приказује нешто необично, молимо да објасните шта је то чини необичном.
- Ова слика је претамна; да ли сте сигурни да је желите отпремити? Викимедијина остава је само за слике са енциклопедијском вредношћу.
- Ова слика је мутна; да ли сте сигурни да је желите отпремити? Викимедијина остава је само за слике са енциклопедијском вредношћу.
+ Ова слика је претамна; да ли сте сигурни да је желите отпремити? Викимедијина остава је само за слике са енциклопедијском вредношћу.
+ Ова слика је мутна; да ли сте сигурни да је желите отпремити? Викимедијина остава је само за слике са енциклопедијском вредношћу.Давање дозволеУпотреба спољашње меморијеСпремање слика направљених камером апликације на Вашем уређају
diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml
index 20378a409..9d7720a4f 100644
--- a/app/src/main/res/values-su/strings.xml
+++ b/app/src/main/res/values-su/strings.xml
@@ -169,7 +169,7 @@
Tempat SabudeureunTeu kapanggih tempat sabudeureunMangkahadé
- Ieu berkas geus aya di Commons. Rék diteruskeun baé ieu téh?
+ Ieu berkas geus aya di Commons. Rék diteruskeun baé ieu téh?NyaHenteuJudul
@@ -230,8 +230,8 @@
Kasalahan nalika muat gambarJudul déskriptif anu unik pikeun berkas, anu bakal miboga fungsi minangka ngaran berkas. Anjeun bisa maké basa basajan kalawan spasi. Ulah ngawuwuhkeun éksténsi berkasPék émbarkeun wincikan média saloba-lobabana: Dimana éta dicokot? Naon nu titojokeunna? Naon kontéksna? Pék jéntrékeun obyék atawa jalmana. Ébré informasi anu teu gampang kajudi, kawas wayah mun éta mangrupa pamandangan. Ari média nu némbongkeun perkara nu teu guyub, pék jéntrékeun naon nu ngabalukarkeun éta téh teu guyub.
- Ieu gambar poék teuing, rék diunggah waé? Wikimedia Commons téh ukur pikeun gambar anu boga ajén énsiklopédik.
- Potona teu cékas, rék diunggah waé? Wikimedia Commons mah ukur pikeun gambar anu boga ajén énsiklopédik.
+ Ieu gambar poék teuing, rék diunggah waé? Wikimedia Commons téh ukur pikeun gambar anu boga ajén énsiklopédik.
+ Potona teu cékas, rék diunggah waé? Wikimedia Commons mah ukur pikeun gambar anu boga ajén énsiklopédik.Béré idinPaké panyimpenan éksternalSimpen gambar nu nyomotna ku aplikasi kaména na parangkat anjeun
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 9a8cd7659..c60cf229b 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -174,7 +174,7 @@
Platser i närhetenInga platser i närheten hittadesVarning
- Denna fil finns redan på Commons. Är du säker på att du vill fortsätta?
+ Denna fil finns redan på Commons. Är du säker på att du vill fortsätta?JaNejTitel
@@ -236,8 +236,8 @@
Fel uppstod när bilder cachelagrasEn unik beskrivande titel för filen, som kommer att fungera som ett filnamn. Du kan använda klarspråk med mellanslag. Ta inte med filändelsenBeskriv mediafilen så mycket som möjligt. Var togs den? Vad visar den? Vad är sammanhanget? Beskriv föremålen eller personerna. Ge information som inte kan gissas fram, t.ex. tidpunkten om det är ett landskap. Om mediafilen visar någonting ovanligt, förklara vad som gör den ovanlig.
- Denna bild är för mörk, är du säker på att du vill ladda upp den? Wikimedia Commons är endast till för bilder med encyklopediskt värde.
- Denna bild är suddig, är du säker på att du vill ladda upp den? Wikimedia Commons är endast till för bilder med encyklopediskt värde.
+ Denna bild är för mörk, är du säker på att du vill ladda upp den? Wikimedia Commons är endast till för bilder med encyklopediskt värde.
+ Denna bild är suddig, är du säker på att du vill ladda upp den? Wikimedia Commons är endast till för bilder med encyklopediskt värde.Ge behörighetAnvänd extern lagringSpara bilder som tas med kameran i appen på din enhet
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index 9a7ec69c0..3206dcf4f 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -155,7 +155,7 @@
สถานที่ใกล้เคียงไม่พบสถานที่ใกล้เคียงคำเตือน
- ไฟล์นี้มีอยู่แล้วบนคอมมอนส์ คุณแน่ใจหรือว่าคุณต้องการดำเนินการต่อ?
+ ไฟล์นี้มีอยู่แล้วบนคอมมอนส์ คุณแน่ใจหรือว่าคุณต้องการดำเนินการต่อ?ใช่ไม่ชื่อเรื่อง
@@ -213,8 +213,8 @@
ข้อผิดพลาดขณะแคชภาพชื่อเรื่องที่อธิบายลักษณะเฉพาะของไฟล์ ซึ่งจะใช้เป็นชื่อไฟล์ คุณอาจใช้ภาษาธรรมดาที่มีเว้นวรรคก็ได้ อย่ารวมนามสกุลไฟล์โปรดอธิบายสื่อดังกล่าวให้มากที่สุดเท่าที่จะได้: สื่อนี้ถูกถ่ายที่ไหน? สื่อนี้แสดงถึงอะไร? บริบทคืออะไร? โปรดอธิบายถึงวัตถุหรือบุคคล เปิดเผยข้อมูลที่ไม่อาจคาดเดาได้อย่างง่ายดาย เช่น เวลาที่ถ่าย หากเป็นภาพทิวทัศน์ หากสื่อแสดงถึงสิ่งที่ไม่ธรรมดา โปรดอธิบายว่าอะไรทำให้สื่อดังกล่าวไม่ธรรมดา
- ภาพนี้มืดเกินไป คุณแน่ใจหรือว่าคุณต้องการอัปโหลดภาพนี้? วิกิมีเดียคอมมอนส์นั้นมีไว้สำหรับรูปภาพที่มีคุณค่าในทางสารานุกรมเท่านั้น
- ภาพนี้มัว คุณแน่ใจหรือว่าคุณต้องการอัปโหลดภาพนี้? วิกิมีเดียคอมมอนส์นั้นมีไว้สำหรับรูปภาพที่มีคุณค่าในทางสารานุกรมเท่านั้น
+ ภาพนี้มืดเกินไป คุณแน่ใจหรือว่าคุณต้องการอัปโหลดภาพนี้? วิกิมีเดียคอมมอนส์นั้นมีไว้สำหรับรูปภาพที่มีคุณค่าในทางสารานุกรมเท่านั้น
+ ภาพนี้มัว คุณแน่ใจหรือว่าคุณต้องการอัปโหลดภาพนี้? วิกิมีเดียคอมมอนส์นั้นมีไว้สำหรับรูปภาพที่มีคุณค่าในทางสารานุกรมเท่านั้นให้สิทธิ์ใช้ที่จัดเก็บข้อมูลภายนอกบันทึกรูปภาพที่ถ่ายด้วยกล้องในแอปบนอุปกรณ์ของคุณ
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 58c5133c2..583619c1d 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -180,7 +180,7 @@
Yakındaki yerlerYakınlardaki yer bulunamadıUyarı
- Bu dosya zaten Commons\'da var. Devam etmek istediğinizden emin misiniz?
+ Bu dosya zaten Commons\'da var. Devam etmek istediğinizden emin misiniz?EvetHayırBaşlık
@@ -242,8 +242,8 @@
Resimler önbelleğe alınırken hata oluştuDosya için dosya adı olarak kullanılacak benzersiz açıklayıcı bir başlık olmalıdır. Boşluk bırakarak sade bir dil kullanabilirsiniz. Dosya uzantısını dahil etmeyinLütfen medyayı mümkün olduğunca açıklayın: Nerede çekildi? Ne gösteriyor? Bağlam nedir? Lütfen nesneleri veya kişileri tanımlayın. Kolay tahmin edilemeyen bilgileri açıklayın, örneğin bir manzara ise günün saatini belirtin. Medya alışılmadık bir şey gösteriyorsa lütfen olağandışı yapan şeyleri açıklayın.
- Bu fotoğraf çok karanlık, yine de yüklemek istiyor musunuz? Wikimedia Commons yalnızca ansiklopedik değeri olan fotoğraflar içindir.
- Bu fotoğraf bulanık, yine de yüklemek istiyor musunuz? Wikimedia Commons yalnızca ansiklopedik değeri olan fotoğraflar içindir.
+ Bu fotoğraf çok karanlık, yine de yüklemek istiyor musunuz? Wikimedia Commons yalnızca ansiklopedik değeri olan fotoğraflar içindir.
+ Bu fotoğraf bulanık, yine de yüklemek istiyor musunuz? Wikimedia Commons yalnızca ansiklopedik değeri olan fotoğraflar içindir.İzin verHarici depolamayı kullanınUygulama kamerası kullanıldığında çekilen fotoğrafları cihazına kaydedin
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 5b3df71c9..42283caec 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -185,7 +185,7 @@
Місця поблизуНе знайдено місць поблизуПопередження
- Цей файл вже існує на Вікісховищі. Ви впевнені, що хочете продовжити?
+ Цей файл вже існує на Вікісховищі. Ви впевнені, що хочете продовжити?ТакНіНазва
@@ -247,8 +247,8 @@
Помилка кешування зображеньУнікальна описова назва файлу. Ви можете використовувати простий текст з пробілами. Не вказуйте розширення файлуБудь ласка, докладно опишіть файл: де його було зроблено? що на ньому зображено? який контекст? Будь ласка, опишіть об\'єкти чи осіб. Додайте інформацію, яку не можна легко здогадатися, наприклад, пору доби для фотографії пейзажу. Якщо зображено щось незвичайне, спробуйте пояснити, що робить його незвичайним.
- Це зображення надто темне. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність.
- Це зображення розмите. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність.
+ Це зображення надто темне. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність.
+ Це зображення розмите. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність.Надати дозвілВикористовувати зовнішнє сховищеЗберігати зображення, виконані вбудованою камерою Вашого пристрою
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index a55d54f7f..aad679fe5 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -165,7 +165,7 @@
Nơi Lân cậnKhông tìm thấy nơi lân cậnCảnh báo
- Tập tin này đã tồn tại ở Commons. Bạn có chắc chắn muốn tiếp tục?
+ Tập tin này đã tồn tại ở Commons. Bạn có chắc chắn muốn tiếp tục?CóKhôngTiêu đề
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index bf3815db9..bb9ebb9f6 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -179,7 +179,7 @@
附近地点找不到附近地点警告
- 此文件在共享资源已存在。您确定要继续么?
+ 此文件在共享资源已存在。您确定要继续么?是否标题
@@ -241,8 +241,8 @@
缓存图片时出错用于文件的唯一描述性标题,它将作为文件名使用。您可以使用有空格的简明语言。请不要包含文件扩展名请尽可能详细地描述媒体:拍摄在何地?显示什么?例文是什么?请描述对象或个人。透露一些不易猜想到的信息,例如这幅风景画的具体日期时间。如果媒体显示了一些不寻常的事物,请说明为什么它显得不寻常。
- 此图片太暗,您确定要上传它么?维基共享资源只接受对百科全书有价值的图片。
- 此图片模糊不清,您确定要上传它么?维基共享资源只接受对百科全书有价值的图片。
+ 此图片太暗,您确定要上传它么?维基共享资源只接受对百科全书有价值的图片。
+ 此图片模糊不清,您确定要上传它么?维基共享资源只接受对百科全书有价值的图片。提供权限使用外部存储在您的设备上,使用应用中的照相机保存照片
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 9cf0363f5..ce164ac76 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -3,6 +3,7 @@
#303030#fafafa
+ #eaeaea#1a1a1a
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index bac816cd4..34ee3e6dc 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -26,6 +26,7 @@
14sp15dp25dp
+ 12sp