diff --git a/app/build.gradle b/app/build.gradle index 2197573ab4..52f240813c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,6 +102,11 @@ dependencies { //swipe_layout implementation 'com.daimajia.swipelayout:library:1.2.0@aar' + + //Room + def room_version= '2.2.3' + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor } android { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad88b720cf..baabce2f08 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -150,18 +150,6 @@ android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator" /> - - - - - - - - categories; // as loaded at runtime? - protected boolean requestedDeletion; - private Map descriptions; // multilingual descriptions as loaded - private HashMap tags = new HashMap<>(); - private @Nullable LatLng coordinates; + @PrimaryKey + @NonNull + public Uri localUri; + public String thumbUrl; + public String imageUrl; + public String filename; + public String description; // monolingual description on input... + public String discussion; + long dataLength; + public Date dateCreated; + @Nullable public Date dateUploaded; + public int width; + public int height; + public String license; + public String licenseUrl; + public String creator; + public ArrayList categories; // as loaded at runtime? + public boolean requestedDeletion; + public HashMap descriptions; // multilingual descriptions as loaded + public HashMap tags = new HashMap<>(); + @Nullable public LatLng coordinates; /** * Provides local constructor @@ -118,7 +123,7 @@ public Media(Parcel in) { dateCreated = (Date) in.readSerializable(); dateUploaded = (Date) in.readSerializable(); creator = in.readString(); - tags = (HashMap) in.readSerializable(); + tags = (HashMap) in.readSerializable(); width = in.readInt(); height = in.readInt(); license = in.readString(); @@ -218,7 +223,7 @@ public Object getTag(String key) { * @param key Media key * @param value Media value */ - public void setTag(String key, Object value) { + public void setTag(String key, String value) { tags.put(key, value); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index a9bda62532..7198b34c6f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import androidx.annotation.StringDef; +import androidx.room.Entity; import org.apache.commons.lang3.StringUtils; @@ -21,6 +22,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; +@Entity public class Contribution extends Media { //{{According to EXIF data|2009-01-09}} @@ -55,16 +57,16 @@ public Contribution[] newArray(int i) { 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; + 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, diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java index e1c134111e..ed0da4dac4 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java @@ -1,331 +1,36 @@ 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.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.Query; -import androidx.annotation.Nullable; +import java.util.List; -import org.apache.commons.lang3.StringUtils; +@Dao +public interface ContributionDao { -import java.util.Date; + @Query("SELECT * FROM contribution") + List loadAllContributions(); -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; + @Insert + public void save(Contribution contribution); -import fr.free.nrw.commons.settings.Prefs; -import timber.log.Timber; + @Insert + public void save(List 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; + @Delete + public void delete(Contribution contribution); -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) + @Query("SELECT * from contribution WHERE contentProviderUri=:uri") + public List getContributionWithUri(String uri); - 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 + ")"; + @Query("SELECT * from contribution WHERE filename=:fileName") + public List getContributionWithTitle(String fileName); - private final Provider clientProvider; + @Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)") + public void updateStates(int state, int[] toUpdateStates); - @Inject - public ContributionDao(@Named("contribution") Provider clientProvider) { - this.clientProvider = clientProvider; - } - - 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(); - } - } - - 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(); - } - } - - 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(); - } - } - - 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; - } - - 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) - ); - - String wikidataEntityId = cursor.getString(cursor.getColumnIndex(COLUMN_WIKI_DATA_ENTITY_ID)); - if (!StringUtils.isBlank(wikidataEntityId)) { - contribution.setWikiDataEntityId(wikidataEntityId); - } - - return contribution; - } - - 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") + void deleteAll(); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java deleted file mode 100644 index 4a504f1228..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java index d62e4358a1..90120d3791 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java @@ -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 interface View { void setUploadCount(int count); - void onDataSetChanged(); - } + void showContributions(List contributionList); - public interface UserActionListener extends BasePresenter, - LoaderManager.LoaderCallbacks { + void showMessage(String localizedMessage); + } - Contribution getContributionsFromCursor(Cursor cursor); + public interface UserActionListener extends BasePresenter { + Contribution getContributionsWithTitle(String uri); void deleteUpload(Contribution contribution); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index 32ddc4400b..7f952d3ae5 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -20,6 +20,8 @@ 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,6 +119,7 @@ public void onServiceDisconnected(ComponentName componentName) { }; private boolean shouldShowMediaDetailsFragment; private int numberOfContributions; + private boolean isAuthCookieAcquired; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -210,20 +212,10 @@ public void openMediaDetail(int position) { 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 void onBackStackChanged() { */ 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 @@ private void showDetail(int i) { @Override public void refreshSource() { - getActivity().getSupportLoaderManager().restartLoader(0, null, contributionsPresenter); + contributionsPresenter.fetchContributions(); } @Override @@ -411,6 +402,10 @@ public void onResume() { } fetchCampaigns(); + if(isAuthCookieAcquired){ + contributionsPresenter.fetchContributions(); + } + } private void checkPermissionsAndShowNearbyCardView() { @@ -578,9 +573,8 @@ public void setUploadCount(int count) { } @Override - public void onDataSetChanged() { - contributionsListFragment.onDataSetChanged(); - mediaDetailPagerFragment.onDataSetChanged(); + public void showContributions(List contributionList) { + contributionsListFragment.setContributions(contributionList); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java index 7bad3c8dee..09dab98d16 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java @@ -6,15 +6,20 @@ 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; public class ContributionsListAdapter extends RecyclerView.Adapter { private Callback callback; + private List contributions; public ContributionsListAdapter(Callback callback) { this.callback = callback; + contributions=new ArrayList<>(); } @NonNull @@ -28,7 +33,7 @@ public ContributionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int @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); @@ -36,7 +41,12 @@ public void onBindViewHolder(@NonNull ContributionViewHolder holder, int positio @Override public int getItemCount() { - return callback.getNumberOfContributions(); + return contributions==null?0:contributions.size(); + } + + public void setContributions(List contributionList) { + this.contributions=contributionList; + notifyDataSetChanged(); } public interface Callback { @@ -47,10 +57,6 @@ public interface Callback { void openMediaDetail(int contribution); - int getNumberOfContributions(); - Contribution getContributionForPosition(int position); - - int findItemPositionWithId(String lastVisibleItemID); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index de564196f6..8a684ef44b 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -20,6 +20,8 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; +import java.util.List; + import javax.inject.Inject; import javax.inject.Named; @@ -72,6 +74,13 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { private String lastVisibleItemID; private int SPAN_COUNT=3; + private List contributions; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_contributions_list, container, false); @@ -178,16 +187,9 @@ public void showNoContributionsUI(boolean shouldShow) { 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 contributionList) { + this.contributions=contributionList; + adapter.setContributions(contributionList); } public interface SourceRefresher { @@ -228,7 +230,7 @@ public void onViewStateRestored(@Nullable Bundle savedInstanceState) { private String findIdOfItemWithPosition(int position) { Contribution contributionForPosition = callback.getContributionForPosition(position); if (null != contributionForPosition) { - return contributionForPosition.getContentUri().getLastPathSegment(); + return contributionForPosition.getFilename(); } return null; } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java index 672d8ba6b2..a4e9ee13fa 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java @@ -1,10 +1,11 @@ package fr.free.nrw.commons.contributions; -import android.database.Cursor; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; +import fr.free.nrw.commons.db.AppDatabase; import fr.free.nrw.commons.kvstore.JsonKvStore; /** @@ -12,15 +13,15 @@ */ class ContributionsLocalDataSource { - private final ContributionDao contributionsDao; + private final ContributionDao contributionDao; private final JsonKvStore defaultKVStore; @Inject public ContributionsLocalDataSource( @Named("default_preferences") JsonKvStore defaultKVStore, - ContributionDao contributionDao) { + AppDatabase appDatabase) { this.defaultKVStore = defaultKVStore; - this.contributionsDao = contributionDao; + this.contributionDao = appDatabase.getContributionDao(); } /** @@ -32,11 +33,15 @@ public int get(String 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 contributionWithUri = contributionDao.getContributionWithTitle(uri); + if(null!=contributionWithUri && contributionWithUri.size()>0){ + return contributionWithUri.get(0); + } + return null; } /** @@ -44,6 +49,6 @@ public Contribution getContributionFromCursor(Cursor cursor) { * @param contribution */ public void deleteContribution(Contribution contribution) { - contributionsDao.delete(contribution); + contributionDao.delete(contribution); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java index cfb848c0fd..3d2fe98cf9 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java @@ -3,104 +3,112 @@ import android.content.Context; import android.database.Cursor; import android.database.DataSetObserver; -import android.os.Bundle; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; + +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; +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.mwapi.UserClient; +import fr.free.nrw.commons.utils.NetworkUtils; +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +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 CompositeDisposable compositeDisposable; private ContributionsContract.View view; - private Cursor cursor; + private List contributionList; @Inject Context context; + @Inject + UserClient userClient; + + @Inject + AppDatabase appDatabase; + + @Inject + SessionManager sessionManager; + @Inject ContributionsPresenter(ContributionsRepository repository) { this.repository = repository; + compositeDisposable=new CompositeDisposable(); } + 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); - } - } - } - - @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 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=new CompositeDisposable(); } - @Override - public void onLoadFinished(@NonNull Loader 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); + void fetchContributions() { + if (NetworkUtils.isInternetConnectionEstablished(CommonsApplication.getInstance())) { + view.showProgress(true); + this.user = sessionManager.getUserName(); + view.showContributions(null); + contributionList=new ArrayList<>(); + compositeDisposable.add(userClient.logEvents(user) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .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 -> new Contribution(null, null, image.title(), + "", -1, image.date(), image.date(), user, + "", "", STATE_COMPLETED)).buffer(100) + .subscribe(imageValues -> { + this.contributionList.addAll(imageValues); + view.showProgress(false); + if (imageValues != null && imageValues.size() > 0) { + view.showWelcomeTip(false); + view.showNoContributionsUI(false); + view.setUploadCount(imageValues.size()); + view.showContributions(contributionList); + } else { + view.showWelcomeTip(true); + view.showNoContributionsUI(true); + } + Observable.fromIterable(contributionList) + .subscribeOn(Schedulers.io()) + .doOnEach(imageValue -> appDatabase.getContributionDao().save(imageValue.getValue())); + }, error -> { + view.showProgress(false); + view.showMessage(error.getLocalizedMessage()); + //TODO show error + })); } - swapCursor(cursor); } @Override - public void onLoaderReset(@NonNull Loader loader) { - this.cursor = null; - //On LoadFinished is not guaranteed to be called - view.showProgress(false); - view.showWelcomeTip(true); - view.showNoContributionsUI(true); - swapCursor(null); + public void onDetachView() { + this.view = null; + compositeDisposable.clear(); } - /** - * Get contribution from the repository - */ @Override - public Contribution getContributionsFromCursor(Cursor cursor) { - return repository.getContributionFromCursor(cursor); + public Contribution getContributionsWithTitle(String title) { + return repository.getContributionWithFileName(title); } /** @@ -120,64 +128,18 @@ public void deleteUpload(Contribution contribution) { @Nullable @Override public Media getItemAtPosition(int i) { - if (null != cursor && cursor.moveToPosition(i)) { - return getContributionsFromCursor(cursor); - } - return null; + return contributionList==null?null:contributionList.get(i); } /** * 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 not - * 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); + public int getChildPositionWithId(String fileName) { + for (Contribution contribution : contributionList) { + if (contribution.getFilename().equals(fileName)) { + return contributionList.indexOf(contribution); } - view.onDataSetChanged(); - } catch (IllegalStateException e) {//Cursor might [not] be already registered/unregistered - Timber.e(e); } + return -1; } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java index 4c479e3000..06e8404a49 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java @@ -1,7 +1,5 @@ package fr.free.nrw.commons.contributions; -import android.database.Cursor; - import javax.inject.Inject; /** @@ -23,16 +21,6 @@ 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); - } - /** * Deletes a failed upload from DB * @param contribution @@ -40,4 +28,13 @@ public Contribution getContributionFromCursor(Cursor cursor) { public void deleteContributionFromDB(Contribution contribution) { localDataSource.deleteContribution(contribution); } + + /** + * Get contribution object with title + * @param fileName + * @return + */ + public Contribution getContributionWithFileName(String fileName) { + return localDataSource.getContributionWithFileName(fileName); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java deleted file mode 100644 index 4ce126f37a..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java +++ /dev/null @@ -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!"); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncService.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncService.java deleted file mode 100644 index 946da69158..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncService.java +++ /dev/null @@ -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(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 96c32d5c46..eff6cc2ce2 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -112,8 +112,6 @@ protected void onSaveInstanceState(Bundle outState) { 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); diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java index aaade6277f..c5498e4566 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java @@ -7,7 +7,6 @@ import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; import fr.free.nrw.commons.category.CategoryDao; -import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; public class DBOpenHelper extends SQLiteOpenHelper { @@ -25,7 +24,6 @@ public DBOpenHelper(Context context) { @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { - ContributionDao.Table.onCreate(sqLiteDatabase); CategoryDao.Table.onCreate(sqLiteDatabase); BookmarkPicturesDao.Table.onCreate(sqLiteDatabase); BookmarkLocationsDao.Table.onCreate(sqLiteDatabase); @@ -34,7 +32,6 @@ public void onCreate(SQLiteDatabase sqLiteDatabase) { @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) { - ContributionDao.Table.onUpdate(sqLiteDatabase, from, to); CategoryDao.Table.onUpdate(sqLiteDatabase, from, to); BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to); BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to); diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.java b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.java new file mode 100644 index 0000000000..fefb69ae28 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.java @@ -0,0 +1,14 @@ +package fr.free.nrw.commons.db; + +import androidx.room.Database; +import androidx.room.RoomDatabase; +import androidx.room.TypeConverters; + +import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.ContributionDao; + +@Database(entities = {Contribution.class}, version = 1) +@TypeConverters({Converters.class}) +abstract public class AppDatabase extends RoomDatabase { + public abstract ContributionDao getContributionDao(); +} diff --git a/app/src/main/java/fr/free/nrw/commons/db/Converters.java b/app/src/main/java/fr/free/nrw/commons/db/Converters.java new file mode 100644 index 0000000000..8a3c60b4fa --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/db/Converters.java @@ -0,0 +1,67 @@ +package fr.free.nrw.commons.db; + +import android.net.Uri; + +import androidx.room.TypeConverter; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; + +import fr.free.nrw.commons.location.LatLng; + +public class Converters { + @TypeConverter + public static Date fromTimestamp(Long value) { + return value == null ? null : new Date(value); + } + + @TypeConverter + public static Long dateToTimestamp(Date date) { + return date == null ? null : date.getTime(); + } + + @TypeConverter + public static Uri fromString(String value) { + return value == null ? null : Uri.parse(value); + } + + @TypeConverter + public static String uriToString(Uri uri) { + return uri == null ? null : uri.toString(); + } + + @TypeConverter + public static String listObjectToString(ArrayList objectList) { + return objectList == null ? null : new Gson().toJson(objectList); + } + + @TypeConverter + public static ArrayList stringToArrayListObject(String objectList) { + return objectList == null ? null : new Gson().fromJson(objectList,new TypeToken>(){}.getType()); + } + + @TypeConverter + public static String mapObjectToString(HashMap objectList) { + return objectList == null ? null : new Gson().toJson(objectList); + } + + @TypeConverter + public static HashMap stringToMap(String objectList) { + return objectList == null ? null : new Gson().fromJson(objectList,new TypeToken>(){}.getType()); + } + + @TypeConverter + public static String latlngObjectToString(LatLng latlng) { + return latlng == null ? null : new Gson().toJson(latlng); + } + + @TypeConverter + public static LatLng stringToLatLng(String objectList) { + return objectList == null ? null : new Gson().fromJson(objectList,LatLng.class); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index 242f889df0..83c0154afd 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -10,7 +10,6 @@ import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.contributions.ContributionViewHolder; import fr.free.nrw.commons.contributions.ContributionsModule; -import fr.free.nrw.commons.contributions.ContributionsSyncAdapter; import fr.free.nrw.commons.nearby.PlaceRenderer; import fr.free.nrw.commons.review.ReviewController; import fr.free.nrw.commons.settings.SettingsFragment; @@ -33,8 +32,6 @@ public interface CommonsApplicationComponent extends AndroidInjector { void inject(CommonsApplication application); - void inject(ContributionsSyncAdapter syncAdapter); - void inject(LoginActivity activity); void inject(SettingsFragment fragment); diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index f1a2ef5396..04a2e04717 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -6,6 +6,7 @@ import android.view.inputmethod.InputMethodManager; import androidx.collection.LruCache; +import androidx.room.Room; import com.github.varunpant.quadtree.QuadTree; import com.google.gson.Gson; @@ -29,6 +30,7 @@ import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.caching.CacheController; import fr.free.nrw.commons.data.DBOpenHelper; +import fr.free.nrw.commons.db.AppDatabase; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.settings.Prefs; @@ -209,4 +211,9 @@ public String provideLoggedInUsername() { public QuadTree providesQuadTres() { return new QuadTree<>(-180, -90, +180, +90); } + + @Provides + public AppDatabase provideAppDataBase() { + return Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db").build(); + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java index 10c0cfcb14..e508bcd2ed 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java @@ -5,16 +5,12 @@ import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider; import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider; import fr.free.nrw.commons.category.CategoryContentProvider; -import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider; @Module @SuppressWarnings({"WeakerAccess", "unused"}) public abstract class ContentProviderBuilderModule { - @ContributesAndroidInjector - abstract ContributionsContentProvider bindContributionsContentProvider(); - @ContributesAndroidInjector abstract CategoryContentProvider bindCategoryContentProvider(); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 306a01c980..0a74184336 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -28,10 +28,11 @@ 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.ContributionsContentProvider; import fr.free.nrw.commons.contributions.MainActivity; +import fr.free.nrw.commons.db.AppDatabase; import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.utils.CommonsDateUtil; +import fr.free.nrw.commons.utils.ExecutorUtils; import fr.free.nrw.commons.wikidata.WikidataEditService; import io.reactivex.Observable; import io.reactivex.schedulers.Schedulers; @@ -46,10 +47,10 @@ public class UploadService extends HandlerService { public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload"; public static final String EXTRA_SOURCE = EXTRA_PREFIX + ".source"; public static final String EXTRA_FILES = EXTRA_PREFIX + ".files"; - @Inject WikidataEditService wikidataEditService; @Inject SessionManager sessionManager; - @Inject ContributionDao contributionDao; + @Inject + AppDatabase appDatabase; @Inject UploadClient uploadClient; @Inject MediaClient mediaClient; @@ -105,7 +106,7 @@ public void onProgress(long transferred, long total) { notificationManager.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build()); contribution.setTransferred(transferred); - contributionDao.save(contribution); + ExecutorUtils.get().submit(() -> appDatabase.getContributionDao().save(contribution)); } } @@ -143,7 +144,7 @@ public void queue(int what, Contribution contribution) { contribution.setState(Contribution.STATE_QUEUED); contribution.setTransferred(0); - contributionDao.save(contribution); + ExecutorUtils.get().submit(() -> appDatabase.getContributionDao().save(contribution)); toUpload++; if (curNotification != null && toUpload != 1) { curNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload)); @@ -163,16 +164,7 @@ public void queue(int what, Contribution contribution) { @Override public int onStartCommand(Intent intent, int flags, int startId) { if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) { - ContentValues failedValues = new ContentValues(); - failedValues.put(ContributionDao.Table.COLUMN_STATE, Contribution.STATE_FAILED); - - int updated = getContentResolver().update(ContributionsContentProvider.BASE_URI, - failedValues, - 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); - Timber.d("Flags is %d id is %d", flags, startId); + ExecutorUtils.get().submit(() -> appDatabase.getContributionDao().updateStates(Contribution.STATE_FAILED,new int[]{Contribution.STATE_QUEUED,Contribution.STATE_IN_PROGRESS})); freshStart = false; } return START_REDELIVER_INTENT; @@ -272,7 +264,7 @@ private void uploadContribution(Contribution contribution) { contribution.setState(Contribution.STATE_COMPLETED); contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatShort() .parse(uploadResult.getImageinfo().getTimestamp())); - contributionDao.save(contribution); + ExecutorUtils.get().submit(() -> appDatabase.getContributionDao().save(contribution)); } }, throwable -> { Timber.w(throwable, "Exception during upload"); @@ -291,7 +283,7 @@ private void showFailedNotification(Contribution contribution) { notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_FAILED, curNotification.build()); contribution.setState(Contribution.STATE_FAILED); - contributionDao.save(contribution); + ExecutorUtils.get().submit(() -> appDatabase.getContributionDao().save(contribution)); } private String findUniqueFilename(String fileName) throws IOException { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java index 57ed86eebb..5ea4a237bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java @@ -4,6 +4,7 @@ import android.os.Looper; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; public class ExecutorUtils { @@ -19,4 +20,10 @@ public static Executor uiExecutor() { return uiExecutor; } + private static final ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(3); + + public static ExecutorService get() { + return executor; + } + } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsPresenterTest.kt index 2dee682c81..9b20593452 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsPresenterTest.kt @@ -47,7 +47,7 @@ class ContributionsPresenterTest { */ @Test fun testGetContributionFromCursor() { - contributionsPresenter?.getContributionsFromCursor(cursor) + contributionsPresenter?.getContributionsWithTitle(cursor) verify(repository)?.getContributionFromCursor(cursor) } diff --git a/captures/fr.free.nrw.commons.beta_2020.01.13_11.01.li b/captures/fr.free.nrw.commons.beta_2020.01.13_11.01.li new file mode 100644 index 0000000000..3d87b4a497 Binary files /dev/null and b/captures/fr.free.nrw.commons.beta_2020.01.13_11.01.li differ