[GSoC] Added Uploaded status table in room database. (#4476)

* added Uploaded status table in room database

* Added unique property, minor refractoring

* Database intigrated

* Database integrated

* Handled result null exception

* Exceptions handled and refractored

* Introduced constants

* moved to sealed class

* No database insert on network error

* queried original image

* documented the code

* Updated uploaded status on upload success
This commit is contained in:
Aditya-Srivastav 2021-06-29 10:09:00 +05:30 committed by GitHub
parent 7a6b24470e
commit 8e9d289628
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 296 additions and 38 deletions

View file

@ -39,7 +39,8 @@ data class Contribution constructor(
var dataLength: Long = 0, var dataLength: Long = 0,
var dateCreated: Date? = null, var dateCreated: Date? = null,
var dateModified: Date? = null, var dateModified: Date? = null,
var hasInvalidLocation : Int = 0 var hasInvalidLocation : Int = 0,
var contentUri: Uri? = null
) : Parcelable { ) : Parcelable {
fun completeWith(media: Media): Contribution { fun completeWith(media: Media): Contribution {
@ -64,7 +65,8 @@ data class Contribution constructor(
decimalCoords = item.gpsCoords.decimalCoords, decimalCoords = item.gpsCoords.decimalCoords,
dateCreatedSource = "", dateCreatedSource = "",
depictedItems = depictedItems, depictedItems = depictedItems,
wikidataPlace = from(item.place) wikidataPlace = from(item.place),
contentUri = item.contentUri
) )
/** /**

View file

@ -0,0 +1,88 @@
package fr.free.nrw.commons.customselector.database
import androidx.room.*
import kotlinx.coroutines.runBlocking
import java.util.*
import kotlinx.coroutines.*
/**
* UploadedStatusDao for Custom Selector.
*/
@Dao
abstract class UploadedStatusDao {
/**
* Insert into uploaded status.
*/
@Insert( onConflict = OnConflictStrategy.REPLACE )
abstract suspend fun insert(uploadedStatus: UploadedStatus)
/**
* Update uploaded status entry.
*/
@Update
abstract suspend fun update(uploadedStatus: UploadedStatus)
/**
* Delete uploaded status entry.
*/
@Delete
abstract suspend fun delete(uploadedStatus: UploadedStatus)
/**
* Get All entries from the uploaded status table.
*/
@Query("SELECT * FROM uploaded_table")
abstract suspend fun getAll() : List<UploadedStatus>
/**
* Query uploaded status with image sha1.
*/
@Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ")
abstract suspend fun getFromImageSHA1(imageSHA1 : String) : UploadedStatus
/**
* Query uploaded status with modified image sha1.
*/
@Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ")
abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1 : String) : UploadedStatus
/**
* Asynchronous insert into uploaded status table.
*/
fun insertUploaded(uploadedStatus: UploadedStatus) = runBlocking {
async {
uploadedStatus.lastUpdated = Calendar.getInstance().time as Date?
insert(uploadedStatus)
}.await()
}
/**
* Asynchronous delete from uploaded status table.
*/
fun deleteUploaded(uploadedStatus: UploadedStatus) = runBlocking {
async { delete(uploadedStatus) }
}
/**
* Asynchronous update entry in uploaded status table.
*/
fun updateUploaded(uploadedStatus: UploadedStatus) = runBlocking {
async { update(uploadedStatus) }
}
/**
* Asynchronous image sha1 query.
*/
fun getUploadedFromImageSHA1(imageSHA1: String) = runBlocking<UploadedStatus?> {
async { getFromImageSHA1(imageSHA1) }.await()
}
/**
* Asynchronous modified image sha1 query.
*/
fun getUploadedFromModifiedImageSHA1(modifiedImageSHA1: String) = runBlocking<UploadedStatus?> {
async { getFromModifiedImageSHA1(modifiedImageSHA1) }.await()
}
}

View file

@ -0,0 +1,39 @@
package fr.free.nrw.commons.customselector.database
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import java.util.*
/**
* Entity class for Uploaded Status.
*/
@Entity(tableName = "uploaded_table", indices = [Index(value = ["modifiedImageSHA1"], unique = true)])
data class UploadedStatus(
/**
* Original image sha1.
*/
@PrimaryKey
val imageSHA1 : String,
/**
* Modified image sha1 (after exif changes).
*/
val modifiedImageSHA1 : String,
/**
* imageSHA1 query result from API.
*/
var imageResult : Boolean,
/**
* modifiedImageSHA1 query result from API.
*/
var modifiedImageResult : Boolean,
/**
* lastUpdated for data validation.
*/
var lastUpdated : Date? = null
)

View file

@ -1,7 +1,10 @@
package fr.free.nrw.commons.customselector.ui.selector package fr.free.nrw.commons.customselector.ui.selector
import android.content.Context import android.content.Context
import android.net.Uri
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import fr.free.nrw.commons.customselector.database.UploadedStatus
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter.ImageViewHolder import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter.ImageViewHolder
import fr.free.nrw.commons.filepicker.PickedFiles import fr.free.nrw.commons.filepicker.PickedFiles
@ -14,6 +17,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.net.UnknownHostException
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.HashMap import kotlin.collections.HashMap
@ -38,22 +42,28 @@ class ImageLoader @Inject constructor(
*/ */
var fileUtilsWrapper: FileUtilsWrapper, var fileUtilsWrapper: FileUtilsWrapper,
/**
* UploadedStatusDao for cache query.
*/
var uploadedStatusDao: UploadedStatusDao,
/** /**
* Context for coroutine. * Context for coroutine.
*/ */
val context: Context) { val context: Context
) {
/** /**
* Maps to facilitate image query. * Maps to facilitate image query.
*/ */
private var mapImageSHA1: HashMap<Image,String> = HashMap() private var mapImageSHA1: HashMap<Image, String> = HashMap()
private var mapHolderImage : HashMap<ImageViewHolder,Image> = HashMap() private var mapHolderImage : HashMap<ImageViewHolder, Image> = HashMap()
private var mapResult: HashMap<String,Boolean> = HashMap() private var mapResult: HashMap<String, Result> = HashMap()
/** /**
* Query image and setUp the view. * Query image and setUp the view.
*/ */
fun queryAndSetView(holder: ImageViewHolder, image: Image){ fun queryAndSetView(holder: ImageViewHolder, image: Image) {
/** /**
* Recycler view uses same view holder, so we can identify the latest query image from holder. * Recycler view uses same view holder, so we can identify the latest query image from holder.
@ -62,26 +72,46 @@ class ImageLoader @Inject constructor(
holder.itemNotUploaded() holder.itemNotUploaded()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
var value = false
var result : Result = Result.NOTFOUND
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
if(mapHolderImage[holder] != image) {
// View holder has a new query image, terminate this query. if (mapHolderImage[holder] == image) {
return@withContext val imageSHA1 = getImageSHA1(image.uri)
val uploadedStatus = uploadedStatusDao.getUploadedFromImageSHA1(imageSHA1)
val sha1 = uploadedStatus?.let {
result = getResultFromUploadedStatus(uploadedStatus)
uploadedStatus.modifiedImageSHA1
} ?: run {
if(mapHolderImage[holder] == image) {
getSHA1(image)
} else {
""
}
}
if (mapHolderImage[holder] == image &&
result in arrayOf(Result.NOTFOUND, Result.INVALID) &&
sha1.isNotEmpty()) {
// Query original image.
result = querySHA1(imageSHA1)
if( result is Result.TRUE ) {
// Original image found.
insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false)
}
else {
// Original image not found, query modified image.
result = querySHA1(sha1)
if (result != Result.ERROR) {
insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE)
}
}
} }
val sha1 = getSHA1(image)
if(mapHolderImage[holder] != image) {
// View holder has a new query image, terminate this query.
return@withContext
} }
value = querySHA1(sha1)
} }
if(mapHolderImage[holder] == image) { if(mapHolderImage[holder] == image) {
// View holder and latest query image match, setup the view. if (result is Result.TRUE) holder.itemUploaded() else holder.itemNotUploaded()
if (value) {
holder.itemUploaded()
} else {
holder.itemNotUploaded()
}
} }
} }
} }
@ -91,13 +121,26 @@ class ImageLoader @Inject constructor(
* *
* @return Query result. * @return Query result.
*/ */
private fun querySHA1(SHA1: String): Boolean { private fun querySHA1(SHA1: String): Result {
if(mapResult[SHA1] != null) { mapResult[SHA1]?.let{
return mapResult[SHA1]!! return it
}
var result : Result = Result.FALSE
try {
if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) {
mapResult[SHA1] = Result.TRUE
result = Result.TRUE
}
} catch (e: Exception) {
if (e is UnknownHostException) {
// Handle no network connection.
Timber.e(e, "Network Connection Error")
}
result = Result.ERROR
e.printStackTrace()
} finally {
return result
} }
val isUploaded = mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()
mapResult[SHA1] = isUploaded
return isUploaded
} }
/** /**
@ -105,15 +148,45 @@ class ImageLoader @Inject constructor(
* *
* @return sha1 of the image * @return sha1 of the image
*/ */
private fun getSHA1(image: Image): String{ private fun getSHA1(image: Image): String {
if(mapImageSHA1[image] != null) { mapImageSHA1[image]?.let{
return mapImageSHA1[image]!! return it
} }
val sha1 = generateModifiedSHA1(image); val sha1 = generateModifiedSHA1(image);
mapImageSHA1[image] = sha1; mapImageSHA1[image] = sha1;
return sha1; return sha1;
} }
/**
* Insert into uploaded status table.
*/
private fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){
uploadedStatusDao.insertUploaded(UploadedStatus(imageSha1, modifiedImageSha1, imageResult, modifiedImageResult))
}
/**
* Get image sha1 from uri, used to retrieve the original image sha1.
*/
private fun getImageSHA1(uri: Uri): String {
return fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri))
}
/**
* Get result data from database.
*/
private fun getResultFromUploadedStatus(uploadedStatus: UploadedStatus): Result {
if (uploadedStatus.imageResult || uploadedStatus.modifiedImageResult) {
return Result.TRUE
} else {
uploadedStatus.lastUpdated?.let {
if (it.date >= Calendar.getInstance().time.date - INVALIDATE_DAY_COUNT) {
return Result.FALSE
}
}
}
return Result.INVALID
}
/** /**
* Generate Modified SHA1 using present Exif settings. * Generate Modified SHA1 using present Exif settings.
* *
@ -133,4 +206,19 @@ class ImageLoader @Inject constructor(
return sha1 return sha1
} }
/**
* Sealed Result class.
*/
sealed class Result {
object TRUE : Result()
object FALSE : Result()
object INVALID : Result()
object NOTFOUND : Result()
object ERROR : Result()
}
companion object {
const val INVALIDATE_DAY_COUNT: Int = 7
}
} }

