From 7c8099104916ef730efce2ab929975b098eb2a0e Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Wed, 20 Dec 2017 20:50:05 -0600 Subject: [PATCH] Extracted and tested the database interactions from Contribution. --- .../free/nrw/commons/CommonsApplication.java | 4 +- .../commons/contributions/Contribution.java | 263 ++----------- .../contributions/ContributionDao.java | 244 ++++++++++++ .../contributions/ContributionsActivity.java | 27 +- .../ContributionsContentProvider.java | 6 +- .../ContributionsListAdapter.java | 2 +- .../ContributionsSyncAdapter.java | 4 +- .../free/nrw/commons/data/DBOpenHelper.java | 6 +- .../ModificationsSyncAdapter.java | 3 +- .../nrw/commons/upload/UploadService.java | 17 +- .../contributions/ContributionDaoTest.java | 367 ++++++++++++++++++ 11 files changed, 687 insertions(+), 256 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java create mode 100644 app/src/test/java/fr/free/nrw/commons/contributions/ContributionDaoTest.java diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 9a7b94a99..282491903 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -21,7 +21,7 @@ import javax.inject.Named; import dagger.android.AndroidInjector; import dagger.android.DaggerApplication; import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.data.Category; import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.di.CommonsApplicationComponent; @@ -142,7 +142,7 @@ public class CommonsApplication extends DaggerApplication { ModifierSequence.Table.onDelete(db); Category.Table.onDelete(db); - Contribution.Table.onDelete(db); + ContributionDao.Table.onDelete(db); } public interface LogoutListener { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index e673c7d9d..00baac847 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -1,14 +1,8 @@ package fr.free.nrw.commons.contributions; -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.RemoteException; import android.support.annotation.NonNull; -import android.text.TextUtils; import java.text.SimpleDateFormat; import java.util.Date; @@ -43,7 +37,6 @@ public class Contribution extends Media { public static final String SOURCE_GALLERY = "gallery"; public static final String SOURCE_EXTERNAL = "external"; - private ContentProviderClient client; private Uri contentUri; private String source; private String editSummary; @@ -51,24 +44,42 @@ public class Contribution extends Media { private int state; private long transferred; private String decimalCoords; - private boolean isMultiple; - public boolean getMultiple() { - return isMultiple; + public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp, + int state, long dataLength, Date dateUploaded, long transferred, + String source, String description, String creator, boolean isMultiple, + int width, int height, String license) { + super(localUri, imageUrl, filename, description, dataLength, timestamp, dateUploaded, creator); + this.contentUri = contentUri; + this.state = state; + this.timestamp = timestamp; + this.transferred = transferred; + this.source = source; + this.isMultiple = isMultiple; + this.width = width; + this.height = height; + this.license = license; } - public void setMultiple(boolean multiple) { - isMultiple = multiple; - } - - public Contribution(Uri localUri, String remoteUri, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) { - super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator); + public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength, + Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) { + super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator); this.decimalCoords = decimalCoords; this.editSummary = editSummary; timestamp = new Date(System.currentTimeMillis()); } + public Contribution(Parcel in) { + super(in); + contentUri = in.readParcelable(Uri.class.getClassLoader()); + source = in.readString(); + timestamp = (Date) in.readSerializable(); + state = in.readInt(); + transferred = in.readLong(); + isMultiple = in.readInt() == 1; + } + @Override public void writeToParcel(Parcel parcel, int flags) { super.writeToParcel(parcel, flags); @@ -80,14 +91,12 @@ public class Contribution extends Media { parcel.writeInt(isMultiple ? 1 : 0); } - public Contribution(Parcel in) { - super(in); - contentUri = in.readParcelable(Uri.class.getClassLoader()); - source = in.readString(); - timestamp = (Date) in.readSerializable(); - state = in.readInt(); - transferred = in.readLong(); - isMultiple = in.readInt() == 1; + public boolean getMultiple() { + return isMultiple; + } + + public void setMultiple(boolean multiple) { + isMultiple = multiple; } public long getTransferred() { @@ -106,10 +115,18 @@ public class Contribution extends Media { return contentUri; } + public void setContentUri(Uri contentUri) { + this.contentUri = contentUri; + } + public Date getTimestamp() { return timestamp; } + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + public int getState() { return state; } @@ -155,62 +172,6 @@ public class Contribution extends Media { return buffer.toString(); } - public void setContentProviderClient(ContentProviderClient client) { - this.client = client; - } - - public void save() { - try { - if (contentUri == null) { - contentUri = client.insert(ContributionsContentProvider.BASE_URI, this.toContentValues()); - } else { - client.update(contentUri, toContentValues(), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - public void delete() { - try { - if (contentUri == null) { - // noooo - throw new RuntimeException("tried to delete item with no content URI"); - } else { - client.delete(contentUri, null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - - 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 (getImageUrl() != null) { - cv.put(Table.COLUMN_IMAGE_URL, getImageUrl()); - } - if (getDateUploaded() != null) { - cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime()); - } - cv.put(Table.COLUMN_LENGTH, getDataLength()); - cv.put(Table.COLUMN_TIMESTAMP, getTimestamp().getTime()); - cv.put(Table.COLUMN_STATE, getState()); - cv.put(Table.COLUMN_TRANSFERRED, transferred); - cv.put(Table.COLUMN_SOURCE, source); - cv.put(Table.COLUMN_DESCRIPTION, description); - cv.put(Table.COLUMN_CREATOR, creator); - cv.put(Table.COLUMN_MULTIPLE, isMultiple ? 1 : 0); - cv.put(Table.COLUMN_WIDTH, width); - cv.put(Table.COLUMN_HEIGHT, height); - cv.put(Table.COLUMN_LICENSE, license); - return cv; - } - @Override public void setFilename(String filename) { this.filename = filename; @@ -224,33 +185,6 @@ public class Contribution extends Media { timestamp = new Date(System.currentTimeMillis()); } - public static Contribution fromCursor(Cursor cursor) { - // Hardcoding column positions! - Contribution c = new Contribution(); - - //Check that cursor has a value to avoid CursorIndexOutOfBoundsException - if (cursor.getCount() > 0) { - c.contentUri = ContributionsContentProvider.uriForId(cursor.getInt(0)); - c.filename = cursor.getString(1); - c.localUri = TextUtils.isEmpty(cursor.getString(2)) ? null : Uri.parse(cursor.getString(2)); - c.imageUrl = cursor.getString(3); - c.timestamp = cursor.getLong(4) == 0 ? null : new Date(cursor.getLong(4)); - c.state = cursor.getInt(5); - c.dataLength = cursor.getLong(6); - c.dateUploaded = cursor.getLong(7) == 0 ? null : new Date(cursor.getLong(7)); - c.transferred = cursor.getLong(8); - c.source = cursor.getString(9); - c.description = cursor.getString(10); - c.creator = cursor.getString(11); - c.isMultiple = cursor.getInt(12) == 1; - c.width = cursor.getInt(13); - c.height = cursor.getInt(14); - c.license = cursor.getString(15); - } - - return c; - } - public String getSource() { return source; } @@ -263,121 +197,6 @@ public class Contribution extends Media { this.localUri = localUri; } - public static class Table { - public static final String TABLE_NAME = "contributions"; - - 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_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"; - public static final String COLUMN_UPLOADED = "uploaded"; - public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes - public static final String COLUMN_SOURCE = "source"; - public static final String COLUMN_DESCRIPTION = "description"; - public static final String COLUMN_CREATOR = "creator"; // Initial uploader - public static final String COLUMN_MULTIPLE = "multiple"; - public static final String COLUMN_WIDTH = "width"; - public static final String COLUMN_HEIGHT = "height"; - public static final String COLUMN_LICENSE = "license"; - - // 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_FILENAME, - COLUMN_LOCAL_URI, - COLUMN_IMAGE_URL, - COLUMN_TIMESTAMP, - COLUMN_STATE, - COLUMN_LENGTH, - COLUMN_UPLOADED, - COLUMN_TRANSFERRED, - COLUMN_SOURCE, - COLUMN_DESCRIPTION, - COLUMN_CREATOR, - COLUMN_MULTIPLE, - COLUMN_WIDTH, - COLUMN_HEIGHT, - COLUMN_LICENSE - }; - - - private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + "_id INTEGER PRIMARY KEY," - + "filename STRING," - + "local_uri STRING," - + "image_url STRING," - + "uploaded INTEGER," - + "timestamp INTEGER," - + "state INTEGER," - + "length INTEGER," - + "transferred INTEGER," - + "source STRING," - + "description STRING," - + "creator STRING," - + "multiple INTEGER," - + "width INTEGER," - + "height INTEGER," - + "LICENSE STRING" - + ");"; - - - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - public static void onDelete(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } - - public static void onUpdate(SQLiteDatabase db, int from, int to) { - if (from == to) { - return; - } - if (from == 1) { - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;"); - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;"); - from++; - onUpdate(db, from, to); - return; - } - if (from == 2) { - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET multiple = 0"); - from++; - onUpdate(db, from, to); - return; - } - if (from == 3) { - // Do nothing - from++; - onUpdate(db, from, to); - return; - } - if (from == 4) { - // Do nothing -- added Category - from++; - onUpdate(db, from, to); - return; - } - if (from == 5) { - // Added width and height fields - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET width = 0"); - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET height = 0"); - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';"); - from++; - onUpdate(db, from, to); - return; - } - } - } - @NonNull private String licenseTemplateFor(String license) { switch (license) { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java new file mode 100644 index 000000000..ffaf3fc8d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java @@ -0,0 +1,244 @@ +package fr.free.nrw.commons.contributions; + +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.support.annotation.Nullable; +import android.text.TextUtils; + +import java.util.Date; + +import fr.free.nrw.commons.settings.Prefs; + +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId; + +public class ContributionDao { + private final ContentProviderClient client; + + public ContributionDao(ContentProviderClient client) { + this.client = client; + } + + public void save(Contribution contribution) { + try { + if (contribution.getContentUri() == null) { + contribution.setContentUri(client.insert(BASE_URI, toContentValues(contribution))); + } else { + client.update(contribution.getContentUri(), toContentValues(contribution), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + public void delete(Contribution contribution) { + try { + if (contribution.getContentUri() == null) { + // noooo + throw new RuntimeException("tried to delete item with no content URI"); + } else { + client.delete(contribution.getContentUri(), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + public static ContentValues toContentValues(Contribution contribution) { + ContentValues cv = new ContentValues(); + cv.put(Table.COLUMN_FILENAME, contribution.getFilename()); + if (contribution.getLocalUri() != null) { + cv.put(Table.COLUMN_LOCAL_URI, contribution.getLocalUri().toString()); + } + if (contribution.getImageUrl() != null) { + cv.put(Table.COLUMN_IMAGE_URL, contribution.getImageUrl()); + } + if (contribution.getDateUploaded() != null) { + cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime()); + } + cv.put(Table.COLUMN_LENGTH, contribution.getDataLength()); + cv.put(Table.COLUMN_TIMESTAMP, contribution.getTimestamp().getTime()); + cv.put(Table.COLUMN_STATE, contribution.getState()); + cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred()); + cv.put(Table.COLUMN_SOURCE, contribution.getSource()); + cv.put(Table.COLUMN_DESCRIPTION, contribution.getDescription()); + cv.put(Table.COLUMN_CREATOR, contribution.getCreator()); + cv.put(Table.COLUMN_MULTIPLE, contribution.getMultiple() ? 1 : 0); + cv.put(Table.COLUMN_WIDTH, contribution.getWidth()); + cv.put(Table.COLUMN_HEIGHT, contribution.getHeight()); + cv.put(Table.COLUMN_LICENSE, contribution.getLicense()); + return cv; + } + + public static Contribution fromCursor(Cursor cursor) { + // Hardcoding column positions! + //Check that cursor has a value to avoid CursorIndexOutOfBoundsException + if (cursor.getCount() > 0) { + return new Contribution( + uriForId(cursor.getInt(0)), + cursor.getString(1), + parseUri(cursor.getString(2)), + cursor.getString(3), + parseTimestamp(cursor.getLong(4)), + cursor.getInt(5), + cursor.getLong(6), + parseTimestamp(cursor.getLong(7)), + cursor.getLong(8), + cursor.getString(9), + cursor.getString(10), + cursor.getString(11), + cursor.getInt(12) == 1, + cursor.getInt(13), + cursor.getInt(14), + cursor.getString(15)); + } + + return null; + } + + @Nullable + private static Date parseTimestamp(long timestamp) { + return timestamp == 0 ? null : new Date(timestamp); + } + + @Nullable + private static Uri parseUri(String uriString) { + return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString); + } + + public static class Table { + public static final String TABLE_NAME = "contributions"; + + 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_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"; + public static final String COLUMN_UPLOADED = "uploaded"; + public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes + public static final String COLUMN_SOURCE = "source"; + public static final String COLUMN_DESCRIPTION = "description"; + public static final String COLUMN_CREATOR = "creator"; // Initial uploader + public static final String COLUMN_MULTIPLE = "multiple"; + public static final String COLUMN_WIDTH = "width"; + public static final String COLUMN_HEIGHT = "height"; + public static final String COLUMN_LICENSE = "license"; + + // 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_FILENAME, + COLUMN_LOCAL_URI, + COLUMN_IMAGE_URL, + COLUMN_TIMESTAMP, + COLUMN_STATE, + COLUMN_LENGTH, + COLUMN_UPLOADED, + COLUMN_TRANSFERRED, + COLUMN_SOURCE, + COLUMN_DESCRIPTION, + COLUMN_CREATOR, + COLUMN_MULTIPLE, + COLUMN_WIDTH, + COLUMN_HEIGHT, + COLUMN_LICENSE + }; + + public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; + + public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" + + "_id INTEGER PRIMARY KEY," + + "filename STRING," + + "local_uri STRING," + + "image_url STRING," + + "uploaded INTEGER," + + "timestamp INTEGER," + + "state INTEGER," + + "length INTEGER," + + "transferred INTEGER," + + "source STRING," + + "description STRING," + + "creator STRING," + + "multiple INTEGER," + + "width INTEGER," + + "height INTEGER," + + "LICENSE STRING" + + ");"; + + // Upgrade from version 1 -> + static final String ADD_CREATOR_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;"; + static final String ADD_DESCRIPTION_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;"; + + // Upgrade from version 2 -> + static final String ADD_MULTIPLE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;"; + static final String SET_DEFAULT_MULTIPLE = "UPDATE " + TABLE_NAME + " SET multiple = 0"; + + // Upgrade from version 5 -> + static final String ADD_WIDTH_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;"; + static final String SET_DEFAULT_WIDTH = "UPDATE " + TABLE_NAME + " SET width = 0"; + static final String ADD_HEIGHT_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;"; + static final String SET_DEFAULT_HEIGHT = "UPDATE " + TABLE_NAME + " SET height = 0"; + static final String ADD_LICENSE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;"; + static final String SET_DEFAULT_LICENSE = "UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';"; + + + public static void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_STATEMENT); + } + + public static void onDelete(SQLiteDatabase db) { + db.execSQL(DROP_TABLE_STATEMENT); + onCreate(db); + } + + public static void onUpdate(SQLiteDatabase db, int from, int to) { + if (from == to) { + return; + } + if (from == 1) { + db.execSQL(ADD_DESCRIPTION_FIELD); + db.execSQL(ADD_CREATOR_FIELD); + from++; + onUpdate(db, from, to); + return; + } + if (from == 2) { + db.execSQL(ADD_MULTIPLE_FIELD); + db.execSQL(SET_DEFAULT_MULTIPLE); + from++; + onUpdate(db, from, to); + return; + } + if (from == 3) { + // Do nothing + from++; + onUpdate(db, from, to); + return; + } + if (from == 4) { + // Do nothing -- added Category + from++; + onUpdate(db, from, to); + return; + } + if (from == 5) { + // Added width and height fields + db.execSQL(ADD_WIDTH_FIELD); + db.execSQL(SET_DEFAULT_WIDTH); + db.execSQL(ADD_HEIGHT_FIELD); + db.execSQL(SET_DEFAULT_HEIGHT); + db.execSQL(ADD_LICENSE_FIELD); + db.execSQL(SET_DEFAULT_LICENSE); + from++; + onUpdate(db, from, to); + return; + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java index 6cda47d2e..91e2ced35 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java @@ -42,7 +42,7 @@ import timber.log.Timber; import static android.content.ContentResolver.requestSync; import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; -import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.AUTHORITY; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING; @@ -76,10 +76,10 @@ public class ContributionsActivity This is why Contribution.STATE_COMPLETED is -1. */ - private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, " - + Contribution.Table.COLUMN_UPLOADED + " DESC , (" - + Contribution.Table.COLUMN_TIMESTAMP + " * " - + Contribution.Table.COLUMN_STATE + ")"; + private String CONTRIBUTION_SORT = ContributionDao.Table.COLUMN_STATE + " DESC, " + + ContributionDao.Table.COLUMN_UPLOADED + " DESC , (" + + ContributionDao.Table.COLUMN_TIMESTAMP + " * " + + ContributionDao.Table.COLUMN_STATE + ")"; private CompositeDisposable compositeDisposable = new CompositeDisposable(); @@ -186,24 +186,23 @@ public class ContributionsActivity public void retryUpload(int i) { allContributions.moveToPosition(i); - Contribution c = Contribution.fromCursor(allContributions); + Contribution c = ContributionDao.fromCursor(allContributions); if (c.getState() == STATE_FAILED) { uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c); - Timber.d("Restarting for %s", c.toContentValues()); + Timber.d("Restarting for %s", c.toString()); } else { - Timber.d("Skipping re-upload for non-failed %s", c.toContentValues()); + Timber.d("Skipping re-upload for non-failed %s", c.toString()); } } public void deleteUpload(int i) { allContributions.moveToPosition(i); - Contribution c = Contribution.fromCursor(allContributions); + Contribution c = ContributionDao.fromCursor(allContributions); if (c.getState() == STATE_FAILED) { - Timber.d("Deleting failed contrib %s", c.toContentValues()); - c.setContentProviderClient(getContentResolver().acquireContentProviderClient(AUTHORITY)); - c.delete(); + Timber.d("Deleting failed contrib %s", c.toString()); + new ContributionDao(getContentResolver().acquireContentProviderClient(AUTHORITY)).delete(c); } else { - Timber.d("Skipping deletion for non-failed contrib %s", c.toContentValues()); + Timber.d("Skipping deletion for non-failed contrib %s", c.toString()); } } @@ -270,7 +269,7 @@ public class ContributionsActivity // not yet ready to return data return null; } else { - return Contribution.fromCursor((Cursor) contributionsList.getAdapter().getItem(i)); + return ContributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i)); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java index 5ec290026..402f91aaa 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java @@ -17,8 +17,8 @@ import fr.free.nrw.commons.data.DBOpenHelper; import timber.log.Timber; import static android.content.UriMatcher.NO_MATCH; -import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS; -import static fr.free.nrw.commons.contributions.Contribution.Table.TABLE_NAME; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.TABLE_NAME; public class ContributionsContentProvider extends ContentProvider { @@ -176,7 +176,7 @@ public class ContributionsContentProvider extends ContentProvider { if (TextUtils.isEmpty(selection)) { rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, - Contribution.Table.COLUMN_ID + " = ?", + ContributionDao.Table.COLUMN_ID + " = ?", new String[]{String.valueOf(id)}); } else { throw new IllegalArgumentException( diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java index 7be6dd663..781b3c4c4 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java @@ -26,7 +26,7 @@ class ContributionsListAdapter extends CursorAdapter { @Override public void bindView(View view, Context context, Cursor cursor) { final ContributionViewHolder views = (ContributionViewHolder)view.getTag(); - final Contribution contribution = Contribution.fromCursor(cursor); + final Contribution contribution = ContributionDao.fromCursor(cursor); views.imageView.setMedia(contribution); views.titleView.setText(contribution.getDisplayTitle()); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java index e67b164a8..4be42b1e0 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java @@ -30,7 +30,7 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED; -import static fr.free.nrw.commons.contributions.Contribution.Table.COLUMN_FILENAME; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_FILENAME; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; @SuppressWarnings("WeakerAccess") @@ -121,7 +121,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { "", -1, dateUpdated, dateUpdated, user, "", ""); contrib.setState(STATE_COMPLETED); - imageValues.add(contrib.toContentValues()); + imageValues.add(ContributionDao.toContentValues(contrib)); if (imageValues.size() % COMMIT_THRESHOLD == 0) { try { diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java index 5e28a8a32..9e4608e46 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java @@ -4,7 +4,7 @@ import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.modifications.ModifierSequence; public class DBOpenHelper extends SQLiteOpenHelper { @@ -21,14 +21,14 @@ public class DBOpenHelper extends SQLiteOpenHelper { @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { - Contribution.Table.onCreate(sqLiteDatabase); + ContributionDao.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); + ContributionDao.Table.onUpdate(sqLiteDatabase, from, to); ModifierSequence.Table.onUpdate(sqLiteDatabase, from, to); Category.Table.onUpdate(sqLiteDatabase, from, to); } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java index 1e886f225..ff7ed5049 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java @@ -18,6 +18,7 @@ import javax.inject.Inject; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; @@ -93,7 +94,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { throw new RuntimeException(e); } contributionCursor.moveToFirst(); - contrib = Contribution.fromCursor(contributionCursor); + contrib = ContributionDao.fromCursor(contributionCursor); if (contrib.getState() == Contribution.STATE_COMPLETED) { String pageContent; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 8013c256c..023252b84 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -31,6 +31,7 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider; @@ -66,6 +67,7 @@ public class UploadService extends HandlerService { public static final int NOTIFICATION_UPLOAD_IN_PROGRESS = 1; public static final int NOTIFICATION_UPLOAD_COMPLETE = 2; public static final int NOTIFICATION_UPLOAD_FAILED = 3; + private ContributionDao dao; public UploadService() { super("UploadService"); @@ -105,7 +107,7 @@ public class UploadService extends HandlerService { startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build()); contribution.setTransferred(transferred); - contribution.save(); + dao.save(contribution); } } @@ -123,6 +125,7 @@ public class UploadService extends HandlerService { notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY); + dao = new ContributionDao(contributionsProviderClient); } @Override @@ -144,9 +147,7 @@ public class UploadService extends HandlerService { contribution.setState(Contribution.STATE_QUEUED); contribution.setTransferred(0); - contribution.setContentProviderClient(contributionsProviderClient); - - contribution.save(); + dao.save(contribution); toUpload++; if (curProgressNotification != null && toUpload != 1) { curProgressNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload)); @@ -167,11 +168,11 @@ public class UploadService extends HandlerService { public int onStartCommand(Intent intent, int flags, int startId) { if (intent.getAction().equals(ACTION_START_SERVICE) && freshStart) { ContentValues failedValues = new ContentValues(); - failedValues.put(Contribution.Table.COLUMN_STATE, Contribution.STATE_FAILED); + failedValues.put(ContributionDao.Table.COLUMN_STATE, Contribution.STATE_FAILED); int updated = getContentResolver().update(ContributionsContentProvider.BASE_URI, failedValues, - Contribution.Table.COLUMN_STATE + " = ? OR " + Contribution.Table.COLUMN_STATE + " = ?", + ContributionDao.Table.COLUMN_STATE + " = ? OR " + ContributionDao.Table.COLUMN_STATE + " = ?", new String[]{ String.valueOf(Contribution.STATE_QUEUED), String.valueOf(Contribution.STATE_IN_PROGRESS) } ); Timber.d("Set %d uploads to failed", updated); @@ -261,7 +262,7 @@ public class UploadService extends HandlerService { contribution.setImageUrl(uploadResult.getImageUrl()); contribution.setState(Contribution.STATE_COMPLETED); contribution.setDateUploaded(uploadResult.getDateUploaded()); - contribution.save(); + dao.save(contribution); } } catch (IOException e) { Timber.d("I have a network fuckup"); @@ -292,7 +293,7 @@ public class UploadService extends HandlerService { notificationManager.notify(NOTIFICATION_UPLOAD_FAILED, failureNotification); contribution.setState(Contribution.STATE_FAILED); - contribution.save(); + dao.save(contribution); } private String findUniqueFilename(String fileName) throws IOException { diff --git a/app/src/test/java/fr/free/nrw/commons/contributions/ContributionDaoTest.java b/app/src/test/java/fr/free/nrw/commons/contributions/ContributionDaoTest.java new file mode 100644 index 000000000..c7394003a --- /dev/null +++ b/app/src/test/java/fr/free/nrw/commons/contributions/ContributionDaoTest.java @@ -0,0 +1,367 @@ +package fr.free.nrw.commons.contributions; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.MatrixCursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.RemoteException; +import android.support.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.Date; + +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.TestCommonsApplication; +import fr.free.nrw.commons.Utils; + +import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA; +import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY; +import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED; +import static fr.free.nrw.commons.contributions.Contribution.STATE_QUEUED; +import static fr.free.nrw.commons.contributions.ContributionDao.*; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId; +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class) +public class ContributionDaoTest { + + private static final String LOCAL_URI = "http://example.com/"; + @Mock + ContentProviderClient client; + @Mock + SQLiteDatabase database; + @Captor + ArgumentCaptor captor; + + private Uri contentUri; + private ContributionDao testObject; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + contentUri = uriForId(111); + + testObject = new ContributionDao(client); + } + + @Test + public void createTable() { + Table.onCreate(database); + verify(database).execSQL(Table.CREATE_TABLE_STATEMENT); + } + + @Test + public void deleteTable() { + Table.onDelete(database); + + InOrder inOrder = Mockito.inOrder(database); + inOrder.verify(database).execSQL(Table.DROP_TABLE_STATEMENT); + inOrder.verify(database).execSQL(Table.CREATE_TABLE_STATEMENT); + } + + @Test + public void upgradeDatabase_v1_to_v2() { + Table.onUpdate(database, 1, 2); + + InOrder inOrder = Mockito.inOrder(database); + inOrder.verify(database).execSQL(Table.ADD_DESCRIPTION_FIELD); + inOrder.verify(database).execSQL(Table.ADD_CREATOR_FIELD); + } + + @Test + public void upgradeDatabase_v2_to_v3() { + Table.onUpdate(database, 2, 3); + + InOrder inOrder = Mockito.inOrder(database); + inOrder.verify(database).execSQL(Table.ADD_MULTIPLE_FIELD); + inOrder.verify(database).execSQL(Table.SET_DEFAULT_MULTIPLE); + } + + @Test + public void upgradeDatabase_v3_to_v4() { + Table.onUpdate(database, 3, 4); + + // No changes + verifyZeroInteractions(database); + } + + @Test + public void upgradeDatabase_v4_to_v5() { + Table.onUpdate(database, 4, 5); + + // No changes + verifyZeroInteractions(database); + } + + @Test + public void upgradeDatabase_v5_to_v6() { + Table.onUpdate(database, 5, 6); + + InOrder inOrder = Mockito.inOrder(database); + inOrder.verify(database).execSQL(Table.ADD_WIDTH_FIELD); + inOrder.verify(database).execSQL(Table.SET_DEFAULT_WIDTH); + inOrder.verify(database).execSQL(Table.ADD_HEIGHT_FIELD); + inOrder.verify(database).execSQL(Table.SET_DEFAULT_HEIGHT); + inOrder.verify(database).execSQL(Table.ADD_LICENSE_FIELD); + inOrder.verify(database).execSQL(Table.SET_DEFAULT_LICENSE); + } + + @Test + public void saveNewContribution_nonNullFields() throws Exception { + when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); + Contribution contribution = createContribution(true, null, null, null, null); + + testObject.save(contribution); + + assertEquals(contentUri, contribution.getContentUri()); + verify(client).insert(eq(BASE_URI), captor.capture()); + ContentValues cv = captor.getValue(); + + // Long fields + assertEquals(222L, cv.getAsLong(Table.COLUMN_LENGTH).longValue()); + assertEquals(321L, cv.getAsLong(Table.COLUMN_TIMESTAMP).longValue()); + assertEquals(333L, cv.getAsLong(Table.COLUMN_TRANSFERRED).longValue()); + + // Integer fields + assertEquals(STATE_COMPLETED, cv.getAsInteger(Table.COLUMN_STATE).intValue()); + assertEquals(640, cv.getAsInteger(Table.COLUMN_WIDTH).intValue()); + assertEquals(480, cv.getAsInteger(Table.COLUMN_HEIGHT).intValue()); + + // String fields + assertEquals(SOURCE_CAMERA, cv.getAsString(Table.COLUMN_SOURCE)); + assertEquals("desc", cv.getAsString(Table.COLUMN_DESCRIPTION)); + assertEquals("create", cv.getAsString(Table.COLUMN_CREATOR)); + assertEquals("007", cv.getAsString(Table.COLUMN_LICENSE)); + } + + @Test + public void saveNewContribution_nullableFieldsAreNull() throws Exception { + when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); + Contribution contribution = createContribution(true, null, null, null, null); + + testObject.save(contribution); + + assertEquals(contentUri, contribution.getContentUri()); + verify(client).insert(eq(BASE_URI), captor.capture()); + ContentValues cv = captor.getValue(); + + // Nullable fields are absent if null + assertFalse(cv.containsKey(Table.COLUMN_LOCAL_URI)); + assertFalse(cv.containsKey(Table.COLUMN_IMAGE_URL)); + assertFalse(cv.containsKey(Table.COLUMN_UPLOADED)); + } + + @Test + public void saveNewContribution_nullableImageUrlUsesFileAsBackup() throws Exception { + when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); + Contribution contribution = createContribution(true, null, null, null, "file"); + + testObject.save(contribution); + + assertEquals(contentUri, contribution.getContentUri()); + verify(client).insert(eq(BASE_URI), captor.capture()); + ContentValues cv = captor.getValue(); + + // Nullable fields are absent if null + assertFalse(cv.containsKey(Table.COLUMN_LOCAL_URI)); + assertFalse(cv.containsKey(Table.COLUMN_UPLOADED)); + + assertEquals(Utils.makeThumbBaseUrl("file"), cv.getAsString(Table.COLUMN_IMAGE_URL)); + } + + @Test + public void saveNewContribution_nullableFieldsAreNonNull() throws Exception { + when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); + Contribution contribution = createContribution(true, Uri.parse(LOCAL_URI), + "image", new Date(456L), null); + + testObject.save(contribution); + + assertEquals(contentUri, contribution.getContentUri()); + verify(client).insert(eq(BASE_URI), captor.capture()); + ContentValues cv = captor.getValue(); + + assertEquals(LOCAL_URI, cv.getAsString(Table.COLUMN_LOCAL_URI)); + assertEquals("image", cv.getAsString(Table.COLUMN_IMAGE_URL)); + assertEquals(456L, cv.getAsLong(Table.COLUMN_UPLOADED).longValue()); + } + + @Test + public void saveNewContribution_booleanEncodesTrue() throws Exception { + when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); + Contribution contribution = createContribution(true, null, null, null, null); + + testObject.save(contribution); + + assertEquals(contentUri, contribution.getContentUri()); + verify(client).insert(eq(BASE_URI), captor.capture()); + ContentValues cv = captor.getValue(); + + // Boolean true --> 1 for ths encoding scheme + assertEquals("Boolean true should be encoded as 1", 1, + cv.getAsInteger(Table.COLUMN_MULTIPLE).intValue()); + } + + @Test + public void saveNewContribution_booleanEncodesFalse() throws Exception { + when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); + Contribution contribution = createContribution(false, null, null, null, null); + + testObject.save(contribution); + + assertEquals(contentUri, contribution.getContentUri()); + verify(client).insert(eq(BASE_URI), captor.capture()); + ContentValues cv = captor.getValue(); + + // Boolean true --> 1 for ths encoding scheme + assertEquals("Boolean false should be encoded as 0", 0, + cv.getAsInteger(Table.COLUMN_MULTIPLE).intValue()); + } + + @Test + public void saveExistingContribution() throws Exception { + Contribution contribution = createContribution(false, null, null, null, null); + contribution.setContentUri(contentUri); + + testObject.save(contribution); + + verify(client).update(eq(contentUri), isA(ContentValues.class), isNull(String.class), isNull(String[].class)); + } + + @Test(expected = RuntimeException.class) + public void saveTranslatesExceptions() throws Exception { + when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenThrow(new RemoteException("")); + + testObject.save(createContribution(false, null, null, null, null)); + } + + @Test(expected = RuntimeException.class) + public void deleteTranslatesExceptions() throws Exception { + when(client.delete(isA(Uri.class), any(), any())).thenThrow(new RemoteException("")); + + Contribution contribution = createContribution(false, null, null, null, null); + contribution.setContentUri(contentUri); + testObject.delete(contribution); + } + + @Test(expected = RuntimeException.class) + public void exceptionThrownWhenAttemptingToDeleteUnsavedContribution() { + testObject.delete(createContribution(false, null, null, null, null)); + } + + @Test + public void deleteExistingContribution() throws Exception { + Contribution contribution = createContribution(false, null, null, null, null); + contribution.setContentUri(contentUri); + + testObject.delete(contribution); + + verify(client).delete(eq(contentUri), isNull(String.class), isNull(String[].class)); + } + + @Test + public void createFromCursor() { + long created = 321L; + long uploaded = 456L; + MatrixCursor mc = createCursor(created, uploaded, false, LOCAL_URI); + + Contribution c = ContributionDao.fromCursor(mc); + + assertEquals(uriForId(111), c.getContentUri()); + assertEquals("file", c.getFilename()); + assertEquals(LOCAL_URI, c.getLocalUri().toString()); + assertEquals("image", c.getImageUrl()); + assertEquals(created, c.getTimestamp().getTime()); + assertEquals(created, c.getDateCreated().getTime()); + assertEquals(STATE_QUEUED, c.getState()); + assertEquals(222L, c.getDataLength()); + assertEquals(uploaded, c.getDateUploaded().getTime()); + assertEquals(88L, c.getTransferred()); + assertEquals(SOURCE_GALLERY, c.getSource()); + assertEquals("desc", c.getDescription()); + assertEquals("create", c.getCreator()); + assertEquals(640, c.getWidth()); + assertEquals(480, c.getHeight()); + assertEquals("007", c.getLicense()); + } + + @Test + public void createFromCursor_nullableTimestamps() { + MatrixCursor mc = createCursor(0L, 0L, false, LOCAL_URI); + + Contribution c = ContributionDao.fromCursor(mc); + + assertNull(c.getTimestamp()); + assertNull(c.getDateCreated()); + assertNull(c.getDateUploaded()); + } + + @Test + public void createFromCursor_nullableLocalUri() { + MatrixCursor mc = createCursor(0L, 0L, false, ""); + + Contribution c = ContributionDao.fromCursor(mc); + + assertNull(c.getLocalUri()); + assertNull(c.getDateCreated()); + assertNull(c.getDateUploaded()); + } + + @Test + public void createFromCursor_booleanEncoding() { + MatrixCursor mcFalse = createCursor(0L, 0L, false, LOCAL_URI); + assertFalse(ContributionDao.fromCursor(mcFalse).getMultiple()); + + MatrixCursor mcHammer = createCursor(0L, 0L, true, LOCAL_URI); + assertTrue(ContributionDao.fromCursor(mcHammer).getMultiple()); + } + + @NonNull + private MatrixCursor createCursor(long created, long uploaded, boolean multiple, String localUri) { + MatrixCursor mc = new MatrixCursor(Table.ALL_FIELDS, 1); + mc.addRow(Arrays.asList("111", "file", localUri, "image", + created, STATE_QUEUED, 222L, uploaded, 88L, SOURCE_GALLERY, "desc", + "create", multiple ? 1 : 0, 640, 480, "007")); + mc.moveToFirst(); + return mc; + } + + @NonNull + private Contribution createContribution(boolean multiple, Uri localUri, + String imageUrl, Date dateUploaded, String filename) { + Contribution contribution = new Contribution(localUri, imageUrl, filename, "desc", 222L, + new Date(321L), dateUploaded, "create", "edit", "coords"); + contribution.setState(STATE_COMPLETED); + contribution.setTransferred(333L); + contribution.setSource(SOURCE_CAMERA); + contribution.setLicense("007"); + contribution.setMultiple(multiple); + contribution.setTimestamp(new Date(321L)); + contribution.setWidth(640); + contribution.setHeight(480); // VGA should be enough for anyone, right? + return contribution; + } +} \ No newline at end of file