mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 21:03:54 +01:00
Swapped the ListView for a RecyclerView, made the fragment cleaner and fixed a whole bunch of code inspection issues.
This commit is contained in:
parent
f73a9f15fc
commit
9c987efbd2
16 changed files with 435 additions and 402 deletions
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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: ");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public interface OnCategoriesSaveHandler {
|
||||||
|
void onCategoriesSave(ArrayList<String> categories);
|
||||||
|
}
|
||||||
|
|
@ -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: ");
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue