diff --git a/commons/AndroidManifest.xml b/commons/AndroidManifest.xml
index c60f0119f..daa741489 100644
--- a/commons/AndroidManifest.xml
+++ b/commons/AndroidManifest.xml
@@ -33,6 +33,9 @@
android:name=".auth.LoginActivity"
android:theme="@style/NoTitle" >
+
+ android:resource="@xml/contributions_sync_adapter" />
+
+
+
+
+
+
+
+
+
diff --git a/commons/res/layout/activity_post_upload.xml b/commons/res/layout/activity_post_upload.xml
new file mode 100644
index 000000000..752ded6d2
--- /dev/null
+++ b/commons/res/layout/activity_post_upload.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/commons/res/values/strings.xml b/commons/res/values/strings.xml
index 2835964c5..fdeb21454 100644
--- a/commons/res/values/strings.xml
+++ b/commons/res/values/strings.xml
@@ -51,6 +51,7 @@
Upload
Name this set
+ Modifications
- No uploads yet
diff --git a/commons/res/xml/syncadapter.xml b/commons/res/xml/contributions_sync_adapter.xml
similarity index 100%
rename from commons/res/xml/syncadapter.xml
rename to commons/res/xml/contributions_sync_adapter.xml
diff --git a/commons/res/xml/modifications_sync_adapter.xml b/commons/res/xml/modifications_sync_adapter.xml
new file mode 100644
index 000000000..e85d9a6d4
--- /dev/null
+++ b/commons/res/xml/modifications_sync_adapter.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/commons/src/main/java/org/wikimedia/commons/ShareActivity.java b/commons/src/main/java/org/wikimedia/commons/ShareActivity.java
index 14c009026..c0b4e2d09 100644
--- a/commons/src/main/java/org/wikimedia/commons/ShareActivity.java
+++ b/commons/src/main/java/org/wikimedia/commons/ShareActivity.java
@@ -14,6 +14,7 @@ import android.view.*;
import org.wikimedia.commons.contributions.*;
import org.wikimedia.commons.auth.*;
+import org.wikimedia.commons.modifications.PostUploadActivity;
public class ShareActivity extends AuthenticatedActivity {
@@ -64,11 +65,13 @@ public class ShareActivity extends AuthenticatedActivity {
@Override
protected void onPostExecute(Contribution contribution) {
super.onPostExecute(contribution);
+ Intent postUploadIntent = new Intent(ShareActivity.this, PostUploadActivity.class);
+ postUploadIntent.putExtra(PostUploadActivity.EXTRA_MEDIA_URI, contribution.getContentUri());
+ startActivity(postUploadIntent);
finish();
}
}
-
@Override
public void onBackPressed() {
super.onBackPressed();
diff --git a/commons/src/main/java/org/wikimedia/commons/auth/LoginActivity.java b/commons/src/main/java/org/wikimedia/commons/auth/LoginActivity.java
index 1455e0c08..7b1ac8553 100644
--- a/commons/src/main/java/org/wikimedia/commons/auth/LoginActivity.java
+++ b/commons/src/main/java/org/wikimedia/commons/auth/LoginActivity.java
@@ -16,6 +16,7 @@ import android.support.v4.app.NavUtils;
import org.wikimedia.commons.*;
import org.wikimedia.commons.EventLog;
import org.wikimedia.commons.contributions.*;
+import org.wikimedia.commons.modifications.ModificationsContentProvider;
public class LoginActivity extends AccountAuthenticatorActivity {
@@ -64,6 +65,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
// FIXME: If the user turns it off, it shouldn't be auto turned back on
ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default!
+ ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
context.finish();
} else {
int response;
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 71a70f1d2..20240a6c0 100644
--- a/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java
+++ b/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java
@@ -297,8 +297,11 @@ public class Contribution extends Media {
onUpdate(db, from, to);
return;
}
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
- onCreate(db);
+ if(from == 3) {
+ // Do nothing
+ from++;
+ return;
+ }
}
}
}
diff --git a/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsContentProvider.java b/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsContentProvider.java
index 438990fda..acb7c62a8 100644
--- a/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsContentProvider.java
+++ b/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsContentProvider.java
@@ -45,16 +45,27 @@ public class ContributionsContentProvider extends ContentProvider{
int uriType = uriMatcher.match(uri);
+ SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
+ Cursor cursor;
+
switch(uriType) {
case CONTRIBUTIONS:
+ cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
+ break;
+ case CONTRIBUTIONS_ID:
+ cursor = queryBuilder.query(db,
+ Contribution.Table.ALL_FIELDS,
+ "_id = ?",
+ new String[] { uri.getLastPathSegment() },
+ null,
+ null,
+ sortOrder
+ );
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
- SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
-
- Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
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 e328a31d9..011baa429 100644
--- a/commons/src/main/java/org/wikimedia/commons/data/DBOpenHelper.java
+++ b/commons/src/main/java/org/wikimedia/commons/data/DBOpenHelper.java
@@ -4,11 +4,12 @@ import android.content.*;
import android.database.sqlite.*;
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 = 3;
+ private static final int DATABASE_VERSION = 4;
public DBOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
@@ -17,10 +18,12 @@ public class DBOpenHelper extends SQLiteOpenHelper{
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
Contribution.Table.onCreate(sqLiteDatabase);
+ ModifierSequence.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);
}
}
diff --git a/commons/src/main/java/org/wikimedia/commons/modifications/CategoryModifier.java b/commons/src/main/java/org/wikimedia/commons/modifications/CategoryModifier.java
new file mode 100644
index 000000000..00838ade3
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/modifications/CategoryModifier.java
@@ -0,0 +1,49 @@
+package org.wikimedia.commons.modifications;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.text.TextUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.*;
+
+public class CategoryModifier extends PageModifier {
+
+
+ public static String PARAM_CATEGORIES = "categories";
+
+ public static String MODIFIER_NAME = "CategoriesModifier";
+
+ public CategoryModifier(String... categories) {
+ super(MODIFIER_NAME);
+ JSONArray categoriesArray = new JSONArray();
+ for(String category: categories) {
+ categoriesArray.put(category);
+ }
+ try {
+ params.putOpt(PARAM_CATEGORIES, categoriesArray);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public CategoryModifier(JSONObject data) {
+ super(MODIFIER_NAME);
+ this.params = data;
+ }
+
+ @Override
+ public String doModification(String pageName, String pageContents) {
+ JSONArray categories;
+ categories = params.optJSONArray(PARAM_CATEGORIES);
+
+ StringBuffer categoriesString = new StringBuffer();
+ for(int i=0; i < categories.length(); i++) {
+ String category = categories.optString(i);
+ categoriesString.append("\n[[Category:").append(category).append("]]");
+ }
+ return pageContents + categoriesString.toString();
+ }
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/modifications/ModificationsContentProvider.java b/commons/src/main/java/org/wikimedia/commons/modifications/ModificationsContentProvider.java
new file mode 100644
index 000000000..dc7beff75
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/modifications/ModificationsContentProvider.java
@@ -0,0 +1,160 @@
+package org.wikimedia.commons.modifications;
+
+import android.content.*;
+import android.database.*;
+import android.database.sqlite.*;
+import android.net.*;
+import android.text.*;
+import android.util.*;
+
+import org.wikimedia.commons.*;
+import org.wikimedia.commons.data.*;
+
+public class ModificationsContentProvider extends ContentProvider{
+
+ private static final int MODIFICATIONS = 1;
+ private static final int MODIFICATIONS_ID = 2;
+
+ public static final String AUTHORITY = "org.wikimedia.commons.modifications.contentprovider";
+ private static final String BASE_PATH = "modifications";
+
+ 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, MODIFICATIONS);
+ uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_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(ModifierSequence.Table.TABLE_NAME);
+
+ int uriType = uriMatcher.match(uri);
+
+ switch(uriType) {
+ case MODIFICATIONS:
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URI" + uri);
+ }
+
+ SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
+
+ Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
+ 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 MODIFICATIONS:
+ id = sqlDB.insert(ModifierSequence.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) {
+ int uriType = uriMatcher.match(uri);
+ SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
+ switch (uriType) {
+ case MODIFICATIONS_ID:
+ String id = uri.getLastPathSegment();
+ sqlDB.delete(ModifierSequence.Table.TABLE_NAME,
+ "_id = ?",
+ new String[] { id }
+ );
+ return 1;
+ default:
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ }
+
+ @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 MODIFICATIONS:
+ for(ContentValues value: values) {
+ Log.d("Commons", "Inserting! " + value.toString());
+ sqlDB.insert(ModifierSequence.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 MODIFICATIONS:
+ rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
+ contentValues,
+ selection,
+ selectionArgs);
+ break;
+ case MODIFICATIONS_ID:
+ int id = Integer.valueOf(uri.getLastPathSegment());
+
+ if (TextUtils.isEmpty(selection)) {
+ rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
+ contentValues,
+ ModifierSequence.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/modifications/ModificationsSyncAdapter.java b/commons/src/main/java/org/wikimedia/commons/modifications/ModificationsSyncAdapter.java
new file mode 100644
index 000000000..12b130398
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/modifications/ModificationsSyncAdapter.java
@@ -0,0 +1,138 @@
+package org.wikimedia.commons.modifications;
+
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.*;
+import android.database.Cursor;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+import android.accounts.Account;
+import android.os.Bundle;
+
+import java.io.*;
+import java.util.*;
+
+import org.mediawiki.api.*;
+import org.wikimedia.commons.Utils;
+import org.wikimedia.commons.*;
+import org.wikimedia.commons.contributions.Contribution;
+import org.wikimedia.commons.contributions.ContributionsContentProvider;
+
+
+public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
+
+ public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ }
+
+ @Override
+ public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
+ // This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
+
+ Cursor allModifications;
+ try {
+ allModifications = contentProviderClient.query(ModificationsContentProvider.BASE_URI, null, null, null, null);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+
+ // Exit early if nothing to do
+ if(allModifications == null || allModifications.getCount() == 0) {
+ Log.d("Commons", "No modifications to perform");
+ return;
+ }
+
+ CommonsApplication app = (CommonsApplication)getContext().getApplicationContext();
+ String authCookie;
+ try {
+ authCookie = AccountManager.get(app).blockingGetAuthToken(account, "", false);
+ } catch (OperationCanceledException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } catch (AuthenticatorException e) {
+ throw new RuntimeException(e);
+ }
+
+ MWApi api = app.getApi();
+ api.setAuthCookie(authCookie);
+ String editToken;
+
+ ApiResult requestResult, responseResult;
+ try {
+ editToken = api.getEditToken();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+
+ allModifications.moveToFirst();
+
+ Log.d("Commons", "Found " + allModifications.getCount() + " modifications to execute");
+
+ ContentProviderClient contributionsClient = null;
+ try {
+ contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
+
+ while(!allModifications.isAfterLast()) {
+ ModifierSequence sequence = ModifierSequence.fromCursor(allModifications);
+ sequence.setContentProviderClient(contentProviderClient);
+ Contribution contrib;
+
+ Cursor contributionCursor;
+ try {
+ contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ contributionCursor.moveToFirst();
+ contrib = Contribution.fromCursor(contributionCursor);
+
+ if(contrib.getState() == Contribution.STATE_COMPLETED) {
+
+ try {
+ requestResult = api.action("query")
+ .param("prop", "revisions")
+ .param("rvprop", "timestamp|content")
+ .param("titles", contrib.getFilename())
+ .get();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ Log.d("Commons", "Page content is " + Utils.getStringFromDOM(requestResult.getDocument()));
+ String pageContent = requestResult.getString("/api/query/pages/page/revisions/rev");
+ String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent);
+
+ try {
+ responseResult = api.action("edit")
+ .param("title", contrib.getFilename())
+ .param("token", editToken)
+ .param("text", processedPageContent)
+ .post();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ Log.d("Commons", "Response is" + Utils.getStringFromDOM(responseResult.getDocument()));
+
+ String result = responseResult.getString("/api/edit/@result");
+ if(!result.equals("Success")) {
+ throw new RuntimeException();
+ }
+
+ sequence.delete();
+ allModifications.moveToNext();
+ }
+
+ }
+ } finally {
+ if(contributionsClient != null) {
+ contributionsClient.release();
+ }
+
+ }
+ }
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/modifications/ModificationsSyncService.java b/commons/src/main/java/org/wikimedia/commons/modifications/ModificationsSyncService.java
new file mode 100644
index 000000000..86a80fa94
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/modifications/ModificationsSyncService.java
@@ -0,0 +1,26 @@
+package org.wikimedia.commons.modifications;
+
+import android.app.*;
+import android.content.*;
+import android.os.*;
+
+public class ModificationsSyncService extends Service {
+
+ private static final Object sSyncAdapterLock = new Object();
+
+ private static ModificationsSyncAdapter sSyncAdapter = null;
+
+ @Override
+ public void onCreate() {
+ synchronized (sSyncAdapterLock) {
+ if (sSyncAdapter == null) {
+ sSyncAdapter = new ModificationsSyncAdapter(getApplicationContext(), true);
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return sSyncAdapter.getSyncAdapterBinder();
+ }
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/modifications/ModifierSequence.java b/commons/src/main/java/org/wikimedia/commons/modifications/ModifierSequence.java
new file mode 100644
index 000000000..c58330185
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/modifications/ModifierSequence.java
@@ -0,0 +1,142 @@
+package org.wikimedia.commons.modifications;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wikimedia.commons.contributions.ContributionsContentProvider;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+
+public class ModifierSequence {
+ private Uri mediaUri;
+ private ArrayList modifiers;
+ private Uri contentUri;
+ private ContentProviderClient client;
+
+ public ModifierSequence(Uri mediaUri) {
+ this.mediaUri = mediaUri;
+ modifiers = new ArrayList();
+ }
+
+ public ModifierSequence(Uri mediaUri, JSONObject data) {
+ this(mediaUri);
+ JSONArray modifiersJSON = data.optJSONArray("modifiers");
+ for(int i=0; i< modifiersJSON.length(); i++) {
+ modifiers.add(PageModifier.fromJSON(modifiersJSON.optJSONObject(i)));
+ }
+ }
+
+ public Uri getMediaUri() {
+ return mediaUri;
+ }
+
+ public void queueModifier(PageModifier modifier) {
+ modifiers.add(modifier);
+ }
+
+ public String executeModifications(String pageName, String pageContents) {
+ for(PageModifier modifier: modifiers) {
+ pageContents = modifier.doModification(pageName, pageContents);
+ }
+ return pageContents;
+ }
+
+ public JSONObject toJSON() {
+ JSONObject data = new JSONObject();
+ try {
+ JSONArray modifiersJSON = new JSONArray();
+ for(PageModifier modifier: modifiers) {
+ modifiersJSON.put(modifier.toJSON());
+ }
+ data.put("modifiers", modifiersJSON);
+ return data;
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public ContentValues toContentValues() {
+ ContentValues cv = new ContentValues();
+ cv.put(Table.COLUMN_MEDIA_URI, mediaUri.toString());
+ cv.put(Table.COLUMN_DATA, toJSON().toString());
+ return cv;
+ }
+
+ public static ModifierSequence fromCursor(Cursor cursor) {
+ // Hardcoding column positions!
+ ModifierSequence ms = null;
+ try {
+ ms = new ModifierSequence(Uri.parse(cursor.getString(1)), new JSONObject(cursor.getString(2)));
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ ms.contentUri = ModificationsContentProvider.uriForId(cursor.getInt(0));
+
+ return ms;
+ }
+
+ public void save() {
+ try {
+ if(contentUri == null) {
+ contentUri = client.insert(ModificationsContentProvider.BASE_URI, this.toContentValues());
+ } else {
+ client.update(contentUri, toContentValues(), null, null);
+ }
+ } catch(RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void delete() {
+ try {
+ client.delete(contentUri, null, null);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void setContentProviderClient(ContentProviderClient client) {
+ this.client = client;
+ }
+
+ public static class Table {
+ public static final String TABLE_NAME = "modifications";
+
+ public static final String COLUMN_ID = "_id";
+ public static final String COLUMN_MEDIA_URI = "mediauri";
+ public static final String COLUMN_DATA = "data";
+
+ // 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_MEDIA_URI,
+ COLUMN_DATA
+ };
+
+
+ private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ + "_id INTEGER PRIMARY KEY,"
+ + "mediauri STRING,"
+ + "data STRING"
+ + ");";
+
+
+ public static void onCreate(SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE_STATEMENT);
+ }
+
+ public static void onUpdate(SQLiteDatabase db, int from, int to) {
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
+ onCreate(db);
+ }
+ }
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/modifications/PageModifier.java b/commons/src/main/java/org/wikimedia/commons/modifications/PageModifier.java
new file mode 100644
index 000000000..fe09307a9
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/modifications/PageModifier.java
@@ -0,0 +1,37 @@
+package org.wikimedia.commons.modifications;
+
+import android.os.Bundle;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public abstract class PageModifier {
+
+ public static PageModifier fromJSON(JSONObject data) {
+ String name = data.optString("name");
+ if(name.equals(CategoryModifier.MODIFIER_NAME)) {
+ return new CategoryModifier(data.optJSONObject("data"));
+ }
+ return null;
+ }
+
+ protected String name;
+ protected JSONObject params;
+
+ protected PageModifier(String name) {
+ this.name = name;
+ params = new JSONObject();
+ }
+
+ public abstract String doModification(String pageName, String pageContents);
+
+ public JSONObject toJSON() {
+ JSONObject data = new JSONObject();
+ try {
+ data.putOpt("name", name);
+ data.put("data", params);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ return data;
+ }
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/modifications/PostUploadActivity.java b/commons/src/main/java/org/wikimedia/commons/modifications/PostUploadActivity.java
new file mode 100644
index 000000000..0ff97a6b4
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/modifications/PostUploadActivity.java
@@ -0,0 +1,28 @@
+package org.wikimedia.commons.modifications;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import org.wikimedia.commons.HandlerService;
+import org.wikimedia.commons.R;
+import org.wikimedia.commons.UploadService;
+
+public class PostUploadActivity extends Activity {
+ public static String EXTRA_MEDIA_URI = "org.wikimedia.commons.modifications.PostUploadActivity.mediauri";
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_post_upload);
+ Uri mediaUri = getIntent().getParcelableExtra(EXTRA_MEDIA_URI);
+ ModifierSequence testSequence = new ModifierSequence(mediaUri);
+ testSequence.queueModifier(new CategoryModifier("Hello, World!"));
+ testSequence.setContentProviderClient(getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY));
+ testSequence.save();
+
+ }
+}
\ No newline at end of file