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.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<String> 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<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler;
protected HashMap<String, ArrayList<String>> categoriesCache;
private HashMap<String, ArrayList<String>> categoriesCache;
private ArrayList<String> selectedCategories = new ArrayList<>();
private ContentProviderClient client;
private PrefixUpdater prefixUpdaterSub;
private MethodAUpdater methodAUpdaterSub;
private final CategoryTextWatcher textWatcher = new CategoryTextWatcher();
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(this);
private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
private final ArrayList<String> titleCatItems = new ArrayList<>();
private final CountDownLatch mergeLatch = new CountDownLatch(1);
// LHS guarantees ordered insertions, allowing for prioritized method A results
private final Set<String> results = new LinkedHashSet<>();
PrefixUpdater prefixUpdaterSub;
MethodAUpdater methodAUpdaterSub;
private final ArrayList<String> titleCatItems = new ArrayList<>();
final CountDownLatch mergeLatch = new CountDownLatch(1);
@SuppressWarnings("unchecked")
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_categorization, container, false);
ButterKnife.bind(this, rootView);
private ContentProviderClient client;
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
protected final static int SEARCH_CATS_LIMIT = 25;
public static class CategoryItem implements Parcelable {
public String name;
public boolean selected;
public static Creator<CategoryItem> CREATOR = new Creator<CategoryItem>() {
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<CategoryItem> items;
if (savedInstanceState == null) {
items = new ArrayList<>();
categoriesCache = new HashMap<>();
} else {
items = savedInstanceState.getParcelableArrayList("currentCategories");
categoriesCache = (HashMap<String, ArrayList<String>>) savedInstanceState.getSerializable("categoriesCache");
}
public CategoryItem(Parcel in) {
name = in.readString();
selected = in.readInt() == 1;
}
categoriesAdapter = adapterFactory.create(items);
categoriesList.setAdapter(categoriesAdapter);
categoriesFilter.addTextChangedListener(textWatcher);
@Override
public int describeContents() {
return 0;
}
startUpdatingCategoryList();
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(name);
parcel.writeInt(selected ? 1 : 0);
return rootView;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.clear();
inflater.inflate(R.menu.fragment_categorization, menu);
}
@Override
public void onResume() {
super.onResume();
View rootView = getView();
if (rootView != null) {
rootView.setFocusableInTouchMode(true);
rootView.requestFocus();
rootView.setOnKeyListener(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
*
* @return a list containing title-related categories
*/
protected ArrayList<String> titleCatQuery() {
private ArrayList<String> titleCatQuery() {
TitleCategories titleCategoriesSub;
//Retrieve the title that was saved when user tapped submit icon
@ -157,37 +255,41 @@ public class CategorizationFragment extends Fragment {
/**
* Retrieves recently-used categories
*
* @return a list containing recent categories
*/
protected ArrayList<String> recentCatQuery() {
private ArrayList<String> recentCatQuery() {
ArrayList<String> items = new ArrayList<>();
Cursor cursor = null;
try {
Cursor cursor = client.query(
cursor = client.query(
CategoryContentProvider.BASE_URI,
Category.Table.ALL_FIELDS,
null,
new String[]{},
Category.Table.COLUMN_LAST_USED + " DESC");
// fixme add a limit on the original query instead of falling out of the loop?
while (cursor.moveToNext() && cursor.getPosition() < SEARCH_CATS_LIMIT) {
while (cursor != null && cursor.moveToNext()
&& cursor.getPosition() < SEARCH_CATS_LIMIT) {
Category cat = Category.fromCursor(cursor);
items.add(cat.getName());
}
cursor.close();
}
catch (RemoteException e) {
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return items;
}
/**
* Merges nearby categories, categories suggested based on title, and recent categories... without duplicates.
*
* @return a list containing merged categories
*/
protected ArrayList<String> mergeItems() {
ArrayList<String> mergeItems() {
Set<String> mergedItems = new LinkedHashSet<>();
Timber.d("Calling APIs for GPS cats, title cats and recent cats...");
@ -213,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<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
*
* @param categories a list of all categories found for the search string
* @param filter the search string
* @param filter the search string
*/
protected void setCatsAfterAsync(ArrayList<String> categories, String filter) {
private void setCatsAfterAsync(ArrayList<String> categories, String filter) {
if (getActivity() != null) {
ArrayList<CategoryItem> items = new ArrayList<>();
HashSet<String> existingKeys = new HashSet<>();
for (CategoryItem item : categoriesAdapter.getItems()) {
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<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() {
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 {

View file

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

View file

@ -7,6 +7,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication;
@ -41,8 +42,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());

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

View file

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

View file

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

View file

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

View file

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