Merge pull request #834 from akaita/667_fix_categories

667 fix categories
This commit is contained in:
neslihanturan 2017-08-09 11:31:23 +03:00 committed by GitHub
commit 401de30630
17 changed files with 380 additions and 714 deletions

View file

@ -17,7 +17,7 @@ dependencies {
compile "com.android.support:design:${project.supportLibVersion}" compile "com.android.support:design:${project.supportLibVersion}"
compile 'com.google.code.gson:gson:2.8.0' compile 'com.google.code.gson:gson:2.8.0'
compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
compile 'com.github.pedrovgs:renderers:3.3.0' compile 'com.github.pedrovgs:renderers:3.3.3'
annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
compile 'com.jakewharton.timber:timber:4.5.1' compile 'com.jakewharton.timber:timber:4.5.1'
compile 'com.squareup.okhttp3:okhttp:3.8.1' compile 'com.squareup.okhttp3:okhttp:3.8.1'
@ -30,7 +30,10 @@ dependencies {
// Because RxAndroid releases are few and far between, it is recommended you also // Because RxAndroid releases are few and far between, it is recommended you also
// explicitly depend on RxJava's latest version for bug fixes and new features. // explicitly depend on RxJava's latest version for bug fixes and new features.
compile 'io.reactivex.rxjava2:rxjava:2.1.2' compile 'io.reactivex.rxjava2:rxjava:2.1.2'
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
compile 'com.facebook.fresco:fresco:1.3.0' compile 'com.facebook.fresco:fresco:1.3.0'
compile 'com.facebook.stetho:stetho:1.5.0' compile 'com.facebook.stetho:stetho:1.5.0'

View file

@ -25,8 +25,8 @@ import java.io.IOException;
import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.caching.CacheController; import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.category.Category;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.data.Category;
import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi; import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;

View file

@ -2,18 +2,13 @@ package fr.free.nrw.commons.category;
import android.content.ContentProviderClient; import android.content.ContentProviderClient;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
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.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -24,24 +19,26 @@ import android.widget.EditText;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import com.pedrogomez.renderers.ListAdapteeCollection; import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxTextView;
import com.pedrogomez.renderers.RVRendererAdapter; import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoriesRenderer.CategoryClickedListener; import fr.free.nrw.commons.data.Category;
import fr.free.nrw.commons.upload.MwVolleyApi; import fr.free.nrw.commons.upload.MwVolleyApi;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
import static android.view.KeyEvent.ACTION_UP; import static android.view.KeyEvent.ACTION_UP;
@ -51,7 +48,8 @@ import static fr.free.nrw.commons.category.CategoryContentProvider.AUTHORITY;
/** /**
* 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 implements CategoryClickedListener { public class CategorizationFragment extends Fragment {
public static final int SEARCH_CATS_LIMIT = 25; public static final int SEARCH_CATS_LIMIT = 25;
@BindView(R.id.categoriesListBox) @BindView(R.id.categoriesListBox)
@ -68,19 +66,18 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
private RVRendererAdapter<CategoryItem> categoriesAdapter; private RVRendererAdapter<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler; private OnCategoriesSaveHandler onCategoriesSaveHandler;
private HashMap<String, ArrayList<String>> categoriesCache; private HashMap<String, ArrayList<String>> categoriesCache;
private ArrayList<String> selectedCategories = new ArrayList<>(); private List<CategoryItem> selectedCategories = new ArrayList<>();
private ContentProviderClient client; private ContentProviderClient databaseClient;
private PrefixUpdater prefixUpdaterSub;
private MethodAUpdater methodAUpdaterSub; private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
private final CategoryTextWatcher textWatcher = new CategoryTextWatcher(); if (item.isSelected()) {
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(this); selectedCategories.add(item);
private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2); updateCategoryCount(item, databaseClient);
private final ArrayList<String> titleCatItems = new ArrayList<>(); } else {
private final CountDownLatch mergeLatch = new CountDownLatch(1); selectedCategories.remove(item);
// LHS guarantees ordered insertions, allowing for prioritized method A results }
private final Set<String> results = new LinkedHashSet<>(); });
@SuppressWarnings("unchecked")
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@ -89,27 +86,29 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
categoriesList.setLayoutManager(new LinearLayoutManager(getContext())); categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
categoriesSkip.setOnClickListener(view -> { RxView.clicks(categoriesSkip)
getActivity().onBackPressed(); .takeUntil(RxView.detaches(categoriesSkip))
getActivity().finish(); .subscribe(o -> {
}); getActivity().onBackPressed();
getActivity().finish();
});
ArrayList<CategoryItem> items; ArrayList<CategoryItem> items = new ArrayList<>();
if (savedInstanceState == null) { categoriesCache = new HashMap<>();
items = new ArrayList<>(); if (savedInstanceState != null) {
categoriesCache = new HashMap<>(); items.addAll(savedInstanceState.getParcelableArrayList("currentCategories"));
} else { categoriesCache.putAll((HashMap<String, ArrayList<String>>) savedInstanceState
items = savedInstanceState.getParcelableArrayList("currentCategories"); .getSerializable("categoriesCache"));
categoriesCache = (HashMap<String, ArrayList<String>>) savedInstanceState
.getSerializable("categoriesCache");
} }
categoriesAdapter = adapterFactory.create(items); categoriesAdapter = adapterFactory.create(items);
categoriesList.setAdapter(categoriesAdapter); categoriesList.setAdapter(categoriesAdapter);
categoriesFilter.addTextChangedListener(textWatcher);
startUpdatingCategoryList();
RxTextView.textChanges(categoriesFilter)
.takeUntil(RxView.detaches(categoriesFilter))
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(filter -> updateCategoryList(filter.toString()));
return rootView; return rootView;
} }
@ -129,7 +128,7 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
rootView.requestFocus(); rootView.requestFocus();
rootView.setOnKeyListener((v, keyCode, event) -> { rootView.setOnKeyListener((v, keyCode, event) -> {
if (event.getAction() == ACTION_UP && keyCode == KEYCODE_BACK) { if (event.getAction() == ACTION_UP && keyCode == KEYCODE_BACK) {
backButtonDialog(); showBackButtonDialog();
return true; return true;
} }
return false; return false;
@ -137,16 +136,10 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
} }
} }
@Override
public void onDestroyView() {
categoriesFilter.removeTextChangedListener(textWatcher);
super.onDestroyView();
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
client.release(); databaseClient.release();
} }
@Override @Override
@ -165,42 +158,17 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
public boolean onOptionsItemSelected(MenuItem menuItem) { public boolean onOptionsItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) { switch (menuItem.getItemId()) {
case R.id.menu_save_categories: case R.id.menu_save_categories:
if (selectedCategories.size() > 0) {
int numberSelected = 0; //Some categories selected, proceed to submission
onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories));
selectedCategories = new ArrayList<>();
int count = categoriesAdapter.getItemCount();
for (int i = 0; i < count; i++) {
CategoryItem item = categoriesAdapter.getItem(i);
if (item.isSelected()) {
selectedCategories.add(item.getName());
numberSelected++;
}
}
//If no categories selected, display warning to user
if (numberSelected == 0) {
new AlertDialog.Builder(getActivity())
.setMessage("Images without categories are rarely usable. "
+ "Are you sure you want to submit without selecting "
+ "categories?")
.setTitle("No Categories Selected")
.setPositiveButton("No, go back", (dialog, id) -> {
//Exit menuItem so user can select their categories
})
.setNegativeButton("Yes, submit", (dialog, id) -> {
//Proceed to submission
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
})
.create()
.show();
} else { } else {
//Proceed to submission //No categories selected, prompt the user to select some
onCategoriesSaveHandler.onCategoriesSave(selectedCategories); showConfirmationDialog();
return true;
} }
return true;
default:
return super.onOptionsItemSelected(menuItem);
} }
return super.onOptionsItemSelected(menuItem);
} }
@Override @Override
@ -209,249 +177,161 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
setHasOptionsMenu(true); setHasOptionsMenu(true);
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity(); onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
getActivity().setTitle(R.string.categories_activity_title); getActivity().setTitle(R.string.categories_activity_title);
client = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY); databaseClient = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY);
} }
public HashMap<String, ArrayList<String>> getCategoriesCache() { private void updateCategoryList(String filter) {
return categoriesCache; Observable.fromIterable(selectedCategories)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(disposable -> {
categoriesSearchInProgress.setVisibility(View.VISIBLE);
categoriesNotFoundView.setVisibility(View.GONE);
categoriesSkip.setVisibility(View.GONE);
categoriesAdapter.clear();
})
.observeOn(Schedulers.io())
.concatWith(
searchAll(filter)
.mergeWith(searchCategories(filter))
.concatWith( TextUtils.isEmpty(filter)
? defaultCategories() : Observable.empty())
)
.filter(categoryItem -> !containsYear(categoryItem.getName()))
.distinct()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
s -> categoriesAdapter.add(s),
throwable -> Timber.e(throwable),
() -> {
categoriesAdapter.notifyDataSetChanged();
categoriesSearchInProgress.setVisibility(View.GONE);
if (categoriesAdapter.getItemCount() == selectedCategories.size()) {
// There are no suggestions
if (TextUtils.isEmpty(filter)) {
// Allow to send image with no categories
categoriesSkip.setVisibility(View.VISIBLE);
} else {
// Inform the user that the searched term matches no category
categoriesNotFoundView.setText(getString(R.string.categories_not_found, filter));
categoriesNotFoundView.setVisibility(View.VISIBLE);
}
}
}
);
} }
/** private List<String> getStringList(List<CategoryItem> input) {
* Retrieves category suggestions from title input List<String> output = new ArrayList<>();
* for (CategoryItem item : input) {
* @return a list containing title-related categories output.add(item.getName());
*/ }
private ArrayList<String> titleCatQuery() { return output;
TitleCategories titleCategoriesSub; }
private Observable<CategoryItem> defaultCategories() {
return gpsCategories()
.concatWith(titleCategories())
.concatWith(recentCategories());
}
private Observable<CategoryItem> gpsCategories() {
return Observable.fromIterable(
MwVolleyApi.GpsCatExists.getGpsCatExists()
? MwVolleyApi.getGpsCat() : new ArrayList<>())
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> titleCategories() {
//Retrieve the title that was saved when user tapped submit icon //Retrieve the title that was saved when user tapped submit icon
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity()); SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity());
String title = titleDesc.getString("Title", ""); String title = titleDesc.getString("Title", "");
Timber.d("Title: %s", title);
//Override onPostExecute to access the results of async API call return CommonsApplication.getInstance().getMWApi()
titleCategoriesSub = new TitleCategories(title) { .searchTitles(title, SEARCH_CATS_LIMIT)
@Override .map(name -> new CategoryItem(name, false));
protected void onPostExecute(List<String> result) {
super.onPostExecute(result);
Timber.d("Results in onPostExecute: %s", result);
titleCatItems.addAll(result);
Timber.d("TitleCatItems in onPostExecute: %s", titleCatItems);
mergeLatch.countDown();
}
};
titleCategoriesSub.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
Timber.d("TitleCatItems in titleCatQuery: %s", titleCatItems);
//Only return titleCatItems after API call has finished
try {
mergeLatch.await(5L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Timber.e(e, "Interrupted exception: ");
}
return titleCatItems;
} }
/** private Observable<CategoryItem> recentCategories() {
* Retrieves recently-used categories return Observable.fromIterable(Category.recentCategories(databaseClient, SEARCH_CATS_LIMIT))
* .map(s -> new CategoryItem(s, false));
* @return a list containing recent categories
*/
private ArrayList<String> recentCatQuery() {
ArrayList<String> items = new ArrayList<>();
Cursor cursor = null;
try {
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 != null && cursor.moveToNext()
&& cursor.getPosition() < SEARCH_CATS_LIMIT) {
Category cat = Category.fromCursor(cursor);
items.add(cat.getName());
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return items;
} }
/** private Observable<CategoryItem> searchAll(String term) {
* Merges nearby categories, categories suggested based on title, and recent categories... //If user hasn't typed anything in yet, get GPS and recent items
* without duplicates. if (TextUtils.isEmpty(term)) {
* return Observable.empty();
* @return a list containing merged categories
*/
ArrayList<String> mergeItems() {
Set<String> mergedItems = new LinkedHashSet<>();
Timber.d("Calling APIs for GPS cats, title cats and recent cats...");
List<String> gpsItems = new ArrayList<>();
if (MwVolleyApi.GpsCatExists.getGpsCatExists()) {
gpsItems.addAll(MwVolleyApi.getGpsCat());
}
List<String> titleItems = new ArrayList<>(titleCatQuery());
List<String> recentItems = new ArrayList<>(recentCatQuery());
//Await results of titleItems, which is likely to come in last
try {
mergeLatch.await(5L, TimeUnit.SECONDS);
Timber.d("Waited for merge");
} catch (InterruptedException e) {
Timber.e(e, "Interrupted Exception: ");
} }
mergedItems.addAll(gpsItems); //if user types in something that is in cache, return cached category
Timber.d("Adding GPS items: %s", gpsItems); if (categoriesCache.containsKey(term)) {
mergedItems.addAll(titleItems); return Observable.fromIterable(categoriesCache.get(term))
Timber.d("Adding title items: %s", titleItems); .map(name -> new CategoryItem(name, false));
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 //otherwise, search API for matching categories
// of preexisting code return CommonsApplication.getInstance().getMWApi()
ArrayList<String> mergedItemsList = new ArrayList<>(mergedItems); .allCategories(term, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
Timber.d("Merged item list: %s", mergedItemsList);
return mergedItemsList;
} }
/** private Observable<CategoryItem> searchCategories(String term) {
* Displays categories found to the user as they type in the search box //If user hasn't typed anything in yet, get GPS and recent items
* if (TextUtils.isEmpty(term)) {
* @param categories a list of all categories found for the search string return Observable.empty();
* @param filter the search string
*/
private void setCatsAfterAsync(ArrayList<String> categories, String filter) {
if (getActivity() != null) {
ArrayList<CategoryItem> items = new ArrayList<>();
HashSet<String> existingKeys = new HashSet<>();
int count = categoriesAdapter.getItemCount();
for (int i = 0; i < count; i++) {
CategoryItem item = categoriesAdapter.getItem(i);
if (item.isSelected()) {
items.add(item);
existingKeys.add(item.getName());
}
}
for (String category : categories) {
if (!existingKeys.contains(category)) {
items.add(new CategoryItem(category, false));
}
}
categoriesAdapter.setCollection(new ListAdapteeCollection<>(items));
categoriesAdapter.notifyDataSetChanged();
categoriesSearchInProgress.setVisibility(View.GONE);
if (categories.isEmpty()) {
if (TextUtils.isEmpty(filter)) {
// If we found no recent cats, show the skip message!
categoriesSkip.setVisibility(View.VISIBLE);
} else {
categoriesNotFoundView.setText(getString(R.string.categories_not_found, filter));
categoriesNotFoundView.setVisibility(View.VISIBLE);
}
} else {
categoriesList.smoothScrollToPosition(existingKeys.size());
}
} else {
Timber.e("Error: Fragment is null");
} }
return CommonsApplication.getInstance().getMWApi()
.searchCategories(term, SEARCH_CATS_LIMIT)
.map(s -> new CategoryItem(s, false));
} }
private boolean containsYear(String item) {
//Check for current and previous year to exclude these categories from removal
Calendar now = Calendar.getInstance();
int year = now.get(Calendar.YEAR);
String yearInString = String.valueOf(year);
/** int prevYear = year - 1;
* Makes asynchronous calls to the Commons MediaWiki API via anonymous subclasses of String prevYearInString = String.valueOf(prevYear);
* 'MethodAUpdater' and 'PrefixUpdater'. Some of their methods are overridden in order to Timber.d("Previous year: %s", prevYearInString);
* aggregate the results. A CountDownLatch is used to ensure that MethodA results are shown
* above Prefix results.
*/
private void requestSearchResults() {
final CountDownLatch latch = new CountDownLatch(1);
prefixUpdaterSub = new PrefixUpdater(this) { //Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
@Override //And that item does not equal the current year or previous year
protected List<String> doInBackground(Void... voids) { //And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
List<String> result = new ArrayList<>(); return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
try { || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)"));
result = super.doInBackground();
latch.await();
} catch (InterruptedException e) {
Timber.w(e);
//Thread.currentThread().interrupt();
}
return result;
}
@Override
protected void onPostExecute(List<String> result) {
super.onPostExecute(result);
results.addAll(result);
Timber.d("Prefix result: %s", result);
String filter = categoriesFilter.getText().toString();
ArrayList<String> resultsList = new ArrayList<>(results);
categoriesCache.put(filter, resultsList);
Timber.d("Final results List: %s", resultsList);
categoriesAdapter.notifyDataSetChanged();
setCatsAfterAsync(resultsList, filter);
}
};
methodAUpdaterSub = new MethodAUpdater(this) {
@Override
protected void onPostExecute(List<String> result) {
results.clear();
super.onPostExecute(result);
results.addAll(result);
Timber.d("Method A result: %s", result);
categoriesAdapter.notifyDataSetChanged();
latch.countDown();
}
};
prefixUpdaterSub.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
methodAUpdaterSub.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
private void startUpdatingCategoryList() { private void updateCategoryCount(CategoryItem item, ContentProviderClient client) {
if (prefixUpdaterSub != null) { Category cat = lookupCategory(item.getName());
prefixUpdaterSub.cancel(true); cat.incTimesUsed();
cat.save(client);
}
private Category lookupCategory(String name) {
Category cat = Category.find(databaseClient, name);
if (cat == null) {
// Newly used category...
cat = new Category();
cat.setName(name);
cat.setLastUsed(new Date());
cat.setTimesUsed(0);
} }
if (methodAUpdaterSub != null) { return cat;
methodAUpdaterSub.cancel(true);
}
requestSearchResults();
} }
public int getCurrentSelectedCount() { public int getCurrentSelectedCount() {
int count = 0; return selectedCategories.size();
int numberOfItems = categoriesAdapter.getItemCount();
for (int i = 0; i < numberOfItems; i++) {
CategoryItem item = categoriesAdapter.getItem(i);
if (item.isSelected()) {
count++;
}
}
return count;
} }
public void backButtonDialog() { /**
* Show dialog asking for confirmation to leave without saving categories.
*/
public void showBackButtonDialog() {
new AlertDialog.Builder(getActivity()) new AlertDialog.Builder(getActivity())
.setMessage("Are you sure you want to go back? The image will not " .setMessage("Are you sure you want to go back? The image will not "
+ "have any categories saved.") + "have any categories saved.")
@ -464,26 +344,20 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
.show(); .show();
} }
@Override private void showConfirmationDialog() {
public void categoryClicked(CategoryItem item) { new AlertDialog.Builder(getActivity())
if (item.isSelected()) { .setMessage("Images without categories are rarely usable. "
new CategoryCountUpdater(item.getName(), client).executeOnExecutor(executor); + "Are you sure you want to submit without selecting "
} + "categories?")
} .setTitle("No Categories Selected")
.setPositiveButton("No, go back", (dialog, id) -> {
private class CategoryTextWatcher implements TextWatcher { //Exit menuItem so user can select their categories
@Override })
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { .setNegativeButton("Yes, submit", (dialog, id) -> {
} //Proceed to submission
onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories));
@Override })
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { .create()
startUpdatingCategoryList(); .show();
}
@Override
public void afterTextChanged(Editable editable) {
}
} }
} }

View file

@ -11,6 +11,7 @@ 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;
import fr.free.nrw.commons.data.Category;
import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.data.DBOpenHelper;
import timber.log.Timber; import timber.log.Timber;

View file

@ -1,59 +0,0 @@
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

@ -51,4 +51,24 @@ class CategoryItem implements Parcelable {
parcel.writeString(name); parcel.writeString(name);
parcel.writeInt(selected ? 1 : 0); parcel.writeInt(selected ? 1 : 0);
} }
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CategoryItem that = (CategoryItem) o;
return name.equals(that.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
} }

View file

@ -1,100 +0,0 @@
package fr.free.nrw.commons.category;
import android.os.AsyncTask;
import android.view.View;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
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
* purpose. This class should be subclassed in CategorizationFragment.java to aggregate the results.
*/
class MethodAUpdater extends AsyncTask<Void, Void, List<String>> {
private final CategorizationFragment catFragment;
private String filter;
MethodAUpdater(CategorizationFragment catFragment) {
this.catFragment = catFragment;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
filter = catFragment.categoriesFilter.getText().toString();
catFragment.categoriesSearchInProgress.setVisibility(View.VISIBLE);
catFragment.categoriesNotFoundView.setVisibility(View.GONE);
catFragment.categoriesSkip.setVisibility(View.GONE);
}
/**
* Remove categories that contain a year in them (starting with 19__ or 20__), except for this year
* and previous year
* Rationale: https://github.com/commons-app/apps-android-commons/issues/47
*
* @param items Unfiltered list of categories
* @return Filtered category list
*/
private List<String> filterYears(List<String> items) {
Iterator<String> iterator;
//Check for current and previous year to exclude these categories from removal
Calendar now = Calendar.getInstance();
int year = now.get(Calendar.YEAR);
String yearInString = String.valueOf(year);
Timber.d("Year: %s", yearInString);
int prevYear = year - 1;
String prevYearInString = String.valueOf(prevYear);
Timber.d("Previous year: %s", prevYearInString);
//Copy to Iterator to prevent ConcurrentModificationException when removing item
for (iterator = items.iterator(); iterator.hasNext(); ) {
String s = iterator.next();
//Check if s contains a 4-digit word anywhere within the string (.* is wildcard)
//And that s does not equal the current year or previous year
if (s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) {
Timber.d("Filtering out year %s", s);
iterator.remove();
}
}
Timber.d("Items: %s", items);
return items;
}
@Override
protected List<String> doInBackground(Void... voids) {
//otherwise if user has typed something in that isn't in cache, search API for matching categories
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
List<String> categories = new ArrayList<>();
//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(SEARCH_CATS_LIMIT, filter);
Timber.d("Method A URL filter %s", categories);
} catch (IOException e) {
Timber.e(e, "IO Exception: ");
//Return empty arraylist
return categories;
}
Timber.d("Found categories from Method A search, waiting for filter");
return new ArrayList<>(filterYears(categories));
}
}

View file

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

View file

@ -1,119 +0,0 @@
package fr.free.nrw.commons.category;
import android.os.AsyncTask;
import android.text.TextUtils;
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;
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.
*/
class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
private final CategorizationFragment catFragment;
private String filter;
PrefixUpdater(CategorizationFragment catFragment) {
this.catFragment = catFragment;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
filter = catFragment.categoriesFilter.getText().toString();
catFragment.categoriesSearchInProgress.setVisibility(View.VISIBLE);
catFragment.categoriesNotFoundView.setVisibility(View.GONE);
catFragment.categoriesSkip.setVisibility(View.GONE);
}
/**
* Remove categories that contain a year in them (starting with 19__ or 20__), except for this year
* and previous year
* Rationale: https://github.com/commons-app/apps-android-commons/issues/47
*
* @param items Unfiltered list of categories
* @return Filtered category list
*/
private List<String> filterIrrelevantResults(List<String> items) {
Iterator<String> iterator;
//Check for current and previous year to exclude these categories from removal
Calendar now = Calendar.getInstance();
int year = now.get(Calendar.YEAR);
String yearInString = String.valueOf(year);
Timber.d("Year: %s", yearInString);
int prevYear = year - 1;
String prevYearInString = String.valueOf(prevYear);
Timber.d("Previous year: %s", prevYearInString);
//Copy to Iterator to prevent ConcurrentModificationException when removing item
for (iterator = items.iterator(); iterator.hasNext();) {
String s = iterator.next();
//Check if s contains a 4-digit word anywhere within the string (.* is wildcard)
//And that s does not equal the current year or previous year
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
if ((s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString))
|| s.matches("(.*)needing(.*)")||s.matches("(.*)taken on(.*)")) {
Timber.d("Filtering out irrelevant result: %s", s);
iterator.remove();
}
}
Timber.d("Items: %s", items);
return items;
}
@Override
protected List<String> doInBackground(Void... voids) {
//If user hasn't typed anything in yet, get GPS and recent items
if (TextUtils.isEmpty(filter)) {
ArrayList<String> mergedItems = new ArrayList<>(catFragment.mergeItems());
Timber.d("Merged items, waiting for filter");
return new ArrayList<>(filterIrrelevantResults(mergedItems));
}
//if user types in something that is in cache, return cached category
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));
}
//otherwise if user has typed something in that isn't in cache, search API for matching categories
//URL: https://commons.wikimedia.org/w/api.php?action=query&list=allcategories&acprefix=filter&aclimit=25
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
List<String> categories = new ArrayList<>();
try {
categories = api.allCategories(SEARCH_CATS_LIMIT, this.filter);
Timber.d("Prefix URL filter %s", categories);
} catch (IOException e) {
Timber.e(e, "IO Exception: ");
//Return empty arraylist
return categories;
}
Timber.d("Found categories from Prefix search, waiting for filter");
return new ArrayList<>(filterIrrelevantResults(categories));
}
}

View file

@ -1,48 +0,0 @@
package fr.free.nrw.commons.category;
import android.os.AsyncTask;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
/**
* Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that are related to
* the title entered in previous screen. The 'srsearch' action-specific parameter is used for this
* purpose. This class should be subclassed in CategorizationFragment.java to add the results to recent and GPS cats.
*/
class TitleCategories extends AsyncTask<Void, Void, List<String>> {
private final static int SEARCH_CATS_LIMIT = 25;
private final String title;
TitleCategories(String title) {
this.title = title;
}
@Override
protected List<String> doInBackground(Void... voids) {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
List<String> titleCategories = new ArrayList<>();
//URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
try {
titleCategories = api.searchTitles(SEARCH_CATS_LIMIT, this.title);
} catch (IOException e) {
Timber.e(e, "IO Exception: ");
//Return empty arraylist
return titleCategories;
}
Timber.d("Title cat query results: %s", titleCategories);
return titleCategories;
}
}

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.category; package fr.free.nrw.commons.data;
import android.content.ContentProviderClient; import android.content.ContentProviderClient;
import android.content.ContentValues; import android.content.ContentValues;
@ -6,11 +6,15 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.net.Uri; import android.net.Uri;
import android.os.RemoteException; import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import fr.free.nrw.commons.category.CategoryContentProvider;
public class Category { public class Category {
private ContentProviderClient client;
private Uri contentUri; private Uri contentUri;
private String name; private String name;
@ -53,12 +57,13 @@ public class Category {
touch(); touch();
} }
// Database/content-provider stuff //region Database/content-provider stuff
public void setContentProviderClient(ContentProviderClient client) {
this.client = client;
}
public void save() { /**
* Persist category.
* @param client ContentProviderClient to handle DB connection
*/
public void save(ContentProviderClient client) {
try { try {
if (contentUri == null) { if (contentUri == null) {
contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues()); contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues());
@ -78,7 +83,7 @@ public class Category {
return cv; return cv;
} }
public static Category fromCursor(Cursor cursor) { private static Category fromCursor(Cursor cursor) {
// Hardcoding column positions! // Hardcoding column positions!
Category c = new Category(); Category c = new Category();
c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0)); c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0));
@ -88,6 +93,65 @@ public class Category {
return c; return c;
} }
/**
* Find persisted category in database, based on its name.
* @param client ContentProviderClient to handle DB connection
* @param name Category's name
* @return category from database, or null if not found
*/
public static @Nullable Category find(ContentProviderClient client, 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();
}
}
return null;
}
/**
* Retrieve recently-used categories, ordered by descending date.
* @return a list containing recent categories
*/
public static @NonNull ArrayList<String> recentCategories(ContentProviderClient client, int limit) {
ArrayList<String> items = new ArrayList<>();
Cursor cursor = null;
try {
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 != null && cursor.moveToNext()
&& cursor.getPosition() < limit) {
Category cat = Category.fromCursor(cursor);
items.add(cat.getName());
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return items;
}
public static class Table { public static class Table {
public static final String TABLE_NAME = "categories"; public static final String TABLE_NAME = "categories";
@ -144,4 +208,5 @@ public class Category {
} }
} }
} }
//endregion
} }

