diff --git a/commons/AndroidManifest.xml b/commons/AndroidManifest.xml
index c6317582c..c6b93aacc 100644
--- a/commons/AndroidManifest.xml
+++ b/commons/AndroidManifest.xml
@@ -130,6 +130,13 @@
android:authorities="org.wikimedia.commons.modifications.contentprovider"
android:exported="false">
+
+
diff --git a/commons/commons.iml b/commons/commons.iml
index 0fec16f50..55cc5b2fb 100644
--- a/commons/commons.iml
+++ b/commons/commons.iml
@@ -5,28 +5,11 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- /res-overlay
-
- true
true
-
diff --git a/commons/res/values/strings.xml b/commons/res/values/strings.xml
index db693cd1b..91ab3261d 100644
--- a/commons/res/values/strings.xml
+++ b/commons/res/values/strings.xml
@@ -80,4 +80,5 @@
Privacy policy
About
Send Feedback (via Email)
+ Recently used categories
diff --git a/commons/src/main/java/org/wikimedia/commons/CategorizationFragment.java b/commons/src/main/java/org/wikimedia/commons/CategorizationFragment.java
index 5e02701bb..a95172937 100644
--- a/commons/src/main/java/org/wikimedia/commons/CategorizationFragment.java
+++ b/commons/src/main/java/org/wikimedia/commons/CategorizationFragment.java
@@ -1,11 +1,10 @@
package org.wikimedia.commons;
import android.app.Activity;
+import android.content.ContentProviderClient;
import android.content.Context;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
+import android.database.Cursor;
+import android.os.*;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@@ -18,10 +17,14 @@ import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
+import org.wikimedia.commons.category.Category;
+import org.wikimedia.commons.category.CategoryContentProvider;
+import org.wikimedia.commons.contributions.Contribution;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -45,6 +48,10 @@ public class CategorizationFragment extends SherlockFragment{
private HashMap> categoriesCache;
+ private ContentProviderClient client;
+
+ private final int SEARCH_CATS_LIMIT = 25;
+
public static class CategoryItem implements Parcelable {
public String name;
public boolean selected;
@@ -114,16 +121,38 @@ public class CategorizationFragment extends SherlockFragment{
categoriesAdapter.setItems(items);
categoriesAdapter.notifyDataSetInvalidated();
categoriesSearchInProgress.setVisibility(View.GONE);
- if(!TextUtils.isEmpty(filter) && categories.size() == 0) {
- categoriesNotFoundView.setText(getString(R.string.categories_not_found, filter));
- categoriesNotFoundView.setVisibility(View.VISIBLE);
+ if (categories.size() == 0) {
+ if(!TextUtils.isEmpty(filter)) {
+ categoriesNotFoundView.setText(getString(R.string.categories_not_found, filter));
+ categoriesNotFoundView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ // If we found recent cats, hide the skip message!
+ categoriesSkip.setVisibility(View.GONE);
}
}
@Override
protected ArrayList doInBackground(Void... voids) {
if(TextUtils.isEmpty(filter)) {
- return new ArrayList();
+ ArrayList items = new ArrayList();
+ try {
+ Cursor cursor = client.query(
+ CategoryContentProvider.BASE_URI,
+ Category.Table.ALL_FIELDS,
+ null,
+ new String[]{},
+ Category.Table.COLUMN_LAST_USED + " DESC");
+ // fixme add a limit on the original query instead of falling out of the loop?
+ while (cursor.moveToNext() && cursor.getPosition() < SEARCH_CATS_LIMIT) {
+ Category cat = Category.fromCursor(cursor);
+ items.add(cat.getName());
+ }
+ } catch (RemoteException e) {
+ // faaaail
+ throw new RuntimeException(e);
+ }
+ return items;
}
if(categoriesCache.containsKey(filter)) {
return categoriesCache.get(filter);
@@ -135,7 +164,7 @@ public class CategorizationFragment extends SherlockFragment{
result = api.action("query")
.param("list", "allcategories")
.param("acprefix", filter)
- .param("aclimit", 25)
+ .param("aclimit", SEARCH_CATS_LIMIT)
.get();
} catch (IOException e) {
throw new RuntimeException(e);
@@ -211,6 +240,55 @@ public class CategorizationFragment extends SherlockFragment{
return count;
}
+ private Category lookupCategory(String name) {
+ try {
+ Cursor cursor = client.query(
+ CategoryContentProvider.BASE_URI,
+ Category.Table.ALL_FIELDS,
+ Category.Table.COLUMN_NAME + "=?",
+ new String[] {name},
+ null);
+ if (cursor.moveToFirst()) {
+ Category cat = Category.fromCursor(cursor);
+ return cat;
+ }
+ } catch (RemoteException e) {
+ // This feels lazy, but to hell with checked exceptions. :)
+ throw new RuntimeException(e);
+ }
+
+ // Newly used category...
+ Category cat = new Category();
+ cat.setName(name);
+ cat.setLastUsed(new Date());
+ cat.setTimesUsed(0);
+ return cat;
+ }
+
+ private class CategoryCountUpdater extends AsyncTask {
+
+ private String name;
+
+ public CategoryCountUpdater(String name) {
+ this.name = name;
+ }
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ Category cat = lookupCategory(name);
+ cat.incTimesUsed();
+
+ cat.setContentProviderClient(client);
+ cat.save();
+
+ return null; // Make the compiler happy.
+ }
+ }
+
+ private void updateCategoryCount(String name) {
+ Utils.executeAsyncTask(new CategoryCountUpdater(name), executor);
+ }
+
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_categorization, null);
@@ -245,6 +323,9 @@ public class CategorizationFragment extends SherlockFragment{
CategoryItem item = (CategoryItem) adapterView.getAdapter().getItem(index);
item.selected = !item.selected;
checkedView.setChecked(item.selected);
+ if (item.selected) {
+ updateCategoryCount(item.name);
+ }
}
});
@@ -253,11 +334,7 @@ public class CategorizationFragment extends SherlockFragment{
}
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
- if(lastUpdater != null) {
- lastUpdater.cancel(true);
- }
- lastUpdater = new CategoriesUpdater();
- Utils.executeAsyncTask(lastUpdater, executor);
+ startUpdatingCategoryList();
}
public void afterTextChanged(Editable editable) {
@@ -265,10 +342,19 @@ public class CategorizationFragment extends SherlockFragment{
}
});
+ startUpdatingCategoryList();
return rootView;
}
+ private void startUpdatingCategoryList() {
+ if (lastUpdater != null) {
+ lastUpdater.cancel(true);
+ }
+ lastUpdater = new CategoriesUpdater();
+ Utils.executeAsyncTask(lastUpdater, executor);
+ }
+
@Override
public void onCreateOptionsMenu(Menu menu, com.actionbarsherlock.view.MenuInflater inflater) {
menu.clear();
@@ -280,6 +366,13 @@ public class CategorizationFragment extends SherlockFragment{
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
getActivity().setTitle(R.string.categories_activity_title);
+ client = getActivity().getContentResolver().acquireContentProviderClient(CategoryContentProvider.AUTHORITY);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ client.release();
}
@Override
diff --git a/commons/src/main/java/org/wikimedia/commons/category/Category.java b/commons/src/main/java/org/wikimedia/commons/category/Category.java
new file mode 100644
index 000000000..ae2045867
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/category/Category.java
@@ -0,0 +1,140 @@
+package org.wikimedia.commons.category;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import org.wikimedia.commons.contributions.ContributionsContentProvider;
+
+import java.util.Date;
+
+public class Category {
+ private ContentProviderClient client;
+ private Uri contentUri;
+
+ private String name;
+ private Date lastUsed;
+ private int timesUsed;
+
+ // Getters/setters
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Date getLastUsed() {
+ // warning: Date objects are mutable.
+ return (Date)lastUsed.clone();
+ }
+
+ public void setLastUsed(Date lastUsed) {
+ // warning: Date objects are mutable.
+ this.lastUsed = (Date)lastUsed.clone();
+ }
+
+ public void touch() {
+ lastUsed = new Date();
+ }
+
+ public int getTimesUsed() {
+ return timesUsed;
+ }
+
+ public void setTimesUsed(int timesUsed) {
+ this.timesUsed = timesUsed;
+ }
+
+ public void incTimesUsed() {
+ timesUsed++;
+ touch();
+ }
+
+ // Database/content-provider stuff
+ public void setContentProviderClient(ContentProviderClient client) {
+ this.client = client;
+ }
+
+ public void save() {
+ try {
+ if(contentUri == null) {
+ contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues());
+ } else {
+ client.update(contentUri, toContentValues(), null, null);
+ }
+ } catch(RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public ContentValues toContentValues() {
+ ContentValues cv = new ContentValues();
+ cv.put(Table.COLUMN_NAME, getName());
+ cv.put(Table.COLUMN_LAST_USED, getLastUsed().getTime());
+ cv.put(Table.COLUMN_TIMES_USED, getTimesUsed());
+ return cv;
+ }
+
+ public static Category fromCursor(Cursor cursor) {
+ // Hardcoding column positions!
+ Category c = new Category();
+ c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0));
+ c.name = cursor.getString(1);
+ c.lastUsed = new Date(cursor.getLong(2));
+ c.timesUsed = cursor.getInt(3);
+ return c;
+ }
+
+ public static class Table {
+ public static final String TABLE_NAME = "categories";
+
+ public static final String COLUMN_ID = "_id";
+ public static final String COLUMN_NAME = "name";
+ public static final String COLUMN_LAST_USED = "last_used";
+ public static final String COLUMN_TIMES_USED = "times_used";
+
+ // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
+ public static final String[] ALL_FIELDS = {
+ COLUMN_ID,
+ COLUMN_NAME,
+ COLUMN_LAST_USED,
+ COLUMN_TIMES_USED
+ };
+
+
+ private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ + COLUMN_ID + " INTEGER PRIMARY KEY,"
+ + COLUMN_NAME + " STRING,"
+ + COLUMN_LAST_USED + " INTEGER,"
+ + COLUMN_TIMES_USED + " INTEGER"
+ + ");";
+
+
+ public static void onCreate(SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE_STATEMENT);
+ }
+
+ public static void onUpdate(SQLiteDatabase db, int from, int to) {
+ if(from == to) {
+ return;
+ }
+ if(from < 4) {
+ // doesn't exist yet
+ from++;
+ onUpdate(db, from, to);
+ return;
+ }
+ if(from == 4) {
+ // table added in version 5
+ onCreate(db);
+ }
+ }
+ }
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/category/CategoryContentProvider.java b/commons/src/main/java/org/wikimedia/commons/category/CategoryContentProvider.java
new file mode 100644
index 000000000..fa1f86c36
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/category/CategoryContentProvider.java
@@ -0,0 +1,157 @@
+package org.wikimedia.commons.category;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import org.wikimedia.commons.CommonsApplication;
+import org.wikimedia.commons.data.DBOpenHelper;
+
+public class CategoryContentProvider extends ContentProvider {
+
+ // For URI matcher
+ private static final int CATEGORIES = 1;
+ private static final int CATEGORIES_ID = 2;
+
+ public static final String AUTHORITY = "org.wikimedia.commons.categories.contentprovider";
+ private static final String BASE_PATH = "categories";
+
+ public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
+
+ private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ static {
+ uriMatcher.addURI(AUTHORITY, BASE_PATH, CATEGORIES);
+ uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID);
+ }
+
+
+ public static Uri uriForId(int id) {
+ return Uri.parse(BASE_URI.toString() + "/" + id);
+ }
+
+ private DBOpenHelper dbOpenHelper;
+ @Override
+ public boolean onCreate() {
+ dbOpenHelper = ((CommonsApplication)this.getContext().getApplicationContext()).getDbOpenHelper();
+ return false;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
+ queryBuilder.setTables(Category.Table.TABLE_NAME);
+
+ int uriType = uriMatcher.match(uri);
+
+ SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
+ Cursor cursor;
+
+ switch(uriType) {
+ case CATEGORIES:
+ cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
+ break;
+ case CATEGORIES_ID:
+ cursor = queryBuilder.query(db,
+ Category.Table.ALL_FIELDS,
+ "_id = ?",
+ new String[] { uri.getLastPathSegment() },
+ null,
+ null,
+ sortOrder
+ );
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URI" + uri);
+ }
+
+ cursor.setNotificationUri(getContext().getContentResolver(), uri);
+
+ return cursor;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues contentValues) {
+ int uriType = uriMatcher.match(uri);
+ SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
+ long id = 0;
+ switch (uriType) {
+ case CATEGORIES:
+ id = sqlDB.insert(Category.Table.TABLE_NAME, null, contentValues);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ getContext().getContentResolver().notifyChange(uri, null);
+ return Uri.parse(BASE_URI + "/" + id);
+ }
+
+ @Override
+ public int delete(Uri uri, String s, String[] strings) {
+ return 0;
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ Log.d("Commons", "Hello, bulk insert!");
+ int uriType = uriMatcher.match(uri);
+ SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
+ sqlDB.beginTransaction();
+ switch (uriType) {
+ case CATEGORIES:
+ for(ContentValues value: values) {
+ Log.d("Commons", "Inserting! " + value.toString());
+ sqlDB.insert(Category.Table.TABLE_NAME, null, value);
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ sqlDB.setTransactionSuccessful();
+ sqlDB.endTransaction();
+ getContext().getContentResolver().notifyChange(uri, null);
+ return values.length;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
+ /*
+ SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
+ Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues
+ should be fine. So only issues are those that pass in via concating.
+
+ In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise.
+ */
+ int uriType = uriMatcher.match(uri);
+ SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
+ int rowsUpdated = 0;
+ switch (uriType) {
+ case CATEGORIES_ID:
+ int id = Integer.valueOf(uri.getLastPathSegment());
+
+ if (TextUtils.isEmpty(selection)) {
+ rowsUpdated = sqlDB.update(Category.Table.TABLE_NAME,
+ contentValues,
+ Category.Table.COLUMN_ID + " = ?",
+ new String[] { String.valueOf(id) } );
+ } else {
+ throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
+ }
+ getContext().getContentResolver().notifyChange(uri, null);
+ return rowsUpdated;
+ }
+}
+
diff --git a/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java b/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java
index 81892ac48..ece2719b6 100644
--- a/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java
+++ b/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java
@@ -302,6 +302,11 @@ public class Contribution extends Media {
from++;
return;
}
+ if(from == 4) {
+ // Do nothing -- added Category
+ from++;
+ return;
+ }
}
}
}
diff --git a/commons/src/main/java/org/wikimedia/commons/data/DBOpenHelper.java b/commons/src/main/java/org/wikimedia/commons/data/DBOpenHelper.java
index 011baa429..4430ac59a 100644
--- a/commons/src/main/java/org/wikimedia/commons/data/DBOpenHelper.java
+++ b/commons/src/main/java/org/wikimedia/commons/data/DBOpenHelper.java
@@ -3,13 +3,14 @@ package org.wikimedia.commons.data;
import android.content.*;
import android.database.sqlite.*;
+import org.wikimedia.commons.category.Category;
import org.wikimedia.commons.contributions.*;
import org.wikimedia.commons.modifications.ModifierSequence;
public class DBOpenHelper extends SQLiteOpenHelper{
private static final String DATABASE_NAME = "commons.db";
- private static final int DATABASE_VERSION = 4;
+ private static final int DATABASE_VERSION = 5;
public DBOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
@@ -19,11 +20,13 @@ public class DBOpenHelper extends SQLiteOpenHelper{
public void onCreate(SQLiteDatabase sqLiteDatabase) {
Contribution.Table.onCreate(sqLiteDatabase);
ModifierSequence.Table.onCreate(sqLiteDatabase);
+ Category.Table.onCreate(sqLiteDatabase);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
Contribution.Table.onUpdate(sqLiteDatabase, from, to);
ModifierSequence.Table.onUpdate(sqLiteDatabase, from, to);
+ Category.Table.onUpdate(sqLiteDatabase, from, to);
}
}