Multiple uploads with over haul (#1968)

* Added new upload activity that receives shared files from the gallery.  Cards show and hide, plus titles are correct.  Displayed thumbnails for the shared images

* Better handling of the view paging plus error handling for required fields.

* Code cleanup to make things more readable.

* Extracted a model from the category search fragment that can possibly be shared with the new upload activity.

* Added category selection to the combined upload screen.

* Cleanup before the home-stretch on the GUI.

* Adding license selection.

* Fixed build warnings + cleanup

* Start to support the dark theme.

* Work in progress to add quality checking.

* Fixing merge.

* GPSExtractor: optimized away the EXifInterface object

* Implemented submit functionality, temporarily fixed jacoco crash by disabling DUMMY UploadView object.

* Implemented uploading of categories along with the picture. The category screen now displays GPS and recent categories when nothing is searched.

* Implemented caching of files. Did some work on picture quality detection.

* Implemented too dark picture detection.

* Added a side card for zoom and map buttons along with pretty animations for stuff.

* Added duplicate image on commons checking and fixed files not getting proper file extensions in several places.

* Added support for map button and switched in-app upload buttons to UploadActivity

* Pretty pretty animations!

* Implemented zoom functionality for th background image. Just pinching on the image works instead of requiring buttons.

* Added multi-language descriptions with categories by region.

* Reimplemented the duplicate title checker and implemented a check against putting the same language twice in the description.

* Javadocs for Description and UploadPresenter, plus some general cleanup.

* Small code changes.

* Implemented login checks for the Upload screen.

* Implement receiving data from Nearby.

* Feature/permissions library (#1855)

* Added permission for Dexter, the runtime permission handling library

* [Preparing fir issue #1773] Added a utility function which would take the user to app settings screen where he could manually give us the required permission

* Added an alert dialog with positive and negative callback [Preparing fir issue #1773]

* Improvements in the way External Storage Permission is handled in MultipleShareActivity[Bug fix #1697]
1. Used dexter to handle the external storage permission
2. Behaviour changes : When user tries to share(uppload) images to commons via MultipleShareActivity, following decision tree is followed
	a. If the app has permission for external storage, normal upload operation is followed
	b. If the app does not has the permission for external storage, dexter is used to ask for the same
	c. If the user gives us the required permission, normal upload flow is proceeded
	d. If the doesnot gives us the required permission a rationale dialog is shown with the appropriate message to let him know why we need the permission
	e. If he presses okay, steps a-c are followed and if he presses cancel, we close the app.
	f. If while asking for permission, the user chooses never ask again, then next time he tries to upload an image via MSA, the rational dialog follows the app setting screen where he could manually give us the required permission and the onActivityResult of same is handled

* Added a Constants class to handle request and result codes from one place and other related constants common to the all app elements

* replaced hardcoded strings ok and cancel in DialogUtil to string resources

* init permission rationale dialog in activities onCreate

* Code formatting, updated access modifiers wherever required, added javadocs for new methods created

* *shifted constants to app class
*Added JavaDocs in PermissionUtils

* removed class REQUEST_CODES from CommonsApplication and instead put the enclosing constants in the App class itself

* Made Codacy happy.

* Abstarcted permission acquisition into new class DexterPermissionObtainer

* Fixed Nearby upload detection

* Migrated bad picture detection from AsyncTask to RxJava.

* Removed ShareActivity and related dead code

* Removed dead or duplicate code from FileProcessor

* Added info button to title EditText

* Fixed the add description button not disappearing.
Added "Starting Upload" toast.
Added link to the license on final screen.
Made it so that the map button is hidden when image lacks gps coords.

* Support in app multiple uploads

* Minor changes to fix build

* Changes to fix pending issues with upload flow

* Fix display of similar image fragment

* When uploading several files at once the date is missing #1854 (#2)

* Bug fix issue #1854
* updated ContributionsDao to save create date, which it was not doing currently [it was instead saving current date]
* UploadItem accepts are dateCreated param
* Added a function in UploadModel, getFileCreatedDate which tries to fetched the file creaction date from all possible content providers.

* Fix pending issues in upload flow

* Make multiple uploads work for Google Photos

* Fix default state for upload activity

* Fix keyboard state for license screen

* Fix descriptions for uploads

* wip

* Fix language spinner
This commit is contained in:
Vivek Maskara 2018-11-19 14:31:35 +05:30
parent 4930a82ea2
commit f607c1c14d
139 changed files with 4012 additions and 3212 deletions

View file

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

View file

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

View file

@ -15,7 +15,7 @@
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
@ -41,42 +41,35 @@
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<activity android:name=".WelcomeActivity" />
<activity
android:name=".upload.ShareActivity"
<activity android:name=".upload.UploadActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" />
</intent-filter>
</activity>
<activity
android:name=".upload.MultipleShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<intent-filter android:label="@string/intent_share_upload_label">
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" />
</intent-filter>
</activity>
<activity
android:name=".contributions.MainActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" />
<activity
android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings" />
<activity
android:name=".AboutActivity"
android:label="@string/title_activity_about"
@ -135,24 +128,24 @@
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name=".contributions.ContributionsSyncService"
android:exported="true">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/contributions_sync_adapter" />
</service>
<service
android:name=".modifications.ModificationsSyncService"
android:exported="true">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/modifications_sync_adapter" />
@ -172,21 +165,18 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name=".contributions.ContributionsContentProvider"
android:authorities="${applicationId}.contributions.contentprovider"
android:exported="false"
android:label="@string/provider_contributions"
android:syncable="true" />
<provider
android:name=".modifications.ModificationsContentProvider"
android:authorities="${applicationId}.modifications.contentprovider"
android:exported="false"
android:label="@string/provider_modifications"
android:syncable="true" />
<provider
android:name=".category.CategoryContentProvider"
android:authorities="${applicationId}.categories.contentprovider"

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
@ -14,12 +15,11 @@ import android.widget.Toast;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -110,6 +110,31 @@ public class Utils {
throw new RuntimeException("Unrecognized license value: " + license);
}
/**
* Generates license url with given ID
* @param license License ID
* @return Url of license
*/
@NonNull
public static 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/";
default:
throw new RuntimeException("Unrecognized license value: " + license);
}
}
/**
* Adds extension to filename. Converts to .jpg if system provides .jpeg, adds .jpg if no extension detected
* @param title File name
@ -176,6 +201,18 @@ public class Utils {
customTabsIntent.launchUrl(context, url);
}
public static void handleGeoCoordinates(Context context, String coords) {
try {
Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + coords);
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
mapIntent.setPackage("com.google.android.apps.maps");
context.startActivity(mapIntent);
} catch (ActivityNotFoundException ex) {
Toast toast = Toast.makeText(context, context.getString(R.string.map_application_missing), LENGTH_SHORT);
toast.show();
}
}
/**
* To take screenshot of the screen and return it in Bitmap format
*
@ -190,4 +227,14 @@ public class Utils {
return bitmap;
}
public static <K,V> Map<K,V> arraysToMap(K[] kArray, V[] vArray){
if(kArray.length!=vArray.length)
throw new RuntimeException("arraysToMap array sizes don't match");
Map<K,V> map=new LinkedHashMap<>();
for (int i=0;i<vArray.length;i++){
map.put(kArray[i], vArray[i]);
}
return map;
}
}

View file

@ -16,7 +16,8 @@ import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
@Inject SessionManager sessionManager;
@Inject
protected SessionManager sessionManager;
@Inject
MediaWikiApi mediaWikiApi;
private String authCookie;

View file

@ -1,24 +1,23 @@
package fr.free.nrw.commons.category;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections;
import java.util.List;
class CategoriesAdapterFactory {
private final CategoriesRenderer.CategoryClickedListener listener;
public class CategoriesAdapterFactory {
private final CategoryClickedListener listener;
CategoriesAdapterFactory(CategoriesRenderer.CategoryClickedListener listener) {
public CategoriesAdapterFactory(CategoryClickedListener listener) {
this.listener = listener;
}
public RVRendererAdapter<CategoryItem> create(List<CategoryItem> placeList) {
public CategoryRendererAdapter create(List<CategoryItem> placeList) {
RendererBuilder<CategoryItem> builder = new RendererBuilder<CategoryItem>()
.bind(CategoryItem.class, new CategoriesRenderer(listener));
ListAdapteeCollection<CategoryItem> collection = new ListAdapteeCollection<>(
placeList != null ? placeList : Collections.<CategoryItem>emptyList());
return new RVRendererAdapter<>(builder, collection);
return new CategoryRendererAdapter(builder, collection);
}
}

View file

@ -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<String, ArrayList<String>> categoriesCache;
private List<CategoryItem> 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<CategoryItem> sortBySimilarity(final String filter) {
Comparator<String> 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<String, ArrayList<String>> categories) {
categoriesCache.putAll(categories);
}
public HashMap<String, ArrayList<String>> getCategoriesCache() {
return categoriesCache;
}
boolean cacheContainsKey(String term) {
return categoriesCache.containsKey(term);
}
//endregion
//region Category searching
public Observable<CategoryItem> searchAll(String term, List<String> imageTitleList) {
//If user hasn't typed anything in yet, get GPS and recent items
if (TextUtils.isEmpty(term)) {
return gpsCategories()
.concatWith(titleCategories(imageTitleList))
.concatWith(recentCategories());
}
//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<CategoryItem> searchCategories(String term, List<String> imageTitleList) {
//If user hasn't typed anything in yet, get GPS and recent items
if (TextUtils.isEmpty(term)) {
return gpsCategories()
.concatWith(titleCategories(imageTitleList))
.concatWith(recentCategories());
}
return mwApi
.searchCategories(term, SEARCH_CATS_LIMIT)
.map(s -> new CategoryItem(s, false));
}
private ArrayList<String> getCachedCategories(String term) {
return categoriesCache.get(term);
}
public Observable<CategoryItem> defaultCategories(List<String> titleList) {
Observable<CategoryItem> directCat = directCategories();
if (hasDirectCategories()) {
Timber.d("Image has direct Cat");
return directCat
.concatWith(gpsCategories())
.concatWith(titleCategories(titleList))
.concatWith(recentCategories());
} else {
Timber.d("Image has no direct Cat");
return gpsCategories()
.concatWith(titleCategories(titleList))
.concatWith(recentCategories());
}
}
private boolean hasDirectCategories() {
return !directPrefs.getString("Category", "").equals("");
}
private Observable<CategoryItem> directCategories() {
String directCategory = directPrefs.getString("Category", "");
List<String> 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<CategoryItem> gpsCategories() {
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> titleCategories(List<String> titleList) {
return Observable.fromIterable(titleList)
.concatMap(this::getTitleCategories);
}
private Observable<CategoryItem> getTitleCategories(String title) {
return mwApi.searchTitles(title, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> recentCategories() {
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
.map(s -> new CategoryItem(s, false));
}
//endregion
//region Category Selection
@Override
public void categoryClicked(CategoryItem item) {
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<CategoryItem> getSelectedCategories() {
return selectedCategories;
}
public List<String> getCategoryStringList() {
List<String> output = new ArrayList<>();
for (CategoryItem item : selectedCategories) {
output.add(item.getName());
}
return output;
}
//endregion
}

View file

@ -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<CategoryItem> {
public class CategoriesRenderer extends Renderer<CategoryItem> {
@BindView(R.id.tvName) CheckedTextView checkedView;
private final CategoryClickedListener listener;
@ -44,11 +45,8 @@ class CategoriesRenderer extends Renderer<CategoryItem> {
@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);
}
}

View file

@ -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<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler;
private HashMap<String, ArrayList<String>> categoriesCache;
private List<CategoryItem> 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<CategoryItem> items = new ArrayList<>();
categoriesCache = new HashMap<>();
if (savedInstanceState != null) {
items.addAll(savedInstanceState.getParcelableArrayList("currentCategories"));
//noinspection unchecked
categoriesCache.putAll((HashMap<String, ArrayList<String>>) 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<CategoryItem> 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<CategoryItem> sortBySimilarity(final String filter) {
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
return (firstItem, secondItem) -> stringSimilarityComparator
.compare(firstItem.getName(), secondItem.getName());
}
private List<String> getStringList(List<CategoryItem> input) {
List<String> output = new ArrayList<>();
for (CategoryItem item : input) {
output.add(item.getName());
}
return output;
}
private Observable<CategoryItem> defaultCategories() {
Observable<CategoryItem> 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<CategoryItem> directCategories() {
String directCategory = directPrefs.getString("Category", "");
// Strip newlines to prevent blank categories, and to tidy existing categories
directCategory = directCategory.replace("\n", "");
List<String> 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<CategoryItem> gpsCategories() {
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> 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<CategoryItem> recentCategories() {
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
.map(s -> new CategoryItem(s, false));
}
private Observable<CategoryItem> 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<CategoryItem> 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();
}
}
}
}

View file

@ -0,0 +1,5 @@
package fr.free.nrw.commons.category;
public interface CategoryClickedListener {
void categoryClicked(CategoryItem item);
}

View file

@ -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 + '\'';
}
}

View file

@ -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<CategoryItem> {
CategoryRendererAdapter(RendererBuilder<CategoryItem> rendererBuilder, AdapteeCollection<CategoryItem> collection) {
super(rendererBuilder, collection);
}
protected ArrayList<CategoryItem> allItems() {
int itemCount = getItemCount();
ArrayList<CategoryItem> items = new ArrayList<>(itemCount);
for (int i = 0; i < itemCount; i++) {
items.add(getItem(i));
}
return items;
}
}

View file

@ -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<Contribution> CREATOR = new Creator<Contribution>() {
@ -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;

View file

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

View file

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

View file

@ -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<Uri> mArrayUri = new ArrayList<Uri>();
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

View file

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

View file

@ -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<String> provideLicenses(Context context) {
List<String> 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<String, String> provideLicensesByName(Context context) {
Map<String, String> 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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<DescriptionsAdapter.ViewHolder> {
List<Description> descriptions;
List<Language> languages;
private Title title;
private List<Description> descriptions;
private Context context;
private Callback callback;
private Subject<String> titleChangedSubject;
public DescriptionsAdapter() {
private BiMap<AdapterView, String> 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<Description> descriptions) {
void setItems(Title title, List<Description> descriptions) {
this.descriptions = descriptions;
this.title = title;
selectedLanguages = new BiMap<>();
notifyDataSetChanged();
}
public void setLanguages(List<Language> 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<Description> getDescriptions() {
List<Description> 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.Adapter<DescriptionsAdapter.ViewH
}
}
private int getIndexOfUserDefaultLocale() {
for (int i = 0; i < languages.size(); i++) {
if (languages.get(i).getLocale()
.equals(context.getResources().getConfiguration().locale)) {
return i;
}
}
return 0;
}
private void updateDescriptionBasedOnSelectedLanguageIndex(Description description,
int position) {
Language language = languages.get(position);
Locale locale = language.getLocale();
description.setSelectedLanguageIndex(position);
description.setLanguageDisplayText(locale.getDisplayName());
description.setLanguageId(locale.getLanguage());
private Drawable getInfoIcon() {
return context.getResources()
.getDrawable(R.drawable.mapbox_info_icon_default);
}
public interface Callback {
void showAlert(int mediaDetailDescription, int descriptionInfo);
}
}

View file

@ -1,82 +0,0 @@
package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.content.Intent;
import android.graphics.BitmapRegionDecoder;
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.utils.ImageUtils;
import timber.log.Timber;
/**
* Created by bluesir9 on 16/9/17.
*
* <p>Responsible 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).
*
* <p>todo: Detect selfies?
*/
public class DetectUnwantedPicturesAsync extends AsyncTask<Void, Void, ImageUtils.Result> {
private final String imageMediaFilePath;
public final WeakReference<Activity> activityWeakReference;
DetectUnwantedPicturesAsync(WeakReference<Activity> 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();
}
}
}
}

View file

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

View file

@ -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<Void, Void, Boolean> {
interface Callback {
void onResult(Result result);
}
public enum Result {
NO_DUPLICATE,
DUPLICATE_PROCEED,
DUPLICATE_CANCELLED
}
private final WeakReference<Activity> activity;
private final MediaWikiApi api;
private final String fileSha1;
private final WeakReference<Context> context;
private final Callback callback;
public ExistingFileAsync(WeakReference<Activity> activity, String fileSha1, WeakReference<Context> 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);
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.upload;
import java.util.Locale;
class Language {
private Locale locale;
private boolean isSet = false;

View file

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

View file

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

View file

@ -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<String> 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<Activity>(this), fileSHA1, new WeakReference<Context>(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);
}
}

View file

@ -0,0 +1,5 @@
package fr.free.nrw.commons.upload;
public interface SimilarImageInterface {
void showSimilarImageFragment(String originalFilePath, String possibleFilePath);
}

View file

@ -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<Description> 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<Description> getDescriptions() {
List<Description> 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<String> 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<String> 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<Language> getLocaleSupportedByDevice() {
List<Language> languages = new ArrayList<>();
Locale[] localesArray = Locale.getAvailableLocales();
List<Locale> 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<List<Description>>() {
}.getType();
List<Description> 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 = "<a href='" + licenseUrlFor(license)+"'>"+ getString(Utils.licenseNameFor(license)) + "</a><br>";
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);
}
}

View file

@ -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<Language> languages;
private List<String> languageNamesList;
private List<String> languageCodesList;
private final BiMap<AdapterView, String> selectedLanguages;
public String selectedLangCode="";
public SpinnerLanguagesAdapter(@NonNull Context context,
int resource) {
int resource, BiMap<AdapterView, String> 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<Language> languages) {
this.languages = languages;
private void prepareLanguages() {
List<Language> languages = getLocaleSupportedByDevice();
for(Language language: languages) {
if(!languageCodesList.contains(language.getLocale().getLanguage())) {
languageNamesList.add(language.getLocale().getDisplayName());
languageCodesList.add(language.getLocale().getLanguage());
}
}
}
private List<Language> getLocaleSupportedByDevice() {
List<Language> 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());
}
}

View file

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

View file

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

View file

@ -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<CategoryItem> 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<UploadModel.UploadItem> uploads) {
int uploadCount = uploads.size();
topCardThumbnails.setAdapter(new UploadThumbnailsAdapterFactory(presenter::thumbnailClicked).create(uploads));
topCardTitle.setText(getResources().getQuantityString(R.plurals.upload_count_title, uploadCount, uploadCount));
}
@Override
public void updateRightCardContent(boolean gpsPresent) {
if(gpsPresent){
rightCardMapButton.setVisibility(View.VISIBLE);
}else{
rightCardMapButton.setVisibility(View.GONE);
}
//The card should be disabled if it has no buttons.
setRightCardVisibility(gpsPresent);
}
@Override
public void updateBottomCardContent(int currentStep,
int stepCount,
UploadModel.UploadItem uploadItem,
boolean isShowingItem) {
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<String> licenses, String selectedLicense) {
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, licenses);
licenseSpinner.setAdapter(adapter);
int position = licenses.indexOf(getString(Utils.licenseNameFor(selectedLicense)));
// Check position is valid
if (position < 0) {
Timber.d("Invalid position: %d. Using default license", position);
position = licenses.size() - 1;
}
Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(selectedLicense)));
licenseSpinner.setSelection(position);
}
@SuppressLint("StringFormatInvalid")
@Override
public void updateLicenseSummary(String selectedLicense) {
String licenseHyperLink = "<a href='" + Utils.licenseUrlFor(selectedLicense)+"'>" +
getString(Utils.licenseNameFor(selectedLicense)) + "</a><br>";
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<Description> 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<String> 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<Uri> 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<Description> getDescriptions() {
return descriptionsAdapter.getDescriptions();
}
private void initRecyclerView() {
descriptionsAdapter = new DescriptionsAdapter(this);
descriptionsAdapter.setCallback(this::showInfoAlert);
rvDescriptions.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
rvDescriptions.setAdapter(descriptionsAdapter);
addNewDescription();
}
private void showInfoAlert(int titleStringID, int messageStringId, String... formatArgs) {
new AlertDialog.Builder(this)
.setTitle(titleStringID)
.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");
}
}

View file

@ -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<CategoryItem> create(List<CategoryItem> placeList) {
RendererBuilder<CategoryItem> builder = new RendererBuilder<CategoryItem>()
.bind(CategoryItem.class, new UploadCategoriesRenderer(listener));
ListAdapteeCollection<CategoryItem> collection = new ListAdapteeCollection<>(
placeList != null ? placeList : Collections.emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

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

View file

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

View file

@ -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<String> licenses;
private String license;
private final Map<String, String> licensesByName;
private List<UploadItem> items = new ArrayList<>();
private boolean topCardState = true;
private boolean bottomCardState = true;
private boolean rightCardState = true;
private int currentStepIndex = 0;
private Context context;
private ContentResolver contentResolver;
private boolean useExtStorage;
private Disposable badImageSubscription;
@Inject
SessionManager sessionManager;
private Uri currentMediaUri;
@Inject
UploadModel(@Named("licenses") List<String> licenses,
@Named("default_preferences") SharedPreferences prefs,
@Named("licenses_by_name") Map<String, String> 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<Uri> mediaUri, String mimeType, String source, SimilarImageInterface similarImageInterface) {
initDefaultValues();
Observable<UploadItem> 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<UploadItem> 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<Description> descriptions) {
setCurrentUploadTitle(title);
setCurrentUploadDescriptions(descriptions);
}
private void setCurrentUploadTitle(Title title) {
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
items.get(currentStepIndex).title = title;
}
}
private void setCurrentUploadDescriptions(List<Description> descriptions) {
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
items.get(currentStepIndex).descriptions = descriptions;
}
}
public void previous() {
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<String> getLicenses() {
return licenses;
}
String getSelectedLicense() {
return license;
}
void setSelectedLicense(String licenseName) {
this.license = licensesByName.get(licenseName);
}
Observable<Contribution> buildContributions(List<String> 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<Integer> 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<Integer> imageQuality;
Title title;
List<Description> 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;
}
}
}

View file

@ -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<Uri> 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<Description> 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<Description> 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<Description> 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<Integer> 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<String> getImageTitleList() {
List<String> titleList = new ArrayList<>();
for (UploadItem item : uploadModel.getUploads()) {
if (item.title.isSet()) {
titleList.add(item.title.toString());
}
}
return titleList;
}
}

View file

@ -62,7 +62,9 @@ public class UploadService extends HandlerService<Contribution> {
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<String> unfinishedUploads = new HashSet<>();
// DO NOT HAVE NOTIFICATION ID OF 0 FOR ANYTHING
@ -314,6 +316,7 @@ public class UploadService extends HandlerService<Contribution> {
}
@SuppressLint("StringFormatInvalid")
@SuppressWarnings("deprecation")
private void showFailedNotification(Contribution contribution) {
Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)

View file

@ -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<UploadModel.UploadItem> {
private ThumbnailClickedListener listener;
private SimpleDraweeView background;
private View space;
private ImageView error;
public UploadThumbnailRenderer(ThumbnailClickedListener listener) {
this.listener = listener;
}
@Override
protected View inflate(LayoutInflater inflater, ViewGroup parent) {
return inflater.inflate(R.layout.item_upload_thumbnail, parent, false);
}
@Override
protected void setUpView(View rootView) {
error = rootView.findViewById(R.id.error);
space = rootView.findViewById(R.id.left_space);
background = rootView.findViewById(R.id.thumbnail);
}
@Override
protected void hookListeners(View rootView) {
background.setOnClickListener(v -> listener.thumbnailClicked(getContent()));
}
@Override
public void render() {
UploadModel.UploadItem content = getContent();
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);
}
}

View file

