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:
|
||||
#https://docs.opencv.org/3.3.0/
|
||||
/libraries/opencv/javadoc/
|
||||
captures/*
|
||||
|
|
|
|||
|
|
@ -102,7 +102,14 @@ dependencies {
|
|||
|
||||
//swipe_layout
|
||||
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
|
||||
|
||||
//Room
|
||||
def room_version= '2.2.3'
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@
|
|||
android:name=".contributions.MainActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="screenSize|keyboard" />
|
||||
android:configChanges="screenSize|keyboard|orientation" />
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/title_activity_settings" />
|
||||
|
|
@ -157,18 +157,6 @@
|
|||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/authenticator" />
|
||||
</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
|
||||
android:name="org.acra.sender.SenderService"
|
||||
|
|
@ -184,12 +172,6 @@
|
|||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
<provider
|
||||
android:name=".contributions.ContributionsContentProvider"
|
||||
android:authorities="${applicationId}.contributions.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_contributions"
|
||||
android:syncable="true" />
|
||||
|
||||
<provider
|
||||
android:name=".category.CategoryContentProvider"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.app.NotificationChannel;
|
|||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
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.concurrency.BackgroundPoolExceptionHandler;
|
||||
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.db.AppDatabase;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.logging.FileLoggingTree;
|
||||
|
|
@ -60,6 +61,7 @@ import io.reactivex.schedulers.Schedulers;
|
|||
import okhttp3.OkHttpClient;
|
||||
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.APP_VERSION_CODE;
|
||||
import static org.acra.ReportField.APP_VERSION_NAME;
|
||||
|
|
@ -126,6 +128,9 @@ public class CommonsApplication extends Application {
|
|||
return languageLookUpTable;
|
||||
}
|
||||
|
||||
@Inject
|
||||
AppDatabase appDatabase;
|
||||
|
||||
/**
|
||||
* Used to declare and initialize various components and dependencies
|
||||
*/
|
||||
|
|
@ -306,11 +311,13 @@ public class CommonsApplication extends Application {
|
|||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
||||
|
||||
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);
|
||||
BookmarkLocationsDao.Table.onDelete(db);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interface used to get log-out events
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import android.os.Parcelable;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.MediaDataExtractorUtil;
|
||||
|
||||
@Entity
|
||||
public class Media implements Parcelable {
|
||||
|
||||
public static final Media EMPTY = new Media("");
|
||||
|
|
@ -42,25 +45,25 @@ public class Media implements Parcelable {
|
|||
};
|
||||
|
||||
// Primary metadata fields
|
||||
protected Uri localUri;
|
||||
private String thumbUrl;
|
||||
protected String imageUrl;
|
||||
protected String filename;
|
||||
protected String description; // monolingual description on input...
|
||||
protected String discussion;
|
||||
protected long dataLength;
|
||||
protected Date dateCreated;
|
||||
protected @Nullable Date dateUploaded;
|
||||
protected int width;
|
||||
protected int height;
|
||||
protected String license;
|
||||
protected String licenseUrl;
|
||||
protected String creator;
|
||||
protected ArrayList<String> categories; // as loaded at runtime?
|
||||
protected boolean requestedDeletion;
|
||||
private Map<String, String> descriptions; // multilingual descriptions as loaded
|
||||
private HashMap<String, Object> tags = new HashMap<>();
|
||||
private @Nullable LatLng coordinates;
|
||||
public Uri localUri;
|
||||
public String thumbUrl;
|
||||
public String imageUrl;
|
||||
public String filename;
|
||||
public String description; // monolingual description on input...
|
||||
public String discussion;
|
||||
long dataLength;
|
||||
public Date dateCreated;
|
||||
@Nullable public Date dateUploaded;
|
||||
public int width;
|
||||
public int height;
|
||||
public String license;
|
||||
public String licenseUrl;
|
||||
public String creator;
|
||||
public ArrayList<String> categories; // as loaded at runtime?
|
||||
public boolean requestedDeletion;
|
||||
public HashMap<String, String> descriptions; // multilingual descriptions as loaded
|
||||
public HashMap<String, String> tags = new HashMap<>();
|
||||
@Nullable public LatLng coordinates;
|
||||
|
||||
/**
|
||||
* Provides local constructor
|
||||
|
|
@ -118,7 +121,7 @@ public class Media implements Parcelable {
|
|||
dateCreated = (Date) in.readSerializable();
|
||||
dateUploaded = (Date) in.readSerializable();
|
||||
creator = in.readString();
|
||||
tags = (HashMap<String, Object>) in.readSerializable();
|
||||
tags = (HashMap<String, String>) in.readSerializable();
|
||||
width = in.readInt();
|
||||
height = in.readInt();
|
||||
license = in.readString();
|
||||
|
|
@ -218,7 +221,7 @@ public class Media implements Parcelable {
|
|||
* @param key Media key
|
||||
* @param value Media value
|
||||
*/
|
||||
public void setTag(String key, Object value) {
|
||||
public void setTag(String key, String value) {
|
||||
tags.put(key, value);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import android.os.Parcel;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringDef;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
|
|
@ -21,6 +23,7 @@ import fr.free.nrw.commons.utils.ConfigUtils;
|
|||
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
@Entity(tableName = "contribution")
|
||||
public class Contribution extends Media {
|
||||
|
||||
//{{According to Exif data|2009-01-09}}
|
||||
|
|
@ -54,17 +57,19 @@ public class Contribution extends Media {
|
|||
public static final String SOURCE_CAMERA = "camera";
|
||||
public static final String SOURCE_GALLERY = "gallery";
|
||||
public static final String SOURCE_EXTERNAL = "external";
|
||||
|
||||
private Uri contentUri;
|
||||
private String source;
|
||||
private String editSummary;
|
||||
private int state;
|
||||
private long transferred;
|
||||
private String decimalCoords;
|
||||
private boolean isMultiple;
|
||||
private String wikiDataEntityId;
|
||||
private Uri contentProviderUri;
|
||||
private String dateCreatedSource;
|
||||
@PrimaryKey (autoGenerate = true)
|
||||
@NonNull
|
||||
public long _id;
|
||||
public Uri contentUri;
|
||||
public String source;
|
||||
public String editSummary;
|
||||
public int state;
|
||||
public long transferred;
|
||||
public String decimalCoords;
|
||||
public boolean isMultiple;
|
||||
public String wikiDataEntityId;
|
||||
public Uri contentProviderUri;
|
||||
public String dateCreatedSource;
|
||||
|
||||
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated,
|
||||
int state, long dataLength, Date dateUploaded, long transferred,
|
||||
|
|
|
|||
|
|
@ -1,331 +1,55 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.text.TextUtils;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Delete;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Transaction;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
import java.util.Date;
|
||||
@Dao
|
||||
public abstract class ContributionDao {
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
@Query("SELECT * FROM contribution order by dateUploaded DESC")
|
||||
abstract LiveData<List<Contribution>> fetchContributions();
|
||||
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import timber.log.Timber;
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
public abstract Single<Long> save(Contribution contribution);
|
||||
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_WIKI_DATA_ENTITY_ID;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId;
|
||||
|
||||
public class ContributionDao {
|
||||
/*
|
||||
This sorts in the following order:
|
||||
Currently Uploading
|
||||
Failed (Sorted in ascending order of time added - FIFO)
|
||||
Queued to Upload (Sorted in ascending order of time added - FIFO)
|
||||
Completed (Sorted in descending order of time added)
|
||||
|
||||
This is why Contribution.STATE_COMPLETED is -1.
|
||||
*/
|
||||
static final String CONTRIBUTION_SORT = Table.COLUMN_STATE + " DESC, "
|
||||
+ Table.COLUMN_UPLOADED + " DESC , ("
|
||||
+ Table.COLUMN_TIMESTAMP + " * "
|
||||
+ Table.COLUMN_STATE + ")";
|
||||
|
||||
private final Provider<ContentProviderClient> clientProvider;
|
||||
|
||||
@Inject
|
||||
public ContributionDao(@Named("contribution") Provider<ContentProviderClient> clientProvider) {
|
||||
this.clientProvider = clientProvider;
|
||||
public Completable deleteAllAndSave(List<Contribution> contributions){
|
||||
return Completable.fromAction(() -> deleteAllAndSaveTransaction(contributions));
|
||||
}
|
||||
|
||||
Cursor loadAllContributions() {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
return db.query(BASE_URI, ALL_FIELDS, "", null, CONTRIBUTION_SORT);
|
||||
} catch (RemoteException e) {
|
||||
return null;
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
@Transaction
|
||||
public void deleteAllAndSaveTransaction(List<Contribution> contributions){
|
||||
deleteAll(Contribution.STATE_COMPLETED);
|
||||
save(contributions);
|
||||
}
|
||||
|
||||
public void save(Contribution contribution) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (contribution.getContentUri() == null) {
|
||||
contribution.setContentUri(db.insert(BASE_URI, toContentValues(contribution)));
|
||||
} else {
|
||||
db.update(contribution.getContentUri(), toContentValues(contribution), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
@Insert
|
||||
public abstract void save(List<Contribution> contribution);
|
||||
|
||||
public void delete(Contribution contribution) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (contribution.getContentUri() == null) {
|
||||
// noooo
|
||||
throw new RuntimeException("tried to delete item with no content URI");
|
||||
} else {
|
||||
db.delete(contribution.getContentUri(), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
@Delete
|
||||
public abstract Single<Integer> delete(Contribution contribution);
|
||||
|
||||
ContentValues toContentValues(Contribution contribution) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(Table.COLUMN_FILENAME, contribution.getFilename());
|
||||
if (contribution.getLocalUri() != null) {
|
||||
cv.put(Table.COLUMN_LOCAL_URI, contribution.getLocalUri().toString());
|
||||
}
|
||||
if (contribution.getImageUrl() != null) {
|
||||
cv.put(Table.COLUMN_IMAGE_URL, contribution.getImageUrl());
|
||||
}
|
||||
if (contribution.getDateUploaded() != null) {
|
||||
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
|
||||
}
|
||||
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength());
|
||||
//This was always meant to store the date created..If somehow date created is not fetched while actually saving the contribution, lets saveValue today's date
|
||||
cv.put(Table.COLUMN_TIMESTAMP, contribution.getDateCreated()==null?System.currentTimeMillis():contribution.getDateCreated().getTime());
|
||||
cv.put(Table.COLUMN_STATE, contribution.getState());
|
||||
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());
|
||||
cv.put(Table.COLUMN_SOURCE, contribution.getSource());
|
||||
cv.put(Table.COLUMN_DESCRIPTION, contribution.getDescription());
|
||||
cv.put(Table.COLUMN_CREATOR, contribution.getCreator());
|
||||
cv.put(Table.COLUMN_MULTIPLE, contribution.getMultiple() ? 1 : 0);
|
||||
cv.put(Table.COLUMN_WIDTH, contribution.getWidth());
|
||||
cv.put(Table.COLUMN_HEIGHT, contribution.getHeight());
|
||||
cv.put(Table.COLUMN_LICENSE, contribution.getLicense());
|
||||
cv.put(Table.COLUMN_WIKI_DATA_ENTITY_ID, contribution.getWikiDataEntityId());
|
||||
return cv;
|
||||
}
|
||||
@Query("SELECT * from contribution WHERE contentProviderUri=:uri")
|
||||
public abstract List<Contribution> getContributionWithUri(String uri);
|
||||
|
||||
public Contribution fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
|
||||
if (cursor.getCount() > 0) {
|
||||
int index;
|
||||
if (cursor.getColumnIndex(Table.COLUMN_LICENSE) == -1){
|
||||
index = 15;
|
||||
} else {
|
||||
index = cursor.getColumnIndex(Table.COLUMN_LICENSE);
|
||||
}
|
||||
Contribution contribution = new Contribution(
|
||||
uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_FILENAME)),
|
||||
parseUri(cursor.getString(cursor.getColumnIndex(Table.COLUMN_LOCAL_URI))),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_IMAGE_URL)),
|
||||
parseTimestamp(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_TIMESTAMP))),
|
||||
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_STATE)),
|
||||
cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LENGTH)),
|
||||
parseTimestamp(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_UPLOADED))),
|
||||
cursor.getLong(cursor.getColumnIndex(Table.COLUMN_TRANSFERRED)),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_SOURCE)),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_CREATOR)),
|
||||
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_MULTIPLE)) == 1,
|
||||
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_WIDTH)),
|
||||
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_HEIGHT)),
|
||||
cursor.getString(index)
|
||||
);
|
||||
@Query("SELECT * from contribution WHERE filename=:fileName")
|
||||
public abstract List<Contribution> getContributionWithTitle(String fileName);
|
||||
|
||||
String wikidataEntityId = cursor.getString(cursor.getColumnIndex(COLUMN_WIKI_DATA_ENTITY_ID));
|
||||
if (!StringUtils.isBlank(wikidataEntityId)) {
|
||||
contribution.setWikiDataEntityId(wikidataEntityId);
|
||||
}
|
||||
@Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
|
||||
public abstract Single<Integer> updateStates(int state, int[] toUpdateStates);
|
||||
|
||||
return contribution;
|
||||
}
|
||||
@Query("Delete FROM contribution")
|
||||
public abstract void deleteAll();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Date parseTimestamp(long timestamp) {
|
||||
return timestamp == 0 ? null : new Date(timestamp);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Uri parseUri(String uriString) {
|
||||
return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString);
|
||||
}
|
||||
|
||||
public static class Table {
|
||||
public static final String TABLE_NAME = "contributions";
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
public static final String COLUMN_FILENAME = "filename";
|
||||
public static final String COLUMN_LOCAL_URI = "local_uri";
|
||||
public static final String COLUMN_IMAGE_URL = "image_url";
|
||||
public static final String COLUMN_TIMESTAMP = "timestamp";
|
||||
public static final String COLUMN_STATE = "state";
|
||||
public static final String COLUMN_LENGTH = "length";
|
||||
public static final String COLUMN_UPLOADED = "uploaded";
|
||||
public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes
|
||||
public static final String COLUMN_SOURCE = "source";
|
||||
public static final String COLUMN_DESCRIPTION = "description";
|
||||
public static final String COLUMN_CREATOR = "creator"; // Initial uploader
|
||||
public static final String COLUMN_MULTIPLE = "multiple";
|
||||
public static final String COLUMN_WIDTH = "width";
|
||||
public static final String COLUMN_HEIGHT = "height";
|
||||
public static final String COLUMN_LICENSE = "license";
|
||||
public static final String COLUMN_WIKI_DATA_ENTITY_ID = "wikidataEntityID";
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_ID,
|
||||
COLUMN_FILENAME,
|
||||
COLUMN_LOCAL_URI,
|
||||
COLUMN_IMAGE_URL,
|
||||
COLUMN_TIMESTAMP,
|
||||
COLUMN_STATE,
|
||||
COLUMN_LENGTH,
|
||||
COLUMN_UPLOADED,
|
||||
COLUMN_TRANSFERRED,
|
||||
COLUMN_SOURCE,
|
||||
COLUMN_DESCRIPTION,
|
||||
COLUMN_CREATOR,
|
||||
COLUMN_MULTIPLE,
|
||||
COLUMN_WIDTH,
|
||||
COLUMN_HEIGHT,
|
||||
COLUMN_LICENSE,
|
||||
COLUMN_WIKI_DATA_ENTITY_ID
|
||||
};
|
||||
|
||||
public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||
|
||||
public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ "_id INTEGER PRIMARY KEY,"
|
||||
+ "filename STRING,"
|
||||
+ "local_uri STRING,"
|
||||
+ "image_url STRING,"
|
||||
+ "uploaded INTEGER,"
|
||||
+ "timestamp INTEGER,"
|
||||
+ "state INTEGER,"
|
||||
+ "length INTEGER,"
|
||||
+ "transferred INTEGER,"
|
||||
+ "source STRING,"
|
||||
+ "description STRING,"
|
||||
+ "creator STRING,"
|
||||
+ "multiple INTEGER,"
|
||||
+ "width INTEGER,"
|
||||
+ "height INTEGER,"
|
||||
+ "LICENSE STRING,"
|
||||
+ "wikidataEntityID STRING"
|
||||
+ ");";
|
||||
|
||||
// Upgrade from version 1 ->
|
||||
static final String ADD_CREATOR_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;";
|
||||
static final String ADD_DESCRIPTION_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;";
|
||||
|
||||
// Upgrade from version 2 ->
|
||||
static final String ADD_MULTIPLE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;";
|
||||
static final String SET_DEFAULT_MULTIPLE = "UPDATE " + TABLE_NAME + " SET multiple = 0";
|
||||
|
||||
// Upgrade from version 5 ->
|
||||
static final String ADD_WIDTH_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;";
|
||||
static final String SET_DEFAULT_WIDTH = "UPDATE " + TABLE_NAME + " SET width = 0";
|
||||
static final String ADD_HEIGHT_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;";
|
||||
static final String SET_DEFAULT_HEIGHT = "UPDATE " + TABLE_NAME + " SET height = 0";
|
||||
static final String ADD_LICENSE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;";
|
||||
static final String SET_DEFAULT_LICENSE = "UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';";
|
||||
|
||||
// Upgrade from version 8 ->
|
||||
static final String ADD_WIKI_DATA_ENTITY_ID_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN wikidataEntityID STRING;";
|
||||
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||
}
|
||||
|
||||
public static void onDelete(SQLiteDatabase db) {
|
||||
db.execSQL(DROP_TABLE_STATEMENT);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Considering the crashes we have been facing recently, lets blindly add this column to any table which has ever existed
|
||||
runQuery(db,ADD_WIKI_DATA_ENTITY_ID_FIELD);
|
||||
|
||||
if (from == 1) {
|
||||
runQuery(db,ADD_DESCRIPTION_FIELD);
|
||||
runQuery(db,ADD_CREATOR_FIELD);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 2) {
|
||||
runQuery(db, ADD_MULTIPLE_FIELD);
|
||||
runQuery(db, SET_DEFAULT_MULTIPLE);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 3) {
|
||||
// Do nothing
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 4) {
|
||||
// Do nothing -- added Category
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 5) {
|
||||
// Added width and height fields
|
||||
runQuery(db, ADD_WIDTH_FIELD);
|
||||
runQuery(db, SET_DEFAULT_WIDTH);
|
||||
runQuery(db, ADD_HEIGHT_FIELD);
|
||||
runQuery(db, SET_DEFAULT_HEIGHT);
|
||||
runQuery(db, ADD_LICENSE_FIELD);
|
||||
runQuery(db, SET_DEFAULT_LICENSE);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from > 5) {
|
||||
// Added place field
|
||||
from=to;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* perform the db.execSQl with handled exceptions
|
||||
*/
|
||||
private static void runQuery(SQLiteDatabase db, String query) {
|
||||
try {
|
||||
db.execSQL(query);
|
||||
} catch (SQLiteException e) {
|
||||
Timber.e("Exception performing query: " + query + " message: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@Query("Delete FROM contribution WHERE state = :state")
|
||||
public abstract void deleteAll(int state);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,186 +0,0 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.content.UriMatcher.NO_MATCH;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.TABLE_NAME;
|
||||
|
||||
public class ContributionsContentProvider extends CommonsDaggerContentProvider {
|
||||
|
||||
private static final int CONTRIBUTIONS = 1;
|
||||
private static final int CONTRIBUTIONS_ID = 2;
|
||||
private static final String BASE_PATH = "contributions";
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
|
||||
|
||||
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.CONTRIBUTION_AUTHORITY + "/" + BASE_PATH);
|
||||
|
||||
static {
|
||||
uriMatcher.addURI(BuildConfig.CONTRIBUTION_AUTHORITY, BASE_PATH, CONTRIBUTIONS);
|
||||
uriMatcher.addURI(BuildConfig.CONTRIBUTION_AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
|
||||
}
|
||||
|
||||
public static Uri uriForId(int id) {
|
||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||
}
|
||||
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
||||
String[] selectionArgs, String sortOrder) {
|
||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||
queryBuilder.setTables(TABLE_NAME);
|
||||
|
||||
int uriType = uriMatcher.match(uri);
|
||||
|
||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||
Cursor cursor;
|
||||
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
cursor = queryBuilder.query(db, projection, selection, selectionArgs,
|
||||
null, null, sortOrder);
|
||||
break;
|
||||
case CONTRIBUTIONS_ID:
|
||||
cursor = queryBuilder.query(db,
|
||||
ALL_FIELDS,
|
||||
"_id = ?",
|
||||
new String[]{uri.getLastPathSegment()},
|
||||
null,
|
||||
null,
|
||||
sortOrder
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI" + uri);
|
||||
}
|
||||
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
long id;
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return Uri.parse(BASE_URI + "/" + id);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String s, String[] strings) {
|
||||
int rows;
|
||||
int uriType = uriMatcher.match(uri);
|
||||
|
||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS_ID:
|
||||
Timber.d("Deleting contribution id %s", uri.getLastPathSegment());
|
||||
rows = db.delete(TABLE_NAME,
|
||||
"_id = ?",
|
||||
new String[]{uri.getLastPathSegment()}
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI" + uri);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return rows;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
||||
Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
sqlDB.beginTransaction();
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
for (ContentValues value : values) {
|
||||
Timber.d("Inserting! %s", value);
|
||||
sqlDB.insert(TABLE_NAME, null, value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
sqlDB.setTransactionSuccessful();
|
||||
sqlDB.endTransaction();
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return values.length;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
|
||||
/*
|
||||
SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
|
||||
Even then, we should make sure to sanitize all user input appropriately.
|
||||
Input that passes through ContentValues should be fine. So only issues are those that pass
|
||||
in via concatenating.
|
||||
|
||||
In here, the only concat created argument is for id. It is cast to an int, and will
|
||||
error out otherwise.
|
||||
*/
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
int rowsUpdated;
|
||||
switch (uriType) {
|
||||
case CONTRIBUTIONS:
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs);
|
||||
break;
|
||||
case CONTRIBUTIONS_ID:
|
||||
int id = Integer.valueOf(uri.getLastPathSegment());
|
||||
|
||||
if (TextUtils.isEmpty(selection)) {
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
ContributionDao.Table.COLUMN_ID + " = ?",
|
||||
new String[]{String.valueOf(id)});
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Parameter `selection` should be empty when updating an ID");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return rowsUpdated;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.BasePresenter;
|
||||
import fr.free.nrw.commons.Media;
|
||||
|
|
@ -22,13 +20,13 @@ public class ContributionsContract {
|
|||
|
||||
void setUploadCount(int count);
|
||||
|
||||
void onDataSetChanged();
|
||||
void showContributions(List<Contribution> contributionList);
|
||||
|
||||
void showMessage(String localizedMessage);
|
||||
}
|
||||
|
||||
public interface UserActionListener extends BasePresenter<ContributionsContract.View>,
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
Contribution getContributionsFromCursor(Cursor cursor);
|
||||
public interface UserActionListener extends BasePresenter<ContributionsContract.View> {
|
||||
Contribution getContributionsWithTitle(String uri);
|
||||
|
||||
void deleteUpload(Contribution contribution);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import androidx.fragment.app.FragmentManager;
|
|||
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
|
|
@ -71,7 +73,6 @@ public class ContributionsFragment
|
|||
LocationUpdateListener,
|
||||
ICampaignsView, ContributionsContract.View {
|
||||
@Inject @Named("default_preferences") JsonKvStore store;
|
||||
@Inject ContributionDao contributionDao;
|
||||
@Inject NearbyController nearbyController;
|
||||
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
@Inject CampaignsPresenter presenter;
|
||||
|
|
@ -118,11 +119,11 @@ public class ContributionsFragment
|
|||
};
|
||||
private boolean shouldShowMediaDetailsFragment;
|
||||
private int numberOfContributions;
|
||||
private boolean isAuthCookieAcquired;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -132,6 +133,7 @@ public class ContributionsFragment
|
|||
ButterKnife.bind(this, view);
|
||||
presenter.onAttachView(this);
|
||||
contributionsPresenter.onAttachView(this);
|
||||
contributionsPresenter.setLifeCycleOwner(this.getViewLifecycleOwner());
|
||||
campaignView.setVisibility(View.GONE);
|
||||
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
|
||||
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
|
||||
|
|
@ -210,20 +212,10 @@ public class ContributionsFragment
|
|||
showDetail(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumberOfContributions() {
|
||||
return numberOfContributions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Contribution getContributionForPosition(int position) {
|
||||
return (Contribution) contributionsPresenter.getItemAtPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findItemPositionWithId(String id) {
|
||||
return contributionsPresenter.getChildPositionWithId(id);
|
||||
}
|
||||
});
|
||||
|
||||
if(null==mediaDetailPagerFragment){
|
||||
|
|
@ -306,11 +298,10 @@ public class ContributionsFragment
|
|||
*/
|
||||
void onAuthCookieAcquired() {
|
||||
// Since we call onAuthCookieAcquired method from onAttach, isAdded is still false. So don't use it
|
||||
|
||||
isAuthCookieAcquired=true;
|
||||
if (getActivity() != null) { // If fragment is attached to parent activity
|
||||
getActivity().bindService(getUploadServiceIntent(), uploadServiceConnection, Context.BIND_AUTO_CREATE);
|
||||
isUploadServiceConnected = true;
|
||||
getActivity().getSupportLoaderManager().initLoader(0, null, contributionsPresenter);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -336,7 +327,7 @@ public class ContributionsFragment
|
|||
|
||||
@Override
|
||||
public void refreshSource() {
|
||||
getActivity().getSupportLoaderManager().restartLoader(0, null, contributionsPresenter);
|
||||
contributionsPresenter.fetchContributions();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -411,6 +402,10 @@ public class ContributionsFragment
|
|||
}
|
||||
|
||||
fetchCampaigns();
|
||||
if(isAuthCookieAcquired){
|
||||
contributionsPresenter.fetchContributions();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void checkPermissionsAndShowNearbyCardView() {
|
||||
|
|
@ -578,9 +573,8 @@ public class ContributionsFragment
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onDataSetChanged() {
|
||||
contributionsListFragment.onDataSetChanged();
|
||||
mediaDetailPagerFragment.onDataSetChanged();
|
||||
public void showContributions(List<Contribution> contributionList) {
|
||||
contributionsListFragment.setContributions(contributionList);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import android.view.ViewGroup;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.model.DisplayableContribution;
|
||||
|
||||
|
|
@ -15,9 +18,11 @@ import fr.free.nrw.commons.contributions.model.DisplayableContribution;
|
|||
public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionViewHolder> {
|
||||
|
||||
private Callback callback;
|
||||
private List<Contribution> contributions;
|
||||
|
||||
public ContributionsListAdapter(Callback callback) {
|
||||
this.callback = callback;
|
||||
contributions=new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -35,7 +40,7 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
|
|||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
|
||||
final Contribution contribution = callback.getContributionForPosition(position);
|
||||
final Contribution contribution = contributions.get(position);
|
||||
DisplayableContribution displayableContribution = new DisplayableContribution(contribution,
|
||||
position);
|
||||
holder.init(position, displayableContribution);
|
||||
|
|
@ -43,7 +48,15 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
|
|||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return callback.getNumberOfContributions();
|
||||
return contributions.size();
|
||||
}
|
||||
|
||||
public void setContributions(List<Contribution> contributionList) {
|
||||
if(null!=contributionList) {
|
||||
this.contributions.clear();
|
||||
this.contributions.addAll(contributionList);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
|
|
@ -54,10 +67,6 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
|
|||
|
||||
void openMediaDetail(int contribution);
|
||||
|
||||
int getNumberOfContributions();
|
||||
|
||||
Contribution getContributionForPosition(int position);
|
||||
|
||||
int findItemPositionWithId(String lastVisibleItemID);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ import androidx.recyclerview.widget.RecyclerView.LayoutManager;
|
|||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
|
|
@ -72,6 +75,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
|||
private String lastVisibleItemID;
|
||||
|
||||
private int SPAN_COUNT=3;
|
||||
private List<Contribution> contributions=new ArrayList<>();
|
||||
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
|
||||
|
|
@ -104,6 +108,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
|
||||
rvContributionsList.setAdapter(adapter);
|
||||
adapter.setContributions(contributions);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -178,16 +183,10 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
|||
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
public void onDataSetChanged() {
|
||||
if (null != adapter) {
|
||||
adapter.notifyDataSetChanged();
|
||||
//Restoring last visible item position in cases of orientation change
|
||||
if (null != lastVisibleItemID) {
|
||||
int itemPositionWithId = callback.findItemPositionWithId(lastVisibleItemID);
|
||||
rvContributionsList.scrollToPosition(itemPositionWithId);
|
||||
lastVisibleItemID = null;//Reset the lastVisibleItemID once we have used it
|
||||
}
|
||||
}
|
||||
public void setContributions(List<Contribution> contributionList) {
|
||||
this.contributions.clear();
|
||||
this.contributions.addAll(contributionList);
|
||||
adapter.setContributions(contributions);
|
||||
}
|
||||
|
||||
public interface SourceRefresher {
|
||||
|
|
@ -228,7 +227,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
|||
private String findIdOfItemWithPosition(int position) {
|
||||
Contribution contributionForPosition = callback.getContributionForPosition(position);
|
||||
if (null != contributionForPosition) {
|
||||
return contributionForPosition.getContentUri().getLastPathSegment();
|
||||
return contributionForPosition.getFilename();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.database.Cursor;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
/**
|
||||
* The LocalDataSource class for Contributions
|
||||
*/
|
||||
class ContributionsLocalDataSource {
|
||||
|
||||
private final ContributionDao contributionsDao;
|
||||
private final ContributionDao contributionDao;
|
||||
private final JsonKvStore defaultKVStore;
|
||||
|
||||
@Inject
|
||||
|
|
@ -20,30 +24,54 @@ class ContributionsLocalDataSource {
|
|||
@Named("default_preferences") JsonKvStore defaultKVStore,
|
||||
ContributionDao contributionDao) {
|
||||
this.defaultKVStore = defaultKVStore;
|
||||
this.contributionsDao = contributionDao;
|
||||
this.contributionDao = contributionDao;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch default number of contributions to be show, based on user preferences
|
||||
*/
|
||||
public int get(String key) {
|
||||
return defaultKVStore.getInt(key);
|
||||
public String getString(String key) {
|
||||
return defaultKVStore.getString(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch default number of contributions to be show, based on user preferences
|
||||
*/
|
||||
public long getLong(String key) {
|
||||
return defaultKVStore.getLong(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contribution object from cursor
|
||||
* @param cursor
|
||||
* @param uri
|
||||
* @return
|
||||
*/
|
||||
public Contribution getContributionFromCursor(Cursor cursor) {
|
||||
return contributionsDao.fromCursor(cursor);
|
||||
public Contribution getContributionWithFileName(String uri) {
|
||||
List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(uri);
|
||||
if(!contributionWithUri.isEmpty()){
|
||||
return contributionWithUri.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a contribution from the contributions table
|
||||
* @param contribution
|
||||
* @return
|
||||
*/
|
||||
public void deleteContribution(Contribution contribution) {
|
||||
contributionsDao.delete(contribution);
|
||||
public Single<Integer> deleteContribution(Contribution contribution) {
|
||||
return contributionDao.delete(contribution);
|
||||
}
|
||||
|
||||
public LiveData<List<Contribution>> getContributions() {
|
||||
return contributionDao.fetchContributions();
|
||||
}
|
||||
|
||||
public Completable saveContributions(List<Contribution> contributions) {
|
||||
return contributionDao.deleteAllAndSave(contributions);
|
||||
}
|
||||
|
||||
public void set(String key, long value) {
|
||||
defaultKVStore.putLong(key,value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,104 +3,156 @@ package fr.free.nrw.commons.contributions;
|
|||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DataSetObserver;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
|
||||
import fr.free.nrw.commons.db.AppDatabase;
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
||||
import fr.free.nrw.commons.mwapi.UserClient;
|
||||
import fr.free.nrw.commons.utils.ExecutorUtils;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.SingleObserver;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
|
||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
|
||||
|
||||
/**
|
||||
* The presenter class for Contributions
|
||||
*/
|
||||
public class ContributionsPresenter extends DataSetObserver implements UserActionListener {
|
||||
public class ContributionsPresenter implements UserActionListener {
|
||||
|
||||
private final ContributionsRepository repository;
|
||||
private final Scheduler mainThreadScheduler;
|
||||
private final Scheduler ioThreadScheduler;
|
||||
private CompositeDisposable compositeDisposable;
|
||||
private ContributionsContract.View view;
|
||||
private Cursor cursor;
|
||||
private List<Contribution> contributionList=new ArrayList<>();
|
||||
|
||||
@Inject
|
||||
Context context;
|
||||
|
||||
@Inject
|
||||
ContributionsPresenter(ContributionsRepository repository) {
|
||||
UserClient userClient;
|
||||
|
||||
@Inject
|
||||
AppDatabase appDatabase;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
private LifecycleOwner lifeCycleOwner;
|
||||
|
||||
@Inject
|
||||
ContributionsPresenter(ContributionsRepository repository, @Named(CommonsApplicationModule.MAIN_THREAD) Scheduler mainThreadScheduler,@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) {
|
||||
this.repository = repository;
|
||||
this.mainThreadScheduler=mainThreadScheduler;
|
||||
this.ioThreadScheduler=ioThreadScheduler;
|
||||
}
|
||||
|
||||
private String user;
|
||||
|
||||
@Override
|
||||
public void onAttachView(ContributionsContract.View view) {
|
||||
this.view = view;
|
||||
if (null != cursor) {
|
||||
try {
|
||||
cursor.registerDataSetObserver(this);
|
||||
} catch (IllegalStateException e) {//Cursor might be already registered
|
||||
Timber.d(e);
|
||||
}
|
||||
compositeDisposable=new CompositeDisposable();
|
||||
}
|
||||
|
||||
public void setLifeCycleOwner(LifecycleOwner lifeCycleOwner){
|
||||
this.lifeCycleOwner=lifeCycleOwner;
|
||||
}
|
||||
|
||||
public void fetchContributions() {
|
||||
Timber.d("fetch Contributions");
|
||||
LiveData<List<Contribution>> liveDataContributions = repository.fetchContributions();
|
||||
if(null!=lifeCycleOwner) {
|
||||
liveDataContributions.observe(lifeCycleOwner, this::showContributions);
|
||||
}
|
||||
|
||||
if (NetworkUtils.isInternetConnectionEstablished(CommonsApplication.getInstance()) && shouldFetchContributions()) {
|
||||
Timber.d("fetching contributions: ");
|
||||
view.showProgress(true);
|
||||
this.user = sessionManager.getUserName();
|
||||
view.showContributions(Collections.emptyList());
|
||||
compositeDisposable.add(userClient.logEvents(user)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.doOnNext(mwQueryLogEvent -> Timber.d("Received image %s", mwQueryLogEvent.title()))
|
||||
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title()))
|
||||
.map(image -> {
|
||||
Contribution contribution = new Contribution(null, null, image.title(),
|
||||
"", -1, image.date(), image.date(), user,
|
||||
"", "", STATE_COMPLETED);
|
||||
return contribution;
|
||||
})
|
||||
.toList()
|
||||
.subscribe(this::saveContributionsToDB, error -> {
|
||||
Timber.e("Failed to fetch contributions: %s", error.getMessage());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private void showContributions(@NonNull List<Contribution> contributions) {
|
||||
view.showProgress(false);
|
||||
if (contributions.isEmpty()) {
|
||||
view.showWelcomeTip(true);
|
||||
view.showNoContributionsUI(true);
|
||||
} else {
|
||||
view.showWelcomeTip(false);
|
||||
view.showNoContributionsUI(false);
|
||||
view.setUploadCount(contributions.size());
|
||||
view.showContributions(contributions);
|
||||
this.contributionList.clear();
|
||||
this.contributionList.addAll(contributions);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveContributionsToDB(List<Contribution> contributions) {
|
||||
Timber.e("Fetched: "+contributions.size()+" contributions "+" saving to db");
|
||||
repository.save(contributions).subscribeOn(ioThreadScheduler).subscribe();
|
||||
repository.set("last_fetch_timestamp",System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private boolean shouldFetchContributions() {
|
||||
long lastFetchTimestamp = repository.getLong("last_fetch_timestamp");
|
||||
Timber.d("last fetch timestamp: %s", lastFetchTimestamp);
|
||||
if(lastFetchTimestamp!=0){
|
||||
return System.currentTimeMillis()-lastFetchTimestamp>15*60*100;
|
||||
}
|
||||
Timber.d("should fetch contributions: %s", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachView() {
|
||||
this.view = null;
|
||||
if (null != cursor) {
|
||||
try {
|
||||
cursor.unregisterDataSetObserver(this);
|
||||
} catch (Exception e) {//Cursor might not be already registered
|
||||
Timber.d(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
|
||||
int preferredNumberOfUploads = repository.get(UPLOADS_SHOWING);
|
||||
return new CursorLoader(context, BASE_URI,
|
||||
ALL_FIELDS, "", null,
|
||||
ContributionDao.CONTRIBUTION_SORT + "LIMIT "
|
||||
+ (preferredNumberOfUploads>0?preferredNumberOfUploads:100));
|
||||
compositeDisposable.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
|
||||
view.showProgress(false);
|
||||
if (null != cursor && cursor.getCount() > 0) {
|
||||
view.showWelcomeTip(false);
|
||||
view.showNoContributionsUI(false);
|
||||
view.setUploadCount(cursor.getCount());
|
||||
} else {
|
||||
view.showWelcomeTip(true);
|
||||
view.showNoContributionsUI(true);
|
||||
}
|
||||
swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
this.cursor = null;
|
||||
//On LoadFinished is not guaranteed to be called
|
||||
view.showProgress(false);
|
||||
view.showWelcomeTip(true);
|
||||
view.showNoContributionsUI(true);
|
||||
swapCursor(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contribution from the repository
|
||||
*/
|
||||
@Override
|
||||
public Contribution getContributionsFromCursor(Cursor cursor) {
|
||||
return repository.getContributionFromCursor(cursor);
|
||||
public Contribution getContributionsWithTitle(String title) {
|
||||
return repository.getContributionWithFileName(title);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -109,75 +161,23 @@ public class ContributionsPresenter extends DataSetObserver implements UserActio
|
|||
*/
|
||||
@Override
|
||||
public void deleteUpload(Contribution contribution) {
|
||||
repository.deleteContributionFromDB(contribution);
|
||||
compositeDisposable.add(repository.deleteContributionFromDB(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a contribution at the specified cursor position
|
||||
*
|
||||
* @param i
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Media getItemAtPosition(int i) {
|
||||
if (null != cursor && cursor.moveToPosition(i)) {
|
||||
return getContributionsFromCursor(cursor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contribution position with id
|
||||
*/
|
||||
public int getChildPositionWithId(String id) {
|
||||
int position = 0;
|
||||
cursor.moveToFirst();
|
||||
while (null != cursor && cursor.moveToNext()) {
|
||||
if (getContributionsFromCursor(cursor).getContentUri().getLastPathSegment()
|
||||
.equals(id)) {
|
||||
position = cursor.getPosition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged() {
|
||||
super.onChanged();
|
||||
view.onDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidated() {
|
||||
super.onInvalidated();
|
||||
//Not letting the view know of this as of now, TODO discuss how to handle this and maybe show a proper ui for this
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap in a new Cursor, returning the old Cursor. The returned old Cursor is <em>not</em>
|
||||
* closed.
|
||||
*
|
||||
* @param newCursor The new cursor to be used.
|
||||
* @return Returns the previously set Cursor, or null if there was not one. If the given new
|
||||
* Cursor is the same instance is the previously set Cursor, null is also returned.
|
||||
*/
|
||||
private void swapCursor(Cursor newCursor) {
|
||||
try {
|
||||
if (newCursor == cursor) {
|
||||
return;
|
||||
}
|
||||
Cursor oldCursor = cursor;
|
||||
if (oldCursor != null) {
|
||||
oldCursor.unregisterDataSetObserver(this);
|
||||
}
|
||||
cursor = newCursor;
|
||||
if (newCursor != null) {
|
||||
newCursor.registerDataSetObserver(this);
|
||||
}
|
||||
view.onDataSetChanged();
|
||||
} catch (IllegalStateException e) {//Cursor might [not] be already registered/unregistered
|
||||
Timber.e(e);
|
||||
if (i == -1 || contributionList.size() < i+1) {
|
||||
return null;
|
||||
}
|
||||
return contributionList.get(i);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.database.Cursor;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
/**
|
||||
* The repository class for contributions
|
||||
*/
|
||||
|
|
@ -19,25 +24,41 @@ public class ContributionsRepository {
|
|||
/**
|
||||
* Fetch default number of contributions to be show, based on user preferences
|
||||
*/
|
||||
public int get(String uploadsShowing) {
|
||||
return localDataSource.get(uploadsShowing);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get contribution object from cursor from LocalDataSource
|
||||
* @param cursor
|
||||
* @return
|
||||
*/
|
||||
public Contribution getContributionFromCursor(Cursor cursor) {
|
||||
return localDataSource.getContributionFromCursor(cursor);
|
||||
public String getString(String key) {
|
||||
return localDataSource.getString(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a failed upload from DB
|
||||
* @param contribution
|
||||
* @return
|
||||
*/
|
||||
public void deleteContributionFromDB(Contribution contribution) {
|
||||
localDataSource.deleteContribution(contribution);
|
||||
public Single<Integer> deleteContributionFromDB(Contribution contribution) {
|
||||
return localDataSource.deleteContribution(contribution);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contribution object with title
|
||||
* @param fileName
|
||||
* @return
|
||||
*/
|
||||
public Contribution getContributionWithFileName(String fileName) {
|
||||
return localDataSource.getContributionWithFileName(fileName);
|
||||
}
|
||||
|
||||
public LiveData<List<Contribution>> fetchContributions() {
|
||||
return localDataSource.getContributions();
|
||||
}
|
||||
|
||||
public Completable save(List<Contribution> contributions) {
|
||||
return localDataSource.saveContributions(contributions);
|
||||
}
|
||||
|
||||
public void set(String key, long value) {
|
||||
localDataSource.set(key,value);
|
||||
}
|
||||
|
||||
public long getLong(String key) {
|
||||
return localDataSource.getLong(key);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SyncResult;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.mwapi.UserClient;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
|
||||
import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_FILENAME;
|
||||
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
|
||||
private static final String[] existsQuery = {COLUMN_FILENAME};
|
||||
private static final String existsSelection = COLUMN_FILENAME + " = ?";
|
||||
private static final ContentValues[] EMPTY = {};
|
||||
|
||||
@Inject
|
||||
UserClient userClient;
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
JsonKvStore defaultKvStore;
|
||||
|
||||
public ContributionsSyncAdapter(Context context, boolean autoInitialize) {
|
||||
super(context, autoInitialize);
|
||||
}
|
||||
|
||||
private boolean fileExists(ContentProviderClient client, String filename) {
|
||||
if (filename == null) {
|
||||
return false;
|
||||
}
|
||||
try (Cursor cursor = client.query(BASE_URI,
|
||||
existsQuery,
|
||||
existsSelection,
|
||||
new String[]{filename},
|
||||
""
|
||||
)) {
|
||||
return cursor != null && cursor.getCount() != 0;
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle bundle, String authority,
|
||||
ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
||||
ApplicationlessInjection
|
||||
.getInstance(getContext()
|
||||
.getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
// This code is(was?) fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
||||
String user = account.name;
|
||||
ContributionDao contributionDao = new ContributionDao(() -> contentProviderClient);
|
||||
userClient.logEvents(user)
|
||||
.doOnNext(mwQueryLogEvent->Timber.d("Received image %s", mwQueryLogEvent.title() ))
|
||||
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted())
|
||||
.filter(mwQueryLogEvent -> !fileExists(contentProviderClient, mwQueryLogEvent.title()))
|
||||
.doOnNext(mwQueryLogEvent->Timber.d("Image %s passed filters", mwQueryLogEvent.title() ))
|
||||
.map(image -> new Contribution(null, null, image.title(),
|
||||
"", -1, image.date(), image.date(), user,
|
||||
"", "", STATE_COMPLETED))
|
||||
.map(contributionDao::toContentValues)
|
||||
.buffer(10)
|
||||
.subscribe(imageValues->contentProviderClient.bulkInsert(BASE_URI, imageValues.toArray(EMPTY)));
|
||||
Timber.d("Oh hai, everyone! Look, a kitty!");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
|
||||
public class ContributionsSyncService extends Service {
|
||||
|
||||
private static final Object sSyncAdapterLock = new Object();
|
||||
|
||||
private static ContributionsSyncAdapter sSyncAdapter = null;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
synchronized (sSyncAdapterLock) {
|
||||
if (sSyncAdapter == null) {
|
||||
sSyncAdapter = new ContributionsSyncAdapter(this, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return sSyncAdapter.getSyncAdapterBinder();
|
||||
}
|
||||
}
|
||||
|
|
@ -113,8 +113,6 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
|
|||
|
||||
private void initMain() {
|
||||
//Do not remove this, this triggers the sync service
|
||||
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(),BuildConfig.CONTRIBUTION_AUTHORITY,true);
|
||||
requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle());
|
||||
Intent uploadServiceIntent = new Intent(this, UploadService.class);
|
||||
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
|
||||
startService(uploadServiceIntent);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public class DisplayableContribution extends Contribution {
|
|||
contribution.getWidth(),
|
||||
contribution.getHeight(),
|
||||
contribution.getLicense());
|
||||
this._id=contribution._id;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,20 @@ package fr.free.nrw.commons.data;
|
|||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
||||
import fr.free.nrw.commons.category.CategoryDao;
|
||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
||||
|
||||
public class DBOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
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
|
||||
|
|
@ -25,7 +27,6 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
|||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
||||
ContributionDao.Table.onCreate(sqLiteDatabase);
|
||||
CategoryDao.Table.onCreate(sqLiteDatabase);
|
||||
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
|
||||
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
|
||||
|
|
@ -34,10 +35,23 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
|||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
||||
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
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() +
|
||||
"}} ~~~~";
|
||||
|
||||
return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
|
||||
return pageEditClient.prependEdit(media.filename, fileDeleteString + "\n", summary)
|
||||
.flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
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.contributions.ContributionViewHolder;
|
||||
import fr.free.nrw.commons.contributions.ContributionsModule;
|
||||
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
|
||||
import fr.free.nrw.commons.nearby.PlaceRenderer;
|
||||
import fr.free.nrw.commons.review.ReviewController;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
|
|
@ -37,8 +38,6 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget;
|
|||
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
|
||||
void inject(CommonsApplication application);
|
||||
|
||||
void inject(ContributionsSyncAdapter syncAdapter);
|
||||
|
||||
void inject(LoginActivity activity);
|
||||
|
||||
void inject(SettingsFragment fragment);
|
||||
|
|
@ -56,9 +55,12 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
|
|||
|
||||
void inject(ContributionViewHolder viewHolder);
|
||||
|
||||
Gson gson();
|
||||
|
||||
@Component.Builder
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
interface Builder {
|
||||
|
||||
Builder appModule(CommonsApplicationModule applicationModule);
|
||||
|
||||
CommonsApplicationComponent build();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.content.Context;
|
|||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import androidx.collection.LruCache;
|
||||
import androidx.room.Room;
|
||||
|
||||
import com.github.varunpant.quadtree.QuadTree;
|
||||
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.auth.AccountUtil;
|
||||
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.db.AppDatabase;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
|
|
@ -53,6 +55,7 @@ public class CommonsApplicationModule {
|
|||
private Context applicationContext;
|
||||
public static final String IO_THREAD="io_thread";
|
||||
public static final String MAIN_THREAD="main_thread";
|
||||
private AppDatabase appDatabase;
|
||||
|
||||
public CommonsApplicationModule(Context applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
|
|
@ -229,4 +232,16 @@ public class CommonsApplicationModule {
|
|||
public QuadTree providesQuadTres() {
|
||||
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.pictures.BookmarkPicturesContentProvider;
|
||||
import fr.free.nrw.commons.category.CategoryContentProvider;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
|
||||
|
||||
/**
|
||||
|
|
@ -17,9 +16,6 @@ import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
|
|||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public abstract class ContentProviderBuilderModule {
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ContributionsContentProvider bindContributionsContentProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract CategoryContentProvider bindCategoryContentProvider();
|
||||
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@ public class UploadModel {
|
|||
contribution.setTag("mimeType", item.mimeType);
|
||||
contribution.setSource(item.source);
|
||||
contribution.setContentProviderUri(item.mediaUri);
|
||||
contribution.setDateUploaded(new Date());
|
||||
|
||||
Timber.d("Created timestamp while building contribution is %s, %s",
|
||||
item.getCreatedTimestamp(),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.upload;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
|
|
@ -20,6 +19,7 @@ import java.util.regex.Matcher;
|
|||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
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.contributions.Contribution;
|
||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditService;
|
||||
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 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 EXTRA_SOURCE = EXTRA_PREFIX + ".source";
|
||||
public static final String EXTRA_FILES = EXTRA_PREFIX + ".files";
|
||||
|
||||
@Inject WikidataEditService wikidataEditService;
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject ContributionDao contributionDao;
|
||||
@Inject
|
||||
ContributionDao contributionDao;
|
||||
@Inject UploadClient uploadClient;
|
||||
@Inject MediaClient mediaClient;
|
||||
@Inject
|
||||
@Named(CommonsApplicationModule.MAIN_THREAD)
|
||||
Scheduler mainThreadScheduler;
|
||||
@Inject
|
||||
@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler;
|
||||
|
||||
private NotificationManagerCompat notificationManager;
|
||||
private NotificationCompat.Builder curNotification;
|
||||
private int toUpload;
|
||||
private CompositeDisposable compositeDisposable;
|
||||
|
||||
/**
|
||||
* 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());
|
||||
|
||||
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
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
compositeDisposable.dispose();
|
||||
Timber.d("UploadService.onDestroy; %s are yet to be uploaded", unfinishedUploads);
|
||||
}
|
||||
|
||||
|
|
@ -120,6 +134,7 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
public void onCreate() {
|
||||
super.onCreate();
|
||||
CommonsApplication.createNotificationChannel(getApplicationContext());
|
||||
compositeDisposable = new CompositeDisposable();
|
||||
notificationManager = NotificationManagerCompat.from(this);
|
||||
curNotification = getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL);
|
||||
}
|
||||
|
|
@ -143,15 +158,20 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
|
||||
contribution.setState(Contribution.STATE_QUEUED);
|
||||
contribution.setTransferred(0);
|
||||
contributionDao.save(contribution);
|
||||
toUpload++;
|
||||
if (curNotification != null && toUpload != 1) {
|
||||
curNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload));
|
||||
Timber.d("%d uploads left", toUpload);
|
||||
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
|
||||
}
|
||||
|
||||
super.queue(what, contribution);
|
||||
compositeDisposable.add(contributionDao
|
||||
.save(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(aLong->{
|
||||
contribution._id = aLong;
|
||||
UploadService.super.queue(what, contribution);
|
||||
}, Throwable::printStackTrace));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown value for what");
|
||||
|
|
@ -163,16 +183,10 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) {
|
||||
ContentValues failedValues = new ContentValues();
|
||||
failedValues.put(ContributionDao.Table.COLUMN_STATE, Contribution.STATE_FAILED);
|
||||
|
||||
int updated = getContentResolver().update(ContributionsContentProvider.BASE_URI,
|
||||
failedValues,
|
||||
ContributionDao.Table.COLUMN_STATE + " = ? OR " + ContributionDao.Table.COLUMN_STATE + " = ?",
|
||||
new String[]{ String.valueOf(Contribution.STATE_QUEUED), String.valueOf(Contribution.STATE_IN_PROGRESS) }
|
||||
);
|
||||
Timber.d("Set %d uploads to failed", updated);
|
||||
Timber.d("Flags is %d id is %d", flags, startId);
|
||||
compositeDisposable.add(contributionDao.updateStates(Contribution.STATE_FAILED, new int[]{Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS})
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe());
|
||||
freshStart = false;
|
||||
}
|
||||
return START_REDELIVER_INTENT;
|
||||
|
|
@ -272,7 +286,11 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
contribution.setState(Contribution.STATE_COMPLETED);
|
||||
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatShort()
|
||||
.parse(uploadResult.getImageinfo().getTimestamp()));
|
||||
contributionDao.save(contribution);
|
||||
compositeDisposable.add(contributionDao
|
||||
.save(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe());
|
||||
}
|
||||
}, throwable -> {
|
||||
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());
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@ class TestCommonsApplication : Application() {
|
|||
.build()
|
||||
}
|
||||
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
|
||||
|
||||
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.Loader
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
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.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
|
@ -15,11 +26,11 @@ import org.mockito.MockitoAnnotations
|
|||
*/
|
||||
class ContributionsPresenterTest {
|
||||
@Mock
|
||||
internal var repository: ContributionsRepository? = null
|
||||
internal lateinit var repository: ContributionsRepository
|
||||
@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
|
||||
|
||||
|
|
@ -27,6 +38,12 @@ class ContributionsPresenterTest {
|
|||
|
||||
lateinit var loader: Loader<Cursor>
|
||||
|
||||
lateinit var liveData: LiveData<List<Contribution>>
|
||||
|
||||
@Rule @JvmField var instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
lateinit var scheduler : Scheduler
|
||||
|
||||
/**
|
||||
* initial setup
|
||||
*/
|
||||
|
|
@ -34,21 +51,24 @@ class ContributionsPresenterTest {
|
|||
@Throws(Exception::class)
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
scheduler=TestScheduler()
|
||||
cursor = Mockito.mock(Cursor::class.java)
|
||||
contribution = Mockito.mock(Contribution::class.java)
|
||||
contributionsPresenter = ContributionsPresenter(repository)
|
||||
contributionsPresenter = ContributionsPresenter(repository,scheduler,scheduler)
|
||||
loader = Mockito.mock(CursorLoader::class.java)
|
||||
contributionsPresenter?.onAttachView(view)
|
||||
contributionsPresenter.onAttachView(view)
|
||||
liveData=MutableLiveData()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test presenter actions onGetContributionFromCursor
|
||||
* Test fetch contributions
|
||||
*/
|
||||
@Test
|
||||
fun testGetContributionFromCursor() {
|
||||
contributionsPresenter?.getContributionsFromCursor(cursor)
|
||||
verify(repository)?.getContributionFromCursor(cursor)
|
||||
fun testFetchContributions(){
|
||||
whenever(repository.getString(ArgumentMatchers.anyString())).thenReturn("10")
|
||||
whenever(repository.fetchContributions()).thenReturn(liveData)
|
||||
contributionsPresenter.fetchContributions()
|
||||
verify(repository).fetchContributions()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -56,55 +76,20 @@ class ContributionsPresenterTest {
|
|||
*/
|
||||
@Test
|
||||
fun testDeleteContribution() {
|
||||
contributionsPresenter?.deleteUpload(contribution)
|
||||
verify(repository)?.deleteContributionFromDB(contribution)
|
||||
whenever(repository.deleteContributionFromDB(ArgumentMatchers.any(Contribution::class.java))).thenReturn(Single.just(1))
|
||||
contributionsPresenter.deleteUpload(contribution)
|
||||
verify(repository).deleteContributionFromDB(contribution)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test presenter actions on loaderFinished and has non zero media objects
|
||||
* Test fetch contribution with filename
|
||||
*/
|
||||
@Test
|
||||
fun testOnLoaderFinishedNonZeroContributions() {
|
||||
Mockito.`when`(cursor.count).thenReturn(1)
|
||||
contributionsPresenter?.onLoadFinished(loader, cursor)
|
||||
verify(view)?.showProgress(false)
|
||||
verify(view)?.showWelcomeTip(false)
|
||||
verify(view)?.showNoContributionsUI(false)
|
||||
verify(view)?.setUploadCount(cursor.count)
|
||||
fun testGetContributionWithFileName(){
|
||||
contributionsPresenter.getContributionsWithTitle("ashish")
|
||||
verify(repository).getContributionWithFileName("ashish")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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))
|
||||
|
||||
`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()
|
||||
assertNotNull(makeDeletion)
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ class ReasonBuilderTest {
|
|||
`when`(okHttpJsonApiClient!!.getAchievements(anyString()))
|
||||
.thenReturn(Single.just(mock(FeedbackResponse::class.java)))
|
||||
|
||||
val media = mock(Media::class.java)
|
||||
`when`(media!!.dateUploaded).thenReturn(Date())
|
||||
val media = Media("test_file")
|
||||
media.dateUploaded=Date()
|
||||
|
||||
reasonBuilder!!.getReason(media, "test")
|
||||
verify(sessionManager, times(0))!!.forceLogin(any(Context::class.java))
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class ReviewHelperTest {
|
|||
.thenReturn(Observable.just(mockResponse))
|
||||
|
||||
val media = mock(Media::class.java)
|
||||
`when`(media.filename).thenReturn("File:Test.jpg")
|
||||
media.filename="File:Test.jpg"
|
||||
`when`(mediaClient?.getMedia(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Single.just(media))
|
||||
}
|
||||
|
|
@ -74,10 +74,10 @@ class ReviewHelperTest {
|
|||
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Single.just(false))
|
||||
|
||||
val randomMedia = reviewHelper?.randomMedia?.blockingGet()
|
||||
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Single.just(false))
|
||||
|
||||
assertNotNull(randomMedia)
|
||||
assertTrue(randomMedia is Media)
|
||||
reviewHelper?.randomMedia
|
||||
verify(reviewInterface, times(1))!!.getRecentChanges(ArgumentMatchers.anyString())
|
||||
}
|
||||
|
||||
|
|
@ -105,10 +105,7 @@ class ReviewHelperTest {
|
|||
`when`(mediaClient?.checkPageExistsUsingTitle("Commons:Deletion_requests/File:Test3.jpg"))
|
||||
.thenReturn(Single.just(true))
|
||||
|
||||
val media = reviewHelper?.randomMedia?.blockingGet()
|
||||
|
||||
assertNotNull(media)
|
||||
assertTrue(media is Media)
|
||||
reviewHelper?.randomMedia
|
||||
verify(reviewInterface, times(1))!!.getRecentChanges(ArgumentMatchers.anyString())
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue