mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +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
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -38,3 +38,4 @@ app/src/main/jniLibs
|
||||||
#Below removes all the HTML files related to OpenCV documentation. The documentation can be otherwise found at:
|
#Below removes all the HTML files related to OpenCV documentation. The documentation can be otherwise found at:
|
||||||
#https://docs.opencv.org/3.3.0/
|
#https://docs.opencv.org/3.3.0/
|
||||||
/libraries/opencv/javadoc/
|
/libraries/opencv/javadoc/
|
||||||
|
captures/*
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,14 @@ dependencies {
|
||||||
|
|
||||||
//swipe_layout
|
//swipe_layout
|
||||||
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
|
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
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.7.1'
|
implementation 'com.squareup.retrofit2:retrofit:2.7.1'
|
||||||
|
implementation "androidx.room:room-rxjava2:$room_version"
|
||||||
|
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@
|
||||||
android:name=".contributions.MainActivity"
|
android:name=".contributions.MainActivity"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:configChanges="screenSize|keyboard" />
|
android:configChanges="screenSize|keyboard|orientation" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
android:label="@string/title_activity_settings" />
|
android:label="@string/title_activity_settings" />
|
||||||
|
|
@ -157,18 +157,6 @@
|
||||||
android:name="android.accounts.AccountAuthenticator"
|
android:name="android.accounts.AccountAuthenticator"
|
||||||
android:resource="@xml/authenticator" />
|
android:resource="@xml/authenticator" />
|
||||||
</service>
|
</service>
|
||||||
<service
|
|
||||||
android:name=".contributions.ContributionsSyncService"
|
|
||||||
android:exported="true"
|
|
||||||
android:process=":sync">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.content.SyncAdapter" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.content.SyncAdapter"
|
|
||||||
android:resource="@xml/contributions_sync_adapter" />
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="org.acra.sender.SenderService"
|
android:name="org.acra.sender.SenderService"
|
||||||
|
|
@ -184,12 +172,6 @@
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
<provider
|
|
||||||
android:name=".contributions.ContributionsContentProvider"
|
|
||||||
android:authorities="${applicationId}.contributions.contentprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/provider_contributions"
|
|
||||||
android:syncable="true" />
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".category.CategoryContentProvider"
|
android:name=".category.CategoryContentProvider"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
@ -44,8 +45,8 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
||||||
import fr.free.nrw.commons.category.CategoryDao;
|
import fr.free.nrw.commons.category.CategoryDao;
|
||||||
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler;
|
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler;
|
||||||
import fr.free.nrw.commons.concurrency.ThreadPoolService;
|
import fr.free.nrw.commons.concurrency.ThreadPoolService;
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
|
import fr.free.nrw.commons.db.AppDatabase;
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.logging.FileLoggingTree;
|
import fr.free.nrw.commons.logging.FileLoggingTree;
|
||||||
|
|
@ -60,6 +61,7 @@ import io.reactivex.schedulers.Schedulers;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static fr.free.nrw.commons.data.DBOpenHelper.CONTRIBUTIONS_TABLE;
|
||||||
import static org.acra.ReportField.ANDROID_VERSION;
|
import static org.acra.ReportField.ANDROID_VERSION;
|
||||||
import static org.acra.ReportField.APP_VERSION_CODE;
|
import static org.acra.ReportField.APP_VERSION_CODE;
|
||||||
import static org.acra.ReportField.APP_VERSION_NAME;
|
import static org.acra.ReportField.APP_VERSION_NAME;
|
||||||
|
|
@ -126,6 +128,9 @@ public class CommonsApplication extends Application {
|
||||||
return languageLookUpTable;
|
return languageLookUpTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AppDatabase appDatabase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to declare and initialize various components and dependencies
|
* Used to declare and initialize various components and dependencies
|
||||||
*/
|
*/
|
||||||
|
|
@ -306,11 +311,13 @@ public class CommonsApplication extends Application {
|
||||||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
||||||
|
|
||||||
CategoryDao.Table.onDelete(db);
|
CategoryDao.Table.onDelete(db);
|
||||||
ContributionDao.Table.onDelete(db);
|
dbOpenHelper.deleteTable(db,CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions
|
||||||
|
appDatabase.getContributionDao().deleteAll();
|
||||||
BookmarkPicturesDao.Table.onDelete(db);
|
BookmarkPicturesDao.Table.onDelete(db);
|
||||||
BookmarkLocationsDao.Table.onDelete(db);
|
BookmarkLocationsDao.Table.onDelete(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface used to get log-out events
|
* Interface used to get log-out events
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import android.os.Parcelable;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryPage;
|
import org.wikipedia.dataclient.mwapi.MwQueryPage;
|
||||||
|
|
@ -26,6 +28,7 @@ import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||||
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
|
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
|
||||||
|
|
||||||
|
@Entity
|
||||||
public class Media implements Parcelable {
|
public class Media implements Parcelable {
|
||||||
|
|
||||||
public static final Media EMPTY = new Media("");
|
public static final Media EMPTY = new Media("");
|
||||||
|
|
@ -42,25 +45,25 @@ public class Media implements Parcelable {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Primary metadata fields
|
// Primary metadata fields
|
||||||
protected Uri localUri;
|
public Uri localUri;
|
||||||
private String thumbUrl;
|
public String thumbUrl;
|
||||||
protected String imageUrl;
|
public String imageUrl;
|
||||||
protected String filename;
|
public String filename;
|
||||||
protected String description; // monolingual description on input...
|
public String description; // monolingual description on input...
|
||||||
protected String discussion;
|
public String discussion;
|
||||||
protected long dataLength;
|
long dataLength;
|
||||||
protected Date dateCreated;
|
public Date dateCreated;
|
||||||
protected @Nullable Date dateUploaded;
|
@Nullable public Date dateUploaded;
|
||||||
protected int width;
|
public int width;
|
||||||
protected int height;
|
public int height;
|
||||||
protected String license;
|
public String license;
|
||||||
protected String licenseUrl;
|
public String licenseUrl;
|
||||||
protected String creator;
|
public String creator;
|
||||||
protected ArrayList<String> categories; // as loaded at runtime?
|
public ArrayList<String> categories; // as loaded at runtime?
|
||||||
protected boolean requestedDeletion;
|
public boolean requestedDeletion;
|
||||||
private Map<String, String> descriptions; // multilingual descriptions as loaded
|
public HashMap<String, String> descriptions; // multilingual descriptions as loaded
|
||||||
private HashMap<String, Object> tags = new HashMap<>();
|
public HashMap<String, String> tags = new HashMap<>();
|
||||||
private @Nullable LatLng coordinates;
|
@Nullable public LatLng coordinates;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides local constructor
|
* Provides local constructor
|
||||||
|
|
@ -118,7 +121,7 @@ public class Media implements Parcelable {
|
||||||
dateCreated = (Date) in.readSerializable();
|
dateCreated = (Date) in.readSerializable();
|
||||||
dateUploaded = (Date) in.readSerializable();
|
dateUploaded = (Date) in.readSerializable();
|
||||||
creator = in.readString();
|
creator = in.readString();
|
||||||
tags = (HashMap<String, Object>) in.readSerializable();
|
tags = (HashMap<String, String>) in.readSerializable();
|
||||||
width = in.readInt();
|
width = in.readInt();
|
||||||
height = in.readInt();
|
height = in.readInt();
|
||||||
license = in.readString();
|
license = in.readString();
|
||||||
|
|
@ -218,7 +221,7 @@ public class Media implements Parcelable {
|
||||||
* @param key Media key
|
* @param key Media key
|
||||||
* @param value Media value
|
* @param value Media value
|
||||||
*/
|
*/
|
||||||
public void setTag(String key, Object value) {
|
public void setTag(String key, String value) {
|
||||||
tags.put(key, value);
|
tags.put(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import android.os.Parcel;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.StringDef;
|
import androidx.annotation.StringDef;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
|
@ -21,6 +23,7 @@ import fr.free.nrw.commons.utils.ConfigUtils;
|
||||||
|
|
||||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
|
|
||||||
|
@Entity(tableName = "contribution")
|
||||||
public class Contribution extends Media {
|
public class Contribution extends Media {
|
||||||
|
|
||||||
//{{According to Exif data|2009-01-09}}
|
//{{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_CAMERA = "camera";
|
||||||
public static final String SOURCE_GALLERY = "gallery";
|
public static final String SOURCE_GALLERY = "gallery";
|
||||||
public static final String SOURCE_EXTERNAL = "external";
|
public static final String SOURCE_EXTERNAL = "external";
|
||||||
|
@PrimaryKey (autoGenerate = true)
|
||||||
private Uri contentUri;
|
@NonNull
|
||||||
private String source;
|
public long _id;
|
||||||
private String editSummary;
|
public Uri contentUri;
|
||||||
private int state;
|
public String source;
|
||||||
private long transferred;
|
public String editSummary;
|
||||||
private String decimalCoords;
|
public int state;
|
||||||
private boolean isMultiple;
|
public long transferred;
|
||||||
private String wikiDataEntityId;
|
public String decimalCoords;
|
||||||
private Uri contentProviderUri;
|
public boolean isMultiple;
|
||||||
private String dateCreatedSource;
|
public String wikiDataEntityId;
|
||||||
|
public Uri contentProviderUri;
|
||||||
|
public String dateCreatedSource;
|
||||||
|
|
||||||
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated,
|
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated,
|
||||||
int state, long dataLength, Date dateUploaded, long transferred,
|
int state, long dataLength, Date dateUploaded, long transferred,
|
||||||
|
|
|
||||||
|
|
@ -1,331 +1,55 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
import androidx.lifecycle.LiveData;
|
||||||
import android.content.ContentValues;
|
import androidx.room.Dao;
|
||||||
import android.database.Cursor;
|
import androidx.room.Delete;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import androidx.room.Insert;
|
||||||
import android.database.sqlite.SQLiteException;
|
import androidx.room.OnConflictStrategy;
|
||||||
import android.net.Uri;
|
import androidx.room.Query;
|
||||||
import android.os.RemoteException;
|
import androidx.room.Transaction;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
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;
|
@Query("SELECT * FROM contribution order by dateUploaded DESC")
|
||||||
import javax.inject.Named;
|
abstract LiveData<List<Contribution>> fetchContributions();
|
||||||
import javax.inject.Provider;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
import timber.log.Timber;
|
public abstract Single<Long> save(Contribution contribution);
|
||||||
|
|
||||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
public Completable deleteAllAndSave(List<Contribution> contributions){
|
||||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_WIKI_DATA_ENTITY_ID;
|
return Completable.fromAction(() -> deleteAllAndSaveTransaction(contributions));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Cursor loadAllContributions() {
|
@Transaction
|
||||||
ContentProviderClient db = clientProvider.get();
|
public void deleteAllAndSaveTransaction(List<Contribution> contributions){
|
||||||
try {
|
deleteAll(Contribution.STATE_COMPLETED);
|
||||||
return db.query(BASE_URI, ALL_FIELDS, "", null, CONTRIBUTION_SORT);
|
save(contributions);
|
||||||
} catch (RemoteException e) {
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save(Contribution contribution) {
|
@Insert
|
||||||
ContentProviderClient db = clientProvider.get();
|
public abstract void save(List<Contribution> contribution);
|
||||||
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) {
|
@Delete
|
||||||
ContentProviderClient db = clientProvider.get();
|
public abstract Single<Integer> delete(Contribution contribution);
|
||||||
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) {
|
@Query("SELECT * from contribution WHERE contentProviderUri=:uri")
|
||||||
ContentValues cv = new ContentValues();
|
public abstract List<Contribution> getContributionWithUri(String uri);
|
||||||
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) {
|
@Query("SELECT * from contribution WHERE filename=:fileName")
|
||||||
// Hardcoding column positions!
|
public abstract List<Contribution> getContributionWithTitle(String fileName);
|
||||||
//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));
|
@Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
|
||||||
if (!StringUtils.isBlank(wikidataEntityId)) {
|
public abstract Single<Integer> updateStates(int state, int[] toUpdateStates);
|
||||||
contribution.setWikiDataEntityId(wikidataEntityId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return contribution;
|
@Query("Delete FROM contribution")
|
||||||
}
|
public abstract void deleteAll();
|
||||||
|
|
||||||
return null;
|
@Query("Delete FROM contribution WHERE state = :state")
|
||||||
}
|
public abstract void deleteAll(int state);
|
||||||
|
|
||||||
@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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import android.database.Cursor;
|
import java.util.List;
|
||||||
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.BasePresenter;
|
import fr.free.nrw.commons.BasePresenter;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
|
|
@ -22,13 +20,13 @@ public class ContributionsContract {
|
||||||
|
|
||||||
void setUploadCount(int count);
|
void setUploadCount(int count);
|
||||||
|
|
||||||
void onDataSetChanged();
|
void showContributions(List<Contribution> contributionList);
|
||||||
|
|
||||||
|
void showMessage(String localizedMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface UserActionListener extends BasePresenter<ContributionsContract.View>,
|
public interface UserActionListener extends BasePresenter<ContributionsContract.View> {
|
||||||
LoaderManager.LoaderCallbacks<Cursor> {
|
Contribution getContributionsWithTitle(String uri);
|
||||||
|
|
||||||
Contribution getContributionsFromCursor(Cursor cursor);
|
|
||||||
|
|
||||||
void deleteUpload(Contribution contribution);
|
void deleteUpload(Contribution contribution);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
|
@ -71,7 +73,6 @@ public class ContributionsFragment
|
||||||
LocationUpdateListener,
|
LocationUpdateListener,
|
||||||
ICampaignsView, ContributionsContract.View {
|
ICampaignsView, ContributionsContract.View {
|
||||||
@Inject @Named("default_preferences") JsonKvStore store;
|
@Inject @Named("default_preferences") JsonKvStore store;
|
||||||
@Inject ContributionDao contributionDao;
|
|
||||||
@Inject NearbyController nearbyController;
|
@Inject NearbyController nearbyController;
|
||||||
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
|
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
|
||||||
@Inject CampaignsPresenter presenter;
|
@Inject CampaignsPresenter presenter;
|
||||||
|
|
@ -118,11 +119,11 @@ public class ContributionsFragment
|
||||||
};
|
};
|
||||||
private boolean shouldShowMediaDetailsFragment;
|
private boolean shouldShowMediaDetailsFragment;
|
||||||
private int numberOfContributions;
|
private int numberOfContributions;
|
||||||
|
private boolean isAuthCookieAcquired;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setRetainInstance(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
@ -132,6 +133,7 @@ public class ContributionsFragment
|
||||||
ButterKnife.bind(this, view);
|
ButterKnife.bind(this, view);
|
||||||
presenter.onAttachView(this);
|
presenter.onAttachView(this);
|
||||||
contributionsPresenter.onAttachView(this);
|
contributionsPresenter.onAttachView(this);
|
||||||
|
contributionsPresenter.setLifeCycleOwner(this.getViewLifecycleOwner());
|
||||||
campaignView.setVisibility(View.GONE);
|
campaignView.setVisibility(View.GONE);
|
||||||
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
|
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
|
||||||
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
|
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
|
||||||
|
|
@ -210,20 +212,10 @@ public class ContributionsFragment
|
||||||
showDetail(position);
|
showDetail(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getNumberOfContributions() {
|
|
||||||
return numberOfContributions;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Contribution getContributionForPosition(int position) {
|
public Contribution getContributionForPosition(int position) {
|
||||||
return (Contribution) contributionsPresenter.getItemAtPosition(position);
|
return (Contribution) contributionsPresenter.getItemAtPosition(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int findItemPositionWithId(String id) {
|
|
||||||
return contributionsPresenter.getChildPositionWithId(id);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if(null==mediaDetailPagerFragment){
|
if(null==mediaDetailPagerFragment){
|
||||||
|
|
@ -306,11 +298,10 @@ public class ContributionsFragment
|
||||||
*/
|
*/
|
||||||
void onAuthCookieAcquired() {
|
void onAuthCookieAcquired() {
|
||||||
// Since we call onAuthCookieAcquired method from onAttach, isAdded is still false. So don't use it
|
// 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
|
if (getActivity() != null) { // If fragment is attached to parent activity
|
||||||
getActivity().bindService(getUploadServiceIntent(), uploadServiceConnection, Context.BIND_AUTO_CREATE);
|
getActivity().bindService(getUploadServiceIntent(), uploadServiceConnection, Context.BIND_AUTO_CREATE);
|
||||||
isUploadServiceConnected = true;
|
isUploadServiceConnected = true;
|
||||||
getActivity().getSupportLoaderManager().initLoader(0, null, contributionsPresenter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -336,7 +327,7 @@ public class ContributionsFragment
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void refreshSource() {
|
public void refreshSource() {
|
||||||
getActivity().getSupportLoaderManager().restartLoader(0, null, contributionsPresenter);
|
contributionsPresenter.fetchContributions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -411,6 +402,10 @@ public class ContributionsFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchCampaigns();
|
fetchCampaigns();
|
||||||
|
if(isAuthCookieAcquired){
|
||||||
|
contributionsPresenter.fetchContributions();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkPermissionsAndShowNearbyCardView() {
|
private void checkPermissionsAndShowNearbyCardView() {
|
||||||
|
|
@ -578,9 +573,8 @@ public class ContributionsFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDataSetChanged() {
|
public void showContributions(List<Contribution> contributionList) {
|
||||||
contributionsListFragment.onDataSetChanged();
|
contributionsListFragment.setContributions(contributionList);
|
||||||
mediaDetailPagerFragment.onDataSetChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ import android.view.ViewGroup;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.contributions.model.DisplayableContribution;
|
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> {
|
public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionViewHolder> {
|
||||||
|
|
||||||
private Callback callback;
|
private Callback callback;
|
||||||
|
private List<Contribution> contributions;
|
||||||
|
|
||||||
public ContributionsListAdapter(Callback callback) {
|
public ContributionsListAdapter(Callback callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
contributions=new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,7 +40,7 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
|
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,
|
DisplayableContribution displayableContribution = new DisplayableContribution(contribution,
|
||||||
position);
|
position);
|
||||||
holder.init(position, displayableContribution);
|
holder.init(position, displayableContribution);
|
||||||
|
|
@ -43,7 +48,15 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
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 {
|
public interface Callback {
|
||||||
|
|
@ -54,10 +67,6 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
|
||||||
|
|
||||||
void openMediaDetail(int contribution);
|
void openMediaDetail(int contribution);
|
||||||
|
|
||||||
int getNumberOfContributions();
|
|
||||||
|
|
||||||
Contribution getContributionForPosition(int position);
|
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 com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
|
@ -72,6 +75,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
private String lastVisibleItemID;
|
private String lastVisibleItemID;
|
||||||
|
|
||||||
private int SPAN_COUNT=3;
|
private int SPAN_COUNT=3;
|
||||||
|
private List<Contribution> contributions=new ArrayList<>();
|
||||||
|
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
|
View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
|
||||||
|
|
@ -104,6 +108,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
rvContributionsList.setAdapter(adapter);
|
rvContributionsList.setAdapter(adapter);
|
||||||
|
adapter.setContributions(contributions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -178,16 +183,10 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onDataSetChanged() {
|
public void setContributions(List<Contribution> contributionList) {
|
||||||
if (null != adapter) {
|
this.contributions.clear();
|
||||||
adapter.notifyDataSetChanged();
|
this.contributions.addAll(contributionList);
|
||||||
//Restoring last visible item position in cases of orientation change
|
adapter.setContributions(contributions);
|
||||||
if (null != lastVisibleItemID) {
|
|
||||||
int itemPositionWithId = callback.findItemPositionWithId(lastVisibleItemID);
|
|
||||||
rvContributionsList.scrollToPosition(itemPositionWithId);
|
|
||||||
lastVisibleItemID = null;//Reset the lastVisibleItemID once we have used it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface SourceRefresher {
|
public interface SourceRefresher {
|
||||||
|
|
@ -228,7 +227,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
private String findIdOfItemWithPosition(int position) {
|
private String findIdOfItemWithPosition(int position) {
|
||||||
Contribution contributionForPosition = callback.getContributionForPosition(position);
|
Contribution contributionForPosition = callback.getContributionForPosition(position);
|
||||||
if (null != contributionForPosition) {
|
if (null != contributionForPosition) {
|
||||||
return contributionForPosition.getContentUri().getLastPathSegment();
|
return contributionForPosition.getFilename();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
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.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
|
import io.reactivex.Completable;
|
||||||
|
import io.reactivex.Single;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The LocalDataSource class for Contributions
|
* The LocalDataSource class for Contributions
|
||||||
*/
|
*/
|
||||||
class ContributionsLocalDataSource {
|
class ContributionsLocalDataSource {
|
||||||
|
|
||||||
private final ContributionDao contributionsDao;
|
private final ContributionDao contributionDao;
|
||||||
private final JsonKvStore defaultKVStore;
|
private final JsonKvStore defaultKVStore;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -20,30 +24,54 @@ class ContributionsLocalDataSource {
|
||||||
@Named("default_preferences") JsonKvStore defaultKVStore,
|
@Named("default_preferences") JsonKvStore defaultKVStore,
|
||||||
ContributionDao contributionDao) {
|
ContributionDao contributionDao) {
|
||||||
this.defaultKVStore = defaultKVStore;
|
this.defaultKVStore = defaultKVStore;
|
||||||
this.contributionsDao = contributionDao;
|
this.contributionDao = contributionDao;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch default number of contributions to be show, based on user preferences
|
* Fetch default number of contributions to be show, based on user preferences
|
||||||
*/
|
*/
|
||||||
public int get(String key) {
|
public String getString(String key) {
|
||||||
return defaultKVStore.getInt(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
|
* Get contribution object from cursor
|
||||||
* @param cursor
|
* @param uri
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public Contribution getContributionFromCursor(Cursor cursor) {
|
public Contribution getContributionWithFileName(String uri) {
|
||||||
return contributionsDao.fromCursor(cursor);
|
List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(uri);
|
||||||
|
if(!contributionWithUri.isEmpty()){
|
||||||
|
return contributionWithUri.get(0);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a contribution from the contributions table
|
* Remove a contribution from the contributions table
|
||||||
* @param contribution
|
* @param contribution
|
||||||
|
* @return
|
||||||
*/
|
*/
|
||||||
public void deleteContribution(Contribution contribution) {
|
public Single<Integer> deleteContribution(Contribution contribution) {
|
||||||
contributionsDao.delete(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.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DataSetObserver;
|
import android.database.DataSetObserver;
|
||||||
import android.os.Bundle;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.loader.content.CursorLoader;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.loader.content.Loader;
|
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.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.Media;
|
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.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 timber.log.Timber;
|
||||||
|
|
||||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
|
||||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
|
||||||
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The presenter class for Contributions
|
* The presenter class for Contributions
|
||||||
*/
|
*/
|
||||||
public class ContributionsPresenter extends DataSetObserver implements UserActionListener {
|
public class ContributionsPresenter implements UserActionListener {
|
||||||
|
|
||||||
private final ContributionsRepository repository;
|
private final ContributionsRepository repository;
|
||||||
|
private final Scheduler mainThreadScheduler;
|
||||||
|
private final Scheduler ioThreadScheduler;
|
||||||
|
private CompositeDisposable compositeDisposable;
|
||||||
private ContributionsContract.View view;
|
private ContributionsContract.View view;
|
||||||
private Cursor cursor;
|
private List<Contribution> contributionList=new ArrayList<>();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
Context context;
|
Context context;
|
||||||
|
|
||||||
@Inject
|
@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.repository = repository;
|
||||||
|
this.mainThreadScheduler=mainThreadScheduler;
|
||||||
|
this.ioThreadScheduler=ioThreadScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String user;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttachView(ContributionsContract.View view) {
|
public void onAttachView(ContributionsContract.View view) {
|
||||||
this.view = view;
|
this.view = view;
|
||||||
if (null != cursor) {
|
compositeDisposable=new CompositeDisposable();
|
||||||
try {
|
}
|
||||||
cursor.registerDataSetObserver(this);
|
|
||||||
} catch (IllegalStateException e) {//Cursor might be already registered
|
public void setLifeCycleOwner(LifecycleOwner lifeCycleOwner){
|
||||||
Timber.d(e);
|
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
|
@Override
|
||||||
public void onDetachView() {
|
public void onDetachView() {
|
||||||
this.view = null;
|
this.view = null;
|
||||||
if (null != cursor) {
|
compositeDisposable.clear();
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
|
public Contribution getContributionsWithTitle(String title) {
|
||||||
view.showProgress(false);
|
return repository.getContributionWithFileName(title);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -109,75 +161,23 @@ public class ContributionsPresenter extends DataSetObserver implements UserActio
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void deleteUpload(Contribution contribution) {
|
public void deleteUpload(Contribution contribution) {
|
||||||
repository.deleteContributionFromDB(contribution);
|
compositeDisposable.add(repository.deleteContributionFromDB(contribution)
|
||||||
|
.subscribeOn(ioThreadScheduler)
|
||||||
|
.subscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a contribution at the specified cursor position
|
* Returns a contribution at the specified cursor position
|
||||||
|
*
|
||||||
* @param i
|
* @param i
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Media getItemAtPosition(int i) {
|
public Media getItemAtPosition(int i) {
|
||||||
if (null != cursor && cursor.moveToPosition(i)) {
|
if (i == -1 || contributionList.size() < i+1) {
|
||||||
return getContributionsFromCursor(cursor);
|
return null;
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
return contributionList.get(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
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.Inject;
|
||||||
|
|
||||||
|
import io.reactivex.Completable;
|
||||||
|
import io.reactivex.Single;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The repository class for contributions
|
* The repository class for contributions
|
||||||
*/
|
*/
|
||||||
|
|
@ -19,25 +24,41 @@ public class ContributionsRepository {
|
||||||
/**
|
/**
|
||||||
* Fetch default number of contributions to be show, based on user preferences
|
* Fetch default number of contributions to be show, based on user preferences
|
||||||
*/
|
*/
|
||||||
public int get(String uploadsShowing) {
|
public String getString(String key) {
|
||||||
return localDataSource.get(uploadsShowing);
|
return localDataSource.getString(key);
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* Deletes a failed upload from DB
|
||||||
* @param contribution
|
* @param contribution
|
||||||
|
* @return
|
||||||
*/
|
*/
|
||||||
public void deleteContributionFromDB(Contribution contribution) {
|
public Single<Integer> deleteContributionFromDB(Contribution contribution) {
|
||||||
localDataSource.deleteContribution(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() {
|
private void initMain() {
|
||||||
//Do not remove this, this triggers the sync service
|
//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);
|
Intent uploadServiceIntent = new Intent(this, UploadService.class);
|
||||||
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
||||||
startService(uploadServiceIntent);
|
startService(uploadServiceIntent);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ public class DisplayableContribution extends Contribution {
|
||||||
contribution.getWidth(),
|
contribution.getWidth(),
|
||||||
contribution.getHeight(),
|
contribution.getHeight(),
|
||||||
contribution.getLicense());
|
contribution.getLicense());
|
||||||
|
this._id=contribution._id;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,20 @@ package fr.free.nrw.commons.data;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
||||||
import fr.free.nrw.commons.category.CategoryDao;
|
import fr.free.nrw.commons.category.CategoryDao;
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
|
||||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
||||||
|
|
||||||
public class DBOpenHelper extends SQLiteOpenHelper {
|
public class DBOpenHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
private static final String DATABASE_NAME = "commons.db";
|
private static final String DATABASE_NAME = "commons.db";
|
||||||
private static final int DATABASE_VERSION = 11;
|
private static final int DATABASE_VERSION = 12;
|
||||||
|
public static final String CONTRIBUTIONS_TABLE = "contributions";
|
||||||
|
private final String DROP_TABLE_STATEMENT="DROP TABLE IF EXISTS %s";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do not use directly - @Inject an instance where it's needed and let
|
* Do not use directly - @Inject an instance where it's needed and let
|
||||||
|
|
@ -25,7 +27,6 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
||||||
ContributionDao.Table.onCreate(sqLiteDatabase);
|
|
||||||
CategoryDao.Table.onCreate(sqLiteDatabase);
|
CategoryDao.Table.onCreate(sqLiteDatabase);
|
||||||
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
|
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
|
||||||
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
|
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
|
||||||
|
|
@ -34,10 +35,23 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
||||||
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
|
|
||||||
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
|
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to);
|
BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
RecentSearchesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
RecentSearchesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||||
|
deleteTable(sqLiteDatabase,CONTRIBUTIONS_TABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete table in the given db
|
||||||
|
* @param db
|
||||||
|
* @param tableName
|
||||||
|
*/
|
||||||
|
public void deleteTable(SQLiteDatabase db, String tableName) {
|
||||||
|
try {
|
||||||
|
db.execSQL(String.format(DROP_TABLE_STATEMENT, tableName));
|
||||||
|
} catch (SQLiteException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
app/src/main/java/fr/free/nrw/commons/db/AppDatabase.java
Normal file
14
app/src/main/java/fr/free/nrw/commons/db/AppDatabase.java
Normal file
|
|
@ -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, exportSchema = false)
|
||||||
|
@TypeConverters({Converters.class})
|
||||||
|
abstract public class AppDatabase extends RoomDatabase {
|
||||||
|
public abstract ContributionDao getContributionDao();
|
||||||
|
}
|
||||||
76
app/src/main/java/fr/free/nrw/commons/db/Converters.java
Normal file
76
app/src/main/java/fr/free/nrw/commons/db/Converters.java
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
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 org.wikipedia.json.GsonUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
|
|
||||||
|
public class Converters {
|
||||||
|
|
||||||
|
public static Gson getGson() {
|
||||||
|
return ApplicationlessInjection.getInstance(CommonsApplication.getInstance()).getCommonsApplicationComponent().gson();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<String> objectList) {
|
||||||
|
return objectList == null ? null : getGson().toJson(objectList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static ArrayList<String> stringToArrayListObject(String objectList) {
|
||||||
|
return objectList == null ? null : getGson().fromJson(objectList,new TypeToken<ArrayList<String>>(){}.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static String mapObjectToString(HashMap<String,String> objectList) {
|
||||||
|
return objectList == null ? null : getGson().toJson(objectList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static HashMap<String,String> stringToMap(String objectList) {
|
||||||
|
return objectList == null ? null : getGson().fromJson(objectList,new TypeToken<HashMap<String,String>>(){}.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static String latlngObjectToString(LatLng latlng) {
|
||||||
|
return latlng == null ? null : getGson().toJson(latlng);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static LatLng stringToLatLng(String objectList) {
|
||||||
|
return objectList == null ? null : getGson().fromJson(objectList,LatLng.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -98,7 +98,7 @@ public class DeleteHelper {
|
||||||
String userPageString = "\n{{subst:idw|" + media.getFilename() +
|
String userPageString = "\n{{subst:idw|" + media.getFilename() +
|
||||||
"}} ~~~~";
|
"}} ~~~~";
|
||||||
|
|
||||||
return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
|
return pageEditClient.prependEdit(media.filename, fileDeleteString + "\n", summary)
|
||||||
.flatMap(result -> {
|
.flatMap(result -> {
|
||||||
if (result) {
|
if (result) {
|
||||||
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
|
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package fr.free.nrw.commons.di;
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import dagger.Component;
|
import dagger.Component;
|
||||||
|
|
@ -10,7 +12,6 @@ import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.auth.LoginActivity;
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
import fr.free.nrw.commons.contributions.ContributionViewHolder;
|
import fr.free.nrw.commons.contributions.ContributionViewHolder;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsModule;
|
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.nearby.PlaceRenderer;
|
||||||
import fr.free.nrw.commons.review.ReviewController;
|
import fr.free.nrw.commons.review.ReviewController;
|
||||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||||
|
|
@ -37,8 +38,6 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget;
|
||||||
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
|
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
|
||||||
void inject(CommonsApplication application);
|
void inject(CommonsApplication application);
|
||||||
|
|
||||||
void inject(ContributionsSyncAdapter syncAdapter);
|
|
||||||
|
|
||||||
void inject(LoginActivity activity);
|
void inject(LoginActivity activity);
|
||||||
|
|
||||||
void inject(SettingsFragment fragment);
|
void inject(SettingsFragment fragment);
|
||||||
|
|
@ -56,9 +55,12 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
|
||||||
|
|
||||||
void inject(ContributionViewHolder viewHolder);
|
void inject(ContributionViewHolder viewHolder);
|
||||||
|
|
||||||
|
Gson gson();
|
||||||
|
|
||||||
@Component.Builder
|
@Component.Builder
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
interface Builder {
|
interface Builder {
|
||||||
|
|
||||||
Builder appModule(CommonsApplicationModule applicationModule);
|
Builder appModule(CommonsApplicationModule applicationModule);
|
||||||
|
|
||||||
CommonsApplicationComponent build();
|
CommonsApplicationComponent build();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import android.content.Context;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
|
||||||
import androidx.collection.LruCache;
|
import androidx.collection.LruCache;
|
||||||
|
import androidx.room.Room;
|
||||||
|
|
||||||
import com.github.varunpant.quadtree.QuadTree;
|
import com.github.varunpant.quadtree.QuadTree;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
|
@ -27,8 +28,9 @@ import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.auth.AccountUtil;
|
import fr.free.nrw.commons.auth.AccountUtil;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.caching.CacheController;
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
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.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
|
|
@ -53,6 +55,7 @@ public class CommonsApplicationModule {
|
||||||
private Context applicationContext;
|
private Context applicationContext;
|
||||||
public static final String IO_THREAD="io_thread";
|
public static final String IO_THREAD="io_thread";
|
||||||
public static final String MAIN_THREAD="main_thread";
|
public static final String MAIN_THREAD="main_thread";
|
||||||
|
private AppDatabase appDatabase;
|
||||||
|
|
||||||
public CommonsApplicationModule(Context applicationContext) {
|
public CommonsApplicationModule(Context applicationContext) {
|
||||||
this.applicationContext = applicationContext;
|
this.applicationContext = applicationContext;
|
||||||
|
|
@ -229,4 +232,16 @@ public class CommonsApplicationModule {
|
||||||
public QuadTree providesQuadTres() {
|
public QuadTree providesQuadTres() {
|
||||||
return new QuadTree<>(-180, -90, +180, +90);
|
return new QuadTree<>(-180, -90, +180, +90);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public AppDatabase provideAppDataBase() {
|
||||||
|
appDatabase=Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db").build();
|
||||||
|
return appDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
public ContributionDao providesContributionsDao() {
|
||||||
|
return appDatabase.getContributionDao();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import dagger.android.ContributesAndroidInjector;
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider;
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider;
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
|
||||||
import fr.free.nrw.commons.category.CategoryContentProvider;
|
import fr.free.nrw.commons.category.CategoryContentProvider;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
|
||||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
|
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,9 +16,6 @@ import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
public abstract class ContentProviderBuilderModule {
|
public abstract class ContentProviderBuilderModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
|
||||||
abstract ContributionsContentProvider bindContributionsContentProvider();
|
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract CategoryContentProvider bindCategoryContentProvider();
|
abstract CategoryContentProvider bindCategoryContentProvider();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,7 @@ public class UploadModel {
|
||||||
contribution.setTag("mimeType", item.mimeType);
|
contribution.setTag("mimeType", item.mimeType);
|
||||||
contribution.setSource(item.source);
|
contribution.setSource(item.source);
|
||||||
contribution.setContentProviderUri(item.mediaUri);
|
contribution.setContentProviderUri(item.mediaUri);
|
||||||
|
contribution.setDateUploaded(new Date());
|
||||||
|
|
||||||
Timber.d("Created timestamp while building contribution is %s, %s",
|
Timber.d("Created timestamp while building contribution is %s, %s",
|
||||||
item.getCreatedTimestamp(),
|
item.getCreatedTimestamp(),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.upload;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
@ -20,6 +19,7 @@ import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
|
|
@ -28,12 +28,16 @@ import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
import fr.free.nrw.commons.contributions.Contribution;
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
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.contributions.MainActivity;
|
||||||
|
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
||||||
import fr.free.nrw.commons.media.MediaClient;
|
import fr.free.nrw.commons.media.MediaClient;
|
||||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||||
import fr.free.nrw.commons.wikidata.WikidataEditService;
|
import fr.free.nrw.commons.wikidata.WikidataEditService;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.Scheduler;
|
||||||
|
import io.reactivex.SingleObserver;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
@ -46,16 +50,22 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload";
|
public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload";
|
||||||
public static final String EXTRA_SOURCE = EXTRA_PREFIX + ".source";
|
public static final String EXTRA_SOURCE = EXTRA_PREFIX + ".source";
|
||||||
public static final String EXTRA_FILES = EXTRA_PREFIX + ".files";
|
public static final String EXTRA_FILES = EXTRA_PREFIX + ".files";
|
||||||
|
|
||||||
@Inject WikidataEditService wikidataEditService;
|
@Inject WikidataEditService wikidataEditService;
|
||||||
@Inject SessionManager sessionManager;
|
@Inject SessionManager sessionManager;
|
||||||
@Inject ContributionDao contributionDao;
|
@Inject
|
||||||
|
ContributionDao contributionDao;
|
||||||
@Inject UploadClient uploadClient;
|
@Inject UploadClient uploadClient;
|
||||||
@Inject MediaClient mediaClient;
|
@Inject MediaClient mediaClient;
|
||||||
|
@Inject
|
||||||
|
@Named(CommonsApplicationModule.MAIN_THREAD)
|
||||||
|
Scheduler mainThreadScheduler;
|
||||||
|
@Inject
|
||||||
|
@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler;
|
||||||
|
|
||||||
private NotificationManagerCompat notificationManager;
|
private NotificationManagerCompat notificationManager;
|
||||||
private NotificationCompat.Builder curNotification;
|
private NotificationCompat.Builder curNotification;
|
||||||
private int toUpload;
|
private int toUpload;
|
||||||
|
private CompositeDisposable compositeDisposable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The filePath names of unfinished uploads, used to prevent overwriting
|
* The filePath names of unfinished uploads, used to prevent overwriting
|
||||||
|
|
@ -105,7 +115,10 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
notificationManager.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
|
notificationManager.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
|
||||||
|
|
||||||
contribution.setTransferred(transferred);
|
contribution.setTransferred(transferred);
|
||||||
contributionDao.save(contribution);
|
compositeDisposable.add(contributionDao.
|
||||||
|
save(contribution).subscribeOn(ioThreadScheduler)
|
||||||
|
.observeOn(mainThreadScheduler)
|
||||||
|
.subscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +126,7 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
compositeDisposable.dispose();
|
||||||
Timber.d("UploadService.onDestroy; %s are yet to be uploaded", unfinishedUploads);
|
Timber.d("UploadService.onDestroy; %s are yet to be uploaded", unfinishedUploads);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,6 +134,7 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
CommonsApplication.createNotificationChannel(getApplicationContext());
|
CommonsApplication.createNotificationChannel(getApplicationContext());
|
||||||
|
compositeDisposable = new CompositeDisposable();
|
||||||
notificationManager = NotificationManagerCompat.from(this);
|
notificationManager = NotificationManagerCompat.from(this);
|
||||||
curNotification = getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL);
|
curNotification = getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL);
|
||||||
}
|
}
|
||||||
|
|
@ -143,15 +158,20 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
|
|
||||||
contribution.setState(Contribution.STATE_QUEUED);
|
contribution.setState(Contribution.STATE_QUEUED);
|
||||||
contribution.setTransferred(0);
|
contribution.setTransferred(0);
|
||||||
contributionDao.save(contribution);
|
|
||||||
toUpload++;
|
toUpload++;
|
||||||
if (curNotification != null && toUpload != 1) {
|
if (curNotification != null && toUpload != 1) {
|
||||||
curNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload));
|
curNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload));
|
||||||
Timber.d("%d uploads left", toUpload);
|
Timber.d("%d uploads left", toUpload);
|
||||||
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
|
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
|
||||||
}
|
}
|
||||||
|
compositeDisposable.add(contributionDao
|
||||||
super.queue(what, contribution);
|
.save(contribution)
|
||||||
|
.subscribeOn(ioThreadScheduler)
|
||||||
|
.observeOn(mainThreadScheduler)
|
||||||
|
.subscribe(aLong->{
|
||||||
|
contribution._id = aLong;
|
||||||
|
UploadService.super.queue(what, contribution);
|
||||||
|
}, Throwable::printStackTrace));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unknown value for what");
|
throw new IllegalArgumentException("Unknown value for what");
|
||||||
|
|
@ -163,16 +183,10 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
@Override
|
@Override
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) {
|
if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) {
|
||||||
ContentValues failedValues = new ContentValues();
|
compositeDisposable.add(contributionDao.updateStates(Contribution.STATE_FAILED, new int[]{Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS})
|
||||||
failedValues.put(ContributionDao.Table.COLUMN_STATE, Contribution.STATE_FAILED);
|
.observeOn(mainThreadScheduler)
|
||||||
|
.subscribeOn(ioThreadScheduler)
|
||||||
int updated = getContentResolver().update(ContributionsContentProvider.BASE_URI,
|
.subscribe());
|
||||||
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);
|
|
||||||
freshStart = false;
|
freshStart = false;
|
||||||
}
|
}
|
||||||
return START_REDELIVER_INTENT;
|
return START_REDELIVER_INTENT;
|
||||||
|
|
@ -272,7 +286,11 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
contribution.setState(Contribution.STATE_COMPLETED);
|
contribution.setState(Contribution.STATE_COMPLETED);
|
||||||
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatShort()
|
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatShort()
|
||||||
.parse(uploadResult.getImageinfo().getTimestamp()));
|
.parse(uploadResult.getImageinfo().getTimestamp()));
|
||||||
contributionDao.save(contribution);
|
compositeDisposable.add(contributionDao
|
||||||
|
.save(contribution)
|
||||||
|
.subscribeOn(ioThreadScheduler)
|
||||||
|
.observeOn(mainThreadScheduler)
|
||||||
|
.subscribe());
|
||||||
}
|
}
|
||||||
}, throwable -> {
|
}, throwable -> {
|
||||||
Timber.w(throwable, "Exception during upload");
|
Timber.w(throwable, "Exception during upload");
|
||||||
|
|
@ -291,7 +309,10 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_FAILED, curNotification.build());
|
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_FAILED, curNotification.build());
|
||||||
|
|
||||||
contribution.setState(Contribution.STATE_FAILED);
|
contribution.setState(Contribution.STATE_FAILED);
|
||||||
contributionDao.save(contribution);
|
compositeDisposable.add(contributionDao.save(contribution)
|
||||||
|
.subscribeOn(ioThreadScheduler)
|
||||||
|
.observeOn(mainThreadScheduler)
|
||||||
|
.subscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String findUniqueFilename(String fileName) throws IOException {
|
private String findUniqueFilename(String fileName) throws IOException {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,14 @@ class TestCommonsApplication : Application() {
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
context=applicationContext
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object{
|
||||||
|
private var context: Context?=null
|
||||||
|
fun getContext(): Context? {
|
||||||
|
return context
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,348 +0,0 @@
|
||||||
package fr.free.nrw.commons.contributions
|
|
||||||
|
|
||||||
import android.content.ContentProviderClient
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.MatrixCursor
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.RemoteException
|
|
||||||
import com.nhaarman.mockitokotlin2.*
|
|
||||||
import fr.free.nrw.commons.BuildConfig
|
|
||||||
import fr.free.nrw.commons.TestCommonsApplication
|
|
||||||
import fr.free.nrw.commons.Utils
|
|
||||||
import fr.free.nrw.commons.contributions.Contribution.*
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao.Table
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId
|
|
||||||
import org.junit.Assert.*
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.robolectric.RobolectricTestRunner
|
|
||||||
import org.robolectric.annotation.Config
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
|
||||||
@Config(sdk = [21], application = TestCommonsApplication::class)
|
|
||||||
class ContributionDaoTest {
|
|
||||||
private val localUri = "http://example.com/"
|
|
||||||
private val client: ContentProviderClient = mock()
|
|
||||||
private val database: SQLiteDatabase = mock()
|
|
||||||
private val captor = argumentCaptor<ContentValues>()
|
|
||||||
|
|
||||||
private lateinit var contentUri: Uri
|
|
||||||
private lateinit var testObject: ContributionDao
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
contentUri = uriForId(111)
|
|
||||||
testObject = ContributionDao { client }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun createTable() {
|
|
||||||
Table.onCreate(database)
|
|
||||||
verify(database).execSQL(Table.CREATE_TABLE_STATEMENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun deleteTable() {
|
|
||||||
Table.onDelete(database)
|
|
||||||
|
|
||||||
inOrder(database) {
|
|
||||||
verify(database).execSQL(Table.DROP_TABLE_STATEMENT)
|
|
||||||
verify(database).execSQL(Table.CREATE_TABLE_STATEMENT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun upgradeDatabase_v1_to_v2() {
|
|
||||||
Table.onUpdate(database, 1, 2)
|
|
||||||
|
|
||||||
inOrder(database) {
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.ADD_DESCRIPTION_FIELD)
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.ADD_CREATOR_FIELD)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun upgradeDatabase_v2_to_v3() {
|
|
||||||
Table.onUpdate(database, 2, 3)
|
|
||||||
|
|
||||||
inOrder(database) {
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.ADD_MULTIPLE_FIELD)
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.SET_DEFAULT_MULTIPLE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun upgradeDatabase_v3_to_v4() {
|
|
||||||
Table.onUpdate(database, 3, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun upgradeDatabase_v4_to_v5() {
|
|
||||||
Table.onUpdate(database, 4, 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun upgradeDatabase_v5_to_v6() {
|
|
||||||
Table.onUpdate(database, 5, 6)
|
|
||||||
|
|
||||||
inOrder(database) {
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.ADD_WIDTH_FIELD)
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.SET_DEFAULT_WIDTH)
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.ADD_HEIGHT_FIELD)
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.SET_DEFAULT_HEIGHT)
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.ADD_LICENSE_FIELD)
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.SET_DEFAULT_LICENSE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun migrateTableVersionFrom_v6_to_v7() {
|
|
||||||
Table.onUpdate(database, 6, 7)
|
|
||||||
// Table has changed in version 7
|
|
||||||
inOrder(database) {
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.ADD_WIKI_DATA_ENTITY_ID_FIELD)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun migrateTableVersionFrom_v7_to_v8() {
|
|
||||||
Table.onUpdate(database, 7, 8)
|
|
||||||
// Table has changed in version 8
|
|
||||||
inOrder(database) {
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.ADD_WIKI_DATA_ENTITY_ID_FIELD)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun migrateTableVersionFrom_v8_to_v9() {
|
|
||||||
Table.onUpdate(database, 8, 9)
|
|
||||||
// Table changed in version 9
|
|
||||||
inOrder(database) {
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.ADD_WIKI_DATA_ENTITY_ID_FIELD)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun migrateTableVersionFrom_v9_to_v10() {
|
|
||||||
Table.onUpdate(database, 8, 9)
|
|
||||||
// Table changed in version 9
|
|
||||||
inOrder(database) {
|
|
||||||
verify<SQLiteDatabase>(database).execSQL(Table.ADD_WIKI_DATA_ENTITY_ID_FIELD)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun saveNewContribution_nonNullFields() {
|
|
||||||
whenever(client.insert(isA(), isA())).thenReturn(contentUri)
|
|
||||||
val contribution = createContribution(true, null, null, null, null)
|
|
||||||
|
|
||||||
testObject.save(contribution)
|
|
||||||
|
|
||||||
assertEquals(contentUri, contribution.contentUri)
|
|
||||||
verify(client).insert(eq(BASE_URI), captor.capture())
|
|
||||||
captor.firstValue.let {
|
|
||||||
// Long fields
|
|
||||||
assertEquals(222L, it.getAsLong(Table.COLUMN_LENGTH))
|
|
||||||
assertEquals(321L, it.getAsLong(Table.COLUMN_TIMESTAMP))
|
|
||||||
assertEquals(333L, it.getAsLong(Table.COLUMN_TRANSFERRED))
|
|
||||||
|
|
||||||
// Integer fields
|
|
||||||
assertEquals(STATE_COMPLETED, it.getAsInteger(Table.COLUMN_STATE))
|
|
||||||
assertEquals(640, it.getAsInteger(Table.COLUMN_WIDTH))
|
|
||||||
assertEquals(480, it.getAsInteger(Table.COLUMN_HEIGHT))
|
|
||||||
|
|
||||||
// String fields
|
|
||||||
assertEquals(SOURCE_CAMERA, it.getAsString(Table.COLUMN_SOURCE))
|
|
||||||
assertEquals("desc", it.getAsString(Table.COLUMN_DESCRIPTION))
|
|
||||||
assertEquals("create", it.getAsString(Table.COLUMN_CREATOR))
|
|
||||||
assertEquals("007", it.getAsString(Table.COLUMN_LICENSE))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun saveNewContribution_nullableFieldsAreNull() {
|
|
||||||
whenever(client.insert(isA(), isA())).thenReturn(contentUri)
|
|
||||||
val contribution = createContribution(true, null, null, null, null)
|
|
||||||
|
|
||||||
testObject.save(contribution)
|
|
||||||
|
|
||||||
assertEquals(contentUri, contribution.contentUri)
|
|
||||||
verify(client).insert(eq(BASE_URI), captor.capture())
|
|
||||||
captor.firstValue.let {
|
|
||||||
// Nullable fields are absent if null
|
|
||||||
assertFalse(it.containsKey(Table.COLUMN_LOCAL_URI))
|
|
||||||
assertFalse(it.containsKey(Table.COLUMN_IMAGE_URL))
|
|
||||||
assertFalse(it.containsKey(Table.COLUMN_UPLOADED))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun saveNewContribution_nullableFieldsAreNonNull() {
|
|
||||||
whenever(client.insert(isA(), isA())).thenReturn(contentUri)
|
|
||||||
val contribution = createContribution(true, Uri.parse(localUri),
|
|
||||||
"image", Date(456L), null)
|
|
||||||
|
|
||||||
testObject.save(contribution)
|
|
||||||
|
|
||||||
assertEquals(contentUri, contribution.contentUri)
|
|
||||||
verify(client).insert(eq(BASE_URI), captor.capture())
|
|
||||||
captor.firstValue.let {
|
|
||||||
assertEquals(localUri, it.getAsString(Table.COLUMN_LOCAL_URI))
|
|
||||||
assertEquals("image", it.getAsString(Table.COLUMN_IMAGE_URL))
|
|
||||||
assertEquals(456L, it.getAsLong(Table.COLUMN_UPLOADED))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun saveNewContribution_booleanEncodesTrue() {
|
|
||||||
whenever(client.insert(isA(), isA())).thenReturn(contentUri)
|
|
||||||
val contribution = createContribution(true, null, null, null, null)
|
|
||||||
|
|
||||||
testObject.save(contribution)
|
|
||||||
|
|
||||||
assertEquals(contentUri, contribution.contentUri)
|
|
||||||
verify(client).insert(eq(BASE_URI), captor.capture())
|
|
||||||
|
|
||||||
// Boolean true --> 1 for ths encoding scheme
|
|
||||||
assertEquals("Boolean true should be encoded as 1", 1,
|
|
||||||
captor.firstValue.getAsInteger(Table.COLUMN_MULTIPLE))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun saveNewContribution_booleanEncodesFalse() {
|
|
||||||
whenever(client.insert(isA(), isA())).thenReturn(contentUri)
|
|
||||||
val contribution = createContribution(false, null, null, null, null)
|
|
||||||
|
|
||||||
testObject.save(contribution)
|
|
||||||
|
|
||||||
assertEquals(contentUri, contribution.contentUri)
|
|
||||||
verify(client).insert(eq(BASE_URI), captor.capture())
|
|
||||||
|
|
||||||
// Boolean true --> 1 for ths encoding scheme
|
|
||||||
assertEquals("Boolean false should be encoded as 0", 0,
|
|
||||||
captor.firstValue.getAsInteger(Table.COLUMN_MULTIPLE))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun saveExistingContribution() {
|
|
||||||
val contribution = createContribution(false, null, null, null, null)
|
|
||||||
contribution.contentUri = contentUri
|
|
||||||
|
|
||||||
testObject.save(contribution)
|
|
||||||
|
|
||||||
verify(client).update(eq(contentUri), isA(), isNull(), isNull())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = RuntimeException::class)
|
|
||||||
fun saveTranslatesExceptions() {
|
|
||||||
whenever(client.insert(isA(), isA())).thenThrow(RemoteException(""))
|
|
||||||
|
|
||||||
testObject.save(createContribution(false, null, null, null, null))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = RuntimeException::class)
|
|
||||||
fun deleteTranslatesExceptions() {
|
|
||||||
whenever(client.delete(anyOrNull(), anyOrNull(), anyOrNull())).thenThrow(RemoteException(""))
|
|
||||||
|
|
||||||
val contribution = createContribution(false, null, null, null, null)
|
|
||||||
contribution.contentUri = contentUri
|
|
||||||
testObject.delete(contribution)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = RuntimeException::class)
|
|
||||||
fun exceptionThrownWhenAttemptingToDeleteUnsavedContribution() {
|
|
||||||
testObject.delete(createContribution(false, null, null, null, null))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun deleteExistingContribution() {
|
|
||||||
val contribution = createContribution(false, null, null, null, null)
|
|
||||||
contribution.contentUri = contentUri
|
|
||||||
|
|
||||||
testObject.delete(contribution)
|
|
||||||
|
|
||||||
verify(client).delete(eq(contentUri), isNull(), isNull())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun createFromCursor() {
|
|
||||||
val created = 321L
|
|
||||||
val uploaded = 456L
|
|
||||||
createCursor(created, uploaded, false, localUri).let { mc ->
|
|
||||||
testObject.fromCursor(mc).let {
|
|
||||||
assertEquals(uriForId(111), it.contentUri)
|
|
||||||
assertEquals("filePath", it.filename)
|
|
||||||
assertEquals(localUri, it.localUri.toString())
|
|
||||||
assertEquals("image", it.imageUrl)
|
|
||||||
assertEquals(created, it.dateCreated.time)
|
|
||||||
assertEquals(STATE_QUEUED, it.state)
|
|
||||||
assertEquals(222L, it.dataLength)
|
|
||||||
assertEquals(uploaded, it.dateUploaded?.time)
|
|
||||||
assertEquals(88L, it.transferred)
|
|
||||||
assertEquals(SOURCE_GALLERY, it.source)
|
|
||||||
assertEquals("desc", it.description)
|
|
||||||
assertEquals("create", it.creator)
|
|
||||||
assertEquals(640, it.width)
|
|
||||||
assertEquals(480, it.height)
|
|
||||||
assertEquals("007", it.license)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun createFromCursor_nullableTimestamps() {
|
|
||||||
createCursor(0L, 0L, false, localUri).let { mc ->
|
|
||||||
testObject.fromCursor(mc).let {
|
|
||||||
assertNull(it.dateCreated)
|
|
||||||
assertNull(it.dateUploaded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun createFromCursor_nullableLocalUri() {
|
|
||||||
createCursor(0L, 0L, false, "").let { mc ->
|
|
||||||
testObject.fromCursor(mc).let {
|
|
||||||
assertNull(it.localUri)
|
|
||||||
assertNull(it.dateCreated)
|
|
||||||
assertNull(it.dateUploaded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun createFromCursor_booleanEncoding() {
|
|
||||||
val mcFalse = createCursor(0L, 0L, false, localUri)
|
|
||||||
assertFalse(testObject.fromCursor(mcFalse).multiple)
|
|
||||||
|
|
||||||
val mcHammer = createCursor(0L, 0L, true, localUri)
|
|
||||||
assertTrue(testObject.fromCursor(mcHammer).multiple)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createCursor(created: Long, uploaded: Long, multiple: Boolean, localUri: String) =
|
|
||||||
MatrixCursor(Table.ALL_FIELDS, 1).apply {
|
|
||||||
addRow(listOf("111", "filePath", localUri, "image",
|
|
||||||
created, STATE_QUEUED, 222L, uploaded, 88L, SOURCE_GALLERY, "desc",
|
|
||||||
"create", if (multiple) 1 else 0, 640, 480, "007", "Q1"))
|
|
||||||
moveToFirst()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createContribution(isMultiple: Boolean, localUri: Uri?, imageUrl: String?, dateUploaded: Date?, filename: String?): Contribution {
|
|
||||||
val contribution = Contribution(localUri, imageUrl, filename, "desc", 222L, Date(321L), dateUploaded,
|
|
||||||
"create", "edit", "coords").apply {
|
|
||||||
state = STATE_COMPLETED
|
|
||||||
transferred = 333L
|
|
||||||
source = SOURCE_CAMERA
|
|
||||||
license = "007"
|
|
||||||
multiple = isMultiple
|
|
||||||
width = 640
|
|
||||||
height = 480 // VGA should be enough for anyone, right?
|
|
||||||
}
|
|
||||||
contribution.wikiDataEntityId = "Q1"
|
|
||||||
return contribution
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,22 @@
|
||||||
package fr.free.nrw.commons.contributions
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.loader.content.CursorLoader
|
import androidx.loader.content.CursorLoader
|
||||||
import androidx.loader.content.Loader
|
import androidx.loader.content.Loader
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
import com.nhaarman.mockitokotlin2.verify
|
import com.nhaarman.mockitokotlin2.verify
|
||||||
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
|
import io.reactivex.Scheduler
|
||||||
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.schedulers.TestScheduler
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.mockito.ArgumentMatchers
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
@ -15,11 +26,11 @@ import org.mockito.MockitoAnnotations
|
||||||
*/
|
*/
|
||||||
class ContributionsPresenterTest {
|
class ContributionsPresenterTest {
|
||||||
@Mock
|
@Mock
|
||||||
internal var repository: ContributionsRepository? = null
|
internal lateinit var repository: ContributionsRepository
|
||||||
@Mock
|
@Mock
|
||||||
internal var view: ContributionsContract.View? = null
|
internal lateinit var view: ContributionsContract.View
|
||||||
|
|
||||||
private var contributionsPresenter: ContributionsPresenter? = null
|
private lateinit var contributionsPresenter: ContributionsPresenter
|
||||||
|
|
||||||
private lateinit var cursor: Cursor
|
private lateinit var cursor: Cursor
|
||||||
|
|
||||||
|
|
@ -27,6 +38,12 @@ class ContributionsPresenterTest {
|
||||||
|
|
||||||
lateinit var loader: Loader<Cursor>
|
lateinit var loader: Loader<Cursor>
|
||||||
|
|
||||||
|
lateinit var liveData: LiveData<List<Contribution>>
|
||||||
|
|
||||||
|
@Rule @JvmField var instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
lateinit var scheduler : Scheduler
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* initial setup
|
* initial setup
|
||||||
*/
|
*/
|
||||||
|
|
@ -34,21 +51,24 @@ class ContributionsPresenterTest {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
|
scheduler=TestScheduler()
|
||||||
cursor = Mockito.mock(Cursor::class.java)
|
cursor = Mockito.mock(Cursor::class.java)
|
||||||
contribution = Mockito.mock(Contribution::class.java)
|
contribution = Mockito.mock(Contribution::class.java)
|
||||||
contributionsPresenter = ContributionsPresenter(repository)
|
contributionsPresenter = ContributionsPresenter(repository,scheduler,scheduler)
|
||||||
loader = Mockito.mock(CursorLoader::class.java)
|
loader = Mockito.mock(CursorLoader::class.java)
|
||||||
contributionsPresenter?.onAttachView(view)
|
contributionsPresenter.onAttachView(view)
|
||||||
|
liveData=MutableLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test presenter actions onGetContributionFromCursor
|
* Test fetch contributions
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testGetContributionFromCursor() {
|
fun testFetchContributions(){
|
||||||
contributionsPresenter?.getContributionsFromCursor(cursor)
|
whenever(repository.getString(ArgumentMatchers.anyString())).thenReturn("10")
|
||||||
verify(repository)?.getContributionFromCursor(cursor)
|
whenever(repository.fetchContributions()).thenReturn(liveData)
|
||||||
|
contributionsPresenter.fetchContributions()
|
||||||
|
verify(repository).fetchContributions()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -56,55 +76,20 @@ class ContributionsPresenterTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testDeleteContribution() {
|
fun testDeleteContribution() {
|
||||||
contributionsPresenter?.deleteUpload(contribution)
|
whenever(repository.deleteContributionFromDB(ArgumentMatchers.any(Contribution::class.java))).thenReturn(Single.just(1))
|
||||||
verify(repository)?.deleteContributionFromDB(contribution)
|
contributionsPresenter.deleteUpload(contribution)
|
||||||
|
verify(repository).deleteContributionFromDB(contribution)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test presenter actions on loaderFinished and has non zero media objects
|
* Test fetch contribution with filename
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testOnLoaderFinishedNonZeroContributions() {
|
fun testGetContributionWithFileName(){
|
||||||
Mockito.`when`(cursor.count).thenReturn(1)
|
contributionsPresenter.getContributionsWithTitle("ashish")
|
||||||
contributionsPresenter?.onLoadFinished(loader, cursor)
|
verify(repository).getContributionWithFileName("ashish")
|
||||||
verify(view)?.showProgress(false)
|
|
||||||
verify(view)?.showWelcomeTip(false)
|
|
||||||
verify(view)?.showNoContributionsUI(false)
|
|
||||||
verify(view)?.setUploadCount(cursor.count)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Test presenter actions on loaderFinished and has Zero media objects
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun testOnLoaderFinishedZeroContributions() {
|
|
||||||
Mockito.`when`(cursor.count).thenReturn(0)
|
|
||||||
contributionsPresenter?.onLoadFinished(loader, cursor)
|
|
||||||
verify(view)?.showProgress(false)
|
|
||||||
verify(view)?.showWelcomeTip(true)
|
|
||||||
verify(view)?.showNoContributionsUI(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test presenter actions on loader reset
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun testOnLoaderReset() {
|
|
||||||
contributionsPresenter?.onLoaderReset(loader)
|
|
||||||
verify(view)?.showProgress(false)
|
|
||||||
verify(view)?.showWelcomeTip(true)
|
|
||||||
verify(view)?.showNoContributionsUI(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test presenter actions on loader change
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun testOnChanged() {
|
|
||||||
contributionsPresenter?.onChanged()
|
|
||||||
verify(view)?.onDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +63,7 @@ class DeleteHelperTest {
|
||||||
.thenReturn(Observable.just(true))
|
.thenReturn(Observable.just(true))
|
||||||
|
|
||||||
`when`(media?.displayTitle).thenReturn("Test file")
|
`when`(media?.displayTitle).thenReturn("Test file")
|
||||||
`when`(media?.filename).thenReturn("Test file.jpg")
|
media?.filename="Test file.jpg"
|
||||||
|
|
||||||
val makeDeletion = deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet()
|
val makeDeletion = deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet()
|
||||||
assertNotNull(makeDeletion)
|
assertNotNull(makeDeletion)
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ class ReasonBuilderTest {
|
||||||
`when`(okHttpJsonApiClient!!.getAchievements(anyString()))
|
`when`(okHttpJsonApiClient!!.getAchievements(anyString()))
|
||||||
.thenReturn(Single.just(mock(FeedbackResponse::class.java)))
|
.thenReturn(Single.just(mock(FeedbackResponse::class.java)))
|
||||||
|
|
||||||
val media = mock(Media::class.java)
|
val media = Media("test_file")
|
||||||
`when`(media!!.dateUploaded).thenReturn(Date())
|
media.dateUploaded=Date()
|
||||||
|
|
||||||
reasonBuilder!!.getReason(media, "test")
|
reasonBuilder!!.getReason(media, "test")
|
||||||
verify(sessionManager, times(0))!!.forceLogin(any(Context::class.java))
|
verify(sessionManager, times(0))!!.forceLogin(any(Context::class.java))
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class ReviewHelperTest {
|
||||||
.thenReturn(Observable.just(mockResponse))
|
.thenReturn(Observable.just(mockResponse))
|
||||||
|
|
||||||
val media = mock(Media::class.java)
|
val media = mock(Media::class.java)
|
||||||
`when`(media.filename).thenReturn("File:Test.jpg")
|
media.filename="File:Test.jpg"
|
||||||
`when`(mediaClient?.getMedia(ArgumentMatchers.anyString()))
|
`when`(mediaClient?.getMedia(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(Single.just(media))
|
.thenReturn(Single.just(media))
|
||||||
}
|
}
|
||||||
|
|
@ -74,10 +74,10 @@ class ReviewHelperTest {
|
||||||
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
|
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(Single.just(false))
|
.thenReturn(Single.just(false))
|
||||||
|
|
||||||
val randomMedia = reviewHelper?.randomMedia?.blockingGet()
|
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
|
||||||
|
.thenReturn(Single.just(false))
|
||||||
|
|
||||||
assertNotNull(randomMedia)
|
reviewHelper?.randomMedia
|
||||||
assertTrue(randomMedia is Media)
|
|
||||||
verify(reviewInterface, times(1))!!.getRecentChanges(ArgumentMatchers.anyString())
|
verify(reviewInterface, times(1))!!.getRecentChanges(ArgumentMatchers.anyString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,10 +105,7 @@ class ReviewHelperTest {
|
||||||
`when`(mediaClient?.checkPageExistsUsingTitle("Commons:Deletion_requests/File:Test3.jpg"))
|
`when`(mediaClient?.checkPageExistsUsingTitle("Commons:Deletion_requests/File:Test3.jpg"))
|
||||||
.thenReturn(Single.just(true))
|
.thenReturn(Single.just(true))
|
||||||
|
|
||||||
val media = reviewHelper?.randomMedia?.blockingGet()
|
reviewHelper?.randomMedia
|
||||||
|
|
||||||
assertNotNull(media)
|
|
||||||
assertTrue(media is Media)
|
|
||||||
verify(reviewInterface, times(1))!!.getRecentChanges(ArgumentMatchers.anyString())
|
verify(reviewInterface, times(1))!!.getRecentChanges(ArgumentMatchers.anyString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue