diff --git a/commons/AndroidManifest.xml b/commons/AndroidManifest.xml
index 6c3bc6a5c..5ad1c7700 100644
--- a/commons/AndroidManifest.xml
+++ b/commons/AndroidManifest.xml
@@ -82,6 +82,15 @@
android:label="@string/title_activity_settings"
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -127,6 +147,13 @@
android:authorities="org.wikimedia.commons.contributions.contentprovider"
android:exported="false">
+
+
com.actionbarsherlock
actionbarsherlock
- 4.2.0
+ 4.4.0
apklib
-
-
- com.google.android
- support-v4
-
-
android
diff --git a/commons/res/layout/activity_campaigns.xml b/commons/res/layout/activity_campaigns.xml
new file mode 100644
index 000000000..cfac7c79b
--- /dev/null
+++ b/commons/res/layout/activity_campaigns.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/commons/res/layout/fragment_campaigns.xml b/commons/res/layout/fragment_campaigns.xml
new file mode 100644
index 000000000..3ff614f0e
--- /dev/null
+++ b/commons/res/layout/fragment_campaigns.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/commons/res/layout/layout_campaign_item.xml b/commons/res/layout/layout_campaign_item.xml
new file mode 100644
index 000000000..79c0355dd
--- /dev/null
+++ b/commons/res/layout/layout_campaign_item.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/commons/res/values/strings.xml b/commons/res/values/strings.xml
index 5a5c1d197..8aa63f709 100644
--- a/commons/res/values/strings.xml
+++ b/commons/res/values/strings.xml
@@ -138,4 +138,5 @@
Categories
Loading...
None selected
+ Campaigns
diff --git a/commons/res/xml/campaigns_sync_adapter.xml b/commons/res/xml/campaigns_sync_adapter.xml
new file mode 100644
index 000000000..778984c07
--- /dev/null
+++ b/commons/res/xml/campaigns_sync_adapter.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/commons/src/main/java/org/wikimedia/commons/Media.java b/commons/src/main/java/org/wikimedia/commons/Media.java
index 2d33c76ff..e09e00f86 100644
--- a/commons/src/main/java/org/wikimedia/commons/Media.java
+++ b/commons/src/main/java/org/wikimedia/commons/Media.java
@@ -59,6 +59,9 @@ public class Media implements Parcelable {
}
public String getImageUrl() {
+ if(imageUrl == null) {
+ imageUrl = Utils.makeThumbBaseUrl(this.getFilename());
+ }
return imageUrl;
}
@@ -103,7 +106,7 @@ public class Media implements Parcelable {
}
public String getThumbnailUrl(int width) {
- return Utils.makeThumbUrl(imageUrl, filename, width);
+ return Utils.makeThumbUrl(getImageUrl(), getFilename(), width);
}
public int getWidth() {
@@ -182,6 +185,10 @@ public class Media implements Parcelable {
}
}
+ public Media(String filename) {
+ this.filename = filename;
+ }
+
public Media(Uri localUri, String imageUrl, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator) {
this();
this.localUri = localUri;
diff --git a/commons/src/main/java/org/wikimedia/commons/MediaWikiImageView.java b/commons/src/main/java/org/wikimedia/commons/MediaWikiImageView.java
index 57bfd4399..1cdc61aac 100644
--- a/commons/src/main/java/org/wikimedia/commons/MediaWikiImageView.java
+++ b/commons/src/main/java/org/wikimedia/commons/MediaWikiImageView.java
@@ -182,7 +182,7 @@ public class MediaWikiImageView extends ImageView {
if (response.getBitmap() != null) {
setImageBitmap(response.getBitmap());
- if(tryOriginal && mMedia instanceof Contribution && response.getBitmap().getWidth() > mMedia.getWidth() || response.getBitmap().getHeight() > mMedia.getHeight()) {
+ if(tryOriginal && mMedia instanceof Contribution && (response.getBitmap().getWidth() > mMedia.getWidth() || response.getBitmap().getHeight() > mMedia.getHeight())) {
// If there is no width information for this image, save it. This speeds up image loading massively for smaller images
mMedia.setHeight(response.getBitmap().getHeight());
mMedia.setWidth(response.getBitmap().getWidth());
diff --git a/commons/src/main/java/org/wikimedia/commons/Utils.java b/commons/src/main/java/org/wikimedia/commons/Utils.java
index 54e5f3013..8f30af191 100644
--- a/commons/src/main/java/org/wikimedia/commons/Utils.java
+++ b/commons/src/main/java/org/wikimedia/commons/Utils.java
@@ -2,6 +2,7 @@ package org.wikimedia.commons;
import android.net.Uri;
import android.os.*;
+import android.util.Log;
import com.nostra13.universalimageloader.core.*;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
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 d4730d7b0..573c0dfe9 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.campaigns.CampaignsContentProvider;
import org.wikimedia.commons.contributions.*;
import org.wikimedia.commons.modifications.ModificationsContentProvider;
@@ -67,6 +68,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!
+ ContentResolver.setSyncAutomatically(account, CampaignsContentProvider.AUTHORITY, true); // Enable sync by default!
context.finish();
} else {
int response;
diff --git a/commons/src/main/java/org/wikimedia/commons/campaigns/Campaign.java b/commons/src/main/java/org/wikimedia/commons/campaigns/Campaign.java
new file mode 100644
index 000000000..61b796eab
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/campaigns/Campaign.java
@@ -0,0 +1,190 @@
+package org.wikimedia.commons.campaigns;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+
+// FIXME: Implement Parcelable
+public class Campaign implements Serializable {
+ private boolean enabled;
+
+ private String autoAddWikitext;
+ private ArrayList autoAddCategories;
+
+ private String name;
+ private String ownWorkLicenseDefault;
+
+ private String defaultDescription;
+
+ private JSONObject config;
+ private String body;
+ private boolean isParsed;
+ private String trackingCategory;
+ private String description;
+ private String title;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public String getAutoAddWikitext() {
+ if(!this.isParsed) {
+ this.parseConfig();
+ }
+ return autoAddWikitext;
+ }
+
+ public ArrayList getAutoAddCategories() {
+ if(!this.isParsed) {
+ this.parseConfig();
+ }
+ return autoAddCategories;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getOwnWorkLicenseDefault() {
+ if(!this.isParsed) {
+ this.parseConfig();
+ }
+ return ownWorkLicenseDefault;
+ }
+
+ public String getDefaultDescription() {
+ if(!this.isParsed) {
+ this.parseConfig();
+ }
+ return defaultDescription;
+ }
+
+ public JSONObject getConfig() {
+ if(!this.isParsed) {
+ this.parseConfig();
+ }
+ return config;
+ }
+
+ private void parseConfig() {
+ try {
+ this.config = new JSONObject(body);
+ } catch (JSONException e) {
+ throw new RuntimeException(e); // because what else are you gonna do?
+ }
+ if(config.has("autoAdd")) {
+ this.autoAddWikitext = config.optJSONObject("autoAdd").optString("wikitext", null);
+ if(config.optJSONObject("autoAdd").has("categories")) {
+ this.autoAddCategories = new ArrayList();
+ JSONArray catsArray = config.optJSONObject("autoAdd").optJSONArray("categories");
+ for(int i=0; i < catsArray.length(); i++) {
+ autoAddCategories.add(catsArray.optString(i));
+ }
+ }
+ }
+ this.title = config.optString("title", name);
+ this.description = config.optString("description", "");
+ this.isParsed = true;
+ }
+ private Campaign(String name, String body, String trackingCategory) {
+ this.name = name;
+ this.body = body;
+ this.trackingCategory = trackingCategory;
+ }
+
+ public ContentValues toContentValues() {
+ ContentValues cv = new ContentValues();
+ cv.put(Table.COLUMN_NAME, this.getName());
+ cv.put(Table.COLUMN_ENABLED, this.isEnabled() ? 1 : 0);
+ cv.put(Table.COLUMN_TITLE, this.getTitle());
+ cv.put(Table.COLUMN_DESCRIPTION, this.getDescription());
+ cv.put(Table.COLUMN_TRACKING_CATEGORY, this.getTrackingCategory());
+ cv.put(Table.COLUMN_BODY, this.body);
+ return cv;
+ }
+
+ public static Campaign parse(String name, String body, String trackingCategory) {
+ Campaign c = new Campaign(name, body, trackingCategory);
+ c.parseConfig();
+ return c;
+ }
+
+ public static Campaign fromCursor(Cursor cursor) {
+ String name = cursor.getString(1);
+ Boolean enabled = cursor.getInt(2) == 1;
+ String title = cursor.getString(3);
+ String description = cursor.getString(4);
+ String trackingCategory = cursor.getString(5);
+ String body = cursor.getString(6);
+ Campaign c = new Campaign(name, body, trackingCategory);
+ c.title = title;
+ c.description = description;
+ c.enabled = enabled;
+ return c;
+ }
+
+ public String getTrackingCategory() {
+ return trackingCategory;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public static class Table {
+ public static final String TABLE_NAME = "campaigns";
+
+ public static final String COLUMN_ID = "_id";
+ public static final String COLUMN_NAME = "name";
+ public static final String COLUMN_ENABLED = "enabled";
+ public static final String COLUMN_TITLE = "title";
+ public static final String COLUMN_DESCRIPTION = "description";
+ public static final String COLUMN_TRACKING_CATEGORY = "tracking_category";
+ public static final String COLUMN_BODY = "body";
+
+ // 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_ENABLED,
+ COLUMN_TITLE,
+ COLUMN_DESCRIPTION,
+ COLUMN_TRACKING_CATEGORY,
+ COLUMN_BODY
+ };
+
+
+ private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ + "_id INTEGER PRIMARY KEY,"
+ + "name STRING,"
+ + "enabled INTEGER,"
+ + "title STRING,"
+ + "description STRING,"
+ + "tracking_category STRING,"
+ + "body STRING"
+ + ");";
+
+
+ public static void onCreate(SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE_STATEMENT);
+ }
+
+ public static void onUpdate(SQLiteDatabase db, int from, int to) {
+ if(to <= 6) {
+ onCreate(db);
+ return;
+ }
+ return;
+ }
+ }
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignActivity.java b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignActivity.java
new file mode 100644
index 000000000..cfcf5f413
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignActivity.java
@@ -0,0 +1,60 @@
+package org.wikimedia.commons.campaigns;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import org.wikimedia.commons.CommonsApplication;
+import org.wikimedia.commons.R;
+import org.wikimedia.commons.contributions.ContributionsActivity;
+
+public class CampaignActivity
+ extends SherlockFragmentActivity
+ implements LoaderManager.LoaderCallbacks {
+
+ private ListView campaignsListView;
+ private CampaignsListAdapter campaignsListAdapter;
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_campaigns);
+
+ ContentResolver.setSyncAutomatically(((CommonsApplication)getApplicationContext()).getCurrentAccount(), CampaignsContentProvider.AUTHORITY, true); // Enable sync by default!
+ campaignsListView = (ListView) findViewById(R.id.campaignsList);
+
+ campaignsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
+ Campaign c = Campaign.fromCursor((Cursor) adapterView.getItemAtPosition(i));
+ Intent intent = new Intent(CampaignActivity.this, ContributionsActivity.class);
+ intent.putExtra("campaign", c);
+ startActivity(intent);
+ }
+ });
+ getSupportLoaderManager().initLoader(0, null, this);
+ }
+
+ public Loader onCreateLoader(int i, Bundle bundle) {
+ return new CursorLoader(this, CampaignsContentProvider.BASE_URI, Campaign.Table.ALL_FIELDS, "", null, "");
+ }
+
+ public void onLoadFinished(Loader cursorLoader, Cursor cursor) {
+ if(campaignsListAdapter == null) {
+ campaignsListAdapter = new CampaignsListAdapter(this, cursor, 0);
+ campaignsListView.setAdapter(campaignsListAdapter);
+ } else {
+ campaignsListAdapter.swapCursor(cursor);
+ }
+ }
+
+ public void onLoaderReset(Loader cursorLoader) {
+ campaignsListAdapter.swapCursor(null);
+ }
+}
\ No newline at end of file
diff --git a/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignContribution.java b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignContribution.java
new file mode 100644
index 000000000..a105c0e62
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignContribution.java
@@ -0,0 +1,49 @@
+package org.wikimedia.commons.campaigns;
+
+import android.net.Uri;
+import org.wikimedia.commons.contributions.Contribution;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+public class CampaignContribution extends Contribution {
+ private Campaign campaign;
+
+ private ArrayList fieldValues;
+
+
+ public CampaignContribution(Uri localUri, String remoteUri, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary, Campaign campaign) {
+ super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator, editSummary);
+ this.campaign = campaign;
+ }
+
+ public Campaign getCampaign() {
+ return campaign;
+ }
+
+ public void setCampaign(Campaign campaign) {
+ this.campaign = campaign;
+ }
+
+ @Override
+ public String getTrackingTemplates() {
+ StringBuffer buffer = new StringBuffer();
+ if(campaign.getAutoAddWikitext() != null) {
+ buffer.append(campaign.getAutoAddWikitext()).append("\n");
+ }
+ if(campaign.getAutoAddCategories() != null && campaign.getAutoAddCategories().size() != 0) {
+ for(String cat : campaign.getAutoAddCategories()) {
+ buffer.append("[[Category:").append(cat).append("]]").append("\n");
+ }
+ } else {
+ buffer.append("{{subst:unc}}\n");
+ }
+ buffer.append("[[Category:").append(campaign.getTrackingCategory()).append("\n");
+ return buffer.toString();
+ }
+
+ @Override
+ public String getDescription() {
+ return super.getDescription();
+ }
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignsContentProvider.java b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignsContentProvider.java
new file mode 100644
index 000000000..80bd972bc
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignsContentProvider.java
@@ -0,0 +1,206 @@
+package org.wikimedia.commons.campaigns;
+
+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 CampaignsContentProvider extends ContentProvider{
+
+ private static final int CAMPAIGNS = 1;
+ private static final int CAMPAIGNS_ID = 2;
+
+ public static final String AUTHORITY = "org.wikimedia.commons.campaigns.contentprovider";
+ private static final String BASE_PATH = "campiagns";
+
+ 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, CAMPAIGNS);
+ uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CAMPAIGNS_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(Campaign.Table.TABLE_NAME);
+
+ int uriType = uriMatcher.match(uri);
+
+ SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
+ Cursor cursor;
+
+ switch(uriType) {
+ case CAMPAIGNS:
+ cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
+ break;
+ case CAMPAIGNS_ID:
+ cursor = queryBuilder.query(db,
+ Campaign.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 CAMPAIGNS:
+ sqlDB.beginTransaction();
+ // if the campaign already exists, rip it out and then re-insert
+ if(campaignExists(sqlDB, contentValues)) {
+ sqlDB.delete(
+ Campaign.Table.TABLE_NAME,
+ Campaign.Table.COLUMN_NAME + " = ?",
+ new String[]{contentValues.getAsString(Campaign.Table.COLUMN_NAME)}
+ );
+ }
+ id = sqlDB.insert(Campaign.Table.TABLE_NAME, null, contentValues);
+ sqlDB.endTransaction();
+ 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 rows = 0;
+ int uriType = uriMatcher.match(uri);
+
+ SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
+
+ switch(uriType) {
+ case CAMPAIGNS_ID:
+ rows = db.delete(Campaign.Table.TABLE_NAME,
+ "_id = ?",
+ new String[] { uri.getLastPathSegment() }
+ );
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URI" + uri);
+ }
+ getContext().getContentResolver().notifyChange(uri, null);
+ return rows;
+ }
+
+ private boolean campaignExists(SQLiteDatabase db, ContentValues campaign) {
+ Cursor cr = db.query(
+ Campaign.Table.TABLE_NAME,
+ new String[]{Campaign.Table.COLUMN_NAME},
+ Campaign.Table.COLUMN_NAME + " = ?",
+ new String[]{campaign.getAsString(Campaign.Table.COLUMN_NAME)},
+ "", "", ""
+ );
+ return cr != null && cr.getCount() != 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 CAMPAIGNS:
+ for(ContentValues value: values) {
+ Log.d("Commons", "Inserting! " + value.toString());
+ // if the campaign already exists, rip it out and then re-insert
+ if(campaignExists(sqlDB, value)) {
+ sqlDB.delete(
+ Campaign.Table.TABLE_NAME,
+ Campaign.Table.COLUMN_NAME + " = ?",
+ new String[]{value.getAsString(Campaign.Table.COLUMN_NAME)}
+ );
+ }
+ sqlDB.insert(Campaign.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 CAMPAIGNS:
+ rowsUpdated = sqlDB.update(Campaign.Table.TABLE_NAME,
+ contentValues,
+ selection,
+ selectionArgs);
+ break;
+ case CAMPAIGNS_ID:
+ int id = Integer.valueOf(uri.getLastPathSegment());
+
+ if (TextUtils.isEmpty(selection)) {
+ rowsUpdated = sqlDB.update(Campaign.Table.TABLE_NAME,
+ contentValues,
+ Campaign.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/campaigns/CampaignsListAdapter.java b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignsListAdapter.java
new file mode 100644
index 000000000..b43d38354
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignsListAdapter.java
@@ -0,0 +1,47 @@
+package org.wikimedia.commons.campaigns;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.support.v4.widget.CursorAdapter;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import com.actionbarsherlock.app.SherlockFragment;
+import com.nostra13.universalimageloader.core.DisplayImageOptions;
+import com.nostra13.universalimageloader.core.assist.SimpleImageLoadingListener;
+import org.wikimedia.commons.CommonsApplication;
+import org.wikimedia.commons.MediaWikiImageView;
+import org.wikimedia.commons.R;
+import org.wikimedia.commons.Utils;
+import org.wikimedia.commons.campaigns.Campaign;
+
+class CampaignsListAdapter extends CursorAdapter {
+
+ private DisplayImageOptions contributionDisplayOptions = Utils.getGenericDisplayOptions().build();;
+ private Activity activity;
+
+ public CampaignsListAdapter(Activity activity, Cursor c, int flags) {
+ super(activity, c, flags);
+ this.activity = activity;
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
+ View parent = activity.getLayoutInflater().inflate(android.R.layout.simple_list_item_1, viewGroup, false);
+ return parent;
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ TextView campaignName = (TextView)view.findViewById(android.R.id.text1);
+
+ Campaign campaign = Campaign.fromCursor(cursor);
+
+ campaignName.setText(campaign.getTitle());
+ }
+
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignsSyncAdapter.java b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignsSyncAdapter.java
new file mode 100644
index 000000000..9365028dd
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignsSyncAdapter.java
@@ -0,0 +1,95 @@
+package org.wikimedia.commons.campaigns;
+
+import android.accounts.Account;
+import android.content.*;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+import org.mediawiki.api.ApiResult;
+import org.mediawiki.api.MWApi;
+import org.wikimedia.commons.CommonsApplication;
+import org.wikimedia.commons.Utils;
+import org.wikimedia.commons.contributions.Contribution;
+import org.wikimedia.commons.contributions.ContributionsContentProvider;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+
+
+public class CampaignsSyncAdapter extends AbstractThreadedSyncAdapter {
+ private static int COMMIT_THRESHOLD = 10;
+ public CampaignsSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ }
+
+ private int getLimit() {
+ return 500; // FIXME: Parameterize!
+ }
+
+ @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!
+ String user = account.name;
+ MWApi api = CommonsApplication.createMWApi();
+ ApiResult result;
+ Boolean done = false;
+ String queryContinue = null;
+ while(!done) {
+
+ try {
+ MWApi.RequestBuilder builder = api.action("query")
+ .param("list", "allcampaigns")
+ // Disabled, since we want to modify local state if the campaign was disabled
+ // FIXME: To be more effecient, delete the disabled campaigns locally
+ //.param("ucenabledonly", "true")
+ .param("uclimit", getLimit());
+ if(!TextUtils.isEmpty(queryContinue)) {
+ builder.param("uccontinue", queryContinue);
+ }
+ result = builder.get();
+ } catch (IOException e) {
+ // There isn't really much we can do, eh?
+ // FIXME: Perhaps add EventLogging?
+ syncResult.stats.numIoExceptions += 1; // Not sure if this does anything. Shitty docs
+ Log.d("Commons", "Syncing failed due to " + e.toString());
+ return;
+ }
+
+ ArrayList campaigns = result.getNodes("/api/query/allcampaigns/campaign");
+ Log.d("Commons", campaigns.size() + " results!");
+ ArrayList campaignValues = new ArrayList();
+ for(ApiResult campaignItem: campaigns) {
+ String name = campaignItem.getString("@name");
+ String body = campaignItem.getString(".");
+ Log.d("Commons", "Campaign body is " + body);
+ String trackingCat = campaignItem.getString("@trackingCategory");
+ Campaign campaign = Campaign.parse(name, body, trackingCat);
+ campaignValues.add(campaign.toContentValues());
+
+ if(campaignValues.size() % COMMIT_THRESHOLD == 0) {
+ try {
+ contentProviderClient.bulkInsert(CampaignsContentProvider.BASE_URI, campaignValues.toArray(new ContentValues[]{}));
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ campaignValues.clear();
+ }
+ }
+
+ if(campaignValues.size() != 0) {
+ try {
+ contentProviderClient.bulkInsert(CampaignsContentProvider.BASE_URI, campaignValues.toArray(new ContentValues[]{}));
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ queryContinue = result.getString("/api/query-continue/allcampaigns/@uccontinue");
+ if(TextUtils.isEmpty(queryContinue)) {
+ done = true;
+ }
+ }
+ }
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignsSyncService.java b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignsSyncService.java
new file mode 100644
index 000000000..d1975cccb
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/campaigns/CampaignsSyncService.java
@@ -0,0 +1,27 @@
+package org.wikimedia.commons.campaigns;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import org.wikimedia.commons.contributions.ContributionsSyncAdapter;
+
+public class CampaignsSyncService extends Service {
+
+ private static final Object sSyncAdapterLock = new Object();
+
+ private static CampaignsSyncAdapter sSyncAdapter = null;
+
+ @Override
+ public void onCreate() {
+ synchronized (sSyncAdapterLock) {
+ if (sSyncAdapter == null) {
+ sSyncAdapter = new CampaignsSyncAdapter(getApplicationContext(), true);
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return sSyncAdapter.getSyncAdapterBinder();
+ }
+}
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 7b0942372..55d9e1cd0 100644
--- a/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java
+++ b/commons/src/main/java/org/wikimedia/commons/contributions/Contribution.java
@@ -117,13 +117,17 @@ public class Contribution extends Media {
this.dateUploaded = date;
}
+ public String getTrackingTemplates() {
+ return "{{subst:unc}}"; // Remove when we have categorization
+ }
+
public String getPageContents() {
StringBuffer buffer = new StringBuffer();
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd");
buffer
.append("== {{int:filedesc}} ==\n")
.append("{{Information\n")
- .append("|description=").append(description).append("\n")
+ .append("|description=").append(getDescription()).append("\n")
.append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(creator).append("|").append(creator).append("]]\n");
if(dateCreated != null) {
@@ -133,9 +137,9 @@ public class Contribution extends Media {
buffer
.append("}}").append("\n")
.append("== {{int:license-header}} ==\n")
- .append(Utils.licenseTemplateFor(license)).append("\n\n")
+ .append(Utils.licenseTemplateFor(getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=").append(CommonsApplication.APPLICATION_VERSION).append("}}\n")
- .append("{{subst:unc}}"); // Remove when we have categorization
+ .append(getTrackingTemplates());
return buffer.toString();
}
diff --git a/commons/src/main/java/org/wikimedia/commons/contributions/ContributionController.java b/commons/src/main/java/org/wikimedia/commons/contributions/ContributionController.java
new file mode 100644
index 000000000..f6beac475
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/contributions/ContributionController.java
@@ -0,0 +1,96 @@
+package org.wikimedia.commons.contributions;
+
+import android.app.*;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Log;
+import com.actionbarsherlock.app.SherlockFragment;
+import org.wikimedia.commons.upload.ShareActivity;
+import org.wikimedia.commons.upload.UploadService;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Date;
+
+public class ContributionController {
+ private SherlockFragment fragment;
+ private Activity activity;
+
+ private final static int SELECT_FROM_GALLERY = 1;
+ private final static int SELECT_FROM_CAMERA = 2;
+
+ public ContributionController(SherlockFragment fragment) {
+ this.fragment = fragment;
+ this.activity = fragment.getActivity();
+ }
+
+ // See http://stackoverflow.com/a/5054673/17865 for why this is done
+ private Uri lastGeneratedCaptureURI;
+
+ private Uri reGenerateImageCaptureURI() {
+ String storageState = Environment.getExternalStorageState();
+ if(storageState.equals(Environment.MEDIA_MOUNTED)) {
+
+ String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Commons/images/" + new Date().getTime() + ".jpg";
+ File _photoFile = new File(path);
+ try {
+ if(_photoFile.exists() == false) {
+ _photoFile.getParentFile().mkdirs();
+ _photoFile.createNewFile();
+ }
+
+ } catch (IOException e) {
+ Log.e("Commons", "Could not create file: " + path, e);
+ }
+
+ return Uri.fromFile(_photoFile);
+ } else {
+ throw new RuntimeException("No external storage found!");
+ }
+ }
+
+ public void startCameraCapture() {
+ Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ lastGeneratedCaptureURI = reGenerateImageCaptureURI();
+ takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, lastGeneratedCaptureURI);
+ fragment.startActivityForResult(takePictureIntent, SELECT_FROM_CAMERA);
+ }
+
+ public void startGalleryPick() {
+ Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
+ pickImageIntent.setType("image/*");
+ fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
+ }
+
+ public void handleImagePicked(int requestCode, Intent data) {
+ Intent shareIntent = new Intent(activity, ShareActivity.class);
+ shareIntent.setAction(Intent.ACTION_SEND);
+ switch(requestCode) {
+ case SELECT_FROM_GALLERY:
+ shareIntent.setType(activity.getContentResolver().getType(data.getData()));
+ shareIntent.putExtra(Intent.EXTRA_STREAM, data.getData());
+ shareIntent.putExtra(UploadService.EXTRA_SOURCE, Contribution.SOURCE_GALLERY);
+ break;
+ case SELECT_FROM_CAMERA:
+ shareIntent.setType("image/jpeg"); //FIXME: Find out appropriate mime type
+ shareIntent.putExtra(Intent.EXTRA_STREAM, lastGeneratedCaptureURI);
+ shareIntent.putExtra(UploadService.EXTRA_SOURCE, Contribution.SOURCE_CAMERA);
+ break;
+ }
+ activity.startActivity(shareIntent);
+ }
+
+ public void saveState(Bundle outState) {
+ outState.putParcelable("lastGeneratedCaptureURI", lastGeneratedCaptureURI);
+ }
+
+ public void loadState(Bundle savedInstanceState) {
+ if(savedInstanceState != null) {
+ lastGeneratedCaptureURI = (Uri) savedInstanceState.getParcelable("lastGeneratedCaptureURI");
+ }
+ }
+
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/contributions/ContributionViewHolder.java b/commons/src/main/java/org/wikimedia/commons/contributions/ContributionViewHolder.java
new file mode 100644
index 000000000..3bf5adf4a
--- /dev/null
+++ b/commons/src/main/java/org/wikimedia/commons/contributions/ContributionViewHolder.java
@@ -0,0 +1,26 @@
+package org.wikimedia.commons.contributions;
+
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import org.wikimedia.commons.MediaWikiImageView;
+import org.wikimedia.commons.R;
+
+class ContributionViewHolder {
+ final MediaWikiImageView imageView;
+ final TextView titleView;
+ final TextView stateView;
+ final TextView seqNumView;
+ final ProgressBar progressView;
+
+ String url;
+
+ ContributionViewHolder(View parent) {
+ imageView = (MediaWikiImageView) parent.findViewById(R.id.contributionImage);
+ titleView = (TextView)parent.findViewById(R.id.contributionTitle);
+ stateView = (TextView)parent.findViewById(R.id.contributionState);
+ seqNumView = (TextView)parent.findViewById(R.id.contributionSequenceNumber);
+ progressView = (ProgressBar)parent.findViewById(R.id.contributionProgress);
+ }
+}
diff --git a/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsActivity.java b/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsActivity.java
index 0a9574960..a1951fa9c 100644
--- a/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsActivity.java
+++ b/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsActivity.java
@@ -8,6 +8,7 @@ import android.support.v4.content.Loader;
import android.content.*;
import android.database.Cursor;
import android.os.Bundle;
+import android.support.v4.widget.CursorAdapter;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
@@ -16,12 +17,15 @@ import com.actionbarsherlock.view.MenuItem;
import org.wikimedia.commons.*;
import org.wikimedia.commons.auth.*;
+import org.wikimedia.commons.campaigns.Campaign;
import org.wikimedia.commons.media.*;
import org.wikimedia.commons.upload.UploadService;
+import java.util.ArrayList;
+
public class ContributionsActivity
extends AuthenticatedActivity
- implements LoaderManager.LoaderCallbacks,
+ implements LoaderManager.LoaderCallbacks