@ -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<UploadModel.UploadItem> create(List<UploadModel.UploadItem> placeList) {
RendererBuilder<UploadModel.UploadItem> builder = new RendererBuilder<UploadModel.UploadItem>()
.bind(UploadModel.UploadItem.class, new UploadThumbnailRenderer(listener));
ListAdapteeCollection<UploadModel.UploadItem> collection = new ListAdapteeCollection<>(
placeList != null ? placeList : Collections.emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

@ -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<Description> 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<UploadModel.UploadItem> 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<String> 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();
}

View file

@ -7,61 +7,61 @@ import java.util.HashMap;
* info in the user language
*/
public class UrlLicense {
HashMap<String,String> urlLicense = new HashMap<String, String>();
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<String,String> urlLicense = new HashMap<>();
static {
urlLicense.put("en", "https://commons.wikimedia.org/wiki/Commons:Licensing");
urlLicense.put("ar", "https://commons.wikimedia.org/wiki/Commons:Licensing/ar");
urlLicense.put("ast", "https://commons.wikimedia.org/wiki/Commons:Licensing/ast");
urlLicense.put("az", "https://commons.wikimedia.org/wiki/Commons:Licensing/az");
urlLicense.put("be", "https://commons.wikimedia.org/wiki/Commons:Licensing/be");
urlLicense.put("bg", "https://commons.wikimedia.org/wiki/Commons:Licensing/bg");
urlLicense.put("bn", "https://commons.wikimedia.org/wiki/Commons:Licensing/bn");
urlLicense.put("ca", "https://commons.wikimedia.org/wiki/Commons:Licensing/ca");
urlLicense.put("cs", "https://commons.wikimedia.org/wiki/Commons:Licensing/cs");
urlLicense.put("da", "https://commons.wikimedia.org/wiki/Commons:Licensing/da");
urlLicense.put("de", "https://commons.wikimedia.org/wiki/Commons:Licensing/de");
urlLicense.put("el", "https://commons.wikimedia.org/wiki/Commons:Licensing/el");
urlLicense.put("eo", "https://commons.wikimedia.org/wiki/Commons:Licensing/eo");
urlLicense.put("es", "https://commons.wikimedia.org/wiki/Commons:Licensing/es");
urlLicense.put("eu", "https://commons.wikimedia.org/wiki/Commons:Licensing/eu");
urlLicense.put("fa", "https://commons.wikimedia.org/wiki/Commons:Licensing/fa");
urlLicense.put("fi", "https://commons.wikimedia.org/wiki/Commons:Licensing/fi");
urlLicense.put("fr", "https://commons.wikimedia.org/wiki/Commons:Licensing/fr");
urlLicense.put("gl", "https://commons.wikimedia.org/wiki/Commons:Licensing/gl");
urlLicense.put("gsw", "https://commons.wikimedia.org/wiki/Commons:Licensing/gsw");
urlLicense.put("he", "https://commons.wikimedia.org/wiki/Commons:Licensing/he");
urlLicense.put("hi", "https://commons.wikimedia.org/wiki/Commons:Licensing/hi");
urlLicense.put("hu", "https://commons.wikimedia.org/wiki/Commons:Licensing/hu");
urlLicense.put("id", "https://commons.wikimedia.org/wiki/Commons:Licensing/id");
urlLicense.put("is", "https://commons.wikimedia.org/wiki/Commons:Licensing/is");
urlLicense.put("it", "https://commons.wikimedia.org/wiki/Commons:Licensing/it");
urlLicense.put("ja", "https://commons.wikimedia.org/wiki/Commons:Licensing/ja");
urlLicense.put("ka", "https://commons.wikimedia.org/wiki/Commons:Licensing/ka");
urlLicense.put("km", "https://commons.wikimedia.org/wiki/Commons:Licensing/km");
urlLicense.put("ko", "https://commons.wikimedia.org/wiki/Commons:Licensing/ko");
urlLicense.put("ku", "https://commons.wikimedia.org/wiki/Commons:Licensing/ku");
urlLicense.put("mk", "https://commons.wikimedia.org/wiki/Commons:Licensing/mk");
urlLicense.put("mr", "https://commons.wikimedia.org/wiki/Commons:Licensing/mr");
urlLicense.put("ms", "https://commons.wikimedia.org/wiki/Commons:Licensing/ms");
urlLicense.put("my", "https://commons.wikimedia.org/wiki/Commons:Licensing/my");
urlLicense.put("nl", "https://commons.wikimedia.org/wiki/Commons:Licensing/nl");
urlLicense.put("oc", "https://commons.wikimedia.org/wiki/Commons:Licensing/oc");
urlLicense.put("pl", "https://commons.wikimedia.org/wiki/Commons:Licensing/pl");
urlLicense.put("pt", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt");
urlLicense.put("pt-br", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br");
urlLicense.put("ro", "https://commons.wikimedia.org/wiki/Commons:Licensing/ro");
urlLicense.put("ru", "https://commons.wikimedia.org/wiki/Commons:Licensing/ru");
urlLicense.put("scn", "https://commons.wikimedia.org/wiki/Commons:Licensing/scn");
urlLicense.put("sk", "https://commons.wikimedia.org/wiki/Commons:Licensing/sk");
urlLicense.put("sl", "https://commons.wikimedia.org/wiki/Commons:Licensing/sl");
urlLicense.put("sv", "https://commons.wikimedia.org/wiki/Commons:Licensing/sv");
urlLicense.put("tr", "https://commons.wikimedia.org/wiki/Commons:Licensing/tr");
urlLicense.put("uk", "https://commons.wikimedia.org/wiki/Commons:Licensing/uk");
urlLicense.put("ur", "https://commons.wikimedia.org/wiki/Commons:Licensing/ur");
urlLicense.put("vi", "https://commons.wikimedia.org/wiki/Commons:Licensing/vi");
urlLicense.put("zh", "https://commons.wikimedia.org/wiki/Commons:Licensing/zh");
}
public String getLicenseUrl ( String language){
public static String getLicenseUrl ( String language){
if (urlLicense.containsKey(language)) {
return urlLicense.get(language);
} else {

View file

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

View file

@ -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<K, V> {
private HashMap<K, V> map = new HashMap<K, V>();
private HashMap<V, K> inversedMap = new HashMap<V, K>();
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<V> getEntrySet(){
return inversedMap.keySet();
}
public void remove(K k){
inversedMap.remove(map.remove(k));
}
public boolean containsKey(V v){
return inversedMap.containsKey(v);
}
}

View file

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

View file

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

View file

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

View file

@ -12,4 +12,8 @@ public class StringUtils {
return Html.fromHtml(source).toString();
}
}
public static boolean isNullOrWhiteSpace(String value) {
return value == null || value.trim().isEmpty();
}
}

View file

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

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FF0000"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
</vector>

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/toolbar"
layout="@layout/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/backgroundImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:background="@color/commons_app_blue_dark"
app:actualImageScaleType="centerCrop" />
<android.support.constraint.ConstraintLayout
android:id="@+id/activity_upload_cards"
android:animateLayoutChanges="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:gravity="bottom">
<include layout="@layout/activity_upload_top_card"/>
<include
layout="@layout/activity_upload_right_card" />
<ViewFlipper
android:id="@+id/view_flipper"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clipChildren="false"
android:measureAllChildren="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<include
layout="@layout/activity_upload_bottom_card"
android:visibility="visible" />
<include layout="@layout/activity_upload_categories" />
<include layout="@layout/activity_upload_license" />
<include layout="@layout/activity_upload_please_wait" />
</ViewFlipper>
</android.support.constraint.ConstraintLayout>
</RelativeLayout>
<android.support.design.widget.NavigationView
android:id="@+id/navigation_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/drawer_header"
app:menu="@menu/drawer" />
</android.support.v4.widget.DrawerLayout>

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bottom_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/cardview_default_elevation"
android:orientation="vertical"
tools:ignore="UnusedAttribute"
tools:showIn="@layout/activity_upload">
<android.support.constraint.ConstraintLayout
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/small_gap">
<TextView
android:id="@+id/bottom_card_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Step 1 of 15" />
<TextView
android:id="@+id/bottom_card_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="@dimen/subtitle_text"
app:layout_constraintTop_toBottomOf="@id/bottom_card_title"
tools:text="1st image" />
<ImageButton
android:id="@+id/bottom_card_expand_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="0dp"
android:rotation="180"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_expand_less_black_24dp" />
<fr.free.nrw.commons.upload.HeightLimitedRecyclerView
android:id="@+id/rv_descriptions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toTopOf="@+id/bottom_card_previous"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottom_card_subtitle"
tools:visibility="gone"/>
<Button
android:id="@+id/bottom_card_next"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<Button
android:id="@+id/bottom_card_previous"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:text="@string/previous"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/bottom_card_next"
app:layout_constraintRight_toLeftOf="@id/bottom_card_next" />
<Button
android:id="@+id/bottom_card_add_desc"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:text="+"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>

View file

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

View file

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

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/standard_gap"
android:layout_marginBottom="8dp"
android:gravity="center"
android:orientation="vertical"
tools:showIn="@layout/activity_upload">
<ProgressBar
android:id="@+id/shareInProgress"
style="?android:progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:indeterminateOnly="true" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_gap"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:gravity="center"
android:text="Receiving shared content,\nthis may take a moment or two." />
</LinearLayout>

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/right_card"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
app:layout_constraintBottom_toTopOf="@+id/view_flipper"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_card"
tools:showIn="@layout/activity_upload"
tools:ignore="UnusedAttribute">
<LinearLayout
android:id="@+id/right_card_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<ImageButton
android:id="@+id/right_card_expand_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_margin="8dp"
android:rotation="90"
app:srcCompat="@drawable/ic_expand_less_black_24dp" />
<ImageButton
android:id="@+id/right_card_map_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:visibility="visible"
app:srcCompat="@drawable/ic_map_white_24dp" />
</LinearLayout>
</android.support.v7.widget.CardView>

View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/top_card"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:elevation="@dimen/cardview_default_elevation"
tools:ignore="UnusedAttribute"
tools:showIn="@layout/activity_upload">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/top_card_title"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="@dimen/small_gap"
android:layout_marginTop="@dimen/small_gap"
android:layout_marginEnd="@dimen/small_gap"
android:layout_marginBottom="@dimen/small_gap"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:gravity="center_vertical"
android:textSize="@dimen/normal_text"
android:textStyle="bold"
tools:text="4 Uploads" />
<ImageButton
android:id="@+id/top_card_expand_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="@dimen/small_gap"
android:layout_marginTop="@dimen/small_gap"
android:layout_marginEnd="@dimen/small_gap"
android:layout_marginBottom="@dimen/small_gap"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:padding="0dp"
app:srcCompat="@drawable/ic_expand_less_black_24dp" />
<android.support.v7.widget.RecyclerView
android:id="@+id/top_card_thumbnails"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginBottom="@dimen/small_gap"
android:layout_below="@id/top_card_title" />
</RelativeLayout>
</android.support.v7.widget.CardView>

View file

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/fragmentCategorisationBackground"
android:clickable="true"
android:focusableInTouchMode="true"
android:orientation="vertical"
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:theme="@style/DarkAppTheme">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusableInTouchMode="true">
<EditText
android:id="@+id/categoriesSearchBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/categories_search_text_hint"
android:maxLines="1"
android:gravity="left"
android:inputType="textCapWords"
android:imeOptions="flagNoExtractUi"/>
<ProgressBar
android:id="@+id/categoriesSearchInProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:indeterminateOnly="true"
android:layout_marginRight="@dimen/tiny_gap"
android:layout_marginEnd="@dimen/tiny_gap"
android:layout_gravity="center_vertical|right"
style="?android:progressBarStyleSmall"
android:visibility="gone"
/>
</FrameLayout>
<TextView
android:id="@+id/categoriesNotFound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:visibility="gone" />
<TextView
android:id="@+id/categoriesExplanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/huge_gap"
android:focusable="true"
android:gravity="center"
android:text="@string/categories_skip_explanation"
android:visibility="gone" />
<android.support.v7.widget.RecyclerView
android:id="@+id/categoriesListBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fadingEdge="none" />
</LinearLayout>

View file

@ -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">
<LinearLayout
@ -31,39 +31,42 @@
android:layout_height="wrap_content"
android:drawableEnd="@drawable/mapbox_info_icon_default"
android:drawableRight="@drawable/mapbox_info_icon_default"
android:maxLines="1"
android:maxLength="80"
android:hint="@string/share_title_hint"
android:imeOptions="flagNoExtractUi"
android:inputType="text"
android:maxLength="80"
android:maxLines="1"
android:scrollHorizontally="false" />
</android.support.design.widget.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_descriptions"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout
android:layout_marginTop="4dp"
android:orientation="horizontal"
android:id="@+id/ll_add_description"
android:layout_width="wrap_content"
android:layout_gravity="right"
android:gravity="right"
android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<TextView
style="@style/TextAppearance.AppCompat.Body1"
android:text="@string/add_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_descriptions"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/ll_add_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginTop="4dp"
android:gravity="right"
android:orientation="horizontal"
android:padding="10dp">
<TextView
style="@style/TextAppearance.AppCompat.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add_description" />
</LinearLayout>
</LinearLayout>
<Spinner
android:id="@+id/licenseSpinner"
android:layout_width="match_parent"
@ -82,10 +85,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_gap"
android:gravity="center"
android:clickable="true"
android:textColorLink="@color/button_blue"
android:text="@string/share_license_summary" />
android:gravity="center"
android:text="@string/share_license_summary"
android:textColorLink="@color/button_blue" />
<fr.free.nrw.commons.ui.widget.HtmlTextView
android:id="@+id/media_upload_policy"

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<android.support.v4.widget.Space
android:id="@+id/left_space"
android:layout_width="8dp"
android:layout_height="90dp" />
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/thumbnail"
android:layout_width="90dp"
android:layout_height="90dp"
fresco:actualImageScaleType="fitCenter" />
<android.support.v4.widget.Space
android:id="@+id/right_space"
android:layout_width="8dp"
android:layout_height="90dp" />
</LinearLayout>
<ImageView
android:id="@+id/error"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="end"
android:visibility="gone"
app:srcCompat="@drawable/ic_error_red_24dp" />
</FrameLayout>

View file

@ -7,6 +7,6 @@
android:checked="false"
android:gravity="center_vertical"
android:padding="@dimen/tiny_gap"
android:theme="@style/DarkAppTheme">
android:textColor="@color/primaryDarkColor">
</CheckedTextView>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checkMark="?android:attr/textCheckMark"
android:checked="false"
android:gravity="center_vertical"
android:padding="@dimen/tiny_gap"
android:textColor="@color/primaryDarkColor" />

View file

@ -1,36 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:weightSum="10">
<android.support.v7.widget.AppCompatSpinner
android:id="@+id/spinner_description_languages"
android:layout_width="0dp"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="3"
tools:listitem="@layout/row_item_languages_spinner"
android:spinnerMode="dialog"></android.support.v7.widget.AppCompatSpinner>
android:orientation="horizontal"
android:weightSum="8">
<android.support.design.widget.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="7">
<android.support.v7.widget.AppCompatSpinner
android:id="@+id/spinner_description_languages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:minWidth="1dp"
android:padding="0dp"
android:spinnerMode="dialog" />
<EditText
android:id="@+id/et_description_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/mapbox_info_icon_default"
android:drawableRight="@drawable/mapbox_info_icon_default"
android:hint="@string/share_description_hint"
android:imeOptions="flagNoExtractUi"
android:inputType="textMultiLine"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="217dp"
android:layout_height="wrap_content"
android:layout_weight="8">
<EditText
android:id="@+id/description_item_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/mapbox_info_icon_default"
android:drawableRight="@drawable/mapbox_info_icon_default"
android:hint="@string/share_description_hint"
android:imeOptions="flagNoExtractUi"
android:inputType="textMultiLine" />
</android.support.design.widget.TextInputLayout>
</LinearLayout>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.TextInputLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/image_title_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:showIn="@layout/activity_upload">
<android.support.design.widget.TextInputEditText
android:id="@+id/description_item_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/share_title_hint"
android:imeOptions="actionNext"
android:inputType="text"
android:maxLines="1"
android:nextFocusForward="@+id/image_description" />
</android.support.design.widget.TextInputLayout>

View file

@ -240,8 +240,8 @@
<string name="error_while_cache">خطأ أثناء تخزين الصور</string>
<string name="title_info">عنوان وصفي فريد للملف، والذي سيكون بمثابة اسم الملف، يمكنك استخدام لغة واضحة مع مسافات، لا تقم بتضمين امتداد الملف</string>
<string name="description_info">يُرجَى وصف الوسائط قدر الإمكان: أين تم التقاطها؟ ما تظهر؟ ما هو السياق؟ يُرجَى وصف الأشياء أو الأشخاص، اكشف المعلومات التي لا يمكن تخمينها بسهولة، على سبيل المثال الوقت في اليوم إذا كان منظرا طبيعيا، إذا أظهرت الوسائط شيئا غير عادي، فيُرجَى توضيح ما يجعله غير عادي.</string>
<string name="upload_image_too_dark">هذه الصورة مظلمة للغاية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا ​​كومنز للصور ذات القيمة الموسوعية فقط.</string>
<string name="upload_image_blurry">هذه الصورة ضبابية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا ​​كومنز للصور ذات القيمة الموسوعية فقط.</string>
<string name="upload_image_problem_dark">هذه الصورة مظلمة للغاية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا ​​كومنز للصور ذات القيمة الموسوعية فقط.</string>
<string name="upload_image_problem_blurry">هذه الصورة ضبابية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا ​​كومنز للصور ذات القيمة الموسوعية فقط.</string>
<string name="give_permission">إعطاء السماح</string>
<string name="use_external_storage">استخدم تخزينا خارجيا</string>
<string name="use_external_storage_summary">احفظ الصور الملتقطة بالكاميرا داخل التطبيق على جهازك</string>

View file

@ -168,7 +168,7 @@
<string name="title_activity_nearby">Llugares cercanos</string>
<string name="no_nearby">Nun s\'alcontraron llugares cercanos</string>
<string name="warning">Avisu</string>
<string name="file_exists">Esti ficheru yá esiste\'n Commons. ¿Confirmes que quies siguir?</string>
<string name="upload_image_problem_duplicate">Esti ficheru yá esiste\'n Commons. ¿Confirmes que quies siguir?</string>
<string name="yes"></string>
<string name="no">Non</string>
<string name="media_detail_title">Títulu</string>
@ -230,8 +230,8 @@
<string name="error_while_cache">Error al poner les fotos na caché</string>
<string name="title_info">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 ficheru</string>
<string name="description_info">Por 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.</string>
<string name="upload_image_too_dark">Esta imaxe ye escura enforma, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.</string>
<string name="upload_image_blurry">Esta imaxe ta borrosa, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.</string>
<string name="upload_image_problem_dark">Esta imaxe ye escura enforma, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.</string>
<string name="upload_image_problem_blurry">Esta imaxe ta borrosa, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.</string>
<string name="give_permission">Dar permisu</string>
<string name="use_external_storage">Usar almacenamientu esternu</string>
<string name="use_external_storage_summary">Guardar nel preséu les imaxes tomaes cola cámara de la app</string>

View file

@ -169,7 +169,7 @@
<string name="title_activity_nearby">Mesta u blizini</string>
<string name="no_nearby">Nisu pronađena obližnja mesta</string>
<string name="warning">Upozorenje</string>
<string name="file_exists">Ova datoteka je već dostupna na Ostavi. Da li ste sigurni da želite da nastavite?</string>
<string name="upload_image_problem_duplicate">Ova datoteka je već dostupna na Ostavi. Da li ste sigurni da želite da nastavite?</string>
<string name="yes">Da</string>
<string name="no">Ne</string>
<string name="media_detail_title">Naslov</string>

View file

@ -157,7 +157,7 @@
<string name="location_permission_rationale">Мотлаҡ булмаған рөхсәт: категория тәҡдиме өсөн ошо урынды алыу</string>
<string name="title_activity_nearby">Яҡындағы урындар</string>
<string name="no_nearby">Яҡындағы урындар табылманы</string>
<string name="file_exists">Был файл Викискладта бар. Дауам итергә ризаһыңмы?</string>
<string name="upload_image_problem_duplicate">Был файл Викискладта бар. Дауам итергә ризаһыңмы?</string>
<string name="media_detail_title">Атама</string>
<string name="media_detail_media_title">Мәғлүмәт йөрөтөүсенең атамаһы</string>
<string name="media_detail_description_explanation">Мәғлүмәт йөрөтөүсене һүрәтләү ошонда яҙыла.Уның ярайһы уҡ оҙон, хатта бер-нисә юлға һуҙылып китеүе лә бар. Шулай булһа ла ул бик матур күренер тип уйлайбыҙ.</string>
@ -204,8 +204,8 @@
<string name="error_while_cache">Рәсемде кэшлағандағы хата</string>
<string name="title_info">Файлдың исеме булараҡ һаҡланасаҡ үҙенсәлекле һәртәләү. Тәбиғи телегеҙҙе, һүҙҙәр араһын айырып, ҡулланырға була. Зинһар, файл киңәйтеүҙәрен күрһәтмәгеҙ.</string>
<string name="description_info">Зинһар, тейәләсәк файлды тәфсирләп һүрәтлә:ҡайҙа төшөрөлгән? нимә һәрәтләнә? һүрәт нимәне аңлата? Рәсемдәге кешеләр йәки объекттарҙы ла һүрәтлә. Һүрәткә ҡарап ҡына белеп булмаған мәғлүмәттәрҙе өҫтә: мәҫәлән, тәүлектең ниндәй мәлендә, ҡасан төшөрөлгән был файл. Әгәр ғәҙәти булмаған әйбер төшөрөлһә, уның нимәһе шаҡ ҡатырғанын аңлат.</string>
<string name="upload_image_too_dark">Был рәсем бик ҡараңғы күренә. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.</string>
<string name="upload_image_blurry">Был рәсем асыҡ түгел. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.</string>
<string name="upload_image_problem_dark">Был рәсем бик ҡараңғы күренә. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.</string>
<string name="upload_image_problem_blurry">Был рәсем асыҡ түгел. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.</string>
<string name="give_permission">Рөхсәт бирәм</string>
<string name="use_external_storage">Тышҡы һаҡлағысты ҡуллан</string>
<string name="use_external_storage_summary">Ҡулайламаның камераһы ярҙамында төшөрөлгән һүрәттәрҙе һаҡлау</string>

View file

@ -76,7 +76,7 @@
<string name="menu_refresh">Обновяване</string>
<string name="ok">Добре</string>
<string name="warning">Предупреждение</string>
<string name="file_exists">Този файл вече съществува в Общомедия. Наистина ли искате да продължите?</string>
<string name="upload_image_problem_duplicate">Този файл вече съществува в Общомедия. Наистина ли искате да продължите?</string>
<string name="yes">Да</string>
<string name="no">Не</string>
<string name="media_detail_title">Заглавие</string>

View file

@ -178,7 +178,7 @@
<string name="title_activity_nearby">কাছাকাছি স্থান</string>
<string name="no_nearby">কাছাকাছি কোন স্থান পাওয়া যায়নি</string>
<string name="warning">সতর্কীকরণ</string>
<string name="file_exists">এই ফাইলটি ইতিমধ্যে কমন্সে বিদ্যমান। আপনি কি নিশ্চিত আপনি সামনে এগুতে চান?</string>
<string name="upload_image_problem_duplicate">এই ফাইলটি ইতিমধ্যে কমন্সে বিদ্যমান। আপনি কি নিশ্চিত আপনি সামনে এগুতে চান?</string>
<string name="yes">হ্যাঁ</string>
<string name="no">না</string>
<string name="media_detail_title">শিরোনাম</string>
@ -239,8 +239,8 @@
<string name="error_while_cache">ছবি আনার সময় ত্রুটি</string>
<string name="title_info">ফাইলের একটি স্বতন্ত্র বর্ণনামূলক নাম যা ফাইলের নাম হিসাবে কাজ করবে। অাপনি সাধারণ ভাষা ব্যবহার করতে পারেন শূন্যস্থানসহ। ফাইলের এক্সটেনশন যুক্ত করবেন না।</string>
<string name="description_info">যতটা সম্ভব মিডিয়াটি বর্ণনা করুন: এটি কোথায় ধারণ করা হয়েছিল? এটি কি প্রদর্শন করে? এটির প্রসঙ্গ কি? ধারণকৃত বস্তু অথবা ব্যক্তির বর্ণনা করুন। সহজে অনুমান করা যায়না সেরকম তথ্য উদঘাটন করুন, উদাহরণস্বরূপ, যদি ল্যান্ডস্কেপ হয় তাহলে দিবসকালের সময় দিন।</string>
<string name="upload_image_too_dark">এই ছবিটি খুবই অন্ধকারময়, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।</string>
<string name="upload_image_blurry">এই ছবিটি অস্পষ্ট, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।</string>
<string name="upload_image_problem_dark">এই ছবিটি খুবই অন্ধকারময়, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।</string>
<string name="upload_image_problem_blurry">এই ছবিটি অস্পষ্ট, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।</string>
<string name="give_permission">অনুমতি দিন</string>
<string name="use_external_storage">বাহ্যিক সঞ্চয়স্থান ব্যবহার করুন</string>
<string name="use_external_storage_summary">অাপনার ডিভাইসের নিজস্ব ক্যামেরায় ধারণকৃত ছবি সংরক্ষণ করুন</string>

View file

@ -166,7 +166,7 @@
<string name="title_activity_nearby">Lec\'hioù nes</string>
<string name="no_nearby">N\'eus bet kavet netra tostik</string>
<string name="warning">Diwallit</string>
<string name="file_exists">Emañ ar restr-mañ war Commons c\'hoazh. Ha sur oc\'h e fell deoc\'h kenderc\'hel ?</string>
<string name="upload_image_problem_duplicate">Emañ ar restr-mañ war Commons c\'hoazh. Ha sur oc\'h e fell deoc\'h kenderc\'hel ?</string>
<string name="yes">Ya</string>
<string name="no">Ket</string>
<string name="media_detail_title">Titl</string>

View file

@ -142,7 +142,7 @@
<string name="title_activity_nearby">Mjesta u blizini</string>
<string name="no_nearby">Nema okolnih mjesta</string>
<string name="warning">Upozorenje</string>
<string name="file_exists">Ova datoteka već postoji na Commonsu. Jeste li sigurni da želite nastaviti?</string>
<string name="upload_image_problem_duplicate">Ova datoteka već postoji na Commonsu. Jeste li sigurni da želite nastaviti?</string>
<string name="yes">Da</string>
<string name="no">Ne</string>
<string name="media_detail_title">Naslov</string>

View file

@ -139,7 +139,7 @@
<string name="title_activity_nearby">Llocs propers</string>
<string name="no_nearby">No s\'han trobat llocs propers</string>
<string name="warning">Avís</string>
<string name="file_exists">El fitxer ja existeix a Commons. Segur que voleu procedir?</string>
<string name="upload_image_problem_duplicate">El fitxer ja existeix a Commons. Segur que voleu procedir?</string>
<string name="yes"></string>
<string name="no">No</string>
<string name="media_detail_title">Títol</string>

View file

@ -41,7 +41,7 @@
<string name="ok">باشە</string>
<string name="title_activity_nearby">شوێنە نزیکەکان</string>
<string name="warning">ئاگاداری</string>
<string name="file_exists">ئەم پەڕگەیە لەسەر کۆمنز ھەیە. دڵنیایت کە دەتەوێت بەردەوام بیت؟</string>
<string name="upload_image_problem_duplicate">ئەم پەڕگەیە لەسەر کۆمنز ھەیە. دڵنیایت کە دەتەوێت بەردەوام بیت؟</string>
<string name="yes">بەڵێ</string>
<string name="no">نەخێر</string>
<string name="media_detail_title">ناونیشان</string>

View file

@ -181,7 +181,7 @@
<string name="title_activity_nearby">Místa v okolí</string>
<string name="no_nearby">Poblíž nebylo nic nalezeno</string>
<string name="warning">Upozornění</string>
<string name="file_exists">Tento soubor již na Commons existuje. Jste si jist, že chcete pokračovat?</string>
<string name="upload_image_problem_duplicate">Tento soubor již na Commons existuje. Jste si jist, že chcete pokračovat?</string>
<string name="yes">Ano</string>
<string name="no">Ne</string>
<string name="media_detail_title">Název</string>
@ -243,8 +243,8 @@
<string name="error_while_cache">Chyba při meziukládání obrázků</string>
<string name="title_info">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.</string>
<string name="description_info">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.</string>
<string name="upload_image_too_dark">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.</string>
<string name="upload_image_blurry">Tento obrázek je rozmazaný, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.</string>
<string name="upload_image_problem_dark">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.</string>
<string name="upload_image_problem_blurry">Tento obrázek je rozmazaný, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.</string>
<string name="give_permission">Dát povolení</string>
<string name="use_external_storage">Použít externí úložiště</string>
<string name="use_external_storage_summary">Uložit obrázky pořízené fotoaparátem, jenž je součástí této aplikace</string>

View file

@ -174,7 +174,7 @@
<string name="title_activity_nearby">Steder i nærheden</string>
<string name="no_nearby">Ingen steder i nærheden fundet</string>
<string name="warning">Advarsel</string>
<string name="file_exists">Denne fil findes allerede på Commons. Er du sikker på, at du ønsker at fortsætte?</string>
<string name="upload_image_problem_duplicate">Denne fil findes allerede på Commons. Er du sikker på, at du ønsker at fortsætte?</string>
<string name="yes">Ja</string>
<string name="no">Nej</string>
<string name="media_detail_title">Titel</string>
@ -235,8 +235,8 @@
<string name="error_while_cache">Fejl under mellemlagring af billeder</string>
<string name="title_info">En unik beskrivelse for filen, som vil fungere som et filnavn. Du kan bruge normalt sprog med mellemrum. Udelad filendelsen.</string>
<string name="description_info">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.</string>
<string name="upload_image_too_dark">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.</string>
<string name="upload_image_blurry">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.</string>
<string name="upload_image_problem_dark">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.</string>
<string name="upload_image_problem_blurry">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.</string>
<string name="give_permission">Giv tilladelse</string>
<string name="use_external_storage">Brug eksternt lager</string>
<string name="use_external_storage_summary">Gem billeder taget med din enheds program på kameraet</string>

View file

@ -173,7 +173,7 @@
<string name="title_activity_nearby">Orte in der Nähe</string>
<string name="no_nearby">Keine Orte in der Nähe gefunden</string>
<string name="warning">Warnung</string>
<string name="file_exists">Diese Datei ist bereits auf Commons vorhanden. Bist du sicher, dass du fortfahren möchtest?</string>
<string name="upload_image_problem_duplicate">Diese Datei ist bereits auf Commons vorhanden. Bist du sicher, dass du fortfahren möchtest?</string>
<string name="yes">Ja</string>
<string name="no">Nein</string>
<string name="media_detail_title">Titel</string>
@ -235,8 +235,8 @@
<string name="error_while_cache">Fehler beim Zwischenspeichern der Bilder</string>
<string name="title_info">Ein eindeutiger beschreibender Titel für die Datei, der als Dateiname dient. Du kannst Klartext mit Leerzeichen verwenden. Gib nicht die Dateierweiterung mit an.</string>
<string name="description_info">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.</string>
<string name="upload_image_too_dark">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.</string>
<string name="upload_image_blurry">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.</string>
<string name="upload_image_problem_dark">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.</string>
<string name="upload_image_problem_blurry">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.</string>
<string name="give_permission">Berechtigung geben</string>
<string name="use_external_storage">Externen Speicher verwenden</string>
<string name="use_external_storage_summary">Mit der In-App-Kamera aufgenommene Bilder auf deinem Gerät speichern</string>

View file

@ -46,7 +46,7 @@
<string name="ok">हुन्छ</string>
<string name="title_activity_nearby">नज्यूकाऽ ठउरअन</string>
<string name="warning">चेतावनी</string>
<string name="file_exists">यो फाइल कमन्स मी पैली बठेइ छ। तम पक्का छऽ कि तम ऐतिर जान चाहन्छऽ?</string>
<string name="upload_image_problem_duplicate">यो फाइल कमन्स मी पैली बठेइ छ। तम पक्का छऽ कि तम ऐतिर जान चाहन्छऽ?</string>
<string name="yes">हो</string>
<string name="no">नाइँ</string>
<string name="media_detail_title">शीर्षक</string>

View file

@ -176,7 +176,7 @@
<string name="title_activity_nearby">Κοντινοί Τόποι</string>
<string name="no_nearby">Δεν βρέθηκαν τόποι εδώ κοντά</string>
<string name="warning">Προειδοποίηση</string>
<string name="file_exists">Αυτό το αρχείο υπάρχει ήδη στα Commons. Είστε σίγουρος ότι θέλετε να συνεχίσετε;</string>
<string name="upload_image_problem_duplicate">Αυτό το αρχείο υπάρχει ήδη στα Commons. Είστε σίγουρος ότι θέλετε να συνεχίσετε;</string>
<string name="yes">Ναι</string>
<string name="no">Όχι</string>
<string name="media_detail_title">Τίτλος</string>
@ -238,8 +238,8 @@
<string name="error_while_cache">Υπήρξε σφάλμα κατά την σκίαση εικόνων</string>
<string name="title_info">Ένας μοναδικός τίτλος περιγραφής του φακέλλου, που θα χρησιμεύσει ως όνομα φακέλλου. Μπορείτε να χρησιμοποιήσετε τις ήδη υπάρχουσες γλώσσες με διαστήματα. Μην συμπεριλάβετε την επέκταση φακέλλου</string>
<string name="description_info">\nΠαρακαλώ περιγράψετε τα μέσα το δυνατό περισσότερο : Πού οδηγήθηκε αυτό; Τι δείχνει; Ποιο είναι το περιεχόμενο του; Παρακαλώ περιγράψετε τα αντικείμενα ή τα πρόσωπα. Αποκαλύψετε πληροφορίες που δεν μπορούν εύκολο να μαντέψει κανείς, για παράδειγμα την ώρα εντός της ημέρας αν πρόκειται για τοπίο. Αν τα μέσα δείξουν κάτι ασύνηθες, παρακαλώ εξηγήστε τι το καθιστά μη συνηθισμένα.</string>
<string name="upload_image_too_dark">Αυτή η εικόνα είναι πολύ σκοτεινή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.</string>
<string name="upload_image_blurry">Αυτή η εικόνα είναι θολή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.</string>
<string name="upload_image_problem_dark">Αυτή η εικόνα είναι πολύ σκοτεινή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.</string>
<string name="upload_image_problem_blurry">Αυτή η εικόνα είναι θολή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.</string>
<string name="give_permission">Χορηγήστε άδεια</string>
<string name="use_external_storage">Χρησιμοποιήσετε την εξωτερική αποθήκευση</string>
<string name="use_external_storage_summary">Αποθηκεύσετε εικόνες που παίρνονται στην κάμερα εφαρμογής στην συσκευή σας</string>

View file

@ -181,7 +181,7 @@
<string name="title_activity_nearby">Lugares cercanos</string>
<string name="no_nearby">No se encontraron lugares cercanos</string>
<string name="warning">Atención</string>
<string name="file_exists">Este archivo ya existe en Commons. ¿Confirmas que quieres continuar?</string>
<string name="upload_image_problem_duplicate">Este archivo ya existe en Commons. ¿Confirmas que quieres continuar?</string>
<string name="yes"></string>
<string name="no">No</string>
<string name="media_detail_title">Título</string>
@ -243,8 +243,8 @@
<string name="error_while_cache">Error al almacenar imágenes en la antememoria</string>
<string name="title_info">Un 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.</string>
<string name="description_info">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.</string>
<string name="upload_image_too_dark">Esta imagen es demasiado oscura. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.</string>
<string name="upload_image_blurry">Esta imagen se ve borrosa. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.</string>
<string name="upload_image_problem_dark">Esta imagen es demasiado oscura. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.</string>
<string name="upload_image_problem_blurry">Esta imagen se ve borrosa. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.</string>
<string name="give_permission">Otorgar permiso</string>
<string name="use_external_storage">Utilizar almacenamiento externo</string>
<string name="use_external_storage_summary">Guardar en el dispositivo imágenes capturadas con la cámara de la aplicación</string>

View file

@ -172,7 +172,7 @@
<string name="title_activity_nearby">Gertuko lekuak</string>
<string name="no_nearby">Ez da hurbileko lekurik aurkitu</string>
<string name="warning">Oharra</string>
<string name="file_exists">Fifxategia dagoeneko Commonsen existitzen da. Ziur zaude jarraitu nahi duzula?</string>
<string name="upload_image_problem_duplicate">Fifxategia dagoeneko Commonsen existitzen da. Ziur zaude jarraitu nahi duzula?</string>
<string name="yes">Bai</string>
<string name="no">Ez</string>
<string name="media_detail_title">Izenburua</string>
@ -229,8 +229,8 @@
<string name="error_while_cache">Argazkiak hartzerakoan sortutako akatsa</string>
<string name="title_info">Fitxategi izenburu deskribatzaile bakarra, fitxategi-izen gisa balioko duena. Hizkuntza arrunta erabil dezakezu espazioekin. Ez sartu fitxategiaren luzapena.</string>
<string name="description_info">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.</string>
<string name="upload_image_too_dark">Argazkia ilunegia da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.</string>
<string name="upload_image_blurry">Argazkia lausoa da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.</string>
<string name="upload_image_problem_dark">Argazkia ilunegia da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.</string>
<string name="upload_image_problem_blurry">Argazkia lausoa da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.</string>
<string name="give_permission">Baimena eman</string>
<string name="use_external_storage">Kanpo-biltegia erabili</string>
<string name="use_external_storage_summary">Aplikazioaren kamerarekin ateratako argazkiak zure gailuan gorde</string>

View file

@ -174,7 +174,7 @@
<string name="title_activity_nearby">مکان‌‌های اطراف</string>
<string name="no_nearby">مکانی در نزدیکی یافت نشد</string>
<string name="warning">هشدار</string>
<string name="file_exists">پرونده در ویکی‌انبار موجود است. آیا مطمئنید که می‌خواهید ادامه دهید؟</string>
<string name="upload_image_problem_duplicate">پرونده در ویکی‌انبار موجود است. آیا مطمئنید که می‌خواهید ادامه دهید؟</string>
<string name="yes">بله</string>
<string name="no">خیر</string>
<string name="media_detail_title">عنوان</string>
@ -236,8 +236,8 @@
<string name="error_while_cache">خطا در زمان دریافت تصاویر</string>
<string name="title_info">عنوانی توصیفی و یکتا برای پرونده که به عنوان نام پرونده در نظر گرفته خواهد شد. ترجیحاً به زبان ساده باشد، می‌توانید فاصله هم به کار ببرید. پسوند پرونده را ننویسید.</string>
<string name="description_info">لطفاً تصویر را تا حد توان شرح دهید. کجا گرفته شده‌است؟ شامل چه چیزی می‌شود؟ لطفاً اشیا یا افراد را شرح دهید. اطلاعاتی که به راحتی قابل مشاهده هستند را صرفه‌نظر کنید. اگر چیزی در تصویر غیر طبیعی به نظر می‌رسد آن را شرح دهید.</string>
<string name="upload_image_too_dark">این تصویر خیلی تیره است آیا مطمئنید که می‌خواهید آن را بارگذاری کنید؟ ویکی‌انبار فقط برای نگهداری از تصاویری که ارزش دانشنامه‌ای داشته باشند، است.</string>
<string name="upload_image_blurry">این تصویر خیلی تار است آیا مطمئنید که می‌خواهید آن را بارگذاری کنید؟ ویکی‌انبار فقط برای نگهداری از تصاویری که ارزش دانشنامه‌ای داشته باشند، است.</string>
<string name="upload_image_problem_dark">این تصویر خیلی تیره است آیا مطمئنید که می‌خواهید آن را بارگذاری کنید؟ ویکی‌انبار فقط برای نگهداری از تصاویری که ارزش دانشنامه‌ای داشته باشند، است.</string>
<string name="upload_image_problem_blurry">این تصویر خیلی تار است آیا مطمئنید که می‌خواهید آن را بارگذاری کنید؟ ویکی‌انبار فقط برای نگهداری از تصاویری که ارزش دانشنامه‌ای داشته باشند، است.</string>
<string name="give_permission">اجازه بده</string>
<string name="use_external_storage">استفاده از حافظهٔ خارجی</string>
<string name="use_external_storage_summary">ذخیرهٔ تصویرهای گرفته شده توسط دوربین درونکار اپلیکیشن بر روی دستگاه شما</string>

View file

@ -176,7 +176,7 @@
<string name="title_activity_nearby">Lähellä olevat paikat</string>
<string name="no_nearby">Lähistöltä ei löytynyt paikkoja</string>
<string name="warning">Varoitus</string>
<string name="file_exists">Tämä tiedosto on jo Wikimedia Commonsissa. Haluatko varmasti jatkaa?</string>
<string name="upload_image_problem_duplicate">Tämä tiedosto on jo Wikimedia Commonsissa. Haluatko varmasti jatkaa?</string>
<string name="yes">Kyllä</string>
<string name="no">Ei</string>
<string name="media_detail_title">Otsikko</string>
@ -237,8 +237,8 @@
<string name="error_while_cache">Virhe varastoidessa kuvia</string>
<string name="title_info">Tiedoston yksilöllinen ja kuvaava otsikko, jota käytetään tiedostonimenä. Voit käyttää tavallista kieltä välilyönnein. Älä sisällytä tiedoston päätettä.</string>
<string name="description_info">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.</string>
<string name="upload_image_too_dark">Tämä kuva on liian tumma, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.</string>
<string name="upload_image_blurry">Tämä kuva on epäselvä, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.</string>
<string name="upload_image_problem_dark">Tämä kuva on liian tumma, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.</string>
<string name="upload_image_problem_blurry">Tämä kuva on epäselvä, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.</string>
<string name="give_permission">Anna lupa</string>
<string name="use_external_storage">Käytä ulkoista tallennustilaa</string>
<string name="use_external_storage_summary">Tallenna sovelluksen sisäisen kameran kanssa otetut kuvat laitteellesi</string>

View file

@ -185,7 +185,7 @@
<string name="title_activity_nearby">Endroits à proximité</string>
<string name="no_nearby">Rien trouvé dans le voisinage</string>
<string name="warning">Avertissement</string>
<string name="file_exists">Ce fichier existe déjà sur Commons. Êtes-vous sûr de vouloir continuer?</string>
<string name="upload_image_problem_duplicate">Ce fichier existe déjà sur Commons. Êtes-vous sûr de vouloir continuer?</string>
<string name="yes">Oui</string>
<string name="no">Non</string>
<string name="media_detail_title">Titre</string>
@ -247,8 +247,8 @@
<string name="error_while_cache">Erreur en mettant les images en cache</string>
<string name="title_info">Un titre descriptif unique pour le fichier, qui servira de nom de fichier. Vous pouvez utiliser un langage simple avec des espaces. Nincluez pas lextension du fichier</string>
<string name="description_info">Veuillez 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 lheure de la journée si cest un paysage. Si le média montre quelque chose dinhabituel, veuillez expliquer ce qui le rend exceptionnel.</string>
<string name="upload_image_too_dark">Cette image est trop sombre, êtes-vous sûr de vouloir la télécharger? Wikimédia Communs nest que pour les images avec une valeur encyclopédique.</string>
<string name="upload_image_blurry">Cette image est floue, êtes-vous sûr de vouloir la télécharger? Wikimédia Communs nest que pour les images ayant une valeur encyclopédique.</string>
<string name="upload_image_problem_dark">Cette image est trop sombre, êtes-vous sûr de vouloir la télécharger? Wikimédia Communs nest que pour les images avec une valeur encyclopédique.</string>
<string name="upload_image_problem_blurry">Cette image est floue, êtes-vous sûr de vouloir la télécharger? Wikimédia Communs nest que pour les images ayant une valeur encyclopédique.</string>
<string name="give_permission">Accorder le droit</string>
<string name="use_external_storage">Utiliser le stockage externe</string>
<string name="use_external_storage_summary">Enregistrer les images prises avec lappareil photo de votre appareil</string>

View file

@ -141,7 +141,7 @@
<string name="title_activity_nearby">Steeden naibi</string>
<string name="no_nearby">Nian steeden uun a naite fünjen</string>
<string name="warning">Wäärnang</string>
<string name="file_exists">Detdiar datei jaft det al üüb Commons. Beest dü seeker, dat dü widjer maage wel?</string>
<string name="upload_image_problem_duplicate">Detdiar datei jaft det al üüb Commons. Beest dü seeker, dat dü widjer maage wel?</string>
<string name="yes">Ja</string>
<string name="no">Naan</string>
<string name="media_detail_title">Tiitel</string>

View file

@ -175,7 +175,7 @@
<string name="title_activity_nearby">Lugares próximos</string>
<string name="no_nearby">Non se atoparon lugares preto</string>
<string name="warning">Aviso</string>
<string name="file_exists">Este ficheiro xa existe en Commons. Está seguro de que quere continuar?</string>
<string name="upload_image_problem_duplicate">Este ficheiro xa existe en Commons. Está seguro de que quere continuar?</string>
<string name="yes">Si</string>
<string name="no">Non</string>
<string name="media_detail_title">Título</string>
@ -237,8 +237,8 @@
<string name="error_while_cache">Erro mentras se gardaban as imaxes na caché</string>
<string name="title_info">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 ficheiro</string>
<string name="description_info">Por 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.</string>
<string name="upload_image_too_dark">Esta imaxe é demasiado escura. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.</string>
<string name="upload_image_blurry">Esta imaxe está borrosa. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.</string>
<string name="upload_image_problem_dark">Esta imaxe é demasiado escura. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.</string>
<string name="upload_image_problem_blurry">Esta imaxe está borrosa. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.</string>
<string name="give_permission">Outorgar permiso</string>
<string name="use_external_storage">Usar o almacenamento externo</string>
<string name="use_external_storage_summary">Gardar as imaxes capturadas coa cámara do seu dispositivo</string>

View file

@ -154,7 +154,7 @@
<string name="title_activity_nearby">आसपास के स्थान</string>
<string name="no_nearby">पास के कोई भी स्थान नहीं मिले</string>
<string name="warning">चेतावनी</string>
<string name="file_exists">यह फ़ाइल कॉमन्स पर पहले से है। क्या आप फिर भी आगे बढ़ना चाहते हैं?</string>
<string name="upload_image_problem_duplicate">यह फ़ाइल कॉमन्स पर पहले से है। क्या आप फिर भी आगे बढ़ना चाहते हैं?</string>
<string name="yes">हाँ</string>
<string name="no">नहीं</string>
<string name="media_detail_title">शीर्षक</string>
@ -211,7 +211,7 @@
<string name="error_while_cache">चित्र कैशिंग करते समय त्रुटि</string>
<string name="title_info">फ़ाइल के लिए एक अद्वितीय वर्णनात्मक शीर्षक, जो एक फ़ाइल नाम के रूप में काम करेगा। आप रिक्त स्थान के साथ सादे भाषा का उपयोग कर सकते हैं। फ़ाइल विस्तार शामिल न करें</string>
<string name="description_info">कृपया मीडिया जितना संभव हो उतना बताएं: यह कहां लिया गया? यह क्या दिखाता है? संदर्भ क्या है? कृपया वस्तुओं या व्यक्तियों का वर्णन करें। ऐसी जानकारी का खुलासा करें जिसे आसानी से अनुमानित नहीं किया जा सकता, उदाहरण के लिए दिन का समय यदि यह परिदृश्य है। अगर मीडिया कुछ असामान्य दिखाता है, तो कृपया बताएं कि इसे क्या असामान्य बनाता है।</string>
<string name="upload_image_too_dark">यह चित्र बहुत गहरा है, क्या आप वाकई इसे अपलोड करना चाहते हैं? विकिमीडिया कॉमन्स केवल विश्वकोषीय मूल्य वाले चित्रों के लिए है।</string>
<string name="upload_image_problem_dark">यह चित्र बहुत गहरा है, क्या आप वाकई इसे अपलोड करना चाहते हैं? विकिमीडिया कॉमन्स केवल विश्वकोषीय मूल्य वाले चित्रों के लिए है।</string>
<string name="give_permission">अनुमति दें</string>
<string name="use_external_storage">बाहरी स्टॉरज का पृयोग करे।</string>
<string name="use_external_storage_summary">आप अपने डिवाइस के इन-ऐप कैमरा से ली गई तस्वीरों को सहेजें।</string>

Some files were not shown because too many files have changed in this diff Show more