Merge remote-tracking branch 'refs/remotes/commons-app/master'

This commit is contained in:
misaochan 2017-07-28 21:15:26 +10:00
commit 9d124aeadb
50 changed files with 629 additions and 428 deletions

View file

@ -24,6 +24,8 @@ their contribution to the product.
* Dmitry Brant
* Adam Shorland
* John Lubbock
* Mikel Pascual
* Jan Piotrowski
3rd party open source libraries used:
* Butterknife

View file

@ -11,6 +11,7 @@ import android.content.pm.PackageManager;
import android.database.sqlite.SQLiteDatabase;
import android.preference.PreferenceManager;
import android.support.v4.util.LruCache;
import android.util.Log;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.stetho.Stetho;
@ -139,6 +140,10 @@ public class CommonsApplication extends Application {
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
Fresco.initialize(this);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
CommonsApplication.getInstance());
// Increase counter by one, starts from 1
prefs.edit().putInt("app_start_counter", prefs.getInt("app_start_counter" ,0) + 1).commit();
//For caching area -> categories
cacheData = new CacheController();

View file

@ -142,7 +142,7 @@ public class Media implements Parcelable {
return coordinates;
}
public void setCoordinates(LatLng coordinates) {
public void setCoordinates(@Nullable LatLng coordinates) {
this.coordinates = coordinates;
}
@ -201,7 +201,7 @@ public class Media implements Parcelable {
this.filename = filename;
}
public Media(Uri localUri, String imageUrl, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator) {
public Media(Uri localUri, String imageUrl, String filename, String description, long dataLength, Date dateCreated, @Nullable Date dateUploaded, String creator) {
this();
this.localUri = localUri;
this.imageUrl = imageUrl;

View file

@ -1,67 +0,0 @@
package fr.free.nrw.commons.category;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckedTextView;
import java.util.ArrayList;
import fr.free.nrw.commons.R;
public class CategoriesAdapter extends BaseAdapter {
private LayoutInflater mInflater;
private ArrayList<CategorizationFragment.CategoryItem> items;
public CategoriesAdapter(Context context, ArrayList<CategorizationFragment.CategoryItem> items) {
this.items = items;
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public int getCount() {
return items.size();
}
@Override
public Object getItem(int i) {
return items.get(i);
}
public ArrayList<CategorizationFragment.CategoryItem> getItems() {
return items;
}
public void setItems(ArrayList<CategorizationFragment.CategoryItem> items) {
this.items = items;
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
CheckedTextView checkedView;
if(view == null) {
checkedView = (CheckedTextView) mInflater.inflate(R.layout.layout_categories_item, null);
} else {
checkedView = (CheckedTextView) view;
}
CategorizationFragment.CategoryItem item = (CategorizationFragment.CategoryItem) this.getItem(i);
checkedView.setChecked(item.selected);
checkedView.setText(item.name);
checkedView.setTag(i);
return checkedView;
}
}

View file

@ -0,0 +1,24 @@
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;
CategoriesAdapterFactory(CategoriesRenderer.CategoryClickedListener listener) {
this.listener = listener;
}
public RVRendererAdapter<CategoryItem> 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);
}
}

View file

@ -0,0 +1,57 @@
package fr.free.nrw.commons.category;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckedTextView;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
class CategoriesRenderer extends Renderer<CategoryItem> {
@BindView(R.id.tvName) CheckedTextView checkedView;
private final CategoryClickedListener listener;
CategoriesRenderer(CategoryClickedListener listener) {
this.listener = listener;
}
@Override
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
return layoutInflater.inflate(R.layout.layout_categories_item, viewGroup, false);
}
@Override
protected void setUpView(View view) {
ButterKnife.bind(this, view);
}
@Override
protected void hookListeners(View view) {
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View 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());
}
interface CategoryClickedListener {
void categoryClicked(CategoryItem item);
}
}

View file

