mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-28 21:33:53 +01:00
Shift contributions to use Room DB (#3324)
* Part of #3127 * Added Room Dependency * Shifted ContributionsDao to use RoomDB * Save and Fetch contributions via RoomDAO * Bugfixes, fixed test cases, injected schedulers for ContributionsPresenter * removed stetho * Fixed ReviewHelperTest cases * Fixed test cases in DeleteHelperTest * Fetch all contributions [TODO add pagination to use this, maybe later in a seperate PR] * Update Schema false in AppDatabase * removed parameter from fetchControbutions * Added logs for fetch contributions * Fixed test case ContributionsPresenter * Added an autogenerate primary key, submit save contributions on executor * fixed getItemAtPosition * MainActivity Config changes +=orientation * BugFixes * Make AppDataBase Singleton * Set _id as autogenerate primary key [replacing the previously used filename, seems like they are not unique] * Replace Execxutor Utils with Subscribers on Singles in UploadService * BugFix, Upload Progress * Remove un-nescessary null check on contributions in ContributionsListAdapter * removed ContributionsListFragment [not-implemeted] * Review suggested changes * removed un-nescessary null checks * provide ContributionsDao * Minor bug fixes * wip * delete existing contributions table (from the existing db) on upgrade * remove un-nescessary null checks in test classes * shifted media to be a local variable in ReviewHelperTest * removed captured folder * Dispose composite disposables in UploadService * replaced size check with isEmpty ContributionsPresenter * transform saveContributions to a Completable * Addressed comments in review * Typo in Contributions * ReasonBuilderTest (create media object instead of mocking) * Use global Gson object instead of creating a new one in Converters * Provide Gson to Converters from the CommonsApplicationComponent * use static method instead of field instead of static field to provide GSON in Converters * Modified gitignore to exclude captures/*
This commit is contained in:
parent
642ed51c8c
commit
99c6f5f105
35 changed files with 557 additions and 1296 deletions
|
|
@ -6,6 +6,8 @@ import android.os.Parcel;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringDef;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
|
|
@ -21,6 +23,7 @@ import fr.free.nrw.commons.utils.ConfigUtils;
|
|||
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
@Entity(tableName = "contribution")
|
||||
public class Contribution extends Media {
|
||||
|
||||
//{{According to Exif data|2009-01-09}}
|
||||
|
|
@ -54,17 +57,19 @@ public class Contribution extends Media {
|
|||
public static final String SOURCE_CAMERA = "camera";
|
||||
public static final String SOURCE_GALLERY = "gallery";
|
||||
public static final String SOURCE_EXTERNAL = "external";
|
||||
|
||||
private Uri contentUri;
|
||||
private String source;
|
||||
private String editSummary;
|
||||
private int state;
|
||||
private long transferred;
|
||||
private String decimalCoords;
|
||||
private boolean isMultiple;
|
||||
private String wikiDataEntityId;
|
||||
private Uri contentProviderUri;
|
||||
private String dateCreatedSource;
|
||||
@PrimaryKey (autoGenerate = true)
|
||||
@NonNull
|
||||
public long _id;
|
||||
public Uri contentUri;
|
||||
public String source;
|
||||
public String editSummary;
|
||||
public int state;
|
||||
public long transferred;
|
||||
public String decimalCoords;
|
||||
public boolean isMultiple;
|
||||
public String wikiDataEntityId;
|
||||
public Uri contentProviderUri;
|
||||
public String dateCreatedSource;
|
||||
|
||||
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated,
|
||||
int state, long dataLength, Date dateUploaded, long transferred,
|
||||
|
|
|
|||
|
|
@ -1,331 +1,55 @@
|
|||
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.database.sqlite.SQLiteException;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.text.TextUtils;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Delete;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Transaction;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
import java.util.Date;
|
||||
@Dao
|
||||
public abstract class ContributionDao {
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
@Query("SELECT * FROM contribution order by dateUploaded DESC")
|
||||
abstract LiveData<List<Contribution>> fetchContributions();
|
||||
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import timber.log.Timber;
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
public abstract Single<Long> save(Contribution contribution);
|
||||
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_WIKI_DATA_ENTITY_ID;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId;
|
||||
|
||||
public class ContributionDao {
|
||||
/*
|
||||
This sorts in the following order:
|
||||
Currently Uploading
|
||||
Failed (Sorted in ascending order of time added - FIFO)
|
||||
Queued to Upload (Sorted in ascending order of time added - FIFO)
|
||||
Completed (Sorted in descending order of time added)
|
||||
|
||||
This is why Contribution.STATE_COMPLETED is -1.
|
||||
*/
|
||||
static final String CONTRIBUTION_SORT = Table.COLUMN_STATE + " DESC, "
|
||||
+ Table.COLUMN_UPLOADED + " DESC , ("
|
||||
+ Table.COLUMN_TIMESTAMP + " * "
|
||||
+ Table.COLUMN_STATE + ")";
|
||||
|
||||
private final Provider<ContentProviderClient> clientProvider;
|
||||
|
||||
@Inject
|
||||
public ContributionDao(@Named("contribution") Provider<ContentProviderClient> clientProvider) {
|
||||
this.clientProvider = clientProvider;
|
||||
public Completable deleteAllAndSave(List<Contribution> contributions){
|
||||
return Completable.fromAction(() -> deleteAllAndSaveTransaction(contributions));
|
||||
}
|
||||
|
||||
Cursor loadAllContributions() {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
return db.query(BASE_URI, ALL_FIELDS, "", null, CONTRIBUTION_SORT);
|
||||
} catch (RemoteException e) {
|
||||
return null;
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
@Transaction
|
||||
public void deleteAllAndSaveTransaction(List<Contribution> contributions){
|
||||
deleteAll(Contribution.STATE_COMPLETED);
|
||||
save(contributions);
|
||||
}
|
||||
|
||||
public void save(Contribution contribution) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (contribution.getContentUri() == null) {
|
||||
contribution.setContentUri(db.insert(BASE_URI, toContentValues(contribution)));
|
||||
} else {
|
||||
db.update(contribution.getContentUri(), toContentValues(contribution), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
@Insert
|
||||
public abstract void save(List<Contribution> contribution);
|
||||
|
||||
public void delete(Contribution contribution) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (contribution.getContentUri() == null) {
|
||||
// noooo
|
||||
throw new RuntimeException("tried to delete item with no content URI");
|
||||
} else {
|
||||
db.delete(contribution.getContentUri(), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
@Delete
|
||||
public abstract Single<Integer> delete(Contribution contribution);
|
||||
|
||||
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());
|
||||
//This was always meant to store the date created..If somehow date created is not fetched while actually saving the contribution, lets saveValue today's date
|
||||
cv.put(Table.COLUMN_TIMESTAMP, contribution.getDateCreated()==null?System.currentTimeMillis():contribution.getDateCreated().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());
|
||||
cv.put(Table.COLUMN_WIKI_DATA_ENTITY_ID, contribution.getWikiDataEntityId());
|
||||
return cv;
|
||||
}
|
||||
@Query("SELECT * from contribution WHERE contentProviderUri=:uri")
|
||||
public abstract List<Contribution> getContributionWithUri(String uri);
|
||||
|
||||
public Contribution fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
|
||||
if (cursor.getCount() > 0) {
|
||||
int index;
|
||||
if (cursor.getColumnIndex(Table.COLUMN_LICENSE) == -1){
|
||||
index = 15;
|
||||
} else {
|
||||
index = cursor.getColumnIndex(Table.COLUMN_LICENSE);
|
||||
}
|
||||
Contribution contribution = new Contribution(
|
||||
uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_FILENAME)),
|
||||
parseUri(cursor.getString(cursor.getColumnIndex(Table.COLUMN_LOCAL_URI))),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_IMAGE_URL)),
|
||||
parseTimestamp(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_TIMESTAMP))),
|
||||
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_STATE)),
|
||||
cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LENGTH)),
|
||||
parseTimestamp(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_UPLOADED))),
|
||||
cursor.getLong(cursor.getColumnIndex(Table.COLUMN_TRANSFERRED)),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_SOURCE)),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_CREATOR)),
|
||||
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_MULTIPLE)) == 1,
|
||||
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_WIDTH)),
|
||||
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_HEIGHT)),
|
||||
cursor.getString(index)
|
||||
);
|
||||
@Query("SELECT * from contribution WHERE filename=:fileName")
|
||||
public abstract List<Contribution> getContributionWithTitle(String fileName);
|
||||
|
||||
String wikidataEntityId = cursor.getString(cursor.getColumnIndex(COLUMN_WIKI_DATA_ENTITY_ID));
|
||||
if (!StringUtils.isBlank(wikidataEntityId)) {
|
||||
contribution.setWikiDataEntityId(wikidataEntityId);
|
||||
}
|
||||
@Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
|
||||
public abstract Single<Integer> updateStates(int state, int[] toUpdateStates);
|
||||
|
||||
return contribution;
|
||||
}
|
||||
@Query("Delete FROM contribution")
|
||||
public abstract void deleteAll();
|
||||
|
||||
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";
|
||||
public static final String COLUMN_WIKI_DATA_ENTITY_ID = "wikidataEntityID";
|
||||
|
||||
// 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,
|
||||
COLUMN_WIKI_DATA_ENTITY_ID
|
||||
};
|
||||
|
||||
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,"
|
||||
+ "wikidataEntityID 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 + "';";
|
||||
|
||||
// Upgrade from version 8 ->
|
||||
static final String ADD_WIKI_DATA_ENTITY_ID_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN wikidataEntityID STRING;";
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
//Considering the crashes we have been facing recently, lets blindly add this column to any table which has ever existed
|
||||
runQuery(db,ADD_WIKI_DATA_ENTITY_ID_FIELD);
|
||||
|
||||
if (from == 1) {
|
||||
runQuery(db,ADD_DESCRIPTION_FIELD);
|
||||
runQuery(db,ADD_CREATOR_FIELD);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 2) {
|
||||
runQuery(db, ADD_MULTIPLE_FIELD);
|
||||
runQuery(db, 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
|
||||
runQuery(db, ADD_WIDTH_FIELD);
|
||||
runQuery(db, SET_DEFAULT_WIDTH);
|
||||
runQuery(db, ADD_HEIGHT_FIELD);
|
||||
runQuery(db, SET_DEFAULT_HEIGHT);
|
||||
runQuery(db, ADD_LICENSE_FIELD);
|
||||
runQuery(db, SET_DEFAULT_LICENSE);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from > 5) {
|
||||
// Added place field
|
||||
from=to;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* perform the db.execSQl with handled exceptions
|
||||
*/
|
||||
private static void runQuery(SQLiteDatabase db, String query) {
|
||||
try {
|
||||
db.execSQL(query);
|
||||
} catch (SQLiteException e) {
|
||||
Timber.e("Exception performing query: " + query + " message: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@Query("Delete FROM contribution WHERE state = :state")
|
||||
public abstract void deleteAll(int state);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,186 +0,0 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.content.UriMatcher.NO_MATCH;
|
||||
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 CommonsDaggerContentProvider {
|
||||
|
||||
private static final int CONTRIBUTIONS = 1;
|
||||
private static final int CONTRIBUTIONS_ID = 2;
|
||||
private static final String BASE_PATH = "contributions";
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
|
||||
|
||||
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.CONTRIBUTION_AUTHORITY + "/" + BASE_PATH);
|
||||
|
||||
static {
|
||||
uriMatcher.addURI(BuildConfig.CONTRIBUTION_AUTHORITY, BASE_PATH, CONTRIBUTIONS);
|
||||
uriMatcher.addURI(BuildConfig.CONTRIBUTION_AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
|
||||
}
|
||||
|
||||
public static Uri uriForId(int id) {
|
||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||
}
|
||||
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
||||
String[] selectionArgs, String sortOrder) {
|
||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||
queryBuilder.setTables(TABLE_NAME);
|
||||
|
||||
int uriType = uriMatcher.match(uri);
|
||||
|
||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||
Cursor cursor;
|
||||
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
cursor = queryBuilder.query(db, projection, selection, selectionArgs,
|
||||
null, null, sortOrder);
|
||||
break;
|
||||
case CONTRIBUTIONS_ID:
|
||||
cursor = queryBuilder.query(db,
|
||||
ALL_FIELDS,
|
||||
"_id = ?",
|
||||
new String[]{uri.getLastPathSegment()},
|
||||
null,
|
||||
null,
|
||||
sortOrder
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI" + uri);
|
||||
}
|
||||
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
long id;
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return Uri.parse(BASE_URI + "/" + id);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String s, String[] strings) {
|
||||
int rows;
|
||||
int uriType = uriMatcher.match(uri);
|
||||
|
||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS_ID:
|
||||
Timber.d("Deleting contribution id %s", uri.getLastPathSegment());
|
||||
rows = db.delete(TABLE_NAME,
|
||||
"_id = ?",
|
||||
new String[]{uri.getLastPathSegment()}
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI" + uri);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return rows;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
||||
Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
sqlDB.beginTransaction();
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
for (ContentValues value : values) {
|
||||
Timber.d("Inserting! %s", value);
|
||||
sqlDB.insert(TABLE_NAME, null, value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
sqlDB.setTransactionSuccessful();
|
||||
sqlDB.endTransaction();
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return values.length;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
|
||||
/*
|
||||
SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
|
||||
Even then, we should make sure to sanitize all user input appropriately.
|
||||
Input that passes through ContentValues should be fine. So only issues are those that pass
|
||||
in via concatenating.
|
||||
|
||||
In here, the only concat created argument is for id. It is cast to an int, and will
|
||||
error out otherwise.
|
||||
*/
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
int rowsUpdated;
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs);
|
||||
break;
|
||||
case CONTRIBUTIONS_ID:
|
||||
int id = Integer.valueOf(uri.getLastPathSegment());
|
||||
|
||||
if (TextUtils.isEmpty(selection)) {
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
ContributionDao.Table.COLUMN_ID + " = ?",
|
||||
new String[]{String.valueOf(id)});
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Parameter `selection` should be empty when updating an ID");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return rowsUpdated;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.BasePresenter;
|
||||
import fr.free.nrw.commons.Media;
|
||||
|
|
@ -22,13 +20,13 @@ public class ContributionsContract {
|
|||
|
||||
void setUploadCount(int count);
|
||||
|
||||
void onDataSetChanged();
|
||||
void showContributions(List<Contribution> contributionList);
|
||||
|
||||
void showMessage(String localizedMessage);
|
||||
}
|
||||
|
||||
public interface UserActionListener extends BasePresenter<ContributionsContract.View>,
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
Contribution getContributionsFromCursor(Cursor cursor);
|
||||
public interface UserActionListener extends BasePresenter<ContributionsContract.View> {
|
||||
Contribution getContributionsWithTitle(String uri);
|
||||
|
||||
void deleteUpload(Contribution contribution);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import androidx.fragment.app.FragmentManager;
|
|||
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
|
|
@ -71,7 +73,6 @@ public class ContributionsFragment
|
|||
LocationUpdateListener,
|
||||
ICampaignsView, ContributionsContract.View {
|
||||
@Inject @Named("default_preferences") JsonKvStore store;
|
||||
@Inject ContributionDao contributionDao;
|
||||
@Inject NearbyController nearbyController;
|
||||
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
@Inject CampaignsPresenter presenter;
|
||||
|
|
@ -118,11 +119,11 @@ public class ContributionsFragment
|
|||
};
|
||||
private boolean shouldShowMediaDetailsFragment;
|
||||
private int numberOfContributions;
|
||||
private boolean isAuthCookieAcquired;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -132,6 +133,7 @@ public class ContributionsFragment
|
|||
ButterKnife.bind(this, view);
|
||||
presenter.onAttachView(this);
|
||||
contributionsPresenter.onAttachView(this);
|
||||
contributionsPresenter.setLifeCycleOwner(this.getViewLifecycleOwner());
|
||||
campaignView.setVisibility(View.GONE);
|
||||
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
|
||||
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
|
||||
|
|
@ -210,20 +212,10 @@ public class ContributionsFragment
|
|||
showDetail(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumberOfContributions() {
|
||||
return numberOfContributions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Contribution getContributionForPosition(int position) {
|
||||
return (Contribution) contributionsPresenter.getItemAtPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findItemPositionWithId(String id) {
|
||||
return contributionsPresenter.getChildPositionWithId(id);
|
||||
}
|
||||
});
|
||||
|
||||
if(null==mediaDetailPagerFragment){
|
||||
|
|
@ -306,11 +298,10 @@ public class ContributionsFragment
|
|||
*/
|
||||
void onAuthCookieAcquired() {
|
||||
// Since we call onAuthCookieAcquired method from onAttach, isAdded is still false. So don't use it
|
||||
|
||||
isAuthCookieAcquired=true;
|
||||
if (getActivity() != null) { // If fragment is attached to parent activity
|
||||
getActivity().bindService(getUploadServiceIntent(), uploadServiceConnection, Context.BIND_AUTO_CREATE);
|
||||
isUploadServiceConnected = true;
|
||||
getActivity().getSupportLoaderManager().initLoader(0, null, contributionsPresenter);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -336,7 +327,7 @@ public class ContributionsFragment
|
|||
|
||||
@Override
|
||||
public void refreshSource() {
|
||||
getActivity().getSupportLoaderManager().restartLoader(0, null, contributionsPresenter);
|
||||
contributionsPresenter.fetchContributions();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -411,6 +402,10 @@ public class ContributionsFragment
|
|||
}
|
||||
|
||||
fetchCampaigns();
|
||||
if(isAuthCookieAcquired){
|
||||
contributionsPresenter.fetchContributions();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void checkPermissionsAndShowNearbyCardView() {
|
||||
|
|
@ -578,9 +573,8 @@ public class ContributionsFragment
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onDataSetChanged() {
|
||||
contributionsListFragment.onDataSetChanged();
|
||||
mediaDetailPagerFragment.onDataSetChanged();
|
||||
public void showContributions(List<Contribution> contributionList) {
|
||||
contributionsListFragment.setContributions(contributionList);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import android.view.ViewGroup;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.model.DisplayableContribution;
|
||||
|
||||
|
|
@ -15,9 +18,11 @@ import fr.free.nrw.commons.contributions.model.DisplayableContribution;
|
|||
public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionViewHolder> {
|
||||
|
||||
private Callback callback;
|
||||
private List<Contribution> contributions;
|
||||
|
||||
public ContributionsListAdapter(Callback callback) {
|
||||
this.callback = callback;
|
||||
contributions=new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -35,7 +40,7 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
|
|||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
|
||||
final Contribution contribution = callback.getContributionForPosition(position);
|
||||
final Contribution contribution = contributions.get(position);
|
||||
DisplayableContribution displayableContribution = new DisplayableContribution(contribution,
|
||||
position);
|
||||
holder.init(position, displayableContribution);
|
||||
|
|
@ -43,7 +48,15 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
|
|||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return callback.getNumberOfContributions();
|
||||
return contributions.size();
|
||||
}
|
||||
|
||||
public void setContributions(List<Contribution> contributionList) {
|
||||
if(null!=contributionList) {
|
||||
this.contributions.clear();
|
||||
this.contributions.addAll(contributionList);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
|
|
@ -54,10 +67,6 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
|
|||
|
||||
void openMediaDetail(int contribution);
|
||||
|
||||
int getNumberOfContributions();
|
||||
|
||||
Contribution getContributionForPosition(int position);
|
||||
|
||||
int findItemPositionWithId(String lastVisibleItemID);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ import androidx.recyclerview.widget.RecyclerView.LayoutManager;
|
|||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
|
|
@ -72,6 +75,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
|||
private String lastVisibleItemID;
|
||||
|
||||
private int SPAN_COUNT=3;
|
||||
private List<Contribution> contributions=new ArrayList<>();
|
||||
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
|
||||
|
|
@ -104,6 +108,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
|
||||
rvContributionsList.setAdapter(adapter);
|
||||
adapter.setContributions(contributions);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -178,16 +183,10 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
|||
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
public void onDataSetChanged() {
|
||||
if (null != adapter) {
|
||||
adapter.notifyDataSetChanged();
|
||||
//Restoring last visible item position in cases of orientation change
|
||||
if (null != lastVisibleItemID) {
|
||||
int itemPositionWithId = callback.findItemPositionWithId(lastVisibleItemID);
|
||||
rvContributionsList.scrollToPosition(itemPositionWithId);
|
||||
lastVisibleItemID = null;//Reset the lastVisibleItemID once we have used it
|
||||
}
|
||||
}
|
||||
public void setContributions(List<Contribution> contributionList) {
|
||||
this.contributions.clear();
|
||||
this.contributions.addAll(contributionList);
|
||||
adapter.setContributions(contributions);
|
||||
}
|
||||
|
||||
public interface SourceRefresher {
|
||||
|
|
@ -228,7 +227,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
|||
private String findIdOfItemWithPosition(int position) {
|
||||
Contribution contributionForPosition = callback.getContributionForPosition(position);
|
||||
if (null != contributionForPosition) {
|
||||
return contributionForPosition.getContentUri().getLastPathSegment();
|
||||
return contributionForPosition.getFilename();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.database.Cursor;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
/**
|
||||
* The LocalDataSource class for Contributions
|
||||
*/
|
||||
class ContributionsLocalDataSource {
|
||||
|
||||
private final ContributionDao contributionsDao;
|
||||
private final ContributionDao contributionDao;
|
||||
private final JsonKvStore defaultKVStore;
|
||||
|
||||
@Inject
|
||||
|
|
@ -20,30 +24,54 @@ class ContributionsLocalDataSource {
|
|||
@Named("default_preferences") JsonKvStore defaultKVStore,
|
||||
ContributionDao contributionDao) {
|
||||
this.defaultKVStore = defaultKVStore;
|
||||
this.contributionsDao = contributionDao;
|
||||
this.contributionDao = contributionDao;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch default number of contributions to be show, based on user preferences
|
||||
*/
|
||||
public int get(String key) {
|
||||
return defaultKVStore.getInt(key);
|
||||
public String getString(String key) {
|
||||
return defaultKVStore.getString(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch default number of contributions to be show, based on user preferences
|
||||
*/
|
||||
public long getLong(String key) {
|
||||
return defaultKVStore.getLong(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contribution object from cursor
|
||||
* @param cursor
|
||||
* @param uri
|
||||
* @return
|
||||
*/
|
||||
public Contribution getContributionFromCursor(Cursor cursor) {
|
||||
return contributionsDao.fromCursor(cursor);
|
||||
public Contribution getContributionWithFileName(String uri) {
|
||||
List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(uri);
|
||||
if(!contributionWithUri.isEmpty()){
|
||||
return contributionWithUri.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a contribution from the contributions table
|
||||
* @param contribution
|
||||
* @return
|
||||
*/
|
||||
public void deleteContribution(Contribution contribution) {
|
||||
contributionsDao.delete(contribution);
|
||||
public Single<Integer> deleteContribution(Contribution contribution) {
|
||||
return contributionDao.delete(contribution);
|
||||
}
|
||||
|
||||
public LiveData<List<Contribution>> getContributions() {
|
||||
return contributionDao.fetchContributions();
|
||||
}
|
||||
|
||||
public Completable saveContributions(List<Contribution> contributions) {
|
||||
return contributionDao.deleteAllAndSave(contributions);
|
||||
}
|
||||
|
||||
public void set(String key, long value) {
|
||||
defaultKVStore.putLong(key,value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,104 +3,156 @@ package fr.free.nrw.commons.contributions;
|
|||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DataSetObserver;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
|
||||
import fr.free.nrw.commons.db.AppDatabase;
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
||||
import fr.free.nrw.commons.mwapi.UserClient;
|
||||
import fr.free.nrw.commons.utils.ExecutorUtils;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.SingleObserver;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
|
||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
|
||||
|
||||
/**
|
||||
* The presenter class for Contributions
|
||||
*/
|
||||
public class ContributionsPresenter extends DataSetObserver implements UserActionListener {
|
||||
public class ContributionsPresenter implements UserActionListener {
|
||||
|
||||
private final ContributionsRepository repository;
|
||||
private final Scheduler mainThreadScheduler;
|
||||
private final Scheduler ioThreadScheduler;
|
||||
private CompositeDisposable compositeDisposable;
|
||||
private ContributionsContract.View view;
|
||||
private Cursor cursor;
|
||||
private List<Contribution> contributionList=new ArrayList<>();
|
||||
|
||||
@Inject
|
||||
Context context;
|
||||
|
||||
@Inject
|
||||
ContributionsPresenter(ContributionsRepository repository) {
|
||||
UserClient userClient;
|
||||
|
||||
@Inject
|
||||
AppDatabase appDatabase;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
private LifecycleOwner lifeCycleOwner;
|
||||
|
||||
@Inject
|
||||
ContributionsPresenter(ContributionsRepository repository, @Named(CommonsApplicationModule.MAIN_THREAD) Scheduler mainThreadScheduler,@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) {
|
||||
this.repository = repository;
|
||||
this.mainThreadScheduler=mainThreadScheduler;
|
||||
this.ioThreadScheduler=ioThreadScheduler;
|
||||
}
|
||||
|
||||
private String user;
|
||||
|
||||
@Override
|
||||
public void onAttachView(ContributionsContract.View view) {
|
||||
this.view = view;
|
||||
if (null != cursor) {
|
||||
try {
|
||||
cursor.registerDataSetObserver(this);
|
||||
} catch (IllegalStateException e) {//Cursor might be already registered
|
||||
Timber.d(e);
|
||||
}
|
||||
compositeDisposable=new CompositeDisposable();
|
||||
}
|
||||
|
||||
public void setLifeCycleOwner(LifecycleOwner lifeCycleOwner){
|
||||
this.lifeCycleOwner=lifeCycleOwner;
|
||||
}
|
||||
|
||||
public void fetchContributions() {
|
||||
Timber.d("fetch Contributions");
|
||||
LiveData<List<Contribution>> liveDataContributions = repository.fetchContributions();
|
||||
if(null!=lifeCycleOwner) {
|
||||
liveDataContributions.observe(lifeCycleOwner, this::showContributions);
|
||||
}
|
||||
|
||||
if (NetworkUtils.isInternetConnectionEstablished(CommonsApplication.getInstance()) && shouldFetchContributions()) {
|
||||
Timber.d("fetching contributions: ");
|
||||
view.showProgress(true);
|
||||
this.user = sessionManager.getUserName();
|
||||
view.showContributions(Collections.emptyList());
|
||||
compositeDisposable.add(userClient.logEvents(user)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.doOnNext(mwQueryLogEvent -> Timber.d("Received image %s", mwQueryLogEvent.title()))
|
||||
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title()))
|
||||
.map(image -> {
|
||||
Contribution contribution = new Contribution(null, null, image.title(),
|
||||
"", -1, image.date(), image.date(), user,
|
||||
"", "", STATE_COMPLETED);
|
||||
return contribution;
|
||||
})
|
||||
.toList()
|
||||
.subscribe(this::saveContributionsToDB, error -> {
|
||||
Timber.e("Failed to fetch contributions: %s", error.getMessage());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private void showContributions(@NonNull List<Contribution> contributions) {
|
||||
view.showProgress(false);
|
||||
if (contributions.isEmpty()) {
|
||||
view.showWelcomeTip(true);
|
||||
view.showNoContributionsUI(true);
|
||||
} else {
|
||||
view.showWelcomeTip(false);
|
||||
view.showNoContributionsUI(false);
|
||||
view.setUploadCount(contributions.size());
|
||||
view.showContributions(contributions);
|
||||
this.contributionList.clear();
|
||||
this.contributionList.addAll(contributions);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveContributionsToDB(List<Contribution> contributions) {
|
||||
Timber.e("Fetched: "+contributions.size()+" contributions "+" saving to db");
|
||||
repository.save(contributions).subscribeOn(ioThreadScheduler).subscribe();
|
||||
repository.set("last_fetch_timestamp",System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private boolean shouldFetchContributions() {
|
||||
long lastFetchTimestamp = repository.getLong("last_fetch_timestamp");
|
||||
Timber.d("last fetch timestamp: %s", lastFetchTimestamp);
|
||||
if(lastFetchTimestamp!=0){
|
||||
return System.currentTimeMillis()-lastFetchTimestamp>15*60*100;
|
||||
}
|
||||
Timber.d("should fetch contributions: %s", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachView() {
|
||||
this.view = null;
|
||||
if (null != cursor) {
|
||||
try {
|
||||
cursor.unregisterDataSetObserver(this);
|
||||
} catch (Exception e) {//Cursor might not be already registered
|
||||
Timber.d(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
|
||||
int preferredNumberOfUploads = repository.get(UPLOADS_SHOWING);
|
||||
return new CursorLoader(context, BASE_URI,
|
||||
ALL_FIELDS, "", null,
|
||||
ContributionDao.CONTRIBUTION_SORT + "LIMIT "
|
||||
+ (preferredNumberOfUploads>0?preferredNumberOfUploads:100));
|
||||
compositeDisposable.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
|
||||
view.showProgress(false);
|
||||
if (null != cursor && cursor.getCount() > 0) {
|
||||
view.showWelcomeTip(false);
|
||||
view.showNoContributionsUI(false);
|
||||
view.setUploadCount(cursor.getCount());
|
||||
} else {
|
||||
view.showWelcomeTip(true);
|
||||
view.showNoContributionsUI(true);
|
||||
}
|
||||
swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
this.cursor = null;
|
||||
//On LoadFinished is not guaranteed to be called
|
||||
view.showProgress(false);
|
||||
view.showWelcomeTip(true);
|
||||
view.showNoContributionsUI(true);
|
||||
swapCursor(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contribution from the repository
|
||||
*/
|
||||
@Override
|
||||
public Contribution getContributionsFromCursor(Cursor cursor) {
|
||||
return repository.getContributionFromCursor(cursor);
|
||||
public Contribution getContributionsWithTitle(String title) {
|
||||
return repository.getContributionWithFileName(title);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -109,75 +161,23 @@ public class ContributionsPresenter extends DataSetObserver implements UserActio
|
|||
*/
|
||||
@Override
|
||||
public void deleteUpload(Contribution contribution) {
|
||||
repository.deleteContributionFromDB(contribution);
|
||||
compositeDisposable.add(repository.deleteContributionFromDB(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a contribution at the specified cursor position
|
||||
*
|
||||
* @param i
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Media getItemAtPosition(int i) {
|
||||
if (null != cursor && cursor.moveToPosition(i)) {
|
||||
return getContributionsFromCursor(cursor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contribution position with id
|
||||
*/
|
||||
public int getChildPositionWithId(String id) {
|
||||
int position = 0;
|
||||
cursor.moveToFirst();
|
||||
while (null != cursor && cursor.moveToNext()) {
|
||||
if (getContributionsFromCursor(cursor).getContentUri().getLastPathSegment()
|
||||
.equals(id)) {
|
||||
position = cursor.getPosition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged() {
|
||||
super.onChanged();
|
||||
view.onDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidated() {
|
||||
super.onInvalidated();
|
||||
//Not letting the view know of this as of now, TODO discuss how to handle this and maybe show a proper ui for this
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap in a new Cursor, returning the old Cursor. The returned old Cursor is <em>not</em>
|
||||
* closed.
|
||||
*
|
||||
* @param newCursor The new cursor to be used.
|
||||
* @return Returns the previously set Cursor, or null if there was not one. If the given new
|
||||
* Cursor is the same instance is the previously set Cursor, null is also returned.
|
||||
*/
|
||||
private void swapCursor(Cursor newCursor) {
|
||||
try {
|
||||
if (newCursor == cursor) {
|
||||
return;
|
||||
}
|
||||
Cursor oldCursor = cursor;
|
||||
if (oldCursor != null) {
|
||||
oldCursor.unregisterDataSetObserver(this);
|
||||
}
|
||||
cursor = newCursor;
|
||||
if (newCursor != null) {
|
||||
newCursor.registerDataSetObserver(this);
|
||||
}
|
||||
view.onDataSetChanged();
|
||||
} catch (IllegalStateException e) {//Cursor might [not] be already registered/unregistered
|
||||
Timber.e(e);
|
||||
if (i == -1 || contributionList.size() < i+1) {
|
||||
return null;
|
||||
}
|
||||
return contributionList.get(i);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.database.Cursor;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
/**
|
||||
* The repository class for contributions
|
||||
*/
|
||||
|
|
@ -19,25 +24,41 @@ public class ContributionsRepository {
|
|||
/**
|
||||
* Fetch default number of contributions to be show, based on user preferences
|
||||
*/
|
||||
public int get(String uploadsShowing) {
|
||||
return localDataSource.get(uploadsShowing);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get contribution object from cursor from LocalDataSource
|
||||
* @param cursor
|
||||
* @return
|
||||
*/
|
||||
public Contribution getContributionFromCursor(Cursor cursor) {
|
||||
return localDataSource.getContributionFromCursor(cursor);
|
||||
public String getString(String key) {
|
||||
return localDataSource.getString(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a failed upload from DB
|
||||
* @param contribution
|
||||
* @return
|
||||
*/
|
||||
public void deleteContributionFromDB(Contribution contribution) {
|
||||
localDataSource.deleteContribution(contribution);
|
||||
public Single<Integer> deleteContributionFromDB(Contribution contribution) {
|
||||
return localDataSource.deleteContribution(contribution);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contribution object with title
|
||||
* @param fileName
|
||||
* @return
|
||||
*/
|
||||
public Contribution getContributionWithFileName(String fileName) {
|
||||
return localDataSource.getContributionWithFileName(fileName);
|
||||
}
|
||||
|
||||
public LiveData<List<Contribution>> fetchContributions() {
|
||||
return localDataSource.getContributions();
|
||||
}
|
||||
|
||||
public Completable save(List<Contribution> contributions) {
|
||||
return localDataSource.saveContributions(contributions);
|
||||
}
|
||||
|
||||
public void set(String key, long value) {
|
||||
localDataSource.set(key,value);
|
||||
}
|
||||
|
||||
public long getLong(String key) {
|
||||
return localDataSource.getLong(key);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SyncResult;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.mwapi.UserClient;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_FILENAME;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
|
||||
private static final String[] existsQuery = {COLUMN_FILENAME};
|
||||
private static final String existsSelection = COLUMN_FILENAME + " = ?";
|
||||
private static final ContentValues[] EMPTY = {};
|
||||
|
||||
@Inject
|
||||
UserClient userClient;
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
JsonKvStore defaultKvStore;
|
||||
|
||||
public ContributionsSyncAdapter(Context context, boolean autoInitialize) {
|
||||
super(context, autoInitialize);
|
||||
}
|
||||
|
||||
private boolean fileExists(ContentProviderClient client, String filename) {
|
||||
if (filename == null) {
|
||||
return false;
|
||||
}
|
||||
try (Cursor cursor = client.query(BASE_URI,
|
||||
existsQuery,
|
||||
existsSelection,
|
||||
new String[]{filename},
|
||||
""
|
||||
)) {
|
||||
return cursor != null && cursor.getCount() != 0;
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle bundle, String authority,
|
||||
ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
||||
ApplicationlessInjection
|
||||
.getInstance(getContext()
|
||||
.getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
// This code is(was?) fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
||||
String user = account.name;
|
||||
ContributionDao contributionDao = new ContributionDao(() -> contentProviderClient);
|
||||
userClient.logEvents(user)
|
||||
.doOnNext(mwQueryLogEvent->Timber.d("Received image %s", mwQueryLogEvent.title() ))
|
||||
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted())
|
||||
.filter(mwQueryLogEvent -> !fileExists(contentProviderClient, mwQueryLogEvent.title()))
|
||||
.doOnNext(mwQueryLogEvent->Timber.d("Image %s passed filters", mwQueryLogEvent.title() ))
|
||||
.map(image -> new Contribution(null, null, image.title(),
|
||||
"", -1, image.date(), image.date(), user,
|
||||
"", "", STATE_COMPLETED))
|
||||
.map(contributionDao::toContentValues)
|
||||
.buffer(10)
|
||||
.subscribe(imageValues->contentProviderClient.bulkInsert(BASE_URI, imageValues.toArray(EMPTY)));
|
||||
Timber.d("Oh hai, everyone! Look, a kitty!");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package fr.free.nrw.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() {
|
||||
super.onCreate();
|
||||
synchronized (sSyncAdapterLock) {
|
||||
if (sSyncAdapter == null) {
|
||||
sSyncAdapter = new ContributionsSyncAdapter(this, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return sSyncAdapter.getSyncAdapterBinder();
|
||||
}
|
||||
}
|
||||
|
|
@ -113,8 +113,6 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
|
|||
|
||||
private void initMain() {
|
||||
//Do not remove this, this triggers the sync service
|
||||
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(),BuildConfig.CONTRIBUTION_AUTHORITY,true);
|
||||
requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle());
|
||||
Intent uploadServiceIntent = new Intent(this, UploadService.class);
|
||||
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
||||
startService(uploadServiceIntent);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public class DisplayableContribution extends Contribution {
|
|||
contribution.getWidth(),
|
||||
contribution.getHeight(),
|
||||
contribution.getLicense());
|
||||
this._id=contribution._id;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue