Added Sync Provider for syncing previous contributions

This commit is contained in:
YuviPanda 2013-02-06 03:04:27 +05:30
parent 4abf3156e7
commit eac6807fe3
12 changed files with 287 additions and 77 deletions

View file

@ -70,6 +70,18 @@
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name=".contributions.ContributionsSyncService"
android:exported="true">
<intent-filter>
<action
android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
</service>
<provider
android:name=".contributions.ContributionsContentProvider"
android:authorities="org.wikimedia.commons.contributions.contentprovider"

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="org.wikimedia.commons.contributions.contentprovider"
android:accountType="org.wikimedia.commons"
android:supportsUploading="false"
android:userVisible="true"
android:isAlwaysSyncable="true"
/>

View file

@ -4,6 +4,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.xml.transform.*;
@ -13,12 +16,17 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import com.nostra13.universalimageloader.cache.disc.impl.TotalSizeLimitedDiscCache;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.download.HttpClientImageDownloader;
import com.nostra13.universalimageloader.core.download.ImageDownloader;
import com.nostra13.universalimageloader.core.download.URLConnectionImageDownloader;
import com.nostra13.universalimageloader.utils.StorageUtils;
import org.acra.ACRA;
import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes;
import org.apache.http.client.HttpClient;
import org.mediawiki.api.*;
import org.w3c.dom.Node;
import org.wikimedia.commons.auth.WikiAccountAuthenticator;
@ -43,13 +51,16 @@ public class CommonsApplication extends Application {
private MWApi api;
private Account currentAccount = null; // Unlike a savings account...
public static final String API_URL = "http://test.wikipedia.org/w/api.php";
public static final String API_URL = "https://test.wikipedia.org/w/api.php";
public static final String IMAGE_URL_BASE = "https://upload.wikimedia.org/wikipedia/test";
public static MWApi createMWApi() {
DefaultHttpClient client = new DefaultHttpClient();
return new MWApi(API_URL, client);
}
public DBOpenHelper getDbOpenHelper() {
if(dbOpenHelper == null) {
dbOpenHelper = new DBOpenHelper(this);
@ -57,13 +68,7 @@ public class CommonsApplication extends Application {
return dbOpenHelper;
}
public class ContentUriImageDownloader extends ImageDownloader {
@Override
protected InputStream getStreamFromNetwork(URI uri) throws IOException {
return super.getStream(uri); // Pass back to parent code, which handles http, https, etc
}
public class ContentUriImageDownloader extends URLConnectionImageDownloader {
@Override
protected InputStream getStreamFromOtherSource(URI imageUri) throws IOException {
if(imageUri.getScheme().equals("content")) {
@ -83,6 +88,7 @@ public class CommonsApplication extends Application {
ImageLoaderConfiguration imageLoaderConfiguration = new ImageLoaderConfiguration.Builder(getApplicationContext())
.discCache(new TotalSizeLimitedDiscCache(StorageUtils.getCacheDirectory(this), 128 * 1024 * 1024))
.imageDownloader(new ContentUriImageDownloader()).build();
ImageLoader.getInstance().init(imageLoaderConfiguration);
}
@ -127,38 +133,5 @@ public class CommonsApplication extends Application {
}
}
public static String getStringFromDOM(Node dom) {
javax.xml.transform.Transformer transformer = null;
try {
transformer = TransformerFactory.newInstance().newTransformer();
} catch (TransformerConfigurationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (TransformerFactoryConfigurationError e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
StringWriter outputStream = new StringWriter();
javax.xml.transform.dom.DOMSource domSource = new javax.xml.transform.dom.DOMSource(dom);
javax.xml.transform.stream.StreamResult strResult = new javax.xml.transform.stream.StreamResult(outputStream);
try {
transformer.transform(domSource, strResult);
} catch (TransformerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return outputStream.toString();
}
static public <T> void executeAsyncTask(AsyncTask<T, ?, ?> task,
T... params) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
}
else {
task.execute(params);
}
}
}

View file

@ -11,8 +11,8 @@ public class Media implements Serializable {
return localUri;
}
public Uri getRemoteUri() {
return remoteUri;
public String getImageUrl() {
return imageUrl;
}
public String getFilename() {
@ -23,10 +23,6 @@ public class Media implements Serializable {
return description;
}
public String getCommonsURL() {
return commonsURL;
}
public long getDataLength() {
return dataLength;
}
@ -43,12 +39,15 @@ public class Media implements Serializable {
return creator;
}
public String getThumbnailUrl(int width) {
return String.format("%s/%dpx-%s", imageUrl, width, filename);
}
protected Uri localUri;
protected Uri remoteUri;
protected String imageUrl;
protected String filename;
protected String description;
protected String commonsURL;
protected long dataLength;
protected Date dateCreated;
protected Date dateUploaded;
@ -57,12 +56,11 @@ public class Media implements Serializable {
protected String creator;
public Media(Uri localUri, Uri remoteUri, String filename, String description, String commonsURL, long dataLength, Date dateCreated, Date dateUploaded, String creator) {
public Media(Uri localUri, String imageUrl, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator) {
this.localUri = localUri;
this.remoteUri = remoteUri;
this.imageUrl = imageUrl;
this.filename = filename;
this.description = description;
this.commonsURL = commonsURL;
this.dataLength = dataLength;
this.dateCreated = dateCreated;
this.dateUploaded = dateUploaded;

View file

@ -165,7 +165,7 @@ public class UploadService extends IntentService {
} /* else if (mimeType.startsWith("audio/")) {
Removed Audio implementationf or now
} */
Contribution contribution = new Contribution(mediaUri, null, filename, description, null, length, dateCreated, null, app.getCurrentAccount().name, editSummary);
Contribution contribution = new Contribution(mediaUri, null, filename, description, length, dateCreated, null, app.getCurrentAccount().name, editSummary);
return contribution;
}
@ -254,22 +254,17 @@ public class UploadService extends IntentService {
toUpload--;
}
Log.d("Commons", "Response is" + CommonsApplication.getStringFromDOM(result.getDocument()));
Log.d("Commons", "Response is" + Utils.getStringFromDOM(result.getDocument()));
stopForeground(true);
curProgressNotification = null;
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); // Assuming MW always gives me UTC
String resultStatus = result.getString("/api/upload/@result");
if(!resultStatus.equals("Success")) {
showFailedNotification(contribution);
} else {
Date dateUploaded = null;
try {
dateUploaded = isoFormat.parse(result.getString("/api/upload/imageinfo/@timestamp"));
} catch(java.text.ParseException e) {
throw new RuntimeException(e); // Hopefully mediawiki doesn't give me bogus stuff?
}
dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
contribution.setState(Contribution.STATE_COMPLETED);
contribution.setDateUploaded(dateUploaded);
contribution.save();

View file

@ -0,0 +1,76 @@
package org.wikimedia.commons;
import android.os.AsyncTask;
import android.os.Build;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.w3c.dom.Node;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import java.io.StringWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
public class Utils {
public static Date parseMWDate(String mwDate) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); // Assuming MW always gives me UTC
try {
return isoFormat.parse(mwDate);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
public static String toMWDate(Date date) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); // Assuming MW always gives me UTC
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return isoFormat.format(date);
}
public static String makeThumbBaseUrl(String filename) {
String name = filename.replaceFirst("File:", "").replace(" ", "_");
String sha = new String(Hex.encodeHex(DigestUtils.md5(name)));
return String.format("%s/%s/%s/%s", CommonsApplication.IMAGE_URL_BASE, sha.substring(0, 1), sha.substring(0, 2), name);
}
public static String getStringFromDOM(Node dom) {
javax.xml.transform.Transformer transformer = null;
try {
transformer = TransformerFactory.newInstance().newTransformer();
} catch (TransformerConfigurationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (TransformerFactoryConfigurationError e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
StringWriter outputStream = new StringWriter();
javax.xml.transform.dom.DOMSource domSource = new javax.xml.transform.dom.DOMSource(dom);
javax.xml.transform.stream.StreamResult strResult = new javax.xml.transform.stream.StreamResult(outputStream);
try {
transformer.transform(domSource, strResult);
} catch (TransformerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return outputStream.toString();
}
static public <T> void executeAsyncTask(AsyncTask<T, ?, ?> task,
T... params) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
}
else {
task.execute(params);
}
}
}

View file

@ -9,6 +9,7 @@ import com.actionbarsherlock.app.*;
import android.accounts.*;
import android.os.AsyncTask;
import android.os.Bundle;
import org.wikimedia.commons.Utils;
public class AuthenticatedActivity extends SherlockFragmentActivity {
@ -113,7 +114,7 @@ public class AuthenticatedActivity extends SherlockFragmentActivity {
// returns, we have a deadlock!
// Fixed by explicitly asking this to be executed in parallel
// See: https://groups.google.com/forum/?fromgroups=#!topic/android-developers/8M0RTFfO7-M
CommonsApplication.executeAsyncTask(addAccountTask);
Utils.executeAsyncTask(addAccountTask);
} else {
GetAuthCookieTask task = new GetAuthCookieTask(curAccount, accountManager);
task.execute();

View file

@ -8,6 +8,7 @@ import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.net.*;
import android.os.RemoteException;
import android.text.TextUtils;
import org.wikimedia.commons.Media;
public class Contribution extends Media {
@ -47,8 +48,8 @@ public class Contribution extends Media {
private Date timestamp;
private int state;
public Contribution(Uri localUri, Uri remoteUri, String filename, String description, String commonsURL, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary) {
super(localUri, remoteUri, filename, description, commonsURL, dataLength, dateCreated, dateUploaded, creator);
public Contribution(Uri localUri, String remoteUri, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary) {
super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.editSummary = editSummary;
timestamp = new Date(System.currentTimeMillis());
}
@ -102,14 +103,24 @@ public class Contribution extends Media {
}
}
private ContentValues toContentValues() {
public static String makeThumbUrl(String imageUrl, String filename, int width) {
// Ugly Hack!
// Update: OH DEAR GOD WHAT A HORRIBLE HACK I AM SO SORRY
String thumbUrl = imageUrl.replaceFirst("test/", "test/thumb/").replace("commons/", "commons/thumb/") + "/" + width + "px-" + filename.replaceAll("File:", "").replaceAll(" ", "_");
if(thumbUrl.endsWith("jpg") || thumbUrl.endsWith("png") || thumbUrl.endsWith("jpeg")) {
return thumbUrl;
} else {
return thumbUrl + ".png";
}
}
public ContentValues toContentValues() {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_FILENAME, getFilename());
if(getLocalUri() != null) {
cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString());
}
if(getRemoteUri() != null) {
cv.put(Table.COLUMN_REMOTE_URI, getRemoteUri().toString());
if(getImageUrl() != null) {
cv.put(Table.COLUMN_IMAGE_URL, getImageUrl().toString());
}
if(getDateUploaded() != null) {
cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime());
@ -128,7 +139,7 @@ public class Contribution extends Media {
public static final String COLUMN_ID = "_id";
public static final String COLUMN_FILENAME = "filename";
public static final String COLUMN_LOCAL_URI = "local_uri";
public static final String COLUMN_REMOTE_URI = "remote_uri";
public static final String COLUMN_IMAGE_URL = "image_url";
public static final String COLUMN_TIMESTAMP = "timestamp";
public static final String COLUMN_STATE = "state";
public static final String COLUMN_LENGTH = "length";
@ -140,7 +151,7 @@ public class Contribution extends Media {
+ "_id INTEGER PRIMARY KEY,"
+ "filename STRING,"
+ "local_uri STRING,"
+ "remote_uri STRING,"
+ "image_url STRING,"
+ "uploaded INTEGER,"
+ "timestamp INTEGER,"
+ "state INTEGER,"

View file

@ -13,6 +13,7 @@ import android.os.Bundle;
import android.support.v4.content.*;
import android.support.v4.widget.CursorAdapter;
import android.support.v4.widget.SimpleCursorAdapter;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@ -63,6 +64,7 @@ public class ContributionsActivity extends AuthenticatedActivity implements Load
private final int COLUMN_UPLOADED;
private final int COLUMN_TRANSFERRED;
private final int COLUMN_LENGTH;
private final int COLUMN_IMAGE_URL;
public ContributionAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
@ -72,6 +74,7 @@ public class ContributionsActivity extends AuthenticatedActivity implements Load
COLUMN_UPLOADED = c.getColumnIndex(Contribution.Table.COLUMN_UPLOADED);
COLUMN_LENGTH = c.getColumnIndex(Contribution.Table.COLUMN_LENGTH);
COLUMN_TRANSFERRED = c.getColumnIndex(Contribution.Table.COLUMN_TRANSFERRED);
COLUMN_IMAGE_URL = c.getColumnIndex(Contribution.Table.COLUMN_IMAGE_URL);
}
@Override
@ -85,15 +88,20 @@ public class ContributionsActivity extends AuthenticatedActivity implements Load
TextView titleView = (TextView)view.findViewById(R.id.contributionTitle);
TextView stateView = (TextView)view.findViewById(R.id.contributionState);
Uri imageUri = Uri.parse(cursor.getString(COLUMN_LOCALURI));
String localUri = cursor.getString(COLUMN_LOCALURI);
String imageUrl = cursor.getString(COLUMN_IMAGE_URL);
String title = cursor.getString(COLUMN_FILENAME);
int state = cursor.getInt(COLUMN_STATE);
if(imageView.getTag() == null || !imageView.getTag().equals(imageUri.toString())) {
ImageLoader.getInstance().displayImage(imageUri.toString(), imageView, contributionDisplayOptions);
imageView.setTag(imageUri.toString());
String actualUrl = TextUtils.isEmpty(imageUrl) ? localUri : Contribution.makeThumbUrl(imageUrl, title, 320);
Log.d("Commons", "Trying URL " + actualUrl);
if(imageView.getTag() == null || !imageView.getTag().equals(actualUrl)) {
ImageLoader.getInstance().displayImage(actualUrl, imageView, contributionDisplayOptions);
imageView.setTag(actualUrl);
}
titleView.setText(cursor.getString(COLUMN_FILENAME));
titleView.setText(title);
switch(state) {
case Contribution.STATE_COMPLETED:
Date uploaded = new Date(cursor.getLong(COLUMN_UPLOADED));
@ -129,7 +137,8 @@ public class ContributionsActivity extends AuthenticatedActivity implements Load
Contribution.Table.COLUMN_STATE,
Contribution.Table.COLUMN_UPLOADED,
Contribution.Table.COLUMN_LENGTH,
Contribution.Table.COLUMN_TRANSFERRED
Contribution.Table.COLUMN_TRANSFERRED,
Contribution.Table.COLUMN_IMAGE_URL
};
private String CONTRIBUTION_SELECTION = "";
@ -142,7 +151,7 @@ public class ContributionsActivity extends AuthenticatedActivity implements Load
This is why Contribution.STATE_COMPLETED is -1.
*/
private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, (" + Contribution.Table.COLUMN_TIMESTAMP + " * " + Contribution.Table.COLUMN_STATE + ")";
private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, (" + Contribution.Table.COLUMN_TIMESTAMP + " * " + Contribution.Table.COLUMN_STATE + "), " + Contribution.Table.COLUMN_UPLOADED + " DESC";
@Override
protected void onResume() {
@ -159,6 +168,8 @@ public class ContributionsActivity extends AuthenticatedActivity implements Load
contributionDisplayOptions = new DisplayImageOptions.Builder().cacheInMemory()
.imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2)
.displayer(new FadeInBitmapDisplayer(300))
.cacheInMemory()
.cacheOnDisc()
.resetViewBeforeLoading().build();
Cursor allContributions = getContentResolver().query(ContributionsContentProvider.BASE_URI, CONTRIBUTIONS_PROJECTION, CONTRIBUTION_SELECTION, null, CONTRIBUTION_SORT);

View file

@ -68,7 +68,6 @@ public class ContributionsContentProvider extends ContentProvider{
public Uri insert(Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsDeleted = 0;
long id = 0;
switch (uriType) {
case CONTRIBUTIONS:
@ -86,6 +85,28 @@ public class ContributionsContentProvider extends ContentProvider{
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 CONTRIBUTIONS:
for(ContentValues value: values) {
Log.d("Commons", "Inserting! " + value.toString());
sqlDB.insert(Contribution.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) {
/*

View file

@ -0,0 +1,77 @@
package org.wikimedia.commons.contributions;
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 org.apache.commons.codec.digest.DigestUtils;
import org.wikimedia.commons.CommonsApplication;
import org.mediawiki.api.*;
import org.wikimedia.commons.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
public ContributionsSyncAdapter(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();
SharedPreferences prefs = this.getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", "");
Date curTime = new Date();
ApiResult result;
try {
MWApi.RequestBuilder builder = api.action("query")
.param("list", "logevents")
.param("leaction", "upload/upload")
.param("leprop", "title|timestamp")
.param("leuser", user)
.param("lelimit", getLimit());
if(!TextUtils.isEmpty(lastModified)) {
builder.param("leend", lastModified);
}
result = builder.get();
} catch (IOException e) {
throw new RuntimeException(e); // FIXME: Maybe something else?
}
Log.d("Commons", "Last modified at " + lastModified);
ArrayList<ApiResult> uploads = result.getNodes("/api/query/logevents/item");
Log.d("Commons", uploads.size() + " results!");
ContentValues[] imageValues = new ContentValues[uploads.size()];
for(int i=0; i < uploads.size(); i++) {
ApiResult image = uploads.get(i);
String filename = image.getString("@title");
String thumbUrl = Utils.makeThumbBaseUrl(filename);
Date dateUpdated = Utils.parseMWDate(image.getString("@timestamp"));
Contribution contrib = new Contribution(null, thumbUrl, filename, "", -1, dateUpdated, dateUpdated, user, "");
contrib.setState(Contribution.STATE_COMPLETED);
imageValues[i] = contrib.toContentValues();
Log.d("Commons", "For " + imageValues[i].toString());
}
try {
contentProviderClient.bulkInsert(ContributionsContentProvider.BASE_URI, imageValues);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
prefs.edit().putString("lastSyncTimestamp", Utils.toMWDate(curTime)).apply();
Log.d("Commons", "Oh hai, everyone! Look, a kitty!");
}
}

View file

@ -0,0 +1,26 @@
package org.wikimedia.commons.contributions;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class ContributionsSyncService extends Service {
private static final Object sSyncAdapterLock = new Object();
private static ContributionsSyncAdapter sSyncAdapter = null;
@Override
public void onCreate() {
synchronized (sSyncAdapterLock) {
if (sSyncAdapter == null) {
sSyncAdapter = new ContributionsSyncAdapter(getApplicationContext(), true);
}
}
}
@Override
public IBinder onBind(Intent intent) {
return sSyncAdapter.getSyncAdapterBinder();
}
}