View file

@ -4,7 +4,6 @@ import android.content.Context;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import fr.free.nrw.commons.category.Category;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.ModifierSequence;

View file

@ -35,6 +35,7 @@ import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import in.yuvi.http.fluent.Http; import in.yuvi.http.fluent.Http;
import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import timber.log.Timber; import timber.log.Timber;
@ -205,78 +206,104 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
@Override @Override
@NonNull @NonNull
public List<String> searchCategories(int searchCatsLimit, String filterValue) throws IOException { public Observable<String> searchCategories(String filterValue, int searchCatsLimit) {
List<ApiResult> categoryNodes = api.action("query") return Single.fromCallable(() -> {
.param("format", "xml") List<ApiResult> categoryNodes = null;
.param("list", "search") try {
.param("srwhat", "text") categoryNodes = api.action("query")
.param("srnamespace", "14") .param("format", "xml")
.param("srlimit", searchCatsLimit) .param("list", "search")
.param("srsearch", filterValue) .param("srwhat", "text")
.get() .param("srnamespace", "14")
.getNodes("/api/query/search/p/@title"); .param("srlimit", searchCatsLimit)
.param("srsearch", filterValue)
.get()
.getNodes("/api/query/search/p/@title");
} catch (IOException e) {
Timber.e("Failed to obtain searchCategories", e);
}
if (categoryNodes == null) { if (categoryNodes == null) {
return Collections.emptyList(); return new ArrayList<String>();
} }
List<String> categories = new ArrayList<>(); List<String> categories = new ArrayList<>();
for (ApiResult categoryNode : categoryNodes) { for (ApiResult categoryNode : categoryNodes) {
String cat = categoryNode.getDocument().getTextContent(); String cat = categoryNode.getDocument().getTextContent();
String catString = cat.replace("Category:", ""); String catString = cat.replace("Category:", "");
categories.add(catString); categories.add(catString);
} }
return categories; return categories;
})
.flatMapObservable(list -> Observable.fromIterable(list));
} }
@Override @Override
@NonNull @NonNull
public List<String> allCategories(int searchCatsLimit, String filterValue) throws IOException { public Observable<String> allCategories(String filterValue, int searchCatsLimit) {
ArrayList<ApiResult> categoryNodes = api.action("query") return Single.fromCallable(() -> {
.param("list", "allcategories") ArrayList<ApiResult> categoryNodes = null;
.param("acprefix", filterValue) try {
.param("aclimit", searchCatsLimit) categoryNodes = api.action("query")
.get() .param("list", "allcategories")
.getNodes("/api/query/allcategories/c"); .param("acprefix", filterValue)
.param("aclimit", searchCatsLimit)
.get()
.getNodes("/api/query/allcategories/c");
} catch (IOException e) {
Timber.e("Failed to obtain allCategories", e);
}
if (categoryNodes == null) { if (categoryNodes == null) {
return Collections.emptyList(); return new ArrayList<String>();
} }
List<String> categories = new ArrayList<>(); List<String> categories = new ArrayList<>();
for (ApiResult categoryNode : categoryNodes) { for (ApiResult categoryNode : categoryNodes) {
categories.add(categoryNode.getDocument().getTextContent()); categories.add(categoryNode.getDocument().getTextContent());
} }
return categories; return categories;
})
.flatMapObservable(list -> Observable.fromIterable(list));
} }
@Override @Override
@NonNull @NonNull
public List<String> searchTitles(int searchCatsLimit, String title) throws IOException { public Observable<String> searchTitles(String title, int searchCatsLimit) {
ArrayList<ApiResult> categoryNodes = api.action("query") return Single.fromCallable(() -> {
.param("format", "xml") ArrayList<ApiResult> categoryNodes = null;
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", searchCatsLimit)
.param("srsearch", title)
.get()
.getNodes("/api/query/search/p/@title");
if (categoryNodes == null) { try {
return Collections.emptyList(); categoryNodes = api.action("query")
} .param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", searchCatsLimit)
.param("srsearch", title)
.get()
.getNodes("/api/query/search/p/@title");
} catch (IOException e) {
Timber.e("Failed to obtain searchTitles", e);
return new ArrayList();
}
List<String> titleCategories = new ArrayList<>(); if (categoryNodes == null) {
for (ApiResult categoryNode : categoryNodes) { return Collections.emptyList();
String cat = categoryNode.getDocument().getTextContent(); }
String catString = cat.replace("Category:", "");
titleCategories.add(catString);
}
return titleCategories; List<String> titleCategories = new ArrayList<>();
for (ApiResult categoryNode : categoryNodes) {
String cat = categoryNode.getDocument().getTextContent();
String catString = cat.replace("Category:", "");
titleCategories.add(catString);
}
return titleCategories;
})
.flatMapObservable(list -> Observable.fromIterable(list));
} }
@Override @Override

View file

@ -7,6 +7,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
public interface MediaWikiApi { public interface MediaWikiApi {
@ -38,13 +39,13 @@ public interface MediaWikiApi {
MediaResult fetchMediaByFilename(String filename) throws IOException; MediaResult fetchMediaByFilename(String filename) throws IOException;
@NonNull @NonNull
List<String> searchCategories(int searchCatsLimit, String filterValue) throws IOException; Observable<String> searchCategories(String filterValue, int searchCatsLimit);
@NonNull @NonNull
List<String> allCategories(int searchCatsLimit, String filter) throws IOException; Observable<String> allCategories(String filter, int searchCatsLimit);
@NonNull @NonNull
List<String> searchTitles(int searchCatsLimit, String title) throws IOException; Observable<String> searchTitles(String title, int searchCatsLimit);
@Nullable @Nullable
String revisionsByFilename(String filename) throws IOException; String revisionsByFilename(String filename) throws IOException;

View file

@ -22,6 +22,7 @@ import android.widget.AdapterView;
import android.widget.Toast; import android.widget.Toast;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
@ -160,7 +161,7 @@ public class MultipleShareActivity
} }
@Override @Override
public void onCategoriesSave(ArrayList<String> categories) { public void onCategoriesSave(List<String> categories) {
if(categories.size() > 0) { if(categories.size() > 0) {
ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY); ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY);
for(Contribution contribution: photosList) { for(Contribution contribution: photosList) {

View file

@ -154,7 +154,7 @@ public class ShareActivity
} }
@Override @Override
public void onCategoriesSave(ArrayList<String> categories) { public void onCategoriesSave(List<String> categories) {
if(categories.size() > 0) { if(categories.size() > 0) {
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri()); ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
@ -525,7 +525,7 @@ public class ShareActivity
switch (item.getItemId()) { switch (item.getItemId()) {
case android.R.id.home: case android.R.id.home:
if(categorizationFragment!=null && categorizationFragment.isVisible()) { if(categorizationFragment!=null && categorizationFragment.isVisible()) {
categorizationFragment.backButtonDialog(); categorizationFragment.showBackButtonDialog();
} else { } else {
onBackPressed(); onBackPressed();
} }

View file

@ -24,6 +24,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:hint="@string/categories_search_text_hint" android:hint="@string/categories_search_text_hint"
android:maxLines="1" android:maxLines="1"
android:inputType="text"
android:imeOptions="flagNoExtractUi" android:imeOptions="flagNoExtractUi"
/> />