diff --git a/CREDITS b/CREDITS index a0e1fcec4..7f2ab0893 100644 --- a/CREDITS +++ b/CREDITS @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 2af125032..94d6e3fb4 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -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(); diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index a1ae1cc84..1d07e9c16 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -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; diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapter.java deleted file mode 100644 index 2c0055c73..000000000 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapter.java +++ /dev/null @@ -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 items; - - public CategoriesAdapter(Context context, ArrayList 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 getItems() { - return items; - } - - public void setItems(ArrayList 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; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java new file mode 100644 index 000000000..417121c44 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java @@ -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 create(List placeList) { + RendererBuilder builder = new RendererBuilder() + .bind(CategoryItem.class, new CategoriesRenderer(listener)); + ListAdapteeCollection collection = new ListAdapteeCollection<>( + placeList != null ? placeList : Collections.emptyList()); + return new RVRendererAdapter<>(builder, collection); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java new file mode 100644 index 000000000..a138736cc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java @@ -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 { + @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); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java index d2d9f317f..bda7eecc9 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java @@ -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 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 categoriesAdapter; private OnCategoriesSaveHandler onCategoriesSaveHandler; - - protected HashMap> categoriesCache; - + private HashMap> categoriesCache; private ArrayList 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 titleCatItems = new ArrayList<>(); + private final CountDownLatch mergeLatch = new CountDownLatch(1); // LHS guarantees ordered insertions, allowing for prioritized method A results private final Set results = new LinkedHashSet<>(); - PrefixUpdater prefixUpdaterSub; - MethodAUpdater methodAUpdaterSub; - private final ArrayList 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 CREATOR = new Creator() { - @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 items; + if (savedInstanceState == null) { + items = new ArrayList<>(); + categoriesCache = new HashMap<>(); + } else { + items = savedInstanceState.getParcelableArrayList("currentCategories"); + categoriesCache = (HashMap>) 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 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> getCategoriesCache() { + return categoriesCache; + } + /** * Retrieves category suggestions from title input + * * @return a list containing title-related categories */ - protected ArrayList titleCatQuery() { - + private ArrayList 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 recentCatQuery() { + private ArrayList recentCatQuery() { ArrayList 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 mergeItems() { - + ArrayList mergeItems() { Set 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 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 categories, String filter) { - + private void setCatsAfterAsync(ArrayList categories, String filter) { if (getActivity() != null) { ArrayList items = new ArrayList<>(); HashSet 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 { - - 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 items; - if(savedInstanceState == null) { - items = new ArrayList<>(); - categoriesCache = new HashMap<>(); - } else { - items = savedInstanceState.getParcelableArrayList("currentCategories"); - categoriesCache = (HashMap>) 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 { diff --git a/app/src/main/java/fr/free/nrw/commons/category/Category.java b/app/src/main/java/fr/free/nrw/commons/category/Category.java index 9cb8d001e..68dd9200e 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/Category.java +++ b/app/src/main/java/fr/free/nrw/commons/category/Category.java @@ -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()); diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java index de157265b..c95e17cf6 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java @@ -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: diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryCountUpdater.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryCountUpdater.java new file mode 100644 index 000000000..bebbc03a8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryCountUpdater.java @@ -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 { + + 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; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java new file mode 100644 index 000000000..1063979ad --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java @@ -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 CREATOR = new Creator() { + @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); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java b/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java index 326f85ee3..72b1f5732 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java +++ b/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java @@ -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> { + private final CategorizationFragment catFragment; private String filter; - private CategorizationFragment catFragment; MethodAUpdater(CategorizationFragment catFragment) { this.catFragment = catFragment; @@ -84,7 +86,7 @@ class MethodAUpdater extends AsyncTask> { //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: "); diff --git a/app/src/main/java/fr/free/nrw/commons/category/OnCategoriesSaveHandler.java b/app/src/main/java/fr/free/nrw/commons/category/OnCategoriesSaveHandler.java new file mode 100644 index 000000000..a7ae0bfed --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/OnCategoriesSaveHandler.java @@ -0,0 +1,7 @@ +package fr.free.nrw.commons.category; + +import java.util.ArrayList; + +public interface OnCategoriesSaveHandler { + void onCategoriesSave(ArrayList categories); +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java b/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java index 773f758d0..7df56eff5 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java +++ b/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java @@ -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> { +class PrefixUpdater extends AsyncTask> { + 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> { } //if user types in something that is in cache, return cached category - if (catFragment.categoriesCache.containsKey(filter)) { - ArrayList cachedItems = new ArrayList<>(catFragment.categoriesCache.get(filter)); + HashMap> categoriesCache = catFragment.getCategoriesCache(); + if (categoriesCache.containsKey(filter)) { + ArrayList 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> { MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); List 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: "); diff --git a/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java b/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java index 414c87e8a..a4a94cf1d 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java +++ b/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java @@ -19,17 +19,12 @@ class TitleCategories extends AsyncTask> { 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 doInBackground(Void... voids) { diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolExecutorService.java b/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolExecutorService.java index 1516d85d8..85f8cbfeb 100644 --- a/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolExecutorService.java +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolExecutorService.java @@ -39,7 +39,7 @@ public class ThreadPoolExecutorService implements Executor { } @Override - public void execute(Runnable command) { + public void execute(@NonNull Runnable command) { backgroundPool.execute(command); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java index f1a4adfe2..4509818a4 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java @@ -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(); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java index 9dc933358..6e84065f2 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index c009243b9..bcbb5a739 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -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); diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java index 9c9421864..11caa94fa 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index f689f04dd..af0d38243 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -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) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java index a3a1f8604..1ab00c88f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java @@ -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 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(); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java index 8b09403f7..1ce86a86a 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -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 diff --git a/app/src/main/res/drawable/ic_help_outline_black_24dp.xml b/app/src/main/res/drawable/ic_help_outline_black_24dp.xml new file mode 100644 index 000000000..e7cf8ea21 --- /dev/null +++ b/app/src/main/res/drawable/ic_help_outline_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_categorization.xml b/app/src/main/res/layout/fragment_categorization.xml index a0b9b3c2f..83a8a746a 100644 --- a/app/src/main/res/layout/fragment_categorization.xml +++ b/app/src/main/res/layout/fragment_categorization.xml @@ -60,7 +60,7 @@ android:visibility="gone" /> - + android:padding="4dp" + android:theme="@style/DarkAppTheme"> \ No newline at end of file diff --git a/app/src/main/res/menu/drawer.xml b/app/src/main/res/menu/drawer.xml index f0a0a5e29..cb6b63ffc 100644 --- a/app/src/main/res/menu/drawer.xml +++ b/app/src/main/res/menu/drawer.xml @@ -30,6 +30,11 @@ android:icon="@drawable/ic_feedback_black_24dp" android:title="@string/navigation_item_feedback"/> + + اكتمال رفع %1$s فشل رفع %1$s انقر لتشاهد - متبقى %d - مرفوعاتي الأخيرة + مرفوعاتي الأخيرة في قائمة الانتظار فشل انتهاء %1$d%% diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 37516ce67..4c59ee7ce 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -148,6 +148,8 @@ বিবরণ মিডিয়ার বিবরণ এখানে যাবে। এই মোটামুটি দীর্ঘ হতে পারে এবং একাধিক লাইনে লিখতে হতে পারে। আমরা আশা করি এটি দেখতে সুন্দর হবে। আপলোডের তারিখ + লাইসেন্স + স্থানাঙ্কসমূহ বিটা টেস্টার হোন উইকিউপাত্ত ব্যবহার করুন (সতর্কতা: এটি নিষ্ক্রিয় করা অধিক পরিমাণে মোবাইল ডেটা খরচ হওয়ার কারণ হতে পারে) @@ -182,6 +184,6 @@ ভূমিকা অবস্থানের অনুমতি ছাড়া কাছাকাছি জায়গাগুলি প্রদর্শন করা যাবে না কোন বিবরণ পাওয়া যায়নি - কমন্স নিবন্ধ + কমন্সে ফাইলের পাতা উইকিউপাত্ত পদ diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 39c90dd15..5bfc72e43 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -8,7 +8,7 @@ Registre S’està iniciant la sessió Espereu… - S\'ha iniciat sessió correctament ! + S\'ha iniciat sessió correctament! Error en iniciar la sessió! No s\'ha trobat el fitxer. Proveu-ho amb un altre fitxer. L\'autenticació ha fallat! @@ -38,9 +38,9 @@ Títol Descripció No s\'ha pogut iniciar la sessió – error de xarxa - No s\'ha pogut iniciar la sessió – siusplau comprova el teu nom d\'usuari + No s\'ha pogut iniciar la sessió – si et plau comprova el teu nom d\'usuari No s’ha pogut iniciar la sessió. Comproveu la vostra contrasenya - Masses intents erronis – Proveu-ho de nou d\'aquí uns minuts. + Massa intents erronis – Proveu-ho de nou d\'aquí uns minuts. Ho sentim, aquest usuari ha estat blocat a Commons Ha fallat l\'inici de sessió Carrega diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b849c81e3..5c429f66a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -88,6 +88,7 @@ Erneut versuchen Abbrechen Dieses Bild wird lizenziert unter %1$s + 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 <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines/de\">Wikimedia-Commons-Richtlinien</a> einhält. Herunterladen Lizenz Vorherige(n) Titel/Beschreibung verwenden diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 136b6f218..ca4611489 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -16,9 +16,9 @@ ¡Se subieron %1$s! Pulsa para ver tu subida Empezando la subida de %1$s - Cargando «%1$s» + Cargando %1$s Finalizando la subida de %1$s - Falló la carga de «%1$s» + Falló la carga de %1$s Toca para ver Subiendo %d archivo @@ -27,7 +27,7 @@ Mis subidas recientes En la cola Fallido - %1$d %% completado + %1$d%% completado Subiendo De la galería Tomar una foto diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 543f630c1..1c3ba42da 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -73,6 +73,7 @@ Berriz saiatu Utzi Irudi hau %1$s lizentziapean egongo da + Irudi hau bidaltzen, nire lan propioa dela aitortzen dut, copyrighta duten materiala edo selfiak ez duela, eta beste motatakoak <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Wikimedia Ohikoaren arauak</a> Jaitsi Lizentzia Aurreko izenburu/deskribapena erabili diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 118ce4b97..43bb62f0d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -88,6 +88,7 @@ Réessayer Annuler Cette image sera sous licence %1$s + 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 à <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">la politique de Wikimedia Commons</a>. Télécharger Licence Utiliser le titre ou la description précédent diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index eaef5a9c5..feb3cf436 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -18,8 +18,7 @@ העלאת %1$s מסתיימת העלאת %1$s נכשלה נא ללחוץ כדי להציג - נשארו %d - ההעלאות שלי + ההעלאות שלי בתור נכשלה %1$d%% הושלמו diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 749b35ab7..d2fc2330c 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -87,6 +87,7 @@ לנסות שוב ביטול התמונה הזאת היא תחת רשיון %1$s + שליחת התמונה הזאת מהווה את הצהרתי על כך שזאת יצירה שלי, שהיא לא מכילה חומר מוגבל בזכויות יוצרים או תמונות עצמיות (סלפי) ומתאימה בכל אופן ל<a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">מדיניות של אתר ויקישיתוף של ויקימדיה</a>. הורדה רישיון להשתמש בכותרת ובתיאור קודמים diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index b3f080ac0..a71473023 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -54,6 +54,7 @@ Atcelt Lejupielādēt Licence + Nakts režīms CC0 Jums šķiet, ka sapratāt? Jā! @@ -66,6 +67,7 @@ Brīdinājums + Augšupielādēt attēlu Lama Tulpe Atcelt diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index a830cc6bc..2323d701d 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -88,6 +88,7 @@ Пробај пак Откажи Сликава ќе се води под лиценцата %1$s + Поднесувајќи ја сликава, изјавувам дека истата е мое сопствено дело, дека не содржи никаков материјал заштитен со авторски права, не содржи самослици, и дека на секој друг начин е во склад со <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines/mk?uselang=mk\">правилата на Ризницата</a>. Преземи Лиценца Користи претходен наслов/опис diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index ff342f3bf..fa5e00766 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -18,7 +18,6 @@ %1$s akan siap dimuat naik %1$s gagal dimuat naik Ketik untuk lihat - %d fail sedang dimuat naik Muat Naik Terbaru Saya Dibaris gilir Gagal @@ -48,8 +47,7 @@ %d muatnaik %d muatnaik - Memulakan %d kerja muat naik - + %d muatnaik %d muatnaik diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index ad1aaf210..ef2cc33e1 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -88,6 +88,7 @@ Prové torna Anulé Costa plancia a sarà sota la licensa %1$s + 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 <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">le régole ëd Wikimedia Commons</a>. Dëscarié Licensa Dovré ël tìtol o la descrission precedent diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3b38b8a3f..91d226aff 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -75,7 +75,7 @@ Configurações Criar conta Sobre - Software livre distribuído sob a <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache License v2</a>. 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. + Software livre distribuído sob a <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache License v2</a>. %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. <a href=\"https://github.com/commons-app/apps-android-commons\">Fonte</a> e <a href=\"https://commons-app.github.io/\">site</a> em GitHub. Crie um novo <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub issue</a> para relatórios de bugs e sugestões. <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\">Política de privacidade</a> <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">Créditos</a> @@ -88,6 +88,7 @@ Repetir Cancelar Essa imagem será licenciada sob %1$s + 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 <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Políticas do Wikimedia Commons</a>. Download Licença Usar o título/descrição anterior @@ -149,6 +150,9 @@ Descrição Descrição da mídia aqui. Isso pode ser potencialmente longo e precisará envolver múltiplas linhas. Esperamos que seja agradável. Data de envio. + Licença + Coordenadas + Nenhum fornecido Seja um Testador Beta Entre no nosso canal beta no Google Play e receba acesso prévio a novos recursos e correções de erros Usa Wikidata @@ -187,7 +191,7 @@ Tutorial Os locais próximos não podem ser exibidos sem permissões de localização Nenhuma descrição encontrada - Artigo de Commons + Página de arquivo do Commons Item do Wikidata Erro durante o cache de imagens diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index eb0684112..4d891fe3f 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -49,8 +49,7 @@ This message is followed by a list of the categories.\n{{Identical|Search category}} Hint text on menu item to save selected categories.\n{{Identical|Save}} {{Identical|Refresh}} - {{Identical|Upload}} - Message shown to the user when no category matching what they searched for was found. %1$s represents the category name + Message shown to the user when no category matching what they searched for was found. %1$s represents the category name Text explaining to users why and how to add categories to images. Users can also tap this message to skip adding categories. Title for the activity where Categories are being selected to add to the Image.\n{{Identical|Category}} Text for preference that enables the user to enable or disable collection of data about the user\'s usage patterns. @@ -59,7 +58,6 @@ {{Identical|Sign up}} {{Identical|About}} License and legal notice. %1$s is {{msg-wm|Commons-android-strings-trademarked name}} - {{Ignored}}\n\nUsed in {{msg-wm|Commons-android-strings-about license}}\n\n{{Identical|Wikimedia Commons}} {{doc-important|Please make sure that your translation of \"source\" means \"source code\", not \"reference source\".}}\nSource and Bugs 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}} {{Identical|Credit}} diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e47aea906..a4fd3f49f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -79,7 +79,7 @@ Настройки Зарегистрироваться О приложении - Приложение с открытым исходным кодом, выпущено по лицензии <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache License v2</a>. Викисклад и его логотип являются товарными знаками Фонда Викимедиа и используются с разрешения Фонда Викимедиа. Мы не поддерживаемся и не связаны с Фондом Викимедиа. + Приложение с открытым исходным кодом, выпущено по лицензии <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache License v2</a>. %1$s и его логотип являются товарными знаками Фонда Викимедиа и используются с разрешения Фонда Викимедиа. Мы не поддерживаемся и не связаны с Фондом Викимедиа. <a href=\"https://github.com/commons-app/apps-android-commons\">Исходный код</a> и <a href=\"https://commons-app.github.io/\">сайт</a> на GitHub. Создайте новый <a href=\"https://github.com/commons-app/apps-android-commons/issues\">запрос на GitHub</a>, чтоб сообщить об ошибке или внести предложение. <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy/ru\">Политика конфиденциальности</a> <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">Благодарности</a> @@ -153,6 +153,8 @@ Описание Здесь располагается описание носителя информации. Оно потенциально может быть весьма длинным и даже располагаться в несколько строк. Однако мы надеемся, что это выглядит симпатично Дата загрузки + Лицензия + Координаты Стать бета-тестером Подпишитесь на наш канал бета-версии на Google Play и получите ранний доступ к новым функциям и исправлениям ошибок Использовать Викиданные @@ -191,7 +193,7 @@ Руководство Ближайшие места не могут быть отображены без разрешения на геолокацию описание не найдено - Статья на Викискладе + Страница файла на Викискладе Элемент Викиданных Ошибка при кэшировании картинок diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 2bfa69c7f..20716949d 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -88,6 +88,7 @@ Försök igen Avbryt Denna bild kommer att licensieras under %1$s + 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 <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Wikimedia Commons-policys</a>. Ladda ned Licens Använd föregående titel/beskrivning diff --git a/app/src/main/res/values-tcy/strings.xml b/app/src/main/res/values-tcy/strings.xml index 376ff1034..4ece0c348 100644 --- a/app/src/main/res/values-tcy/strings.xml +++ b/app/src/main/res/values-tcy/strings.xml @@ -1,5 +1,6 @@ + ಕಾಮನ್ಸ್ ಸಂಯೋಜನೆಲು ಸದಸ್ಯೆರ್ನ ಪುದರ್ ಪ್ರವೇಸೊ ಪದೊ @@ -23,7 +24,7 @@ %d ಕಡತ ಅಪ್ಲೊಡ್ ಆವೊಂದುಂಡು %d ಕಡತೊಲು ಅಪ್ಲೋಡ್ ಆವೊಂದುಂಡು - ಎನ್ನ ದಿಂಜಯೀನಾ ವಿಚಾರೊಳು + ಎನ್ನ ದಿಂಜಯೀನಾ ವಿಚಾರೊಳು ದಿಂಜೊಂತುಂಡು ದಿಂಜಿಜಿ %1$d%% ಮುಗಿಂಡ್ @@ -48,6 +49,7 @@ ಎಂಕ್ಲೆನ ಬಗ್ಗೆ ನನೊರ ಪ್ರಯತ್ನ ಮಾನ್ಪುಲೇ ವಜಾ ಮಲ್ಪುಲೆ + ಡೌನ್‍ಲೋಡ್ ಪರವಾನಗಿ ಕತ್ತಲೆದ ಕ್ರಮೊ CC0 @@ -63,6 +65,7 @@ CC BY 3.0 CC BY-SA 4.0 CC BY 4.0 + CC Zero ಅಂದ್! ವರ್ಗೊಲು ದಿಂಜಾವೊಂದುಂಡು…… @@ -73,4 +76,16 @@ ಅಂದ್ ಅತ್ತ್ ತರೆಬರವು + ವಿವರಣೆ + ಪರವಾನಿಗೆ + ವಜಾ ಮಲ್ಪುಲೆ + ತೋಜಾಲೇ + ಮುಚ್ಚಿಲೆ + ಮುಖ್ಯಪುಟೊ + ದಿಂಜಾಲೆ + ಕೈತಲ್‍ದ + ಎಂಕ್ಲೆನ ಬಗ್ಗೆ + ಸಂಯೋಜನೆಲು + ಅಬಿಪ್ರಾಯೊ + ನಿರ್ಗಮಿಸಾಲೆ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 5c92238f7..d7229008e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -107,4 +107,6 @@ Açıklama yok Bilinmeyen lisans Yenile + Lisans + Koordinatlar diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 714ef04a3..197f88c26 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -20,7 +20,6 @@ Đang hoàn thành việc tải lên tập tin %1$s Tải lên tập tin %1$s thất bại Chạm để xem - %d tập tin đang được tải lên Tập tin do tôi tải lên Đang chờ Thất bại @@ -51,8 +50,6 @@ Chưa tải lên tập tin %d tập tin tải lên - Đang bắt đầu tải lên %d tập tin - %d tập tin tải lên Không tìm thấy thể loại khớp với %1$s 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. Thể loại diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 0961ebe06..55c983886 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -88,6 +88,7 @@ 重試 取消 此圖片會按 %1$s 協議授權上載 + 透過提交此圖片,我宣佈這是我個人創作的成品,且不包含受版權保護或自拍內容,並除此之外遵守<a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">維基媒體共享資源方針</a>。 下載 協議授權 使用先前標題/描述 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index d2c740f89..0d03da14f 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -88,6 +88,7 @@ 重试 取消 该图像会采用%1$s授权 + 通过提交该图片,我声明这是我自己的作品,其不包含受版权保护的材料或自拍像,并遵循<a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">维基共享资源</a>方针。 下载 许可协议 使用之前的标题/描述 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1979c60b0..811d59e65 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -198,11 +198,17 @@ Tap this message (or hit back) to skip this step. Feedback Logout Tutorial + Developer plans Nearby places cannot be displayed without location permissions no description found Commons file page Wikidata item - Error while caching pictures + Error while caching pictures + Feedback wanted + 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) + No thanks + Sure, take me there! + https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback