From 9c987efbd2d448ecd5c587e9dcd36201cb81b650 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Sat, 22 Jul 2017 13:57:46 -0500 Subject: [PATCH] Swapped the ListView for a RecyclerView, made the fragment cleaner and fixed a whole bunch of code inspection issues. --- .../commons/category/CategoriesAdapter.java | 67 --- .../category/CategoriesAdapterFactory.java | 24 + .../commons/category/CategoriesRenderer.java | 57 ++ .../category/CategorizationFragment.java | 509 +++++++----------- .../free/nrw/commons/category/Category.java | 8 +- .../category/CategoryContentProvider.java | 21 +- .../category/CategoryCountUpdater.java | 59 ++ .../nrw/commons/category/CategoryItem.java | 42 ++ .../nrw/commons/category/MethodAUpdater.java | 6 +- .../category/OnCategoriesSaveHandler.java | 7 + .../nrw/commons/category/PrefixUpdater.java | 16 +- .../nrw/commons/category/TitleCategories.java | 7 +- .../commons/upload/MultipleShareActivity.java | 3 +- .../nrw/commons/upload/ShareActivity.java | 3 +- .../res/layout/fragment_categorization.xml | 2 +- .../res/layout/layout_categories_item.xml | 6 +- 16 files changed, 435 insertions(+), 402 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java create mode 100644 app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java create mode 100644 app/src/main/java/fr/free/nrw/commons/category/CategoryCountUpdater.java create mode 100644 app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java create mode 100644 app/src/main/java/fr/free/nrw/commons/category/OnCategoriesSaveHandler.java 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..426b0640e --- /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.selected = !item.selected; + checkedView.setChecked(item.selected); + if (listener != null) { + listener.categoryClicked(item); + } + } + }); + } + + @Override + public void render() { + CategoryItem item = getContent(); + checkedView.setChecked(item.selected); + checkedView.setText(item.name); + } + + 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 3f467183d..748329ab8 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 @@ -6,12 +6,12 @@ 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; @@ -22,15 +22,14 @@ 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,6 +39,8 @@ 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.upload.MwVolleyApi; import timber.log.Timber; @@ -47,83 +48,180 @@ import timber.log.Timber; /** * 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 CategoriesRenderer.CategoryClickedListener { + public static final int SEARCH_CATS_LIMIT = 25; - ListView categoriesList; - protected EditText categoriesFilter; - ProgressBar categoriesSearchInProgress; - TextView categoriesNotFoundView; - TextView categoriesSkip; - private CategoryTextWatcher textWatcher = new CategoryTextWatcher(); - - CategoriesAdapter categoriesAdapter; - ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2); + @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 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; - - public static class CategoryItem implements Parcelable { - public String name; - public boolean selected; - - public static Creator CREATOR = new Creator() { + categoriesSkip.setOnClickListener(new View.OnClickListener() { @Override - public CategoryItem createFromParcel(Parcel parcel) { - return new CategoryItem(parcel); + public void onClick(View view) { + getActivity().onBackPressed(); + getActivity().finish(); } + }); - @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(new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent 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(); + } + + @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.selected) { + selectedCategories.add(item.name); + 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", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + //Exit menuItem so user can select their categories + } + }) + .setNegativeButton("Yes, submit", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int 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(CategoryContentProvider.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,41 @@ 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. + * * @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,7 +315,7 @@ 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 ArrayList mergedItemsList = new ArrayList<>(mergedItems); @@ -223,15 +325,17 @@ 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()) { + int count = categoriesAdapter.getItemCount(); + for (int i = 0; i < count; i++) { + CategoryItem item = categoriesAdapter.getItem(i); if (item.selected) { items.add(item); existingKeys.add(item.name); @@ -243,8 +347,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 +362,7 @@ public class CategorizationFragment extends Fragment { } else { categoriesList.smoothScrollToPosition(existingKeys.size()); } - } - else { + } else { Timber.e("Error: Fragment is null"); } } @@ -272,7 +375,6 @@ public class CategorizationFragment extends Fragment { * above Prefix results. */ private void requestSearchResults() { - final CountDownLatch latch = new CountDownLatch(1); prefixUpdaterSub = new PrefixUpdater(this) { @@ -282,8 +384,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 +426,6 @@ public class CategorizationFragment extends Fragment { } private void startUpdatingCategoryList() { - if (prefixUpdaterSub != null) { prefixUpdaterSub.cancel(true); } @@ -339,238 +439,41 @@ 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.selected) { 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(new View.OnClickListener() { - @Override - public void onClick(View 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(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int index, long 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(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent 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", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - //No need to do anything, user remains on categorization screen - } - }); - builder.setNegativeButton("Yes", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - getActivity().finish(); - } - }); - - AlertDialog dialog = builder.create(); - dialog.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++; + 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", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + //No need to do anything, user remains on categorization screen } - } - - //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", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - //Exit menuItem so user can select their categories - return; - } - }); - builder.setNegativeButton("Yes, submit", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - //Proceed to submission - onCategoriesSaveHandler.onCategoriesSave(selectedCategories); - return; - } - }); - - AlertDialog dialog = builder.create(); - dialog.show(); - } else { - //Proceed to submission - onCategoriesSaveHandler.onCategoriesSave(selectedCategories); - return true; - } - } - return super.onOptionsItemSelected(menuItem); + }) + .setNegativeButton("Yes", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + getActivity().finish(); + } + }) + .create() + .show(); } @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); + public void categoryClicked(CategoryItem item) { + if (item.selected) { + new CategoryCountUpdater(item.name, client).executeOnExecutor(executor); + } } 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..b7bc96ab2 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,9 @@ 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); @@ -75,15 +77,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 +99,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 +126,9 @@ 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 +138,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()); 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..7198b6207 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java @@ -0,0 +1,42 @@ +package fr.free.nrw.commons.category; + +import android.os.Parcel; +import android.os.Parcelable; + +class CategoryItem implements Parcelable { + public final 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]; + } + }; + + CategoryItem(String name, boolean selected) { + this.name = name; + this.selected = selected; + } + + private CategoryItem(Parcel in) { + name = in.readString(); + selected = in.readInt() == 1; + } + + @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/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java index d7c807777..69a61cdfb 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 @@ -28,6 +28,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 +44,7 @@ public class MultipleShareActivity AdapterView.OnItemClickListener, FragmentManager.OnBackStackChangedListener, MultipleUploadListFragment.OnMultipleUploadInitiatedHandler, - CategorizationFragment.OnCategoriesSaveHandler { + OnCategoriesSaveHandler { private CommonsApplication app; private ArrayList photosList = null; 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 7b9111841..95d091006 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 @@ -36,6 +36,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; @@ -51,7 +52,7 @@ import timber.log.Timber; 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; 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