Swapped the ListView for a RecyclerView, made the fragment cleaner and fixed a whole bunch of code inspection issues.

This commit is contained in:
Paul Hawke 2017-07-22 13:57:46 -05:00
parent f73a9f15fc
commit 9c987efbd2
16 changed files with 435 additions and 402 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder; import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
@ -41,8 +42,9 @@ public class CategoryContentProvider extends ContentProvider {
return false; return false;
} }
@SuppressWarnings("ConstantConditions")
@Override @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(); SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(Category.Table.TABLE_NAME); queryBuilder.setTables(Category.Table.TABLE_NAME);
@ -75,15 +77,16 @@ public class CategoryContentProvider extends ContentProvider {
} }
@Override @Override
public String getType(Uri uri) { public String getType(@NonNull Uri uri) {
return null; return null;
} }
@SuppressWarnings("ConstantConditions")
@Override @Override
public Uri insert(Uri uri, ContentValues contentValues) { public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri); int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id = 0; long id;
switch (uriType) { switch (uriType) {
case CATEGORIES: case CATEGORIES:
id = sqlDB.insert(Category.Table.TABLE_NAME, null, contentValues); id = sqlDB.insert(Category.Table.TABLE_NAME, null, contentValues);
@ -96,12 +99,13 @@ public class CategoryContentProvider extends ContentProvider {
} }
@Override @Override
public int delete(Uri uri, String s, String[] strings) { public int delete(@NonNull Uri uri, String s, String[] strings) {
return 0; return 0;
} }
@SuppressWarnings("ConstantConditions")
@Override @Override
public int bulkInsert(Uri uri, ContentValues[] values) { public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (CategoryContentProvider)"); Timber.d("Hello, bulk insert! (CategoryContentProvider)");
int uriType = uriMatcher.match(uri); int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
@ -122,8 +126,9 @@ public class CategoryContentProvider extends ContentProvider {
return values.length; return values.length;
} }
@SuppressWarnings("ConstantConditions")
@Override @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") 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 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); int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated = 0; int rowsUpdated;
switch (uriType) { switch (uriType) {
case CATEGORIES_ID: case CATEGORIES_ID:
int id = Integer.valueOf(uri.getLastPathSegment()); int id = Integer.valueOf(uri.getLastPathSegment());

View file

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

View file

@ -0,0 +1,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<CategoryItem> CREATOR = new Creator<CategoryItem>() {
@Override
public CategoryItem createFromParcel(Parcel parcel) {
return new CategoryItem(parcel);
}
@Override
public CategoryItem[] newArray(int i) {
return new CategoryItem[0];
}
};
CategoryItem(String name, boolean selected) {
this.name = name;
this.selected = selected;
}
private CategoryItem(Parcel in) {
name = in.readString();
selected = in.readInt() == 1;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(name);
parcel.writeInt(selected ? 1 : 0);
}
}

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import android.view.View;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -14,18 +15,20 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; 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 * 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 * 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 * for this purpose. This class should be subclassed in CategorizationFragment.java to aggregate
* the results. * the results.
*/ */
public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> { class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
private final CategorizationFragment catFragment;
private String filter; private String filter;
private CategorizationFragment catFragment;
public PrefixUpdater(CategorizationFragment catFragment) { PrefixUpdater(CategorizationFragment catFragment) {
this.catFragment = catFragment; this.catFragment = catFragment;
} }
@ -90,8 +93,9 @@ public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
} }
//if user types in something that is in cache, return cached category //if user types in something that is in cache, return cached category
if (catFragment.categoriesCache.containsKey(filter)) { HashMap<String, ArrayList<String>> categoriesCache = catFragment.getCategoriesCache();
ArrayList<String> cachedItems = new ArrayList<>(catFragment.categoriesCache.get(filter)); if (categoriesCache.containsKey(filter)) {
ArrayList<String> cachedItems = new ArrayList<>(categoriesCache.get(filter));
Timber.d("Found cache items, waiting for filter"); Timber.d("Found cache items, waiting for filter");
return new ArrayList<>(filterIrrelevantResults(cachedItems)); return new ArrayList<>(filterIrrelevantResults(cachedItems));
} }
@ -101,7 +105,7 @@ public class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
List<String> categories = new ArrayList<>(); List<String> categories = new ArrayList<>();
try { 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); Timber.d("Prefix URL filter %s", categories);
} catch (IOException e) { } catch (IOException e) {
Timber.e(e, "IO Exception: "); Timber.e(e, "IO Exception: ");

View file

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

View file

@ -28,6 +28,7 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity; import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.category.CategorizationFragment; 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.contributions.Contribution;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.CategoryModifier;
@ -43,7 +44,7 @@ public class MultipleShareActivity
AdapterView.OnItemClickListener, AdapterView.OnItemClickListener,
FragmentManager.OnBackStackChangedListener, FragmentManager.OnBackStackChangedListener,
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler, MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
CategorizationFragment.OnCategoriesSaveHandler { OnCategoriesSaveHandler {
private CommonsApplication app; private CommonsApplication app;
private ArrayList<Contribution> photosList = null; private ArrayList<Contribution> photosList = null;

View file

@ -36,6 +36,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.AuthenticatedActivity; import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.category.CategorizationFragment; 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.contributions.Contribution;
import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.CategoryModifier;
import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider;
@ -51,7 +52,7 @@ import timber.log.Timber;
public class ShareActivity public class ShareActivity
extends AuthenticatedActivity extends AuthenticatedActivity
implements SingleUploadFragment.OnUploadActionInitiated, implements SingleUploadFragment.OnUploadActionInitiated,
CategorizationFragment.OnCategoriesSaveHandler { OnCategoriesSaveHandler {
private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1; private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1;
private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2; private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2;

View file

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

View file

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