@ -1,36 +1,33 @@
package fr.free.nrw.commons.category;
import android.content.ContentProviderClient;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
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.KeyEvent;
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.CheckedTextView;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
@ -40,90 +37,191 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoriesRenderer.CategoryClickedListener;
import fr.free.nrw.commons.upload.MwVolleyApi;
import timber.log.Timber;
import static android.view.KeyEvent.ACTION_UP;
import static android.view.KeyEvent.KEYCODE_BACK;
import static fr.free.nrw.commons.category.CategoryContentProvider.AUTHORITY;
/**
* Displays the category suggestion and selection screen. Category search is initiated here.
*/
public class CategorizationFragment extends Fragment {
public interface OnCategoriesSaveHandler {
void onCategoriesSave(ArrayList<String> categories);
}
public class CategorizationFragment extends Fragment implements CategoryClickedListener {
public static final int SEARCH_CATS_LIMIT = 25;
ListView categoriesList;
protected EditText categoriesFilter;
@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;
private CategoryTextWatcher textWatcher = new CategoryTextWatcher();
CategoriesAdapter categoriesAdapter;
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
private RVRendererAdapter<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler;
protected HashMap<String, ArrayList<String>> categoriesCache;
private HashMap<String, ArrayList<String>> categoriesCache;
private ArrayList<String> selectedCategories = new ArrayList<>();
private ContentProviderClient client;
private PrefixUpdater prefixUpdaterSub;
private MethodAUpdater methodAUpdaterSub;
private final CategoryTextWatcher textWatcher = new CategoryTextWatcher();
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(this);
private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
private final ArrayList<String> titleCatItems = new ArrayList<>();
private final CountDownLatch mergeLatch = new CountDownLatch(1);
// LHS guarantees ordered insertions, allowing for prioritized method A results
private final Set<String> results = new LinkedHashSet<>();
PrefixUpdater prefixUpdaterSub;
MethodAUpdater methodAUpdaterSub;
private final ArrayList<String> titleCatItems = new ArrayList<>();
final CountDownLatch mergeLatch = new CountDownLatch(1);
@SuppressWarnings("unchecked")
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_categorization, container, false);
ButterKnife.bind(this, rootView);
private ContentProviderClient client;
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
protected final static int SEARCH_CATS_LIMIT = 25;
categoriesSkip.setOnClickListener(view -> {
getActivity().onBackPressed();
getActivity().finish();
});
public static class CategoryItem implements Parcelable {
public String name;
public boolean selected;
public static Creator<CategoryItem> CREATOR = new Creator<CategoryItem>() {
@Override
public CategoryItem createFromParcel(Parcel parcel) {
return new CategoryItem(parcel);
}
@Override
public CategoryItem[] newArray(int i) {
return new CategoryItem[0];
}
};
public CategoryItem(String name, boolean selected) {
this.name = name;
this.selected = selected;
ArrayList<CategoryItem> items;
if (savedInstanceState == null) {
items = new ArrayList<>();
categoriesCache = new HashMap<>();
} else {
items = savedInstanceState.getParcelableArrayList("currentCategories");
categoriesCache = (HashMap<String, ArrayList<String>>) savedInstanceState
.getSerializable("categoriesCache");
}
public CategoryItem(Parcel in) {
name = in.readString();
selected = in.readInt() == 1;
}
categoriesAdapter = adapterFactory.create(items);
categoriesList.setAdapter(categoriesAdapter);
categoriesFilter.addTextChangedListener(textWatcher);
@Override
public int describeContents() {
return 0;
}
startUpdatingCategoryList();
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(name);
parcel.writeInt(selected ? 1 : 0);
return rootView;
}
@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) {
backButtonDialog();
return true;
}
return false;
});
}
}
@Override
public void onDestroyView() {
categoriesFilter.removeTextChangedListener(textWatcher);
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
client.release();
}
@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:
int numberSelected = 0;
selectedCategories = new ArrayList<>();
int count = categoriesAdapter.getItemCount();
for (int i = 0; i < count; i++) {
CategoryItem item = categoriesAdapter.getItem(i);
if (item.isSelected()) {
selectedCategories.add(item.getName());
numberSelected++;
}
}
//If no categories selected, display warning to user
if (numberSelected == 0) {
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("No, go back", (dialog, id) -> {
//Exit menuItem so user can select their categories
})
.setNegativeButton("Yes, submit", (dialog, id) -> {
//Proceed to submission
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
})
.create()
.show();
} else {
//Proceed to submission
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
return true;
}
}
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);
client = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY);
}
public HashMap<String, ArrayList<String>> getCategoriesCache() {
return categoriesCache;
}
/**
* Retrieves category suggestions from title input
*
* @return a list containing title-related categories
*/
protected ArrayList<String> titleCatQuery() {
private ArrayList<String> titleCatQuery() {
TitleCategories titleCategoriesSub;
//Retrieve the title that was saved when user tapped submit icon
@ -157,37 +255,42 @@ public class CategorizationFragment extends Fragment {
/**
* Retrieves recently-used categories
*
* @return a list containing recent categories
*/
protected ArrayList<String> recentCatQuery() {
private ArrayList<String> recentCatQuery() {
ArrayList<String> items = new ArrayList<>();
Cursor cursor = null;
try {
Cursor cursor = client.query(
cursor = client.query(
CategoryContentProvider.BASE_URI,
Category.Table.ALL_FIELDS,
null,
new String[]{},
Category.Table.COLUMN_LAST_USED + " DESC");
// fixme add a limit on the original query instead of falling out of the loop?
while (cursor.moveToNext() && cursor.getPosition() < SEARCH_CATS_LIMIT) {
while (cursor != null && cursor.moveToNext()
&& cursor.getPosition() < SEARCH_CATS_LIMIT) {
Category cat = Category.fromCursor(cursor);
items.add(cat.getName());
}
cursor.close();
}
catch (RemoteException e) {
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return items;
}
/**
* Merges nearby categories, categories suggested based on title, and recent categories... without duplicates.
* Merges nearby categories, categories suggested based on title, and recent categories...
* without duplicates.
*
* @return a list containing merged categories
*/
protected ArrayList<String> mergeItems() {
ArrayList<String> mergeItems() {
Set<String> mergedItems = new LinkedHashSet<>();
Timber.d("Calling APIs for GPS cats, title cats and recent cats...");
@ -213,8 +316,9 @@ public class CategorizationFragment extends Fragment {
Timber.d("Adding title items: %s", titleItems);
mergedItems.addAll(recentItems);
Timber.d("Adding recent items: %s", recentItems);
//Needs to be an ArrayList and not a List unless we want to modify a big portion of preexisting code
// Needs to be an ArrayList and not a List unless we want to modify a big portion
// of preexisting code
ArrayList<String> mergedItemsList = new ArrayList<>(mergedItems);
Timber.d("Merged item list: %s", mergedItemsList);
@ -223,18 +327,20 @@ public class CategorizationFragment extends Fragment {
/**
* Displays categories found to the user as they type in the search box
*
* @param categories a list of all categories found for the search string
* @param filter the search string
* @param filter the search string
*/
protected void setCatsAfterAsync(ArrayList<String> categories, String filter) {
private void setCatsAfterAsync(ArrayList<String> categories, String filter) {
if (getActivity() != null) {
ArrayList<CategoryItem> items = new ArrayList<>();
HashSet<String> existingKeys = new HashSet<>();
for (CategoryItem item : categoriesAdapter.getItems()) {
if (item.selected) {
int count = categoriesAdapter.getItemCount();
for (int i = 0; i < count; i++) {
CategoryItem item = categoriesAdapter.getItem(i);
if (item.isSelected()) {
items.add(item);
existingKeys.add(item.name);
existingKeys.add(item.getName());
}
}
for (String category : categories) {
@ -243,8 +349,8 @@ public class CategorizationFragment extends Fragment {
}
}
categoriesAdapter.setItems(items);
categoriesAdapter.notifyDataSetInvalidated();
categoriesAdapter.setCollection(new ListAdapteeCollection<>(items));
categoriesAdapter.notifyDataSetChanged();
categoriesSearchInProgress.setVisibility(View.GONE);
if (categories.isEmpty()) {
@ -258,8 +364,7 @@ public class CategorizationFragment extends Fragment {
} else {
categoriesList.smoothScrollToPosition(existingKeys.size());
}
}
else {
} else {
Timber.e("Error: Fragment is null");
}
}
@ -272,7 +377,6 @@ public class CategorizationFragment extends Fragment {
* above Prefix results.
*/
private void requestSearchResults() {
final CountDownLatch latch = new CountDownLatch(1);
prefixUpdaterSub = new PrefixUpdater(this) {
@ -282,8 +386,7 @@ public class CategorizationFragment extends Fragment {
try {
result = super.doInBackground();
latch.await();
}
catch (InterruptedException e) {
} catch (InterruptedException e) {
Timber.w(e);
//Thread.currentThread().interrupt();
}
@ -325,7 +428,6 @@ public class CategorizationFragment extends Fragment {
}
private void startUpdatingCategoryList() {
if (prefixUpdaterSub != null) {
prefixUpdaterSub.cancel(true);
}
@ -339,213 +441,34 @@ public class CategorizationFragment extends Fragment {
public int getCurrentSelectedCount() {
int count = 0;
for(CategoryItem item: categoriesAdapter.getItems()) {
if(item.selected) {
int numberOfItems = categoriesAdapter.getItemCount();
for (int i = 0; i < numberOfItems; i++) {
CategoryItem item = categoriesAdapter.getItem(i);
if (item.isSelected()) {
count++;
}
}
return count;
}
private Category lookupCategory(String name) {
Cursor cursor = null;
try {
cursor = client.query(
CategoryContentProvider.BASE_URI,
Category.Table.ALL_FIELDS,
Category.Table.COLUMN_NAME + "=?",
new String[] {name},
null);
if (cursor.moveToFirst()) {
return Category.fromCursor(cursor);
}
} catch (RemoteException e) {
// This feels lazy, but to hell with checked exceptions. :)
throw new RuntimeException(e);
} finally {
if ( cursor != null ) {
cursor.close();
}
}
// Newly used category...
Category cat = new Category();
cat.setName(name);
cat.setLastUsed(new Date());
cat.setTimesUsed(0);
return cat;
}
private class CategoryCountUpdater extends AsyncTask<Void, Void, Void> {
private String name;
public CategoryCountUpdater(String name) {
this.name = name;
}
@Override
protected Void doInBackground(Void... voids) {
Category cat = lookupCategory(name);
cat.incTimesUsed();
cat.setContentProviderClient(client);
cat.save();
return null; // Make the compiler happy.
}
}
private void updateCategoryCount(String name) {
new CategoryCountUpdater(name).executeOnExecutor(executor);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_categorization, null);
categoriesList = (ListView) rootView.findViewById(R.id.categoriesListBox);
categoriesFilter = (EditText) rootView.findViewById(R.id.categoriesSearchBox);
categoriesSearchInProgress = (ProgressBar) rootView.findViewById(R.id.categoriesSearchInProgress);
categoriesNotFoundView = (TextView) rootView.findViewById(R.id.categoriesNotFound);
categoriesSkip = (TextView) rootView.findViewById(R.id.categoriesExplanation);
categoriesSkip.setOnClickListener(view -> {
getActivity().onBackPressed();
getActivity().finish();
});
ArrayList<CategoryItem> items;
if(savedInstanceState == null) {
items = new ArrayList<>();
categoriesCache = new HashMap<>();
} else {
items = savedInstanceState.getParcelableArrayList("currentCategories");
categoriesCache = (HashMap<String, ArrayList<String>>) savedInstanceState.getSerializable("categoriesCache");
}
categoriesAdapter = new CategoriesAdapter(getActivity(), items);
categoriesList.setAdapter(categoriesAdapter);
categoriesList.setOnItemClickListener((adapterView, view, index, id) -> {
CheckedTextView checkedView = (CheckedTextView) view;
CategoryItem item = (CategoryItem) adapterView.getAdapter().getItem(index);
item.selected = !item.selected;
checkedView.setChecked(item.selected);
if (item.selected) {
updateCategoryCount(item.name);
}
});
categoriesFilter.addTextChangedListener(textWatcher);
startUpdatingCategoryList();
return rootView;
}
@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() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
backButtonDialog();
return true;
}
return false;
});
}
}
@Override
public void onDestroyView() {
categoriesFilter.removeTextChangedListener(textWatcher);
super.onDestroyView();
}
public void backButtonDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage("Are you sure you want to go back? The image will not have any categories saved.")
.setTitle("Warning");
builder.setPositiveButton("No", (dialog, id) -> {
//No need to do anything, user remains on categorization screen
});
builder.setNegativeButton("Yes", (dialog, id) -> getActivity().finish());
AlertDialog dialog = builder.create();
dialog.show();
new AlertDialog.Builder(getActivity())
.setMessage("Are you sure you want to go back? The image will not "
+ "have any categories saved.")
.setTitle("Warning")
.setPositiveButton("No", (dialog, id) -> {
//No need to do anything, user remains on categorization screen
})
.setNegativeButton("Yes", (dialog, id) -> getActivity().finish())
.create()
.show();
}
@Override
public void onDestroy() {
super.onDestroy();
client.release();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList("currentCategories", categoriesAdapter.getItems());
outState.putSerializable("categoriesCache", categoriesCache);
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
switch(menuItem.getItemId()) {
case R.id.menu_save_categories:
int numberSelected = 0;
for(CategoryItem item: categoriesAdapter.getItems()) {
if(item.selected) {
selectedCategories.add(item.name);
numberSelected++;
}
}
//If no categories selected, display warning to user
if (numberSelected == 0) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage("Images without categories are rarely usable. Are you sure you want to submit without selecting categories?")
.setTitle("No Categories Selected");
builder.setPositiveButton("No, go back", (dialog, id) -> {
//Exit menuItem so user can select their categories
});
builder.setNegativeButton("Yes, submit", (dialog, id) -> {
//Proceed to submission
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
});
AlertDialog dialog = builder.create();
dialog.show();
} else {
//Proceed to submission
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
return true;
}
public void categoryClicked(CategoryItem item) {
if (item.isSelected()) {
new CategoryCountUpdater(item.getName(), client).executeOnExecutor(executor);
}
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);
client = getActivity().getContentResolver().acquireContentProviderClient(CategoryContentProvider.AUTHORITY);
}
private class CategoryTextWatcher implements TextWatcher {

View file

@ -26,7 +26,7 @@ public class Category {
this.name = name;
}
public Date getLastUsed() {
private Date getLastUsed() {
// warning: Date objects are mutable.
return (Date)lastUsed.clone();
}
@ -36,11 +36,11 @@ public class Category {
this.lastUsed = (Date)lastUsed.clone();
}
public void touch() {
private void touch() {
lastUsed = new Date();
}
public int getTimesUsed() {
private int getTimesUsed() {
return timesUsed;
}
@ -70,7 +70,7 @@ public class Category {
}
}
public ContentValues toContentValues() {
private ContentValues toContentValues() {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_NAME, getName());
cv.put(Table.COLUMN_LAST_USED, getLastUsed().getTime());

View file

@ -7,6 +7,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication;
@ -41,8 +42,10 @@ public class CategoryContentProvider extends ContentProvider {
return false;
}
@SuppressWarnings("ConstantConditions")
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(Category.Table.TABLE_NAME);
@ -53,7 +56,8 @@ public class CategoryContentProvider extends ContentProvider {
switch(uriType) {
case CATEGORIES:
cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor = queryBuilder.query(db, projection, selection, selectionArgs,
null, null, sortOrder);
break;
case CATEGORIES_ID:
cursor = queryBuilder.query(db,
@ -75,15 +79,16 @@ public class CategoryContentProvider extends ContentProvider {
}
@Override
public String getType(Uri uri) {
public String getType(@NonNull Uri uri) {
return null;
}
@SuppressWarnings("ConstantConditions")
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id = 0;
long id;
switch (uriType) {
case CATEGORIES:
id = sqlDB.insert(Category.Table.TABLE_NAME, null, contentValues);
@ -96,12 +101,13 @@ public class CategoryContentProvider extends ContentProvider {
}
@Override
public int delete(Uri uri, String s, String[] strings) {
public int delete(@NonNull Uri uri, String s, String[] strings) {
return 0;
}
@SuppressWarnings("ConstantConditions")
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (CategoryContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
@ -122,8 +128,10 @@ public class CategoryContentProvider extends ContentProvider {
return values.length;
}
@SuppressWarnings("ConstantConditions")
@Override
public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
public int update(@NonNull Uri uri, ContentValues contentValues, String selection,
String[] selectionArgs) {
/*
SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues
@ -133,7 +141,7 @@ public class CategoryContentProvider extends ContentProvider {
*/
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated = 0;
int rowsUpdated;
switch (uriType) {
case CATEGORIES_ID:
int id = Integer.valueOf(uri.getLastPathSegment());
@ -144,7 +152,8 @@ public class CategoryContentProvider extends ContentProvider {
Category.Table.COLUMN_ID + " = ?",
new String[] { String.valueOf(id) } );
} else {
throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
}
break;
default:

View file

@ -0,0 +1,59 @@
package fr.free.nrw.commons.category;
import android.content.ContentProviderClient;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.RemoteException;
import java.util.Date;
class CategoryCountUpdater extends AsyncTask<Void, Void, Void> {
private final String name;
private final ContentProviderClient client;
CategoryCountUpdater(String name, ContentProviderClient client) {
this.name = name;
this.client = client;
}
@Override
protected Void doInBackground(Void... voids) {
Category cat = lookupCategory(name);
cat.incTimesUsed();
cat.setContentProviderClient(client);
cat.save();
return null; // Make the compiler happy.
}
private Category lookupCategory(String name) {
Cursor cursor = null;
try {
cursor = client.query(
CategoryContentProvider.BASE_URI,
Category.Table.ALL_FIELDS,
Category.Table.COLUMN_NAME + "=?",
new String[]{name},
null);
if (cursor != null && cursor.moveToFirst()) {
return Category.fromCursor(cursor);
}
} catch (RemoteException e) {
// This feels lazy, but to hell with checked exceptions. :)
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
}
// Newly used category...
Category cat = new Category();
cat.setName(name);
cat.setLastUsed(new Date());
cat.setTimesUsed(0);
return cat;
}
}

View file

@ -0,0 +1,54 @@
package fr.free.nrw.commons.category;
import android.os.Parcel;
import android.os.Parcelable;
class CategoryItem implements Parcelable {
private final String name;
private boolean selected;
public static Creator<CategoryItem> CREATOR = new Creator<CategoryItem>() {
@Override
public CategoryItem createFromParcel(Parcel parcel) {
return new CategoryItem(parcel);
}
@Override
public CategoryItem[] newArray(int i) {
return new CategoryItem[0];
}
};
CategoryItem(String name, boolean selected) {
this.name = name;
this.selected = selected;
}
private CategoryItem(Parcel in) {
name = in.readString();
selected = in.readInt() == 1;
}
public String getName() {
return name;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(name);
parcel.writeInt(selected ? 1 : 0);
}
}

View file

@ -13,6 +13,8 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
import static fr.free.nrw.commons.category.CategorizationFragment.SEARCH_CATS_LIMIT;
/**
* Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that are close to
* the keyword typed in by the user. The 'srsearch' action-specific parameter is used for this
@ -20,8 +22,8 @@ import timber.log.Timber;
*/
class MethodAUpdater extends AsyncTask<Void, Void, List<String>> {
private final CategorizationFragment catFragment;
private String filter;
private CategorizationFragment catFragment;
MethodAUpdater(CategorizationFragment catFragment) {
this.catFragment = catFragment;
@ -84,7 +86,7 @@ class MethodAUpdater extends AsyncTask<Void, Void, List<String>> {
//URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
try {
categories = api.searchCategories(CategorizationFragment.SEARCH_CATS_LIMIT, filter);
categories = api.searchCategories(SEARCH_CATS_LIMIT, filter);
Timber.d("Method A URL filter %s", categories);
} catch (IOException e) {
Timber.e(e, "IO Exception: ");

View file

@ -0,0 +1,7 @@
package fr.free.nrw.commons.category;
import java.util.ArrayList;
public interface OnCategoriesSaveHandler {
void onCategoriesSave(ArrayList<String> categories);
}

View file

@ -7,6 +7,7 @@ import android.view.View;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@ -14,18 +15,20 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
import static fr.free.nrw.commons.category.CategorizationFragment.SEARCH_CATS_LIMIT;
/**
* Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that share the
* same prefix as the keyword typed in by the user. The 'acprefix' action-specific parameter is used
* for this purpose. This class should be subclassed in CategorizationFragment.java to aggregate
* the results.
*/
public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
private final CategorizationFragment catFragment;
private String filter;
private CategorizationFragment catFragment;
public PrefixUpdater(CategorizationFragment catFragment) {
PrefixUpdater(CategorizationFragment catFragment) {
this.catFragment = catFragment;
}
@ -90,8 +93,9 @@ public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
}
//if user types in something that is in cache, return cached category
if (catFragment.categoriesCache.containsKey(filter)) {
ArrayList<String> cachedItems = new ArrayList<>(catFragment.categoriesCache.get(filter));
HashMap<String, ArrayList<String>> categoriesCache = catFragment.getCategoriesCache();
if (categoriesCache.containsKey(filter)) {
ArrayList<String> cachedItems = new ArrayList<>(categoriesCache.get(filter));
Timber.d("Found cache items, waiting for filter");
return new ArrayList<>(filterIrrelevantResults(cachedItems));
}
@ -101,7 +105,7 @@ public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
List<String> categories = new ArrayList<>();
try {
categories = api.allCategories(CategorizationFragment.SEARCH_CATS_LIMIT, this.filter);
categories = api.allCategories(SEARCH_CATS_LIMIT, this.filter);
Timber.d("Prefix URL filter %s", categories);
} catch (IOException e) {
Timber.e(e, "IO Exception: ");

View file

@ -19,17 +19,12 @@ class TitleCategories extends AsyncTask<Void, Void, List<String>> {
private final static int SEARCH_CATS_LIMIT = 25;
private String title;
private final String title;
TitleCategories(String title) {
this.title = title;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected List<String> doInBackground(Void... voids) {

View file

@ -39,7 +39,7 @@ public class ThreadPoolExecutorService implements Executor {
}
@Override
public void execute(Runnable command) {
public void execute(@NonNull Runnable command) {
backgroundPool.execute(command);
}

View file

@ -3,11 +3,13 @@ package fr.free.nrw.commons.contributions;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
@ -17,6 +19,7 @@ import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.support.v7.app.AlertDialog;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -27,7 +30,10 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
@ -110,6 +116,12 @@ public class ContributionsActivity
super.onPause();
}
@Override
protected void onStart() {
super.onStart();
displayFeedbackPopup();
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
// Do a sync everytime we get here!
@ -342,4 +354,56 @@ public class ContributionsActivity
Intent contributionsIntent = new Intent(context, ContributionsActivity.class);
context.startActivity(contributionsIntent);
}
private void displayFeedbackPopup() {
Date popupMessageEndDate = null;
try {
String validUntil = "23/08/2017";
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
popupMessageEndDate = sdf.parse(validUntil);
} catch (ParseException e) {
e.printStackTrace();
}
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
CommonsApplication.getInstance());
// boolean to save users request about displaying popup
boolean displayFeedbackPopup = prefs.getBoolean("display_feedbak_popup", true);
// boolean to recognize is application re-started. Will be used for "remind me later" option
int appStartCounter = prefs.getInt("app_start_counter" ,0);
// if time is valid and shared pref says display
if (new Date().before(popupMessageEndDate) && displayFeedbackPopup && (appStartCounter == 4)) {
new AlertDialog.Builder(this)
.setTitle(getResources().getString(R.string.feedback_popup_title))
.setMessage(getResources().getString(R.string.feedback_popup_description))
.setPositiveButton(getResources().getString(R.string.feedback_popup_accept),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// Go to the page
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri
.parse(getResources()
.getString(R.string.feedback_page_url)));
startActivity(browserIntent);
// No need to dislay this window to the user again.
prefs.edit().putBoolean("display_feedbak_popup" , false).commit();
dialog.dismiss();
}
})
.setNegativeButton(getResources().getString(R.string.feedback_popup_decline),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Dismiss the dialog and not to show it later
prefs.edit().putBoolean("display_feedbak_popup", false).commit();
dialog.dismiss();
}
})
.create().show();
}
}
}

View file

@ -7,6 +7,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication;
@ -38,7 +39,7 @@ public class ContributionsContentProvider extends ContentProvider{
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(Contribution.Table.TABLE_NAME);
@ -71,12 +72,12 @@ public class ContributionsContentProvider extends ContentProvider{
}
@Override
public String getType(Uri uri) {
public String getType(@NonNull Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
long id = 0;
@ -92,7 +93,7 @@ public class ContributionsContentProvider extends ContentProvider{
}
@Override
public int delete(Uri uri, String s, String[] strings) {
public int delete(@NonNull Uri uri, String s, String[] strings) {
int rows = 0;
int uriType = uriMatcher.match(uri);
@ -114,7 +115,7 @@ public class ContributionsContentProvider extends ContentProvider{
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
@ -136,7 +137,7 @@ public class ContributionsContentProvider extends ContentProvider{
}
@Override
public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
public int update(@NonNull Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
/*
SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues

View file

@ -8,6 +8,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
@ -162,7 +163,7 @@ public class ContributionsListFragment extends Fragment {
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + grantResults);

View file

@ -7,6 +7,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication;
@ -39,7 +40,7 @@ public class ModificationsContentProvider extends ContentProvider{
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(ModifierSequence.Table.TABLE_NAME);
@ -61,12 +62,12 @@ public class ModificationsContentProvider extends ContentProvider{
}
@Override
public String getType(Uri uri) {
public String getType(@NonNull Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
long id = 0;
@ -82,7 +83,7 @@ public class ModificationsContentProvider extends ContentProvider{
}
@Override
public int delete(Uri uri, String s, String[] strings) {
public int delete(@NonNull Uri uri, String s, String[] strings) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
switch (uriType) {
@ -99,7 +100,7 @@ public class ModificationsContentProvider extends ContentProvider{
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
@ -121,7 +122,7 @@ public class ModificationsContentProvider extends ContentProvider{
}
@Override
public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
public int update(@NonNull Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
/*
SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.theme;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.design.widget.NavigationView;
import android.support.v4.widget.DrawerLayout;
@ -109,6 +110,14 @@ public class NavigationBaseActivity extends BaseActivity
Toast.makeText(this, R.string.no_email_client, Toast.LENGTH_SHORT).show();
}
return true;
case R.id.action_developer_plans:
drawerLayout.closeDrawer(navigationView);
// Go to the page
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri
.parse(getResources()
.getString(R.string.feedback_page_url)));
startActivity(browserIntent);
return true;
case R.id.action_logout:
new AlertDialog.Builder(this)
.setMessage(R.string.logout_verification)

View file

@ -11,6 +11,7 @@ import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
@ -28,6 +29,7 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
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;
@ -43,7 +45,7 @@ public class MultipleShareActivity
AdapterView.OnItemClickListener,
FragmentManager.OnBackStackChangedListener,
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
CategorizationFragment.OnCategoriesSaveHandler {
OnCategoriesSaveHandler {
private CommonsApplication app;
private ArrayList<Contribution> photosList = null;
@ -104,7 +106,7 @@ public class MultipleShareActivity
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
multipleUploadBegins();
}

View file

@ -9,6 +9,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.design.widget.Snackbar;
@ -36,6 +37,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
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.ModificationsContentProvider;
@ -54,7 +56,7 @@ import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE;
public class ShareActivity
extends AuthenticatedActivity
implements SingleUploadFragment.OnUploadActionInitiated,
CategorizationFragment.OnCategoriesSaveHandler {
OnCategoriesSaveHandler {
private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1;
private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2;
@ -317,7 +319,7 @@ public class ShareActivity
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
@NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_PERM_ON_CREATE_STORAGE: {
if (grantResults.length >= 1

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="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z"/>
</vector>

View file

@ -60,7 +60,7 @@
android:visibility="gone"
/>
<ListView
<android.support.v7.widget.RecyclerView
android:id="@+id/categoriesListBox"
android:layout_height="wrap_content"
android:layout_width="match_parent"

View file

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
android:checkMark="?android:attr/textCheckMark"
android:checked="false"
android:gravity="center_vertical"
android:theme="@style/DarkAppTheme"
>
android:padding="4dp"
android:theme="@style/DarkAppTheme">
</CheckedTextView>

View file

@ -30,6 +30,11 @@
android:icon="@drawable/ic_feedback_black_24dp"
android:title="@string/navigation_item_feedback"/>
<item
android:id="@+id/action_developer_plans"
android:icon="@drawable/ic_help_outline_black_24dp"
android:title="@string/navigation_item_developer_plans"/>
<item
android:id="@+id/action_logout"
android:icon="@drawable/ic_exit_to_app_black_24dp"

View file

@ -20,8 +20,7 @@
<string name="upload_progress_notification_title_finishing">اكتمال رفع %1$s</string>
<string name="upload_failed_notification_title">فشل رفع %1$s</string>
<string name="upload_failed_notification_subtitle">انقر لتشاهد</string>
<string name="uploads_pending_notification_indicator" fuzzy="true">متبقى %d</string>
<string name="title_activity_contributions">مرفوعاتي الأخيرة</string>
<string name="title_activity_contributions">مرفوعاتي الأخيرة</string>
<string name="contribution_state_queued">في قائمة الانتظار</string>
<string name="contribution_state_failed">فشل</string>
<string name="contribution_state_in_progress">انتهاء %1$d%%</string>

View file

@ -148,6 +148,8 @@
<string name="media_detail_description">বিবরণ</string>
<string name="media_detail_description_explanation">মিডিয়ার বিবরণ এখানে যাবে। এই মোটামুটি দীর্ঘ হতে পারে এবং একাধিক লাইনে লিখতে হতে পারে। আমরা আশা করি এটি দেখতে সুন্দর হবে।</string>
<string name="media_detail_uploaded_date">আপলোডের তারিখ</string>
<string name="media_detail_license">লাইসেন্স</string>
<string name="media_detail_coordinates">স্থানাঙ্কসমূহ</string>
<string name="become_a_tester_title">বিটা টেস্টার হোন</string>
<string name="use_wikidata">উইকিউপাত্ত ব্যবহার করুন</string>
<string name="use_wikidata_summary">(সতর্কতা: এটি নিষ্ক্রিয় করা অধিক পরিমাণে মোবাইল ডেটা খরচ হওয়ার কারণ হতে পারে)</string>
@ -182,6 +184,6 @@
<string name="navigation_item_info">ভূমিকা</string>
<string name="nearby_needs_permissions">অবস্থানের অনুমতি ছাড়া কাছাকাছি জায়গাগুলি প্রদর্শন করা যাবে না</string>
<string name="no_description_found">কোন বিবরণ পাওয়া যায়নি</string>
<string name="nearby_info_menu_commons_article" fuzzy="true">কমন্স নিবন্ধ</string>
<string name="nearby_info_menu_commons_article">কমন্সে ফাইলের পাতা</string>
<string name="nearby_info_menu_wikidata_article">উইকিউপাত্ত পদ</string>
</resources>

View file

@ -8,7 +8,7 @@
<string name="signup">Registre</string>
<string name="logging_in_title">Sestà iniciant la sessió</string>
<string name="logging_in_message">Espereu…</string>
<string name="login_success">S\'ha iniciat sessió correctament !</string>
<string name="login_success">S\'ha iniciat sessió correctament!</string>
<string name="login_failed">Error en iniciar la sessió!</string>
<string name="upload_failed">No s\'ha trobat el fitxer. Proveu-ho amb un altre fitxer.</string>
<string name="authentication_failed">L\'autenticació ha fallat!</string>
@ -38,9 +38,9 @@
<string name="share_title_hint">Títol</string>
<string name="share_description_hint">Descripció</string>
<string name="login_failed_network">No s\'ha pogut iniciar la sessió error de xarxa</string>
<string name="login_failed_username">No s\'ha pogut iniciar la sessió siusplau comprova el teu nom d\'usuari</string>
<string name="login_failed_username">No s\'ha pogut iniciar la sessió si et plau comprova el teu nom d\'usuari</string>
<string name="login_failed_password">No sha pogut iniciar la sessió. Comproveu la vostra contrasenya</string>
<string name="login_failed_throttled">Masses intents erronis Proveu-ho de nou d\'aquí uns minuts.</string>
<string name="login_failed_throttled">Massa intents erronis Proveu-ho de nou d\'aquí uns minuts.</string>
<string name="login_failed_blocked">Ho sentim, aquest usuari ha estat blocat a Commons</string>
<string name="login_failed_generic">Ha fallat l\'inici de sessió</string>
<string name="share_upload_button">Carrega</string>

View file

@ -88,6 +88,7 @@
<string name="menu_retry_upload">Erneut versuchen</string>
<string name="menu_cancel_upload">Abbrechen</string>
<string name="share_license_summary">Dieses Bild wird lizenziert unter %1$s</string>
<string name="media_upload_policy">Durch das Hochladen dieses Bildes erkläre ich, dass dies mein eigenes Werk ist, das kein urheberrechtlich geschütztes Material oder Selfies enthält und das auch sonst die &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines/de\"&gt;Wikimedia-Commons-Richtlinien&lt;/a&gt; einhält.</string>
<string name="menu_download">Herunterladen</string>
<string name="preference_license">Lizenz</string>
<string name="use_previous">Vorherige(n) Titel/Beschreibung verwenden</string>

View file

@ -16,9 +16,9 @@
<string name="upload_completed_notification_title">¡Se subieron %1$s!</string>
<string name="upload_completed_notification_text">Pulsa para ver tu subida</string>
<string name="upload_progress_notification_title_start">Empezando la subida de %1$s</string>
<string name="upload_progress_notification_title_in_progress">Cargando «%1$s»</string>
<string name="upload_progress_notification_title_in_progress">Cargando %1$s</string>
<string name="upload_progress_notification_title_finishing">Finalizando la subida de %1$s</string>
<string name="upload_failed_notification_title">Falló la carga de «%1$s»</string>
<string name="upload_failed_notification_title">Falló la carga de %1$s</string>
<string name="upload_failed_notification_subtitle">Toca para ver</string>
<plurals name="uploads_pending_notification_indicator">
<item quantity="one">Subiendo %d archivo</item>
@ -27,7 +27,7 @@
<string name="title_activity_contributions">Mis subidas recientes</string>
<string name="contribution_state_queued">En la cola</string>
<string name="contribution_state_failed">Fallido</string>
<string name="contribution_state_in_progress">%1$d %% completado</string>
<string name="contribution_state_in_progress">%1$d%% completado</string>
<string name="contribution_state_starting">Subiendo</string>
<string name="menu_from_gallery">De la galería</string>
<string name="menu_from_camera">Tomar una foto</string>

View file

@ -73,6 +73,7 @@
<string name="menu_retry_upload">Berriz saiatu</string>
<string name="menu_cancel_upload">Utzi</string>
<string name="share_license_summary">Irudi hau %1$s lizentziapean egongo da</string>
<string name="media_upload_policy">Irudi hau bidaltzen, nire lan propioa dela aitortzen dut, copyrighta duten materiala edo selfiak ez duela, eta beste motatakoak &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\"&gt;Wikimedia Ohikoaren arauak&lt;/a&gt;</string>
<string name="menu_download">Jaitsi</string>
<string name="preference_license">Lizentzia</string>
<string name="use_previous">Aurreko izenburu/deskribapena erabili</string>

View file

@ -88,6 +88,7 @@
<string name="menu_retry_upload">Réessayer</string>
<string name="menu_cancel_upload">Annuler</string>
<string name="share_license_summary">Cette image sera sous licence %1$s</string>
<string name="media_upload_policy">En soumettant cette image, je déclare qu\'elle est l\'oeuvre de mon travail, qu\'elle ne contient pas d\'élément protégé par les droits d\'auteurs ni de portraits, et qu\'elle est par ailleur conforme à &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\"&gt;la politique de Wikimedia Commons&lt;/a&gt;.</string>
<string name="menu_download">Télécharger</string>
<string name="preference_license">Licence</string>
<string name="use_previous">Utiliser le titre ou la description précédent</string>

View file

@ -18,8 +18,7 @@
<string name="upload_progress_notification_title_finishing">העלאת %1$s מסתיימת</string>
<string name="upload_failed_notification_title">העלאת %1$s נכשלה</string>
<string name="upload_failed_notification_subtitle">נא ללחוץ כדי להציג</string>
<string name="uploads_pending_notification_indicator" fuzzy="true">נשארו %d</string>
<string name="title_activity_contributions">ההעלאות שלי</string>
<string name="title_activity_contributions">ההעלאות שלי</string>
<string name="contribution_state_queued">בתור</string>
<string name="contribution_state_failed">נכשלה</string>
<string name="contribution_state_in_progress">%1$d%% הושלמו</string>

View file

@ -87,6 +87,7 @@
<string name="menu_retry_upload">לנסות שוב</string>
<string name="menu_cancel_upload">ביטול</string>
<string name="share_license_summary">התמונה הזאת היא תחת רשיון %1$s</string>
<string name="media_upload_policy">שליחת התמונה הזאת מהווה את הצהרתי על כך שזאת יצירה שלי, שהיא לא מכילה חומר מוגבל בזכויות יוצרים או תמונות עצמיות (סלפי) ומתאימה בכל אופן ל&lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\"&gt;מדיניות של אתר ויקישיתוף של ויקימדיה&lt;/a&gt;.</string>
<string name="menu_download">הורדה</string>
<string name="preference_license">רישיון</string>
<string name="use_previous">להשתמש בכותרת ובתיאור קודמים</string>

View file

@ -54,6 +54,7 @@
<string name="menu_cancel_upload">Atcelt</string>
<string name="menu_download">Lejupielādēt</string>
<string name="preference_license">Licence</string>
<string name="preference_theme">Nakts režīms</string>
<string name="license_name_cc0">CC0</string>
<string name="welcome_final_text">Jums šķiet, ka sapratāt?</string>
<string name="welcome_final_button_text">Jā!</string>
@ -66,6 +67,7 @@
<string name="warning">Brīdinājums</string>
<string name="yes"></string>
<string name="no"></string>
<string name="upload_image">Augšupielādēt attēlu</string>
<string name="welcome_image_llamas">Lama</string>
<string name="welcome_image_tulip">Tulpe</string>
<string name="cancel">Atcelt</string>

View file

@ -88,6 +88,7 @@
<string name="menu_retry_upload">Пробај пак</string>
<string name="menu_cancel_upload">Откажи</string>
<string name="share_license_summary">Сликава ќе се води под лиценцата %1$s</string>
<string name="media_upload_policy">Поднесувајќи ја сликава, изјавувам дека истата е мое сопствено дело, дека не содржи никаков материјал заштитен со авторски права, не содржи самослици, и дека на секој друг начин е во склад со &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines/mk?uselang=mk\"&gt;правилата на Ризницата&lt;/a&gt;.</string>
<string name="menu_download">Преземи</string>
<string name="preference_license">Лиценца</string>
<string name="use_previous">Користи претходен наслов/опис</string>

View file

@ -18,7 +18,6 @@
<string name="upload_progress_notification_title_finishing">%1$s akan siap dimuat naik</string>
<string name="upload_failed_notification_title">%1$s gagal dimuat naik</string>
<string name="upload_failed_notification_subtitle">Ketik untuk lihat</string>
<string name="uploads_pending_notification_indicator" fuzzy="true">%d fail sedang dimuat naik</string>
<string name="title_activity_contributions">Muat Naik Terbaru Saya</string>
<string name="contribution_state_queued">Dibaris gilir</string>
<string name="contribution_state_failed">Gagal</string>
@ -48,8 +47,7 @@
<item quantity="one">%d muatnaik</item>
<item quantity="other">%d muatnaik</item>
</plurals>
<string name="starting_multiple_uploads" fuzzy="true">Memulakan %d kerja muat naik</string>
<plurals name="multiple_uploads_title">
<plurals name="multiple_uploads_title">
<item quantity="one">%d muatnaik</item>
<item quantity="other">%d muatnaik</item>
</plurals>

View file

@ -88,6 +88,7 @@
<string name="menu_retry_upload">Prové torna</string>
<string name="menu_cancel_upload">Anulé</string>
<string name="share_license_summary">Costa plancia a sarà sota la licensa %1$s</string>
<string name="media_upload_policy">An mandand costa plancia, i diciaro ch\'a l\'é euvra ëd mè travaj, ch\'a conten nen d\'element sota drit d\'autor o d\'àutoscat, e che comsëssìa a l\'é conforma a &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\"&gt;le régole ëd Wikimedia Commons&lt;/a&gt;.</string>
<string name="menu_download">Dëscarié</string>
<string name="preference_license">Licensa</string>
<string name="use_previous">Dovré ël tìtol o la descrission precedent</string>

View file

@ -75,7 +75,7 @@
<string name="title_activity_settings">Configurações</string>
<string name="title_activity_signup">Criar conta</string>
<string name="menu_about">Sobre</string>
<string name="about_license" fuzzy="true">Software livre distribuído sob a &lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\"&gt;Apache License v2&lt;/a&gt;. Wikimedia Commons e seu logotipo são marcas registradas da Wikimedia Foundation e são usadas com a permissão da Wikimedia Foundation. Não somos endossados nem afiliados à Wikimedia Foundation.</string>
<string name="about_license">Software livre distribuído sob a &lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\"&gt;Apache License v2&lt;/a&gt;. %1$s e seu logotipo são marcas registradas da Wikimedia Foundation e são usadas com a permissão da Wikimedia Foundation. Não somos endossados nem afiliados à Wikimedia Foundation.</string>
<string name="about_improve">&lt;a href=\"https://github.com/commons-app/apps-android-commons\"&gt;Fonte&lt;/a&gt; e &lt;a href=\"https://commons-app.github.io/\"&gt;site&lt;/a&gt; em GitHub. Crie um novo &lt;a href=\"https://github.com/commons-app/apps-android-commons/issues\"&gt;GitHub issue&lt;/a&gt; para relatórios de bugs e sugestões.</string>
<string name="about_privacy_policy">&lt;a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\"&gt;Política de privacidade&lt;/a&gt;</string>
<string name="about_credits">&lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\"&gt;Créditos&lt;/a&gt;</string>
@ -88,6 +88,7 @@
<string name="menu_retry_upload">Repetir</string>
<string name="menu_cancel_upload">Cancelar</string>
<string name="share_license_summary">Essa imagem será licenciada sob %1$s</string>
<string name="media_upload_policy">Ao enviar esta imagem, declaro que este é o meu próprio trabalho, que não contém material protegido ou selfies, e, de outra forma, adere a &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\"&gt;Políticas do Wikimedia Commons&lt;/a&gt;.</string>
<string name="menu_download">Download</string>
<string name="preference_license">Licença</string>
<string name="use_previous">Usar o título/descrição anterior</string>
@ -149,6 +150,9 @@
<string name="media_detail_description">Descrição</string>
<string name="media_detail_description_explanation">Descrição da mídia aqui. Isso pode ser potencialmente longo e precisará envolver múltiplas linhas. Esperamos que seja agradável.</string>
<string name="media_detail_uploaded_date">Data de envio.</string>
<string name="media_detail_license">Licença</string>
<string name="media_detail_coordinates">Coordenadas</string>
<string name="media_detail_coordinates_empty">Nenhum fornecido</string>
<string name="become_a_tester_title">Seja um Testador Beta</string>
<string name="become_a_tester_description">Entre no nosso canal beta no Google Play e receba acesso prévio a novos recursos e correções de erros</string>
<string name="use_wikidata">Usa Wikidata</string>
@ -187,7 +191,7 @@
<string name="navigation_item_info">Tutorial</string>
<string name="nearby_needs_permissions">Os locais próximos não podem ser exibidos sem permissões de localização</string>
<string name="no_description_found">Nenhuma descrição encontrada</string>
<string name="nearby_info_menu_commons_article" fuzzy="true">Artigo de Commons</string>
<string name="nearby_info_menu_commons_article">Página de arquivo do Commons</string>
<string name="nearby_info_menu_wikidata_article">Item do Wikidata</string>
<string name="error_while_cache">Erro durante o cache de imagens</string>
</resources>

View file

@ -49,8 +49,7 @@
<string name="categories_search_text_hint">This message is followed by a list of the categories.\n{{Identical|Search category}}</string>
<string name="menu_save_categories">Hint text on menu item to save selected categories.\n{{Identical|Save}}</string>
<string name="refresh_button">{{Identical|Refresh}}</string>
<string name="multiple_uploads_title">{{Identical|Upload}}</string>
<string name="categories_not_found">Message shown to the user when no category matching what they searched for was found. %1$s represents the category name</string>
<string name="categories_not_found">Message shown to the user when no category matching what they searched for was found. %1$s represents the category name</string>
<string name="categories_skip_explanation">Text explaining to users why and how to add categories to images. Users can also tap this message to skip adding categories.</string>
<string name="categories_activity_title">Title for the activity where Categories are being selected to add to the Image.\n{{Identical|Category}}</string>
<string name="preference_tracking">Text for preference that enables the user to enable or disable collection of data about the user\'s usage patterns.</string>
@ -59,7 +58,6 @@
<string name="title_activity_signup">{{Identical|Sign up}}</string>
<string name="menu_about">{{Identical|About}}</string>
<string name="about_license">License and legal notice. %1$s is {{msg-wm|Commons-android-strings-trademarked name}}</string>
<string name="trademarked_name">{{Ignored}}\n\nUsed in {{msg-wm|Commons-android-strings-about license}}\n\n{{Identical|Wikimedia Commons}}</string>
<string name="about_improve">{{doc-important|Please make sure that your translation of \"source\" means \"source code\", not \"reference source\".}}\nSource and Bugs</string>
<string name="about_privacy_policy">HTML fragment linking to Wikimedia\'s privacy policy. Note: avoid percent-encoding in the URL, as this causes problems with Android\'s resource compiler. Use accented/non-ASCII characters \"as is\" if possible. (We can fix it manually, so don\'t worry too much about it.)\n{{Identical|Privacy policy}}</string>
<string name="about_credits">{{Identical|Credit}}</string>

View file

@ -79,7 +79,7 @@
<string name="title_activity_settings">Настройки</string>
<string name="title_activity_signup">Зарегистрироваться</string>
<string name="menu_about">О приложении</string>
<string name="about_license" fuzzy="true">Приложение с открытым исходным кодом, выпущено по лицензии &lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\"&gt;Apache License v2&lt;/a&gt;. Викисклад и его логотип являются товарными знаками Фонда Викимедиа и используются с разрешения Фонда Викимедиа. Мы не поддерживаемся и не связаны с Фондом Викимедиа.</string>
<string name="about_license">Приложение с открытым исходным кодом, выпущено по лицензии &lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\"&gt;Apache License v2&lt;/a&gt;. %1$s и его логотип являются товарными знаками Фонда Викимедиа и используются с разрешения Фонда Викимедиа. Мы не поддерживаемся и не связаны с Фондом Викимедиа.</string>
<string name="about_improve">&lt;a href=\"https://github.com/commons-app/apps-android-commons\"&gt;Исходный код&lt;/a&gt; и &lt;a href=\"https://commons-app.github.io/\"&gt;сайт&lt;/a&gt; на GitHub. Создайте новый &lt;a href=\"https://github.com/commons-app/apps-android-commons/issues\"&gt;запрос на GitHub&lt;/a&gt;, чтоб сообщить об ошибке или внести предложение.</string>
<string name="about_privacy_policy">&lt;a href=\"https://wikimediafoundation.org/wiki/Privacy_policy/ru\"&gt;Политика конфиденциальности&lt;/a&gt;</string>
<string name="about_credits">&lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\"&gt;Благодарности&lt;/a&gt;</string>
@ -153,6 +153,8 @@
<string name="media_detail_description">Описание</string>
<string name="media_detail_description_explanation">Здесь располагается описание носителя информации. Оно потенциально может быть весьма длинным и даже располагаться в несколько строк. Однако мы надеемся, что это выглядит симпатично</string>
<string name="media_detail_uploaded_date">Дата загрузки</string>
<string name="media_detail_license">Лицензия</string>
<string name="media_detail_coordinates">Координаты</string>
<string name="become_a_tester_title">Стать бета-тестером</string>
<string name="become_a_tester_description">Подпишитесь на наш канал бета-версии на Google Play и получите ранний доступ к новым функциям и исправлениям ошибок</string>
<string name="use_wikidata">Использовать Викиданные</string>
@ -191,7 +193,7 @@
<string name="navigation_item_info">Руководство</string>
<string name="nearby_needs_permissions">Ближайшие места не могут быть отображены без разрешения на геолокацию</string>
<string name="no_description_found">описание не найдено</string>
<string name="nearby_info_menu_commons_article" fuzzy="true">Статья на Викискладе</string>
<string name="nearby_info_menu_commons_article">Страница файла на Викискладе</string>
<string name="nearby_info_menu_wikidata_article">Элемент Викиданных</string>
<string name="error_while_cache">Ошибка при кэшировании картинок</string>
</resources>

View file

@ -88,6 +88,7 @@
<string name="menu_retry_upload">Försök igen</string>
<string name="menu_cancel_upload">Avbryt</string>
<string name="share_license_summary">Denna bild kommer att licensieras under %1$s</string>
<string name="media_upload_policy">Genom att skicka in denna bild intygar jag att detta är mitt eget verk, som inte innehåller upphovsrättsskyddat material eller selfies samt annars följer &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\"&gt;Wikimedia Commons-policys&lt;/a&gt;.</string>
<string name="menu_download">Ladda ned</string>
<string name="preference_license">Licens</string>
<string name="use_previous">Använd föregående titel/beskrivning</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ಕಾಮನ್ಸ್</string>
<string name="menu_settings">ಸಂಯೋಜನೆಲು</string>
<string name="username">ಸದಸ್ಯೆರ್ನ ಪುದರ್</string>
<string name="password">ಪ್ರವೇಸೊ ಪದೊ</string>
@ -23,7 +24,7 @@
<item quantity="one">%d ಕಡತ ಅಪ್ಲೊಡ್ ಆವೊಂದುಂಡು</item>
<item quantity="other">%d ಕಡತೊಲು ಅಪ್ಲೋಡ್ ಆವೊಂದುಂಡು</item>
</plurals>
<string name="title_activity_contributions" fuzzy="true">ಎನ್ನ ದಿಂಜಯೀನಾ ವಿಚಾರೊಳು</string>
<string name="title_activity_contributions">ಎನ್ನ ದಿಂಜಯೀನಾ ವಿಚಾರೊಳು</string>
<string name="contribution_state_queued">ದಿಂಜೊಂತುಂಡು</string>
<string name="contribution_state_failed">ದಿಂಜಿಜಿ</string>
<string name="contribution_state_in_progress">%1$d%% ಮುಗಿಂಡ್</string>
@ -48,6 +49,7 @@
<string name="title_activity_about">ಎಂಕ್ಲೆನ ಬಗ್ಗೆ</string>
<string name="menu_retry_upload">ನನೊರ ಪ್ರಯತ್ನ ಮಾನ್ಪುಲೇ</string>
<string name="menu_cancel_upload">ವಜಾ ಮಲ್ಪುಲೆ</string>
<string name="menu_download">ಡೌನ್‍ಲೋಡ್</string>
<string name="preference_license">ಪರವಾನಗಿ</string>
<string name="preference_theme">ಕತ್ತಲೆದ ಕ್ರಮೊ</string>
<string name="license_name_cc0">CC0</string>
@ -63,6 +65,7 @@
<string name="license_name_cc_by_3_0">CC BY 3.0</string>
<string name="license_name_cc_by_sa_4_0">CC BY-SA 4.0</string>
<string name="license_name_cc_by_4_0">CC BY 4.0</string>
<string name="license_name_cc_zero">CC Zero</string>
<string name="welcome_final_button_text">ಅಂದ್!</string>
<string name="detail_panel_cats_label">ವರ್ಗೊಲು</string>
<string name="detail_panel_cats_loading">ದಿಂಜಾವೊಂದುಂಡು……</string>
@ -73,4 +76,16 @@
<string name="yes">ಅಂದ್</string>
<string name="no">ಅತ್ತ್</string>
<string name="media_detail_title">ತರೆಬರವು</string>
<string name="media_detail_description">ವಿವರಣೆ</string>
<string name="media_detail_license">ಪರವಾನಿಗೆ</string>
<string name="cancel">ವಜಾ ಮಲ್ಪುಲೆ</string>
<string name="navigation_drawer_open">ತೋಜಾಲೇ</string>
<string name="navigation_drawer_close">ಮುಚ್ಚಿಲೆ</string>
<string name="navigation_item_home">ಮುಖ್ಯಪುಟೊ</string>
<string name="navigation_item_upload">ದಿಂಜಾಲೆ</string>
<string name="navigation_item_nearby">ಕೈತಲ್‍ದ</string>
<string name="navigation_item_about">ಎಂಕ್ಲೆನ ಬಗ್ಗೆ</string>
<string name="navigation_item_settings">ಸಂಯೋಜನೆಲು</string>
<string name="navigation_item_feedback">ಅಬಿಪ್ರಾಯೊ</string>
<string name="navigation_item_logout">ನಿರ್ಗಮಿಸಾಲೆ</string>
</resources>

View file

@ -107,4 +107,6 @@
<string name="detail_description_empty">ıklama yok</string>
<string name="detail_license_empty">Bilinmeyen lisans</string>
<string name="menu_refresh">Yenile</string>
<string name="media_detail_license">Lisans</string>
<string name="media_detail_coordinates">Koordinatlar</string>
</resources>

View file

@ -20,7 +20,6 @@
<string name="upload_progress_notification_title_finishing">Đang hoàn thành việc tải lên tập tin %1$s</string>
<string name="upload_failed_notification_title">Tải lên tập tin %1$s thất bại</string>
<string name="upload_failed_notification_subtitle">Chạm để xem</string>
<string name="uploads_pending_notification_indicator" fuzzy="true">%d tập tin đang được tải lên</string>
<string name="title_activity_contributions" fuzzy="true">Tập tin do tôi tải lên</string>
<string name="contribution_state_queued">Đang chờ</string>
<string name="contribution_state_failed">Thất bại</string>
@ -51,8 +50,6 @@
<item quantity="zero">Chưa tải lên tập tin</item>
<item quantity="other">%d tập tin tải lên</item>
</plurals>
<string name="starting_multiple_uploads" fuzzy="true">Đang bắt đầu tải lên %d tập tin</string>
<string name="multiple_uploads_title" fuzzy="true">%d tập tin tải lên</string>
<string name="categories_not_found">Không tìm thấy thể loại khớp với %1$s</string>
<string name="categories_skip_explanation">Xếp các hình ảnh vào thể loại để cho chúng dễ tìm kiếm hơn trên Wikimedia Commons.\n\nHãy bắt đầu nhập tên thể loại để tìm kiếm.\nChạm vào thông điệp này (hoặc bấm Quay lại) để bỏ qua bước này.</string>
<string name="categories_activity_title">Thể loại</string>

View file

@ -88,6 +88,7 @@
<string name="menu_retry_upload">重試</string>
<string name="menu_cancel_upload">取消</string>
<string name="share_license_summary">此圖片會按 %1$s 協議授權上載</string>
<string name="media_upload_policy">透過提交此圖片,我宣佈這是我個人創作的成品,且不包含受版權保護或自拍內容,並除此之外遵守&lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\"&gt;維基媒體共享資源方針&lt;/a&gt;</string>
<string name="menu_download">下載</string>
<string name="preference_license">協議授權</string>
<string name="use_previous">使用先前標題/描述</string>

View file

@ -88,6 +88,7 @@
<string name="menu_retry_upload">重试</string>
<string name="menu_cancel_upload">取消</string>
<string name="share_license_summary">该图像会采用%1$s授权</string>
<string name="media_upload_policy">通过提交该图片,我声明这是我自己的作品,其不包含受版权保护的材料或自拍像,并遵循&lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\"&gt;维基共享资源&lt;/a&gt;方针。</string>
<string name="menu_download">下载</string>
<string name="preference_license">许可协议</string>
<string name="use_previous">使用之前的标题/描述</string>

View file

@ -198,11 +198,17 @@ Tap this message (or hit back) to skip this step.</string>
<string name="navigation_item_feedback">Feedback</string>
<string name="navigation_item_logout">Logout</string>
<string name="navigation_item_info">Tutorial</string>
<string name="navigation_item_developer_plans">Developer plans</string>
<string name="nearby_needs_permissions">Nearby places cannot be displayed without location permissions</string>
<string name="no_description_found">no description found</string>
<string name="nearby_info_menu_commons_article">Commons file page</string>
<string name="nearby_info_menu_wikidata_article">Wikidata item</string>
<string name="error_while_cache">Error while caching pictures</string>
<string name="error_while_cache">Error while caching pictures</string>
<string name="feedback_popup_title">Feedback wanted</string>
<string name="feedback_popup_description">We are planning several new features and improvements for the app! Would you like to review them and provide feedback? \n\n(You can always access this by selecting "Developer plans" in the navigation drawer)</string>
<string name="feedback_popup_decline">No thanks</string>
<string name="feedback_popup_accept">Sure, take me there!</string>
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
</resources>