View file

@ -5,6 +5,8 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.customselector.database.UploadedStatus
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.upload.depicts.Depicts import fr.free.nrw.commons.upload.depicts.Depicts
import fr.free.nrw.commons.upload.depicts.DepictsDao import fr.free.nrw.commons.upload.depicts.DepictsDao
@ -12,9 +14,10 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
* The database for accessing the respective DAOs * The database for accessing the respective DAOs
* *
*/ */
@Database(entities = [Contribution::class,Depicts::class], version = 7, exportSchema = false) @Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class], version = 8, exportSchema = false)
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao abstract fun contributionDao(): ContributionDao
abstract fun DepictsDao (): DepictsDao; abstract fun DepictsDao (): DepictsDao;
abstract fun UploadedStatusDao(): UploadedStatusDao;
} }

View file

@ -17,6 +17,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.customselector.database.UploadedStatusDao;
import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader; import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader;
import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.db.AppDatabase; import fr.free.nrw.commons.db.AppDatabase;
@ -68,8 +69,8 @@ public class CommonsApplicationModule {
} }
@Provides @Provides
public ImageFileLoader providesImageFileLoader() { public ImageFileLoader providesImageFileLoader(Context context) {
return new ImageFileLoader(this.applicationContext); return new ImageFileLoader(context);
} }
@Provides @Provides
@ -250,13 +251,21 @@ public class CommonsApplicationModule {
} }
/** /**
* Get the reference of DepictsDao class * Get the reference of DepictsDao class.
*/ */
@Provides @Provides
public DepictsDao providesDepictDao(AppDatabase appDatabase) { public DepictsDao providesDepictDao(AppDatabase appDatabase) {
return appDatabase.DepictsDao(); return appDatabase.DepictsDao();
} }
/**
* Get the reference of UploadedStatus class.
*/
@Provides
public UploadedStatusDao providesUploadedStatusDao(AppDatabase appDatabase) {
return appDatabase.UploadedStatusDao();
}
@Provides @Provides
public ContentResolver providesContentResolver(Context context){ public ContentResolver providesContentResolver(Context context){
return context.getContentResolver(); return context.getContentResolver();

View file

@ -23,6 +23,7 @@ public class UploadItem {
private final String createdTimestampSource; private final String createdTimestampSource;
private final BehaviorSubject<Integer> imageQuality; private final BehaviorSubject<Integer> imageQuality;
private boolean hasInvalidLocation; private boolean hasInvalidLocation;
private final Uri contentUri;
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
@ -31,7 +32,8 @@ public class UploadItem {
final ImageCoordinates gpsCoords, final ImageCoordinates gpsCoords,
final Place place, final Place place,
final long createdTimestamp, final long createdTimestamp,
final String createdTimestampSource) { final String createdTimestampSource,
final Uri contentUri) {
this.createdTimestampSource = createdTimestampSource; this.createdTimestampSource = createdTimestampSource;
uploadMediaDetails = new ArrayList<>(Collections.singletonList(new UploadMediaDetail())); uploadMediaDetails = new ArrayList<>(Collections.singletonList(new UploadMediaDetail()));
this.place = place; this.place = place;
@ -39,6 +41,7 @@ public class UploadItem {
this.mimeType = mimeType; this.mimeType = mimeType;
this.gpsCoords = gpsCoords; this.gpsCoords = gpsCoords;
this.createdTimestamp = createdTimestamp; this.createdTimestamp = createdTimestamp;
this.contentUri = contentUri;
imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
} }
@ -66,6 +69,8 @@ public class UploadItem {
return imageQuality.getValue(); return imageQuality.getValue();
} }
public Uri getContentUri() { return contentUri; }
public void setImageQuality(final int imageQuality) { public void setImageQuality(final int imageQuality) {
this.imageQuality.onNext(imageQuality); this.imageQuality.onNext(imageQuality);
} }

View file

@ -106,7 +106,8 @@ public class UploadModel {
final UploadItem uploadItem = new UploadItem( final UploadItem uploadItem = new UploadItem(
Uri.parse(uploadableFile.getFilePath()), Uri.parse(uploadableFile.getFilePath()),
uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate, uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate,
createdTimestampSource); createdTimestampSource,
uploadableFile.getContentUri());
if (place != null) { if (place != null) {
uploadItem.getUploadMediaDetails().set(0, new UploadMediaDetail(place)); uploadItem.getUploadMediaDetails().set(0, new UploadMediaDetail(place));
} }

View file

@ -17,8 +17,11 @@ import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.contributions.ChunkInfo import fr.free.nrw.commons.contributions.ChunkInfo
import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.customselector.database.UploadedStatus
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.di.ApplicationlessInjection import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.upload.FileUtilsWrapper
import fr.free.nrw.commons.upload.StashUploadState import fr.free.nrw.commons.upload.StashUploadState
import fr.free.nrw.commons.upload.UploadClient import fr.free.nrw.commons.upload.UploadClient
import fr.free.nrw.commons.upload.UploadResult import fr.free.nrw.commons.upload.UploadResult
@ -48,12 +51,18 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
@Inject @Inject
lateinit var contributionDao: ContributionDao lateinit var contributionDao: ContributionDao
@Inject
lateinit var uploadedStatusDao: UploadedStatusDao
@Inject @Inject
lateinit var uploadClient: UploadClient lateinit var uploadClient: UploadClient
@Inject @Inject
lateinit var mediaClient: MediaClient lateinit var mediaClient: MediaClient
@Inject
lateinit var fileUtilsWrapper: FileUtilsWrapper
private val PROCESSING_UPLOADS_NOTIFICATION_TAG = BuildConfig.APPLICATION_ID + " : upload_tag" private val PROCESSING_UPLOADS_NOTIFICATION_TAG = BuildConfig.APPLICATION_ID + " : upload_tag"
private val PROCESSING_UPLOADS_NOTIFICATION_ID = 101 private val PROCESSING_UPLOADS_NOTIFICATION_ID = 101
@ -383,6 +392,20 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
.blockingGet() .blockingGet()
contributionFromUpload.dateModified=Date() contributionFromUpload.dateModified=Date()
contributionDao.deleteAndSaveContribution(contribution, contributionFromUpload) contributionDao.deleteAndSaveContribution(contribution, contributionFromUpload)
// Upload success, save to uploaded status.
saveIntoUploadedStatus(contribution)
}
/**
* Save to uploadedStatusDao.
*/
private fun saveIntoUploadedStatus(contribution: Contribution) {
contribution.contentUri?.let {
val imageSha1 = fileUtilsWrapper.getSHA1(appContext.contentResolver.openInputStream(it))
val modifiedSha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(contribution.localUri?.path))
uploadedStatusDao.insertUploaded(UploadedStatus(imageSha1, modifiedSha1, imageSha1 == modifiedSha1, true));
}
} }
private fun findUniqueFileName(fileName: String): String { private fun findUniqueFileName(fileName: String): String {