Extracted and tested the database interactions from Contribution.

This commit is contained in:
Paul Hawke 2017-12-20 20:50:05 -06:00 committed by Paul Hawke
parent d45ff218f5
commit 7c80991049
11 changed files with 687 additions and 256 deletions

View file

@ -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 {

View file

@ -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) {

View file

@ -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;
}
}
}
}

View file

@ -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));
}
}

View file

@ -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(

View file

@ -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());

View file

@ -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 {

View file

@ -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);
}

View file

@ -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;

View file

@ -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<Contribution> {
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<Contribution> {
startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build());
contribution.setTransferred(transferred);
contribution.save();
dao.save(contribution);
}
}
@ -123,6 +125,7 @@ public class UploadService extends HandlerService<Contribution> {
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> {
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<Contribution> {
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> {
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<Contribution> {
notificationManager.notify(NOTIFICATION_UPLOAD_FAILED, failureNotification);
contribution.setState(Contribution.STATE_FAILED);
contribution.save();
dao.save(contribution);
}
private String findUniqueFilename(String fileName) throws IOException {

View file

@ -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<ContentValues> 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;
}
}