[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 dateCreated: Date? = null,
var dateModified: Date? = null,
var hasInvalidLocation : Int = 0
var hasInvalidLocation : Int = 0,
var contentUri: Uri? = null
) : Parcelable {
fun completeWith(media: Media): Contribution {
@ -64,7 +65,8 @@ data class Contribution constructor(
decimalCoords = item.gpsCoords.decimalCoords,
dateCreatedSource = "",
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
import android.content.Context
import android.net.Uri
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.ui.adapter.ImageAdapter.ImageViewHolder
import fr.free.nrw.commons.filepicker.PickedFiles
@ -14,6 +17,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
import java.net.UnknownHostException
import java.util.*
import javax.inject.Inject
import kotlin.collections.HashMap
@ -38,17 +42,23 @@ class ImageLoader @Inject constructor(
*/
var fileUtilsWrapper: FileUtilsWrapper,
/**
* UploadedStatusDao for cache query.
*/
var uploadedStatusDao: UploadedStatusDao,
/**
* Context for coroutine.
*/
val context: Context) {
val context: Context
) {
/**
* Maps to facilitate image query.
*/
private var mapImageSHA1: HashMap<Image, String> = 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.
@ -62,26 +72,46 @@ class ImageLoader @Inject constructor(
holder.itemNotUploaded()
CoroutineScope(Dispatchers.Main).launch {
var value = false
var result : Result = Result.NOTFOUND
withContext(Dispatchers.Default) {
if(mapHolderImage[holder] != image) {
// View holder has a new query image, terminate this query.
return@withContext
if (mapHolderImage[holder] == image) {
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) {
// View holder and latest query image match, setup the view.
if (value) {
holder.itemUploaded()
} else {
holder.itemNotUploaded()
}
if (result is Result.TRUE) holder.itemUploaded() else holder.itemNotUploaded()
}
}
}
@ -91,13 +121,26 @@ class ImageLoader @Inject constructor(
*
* @return Query result.
*/
private fun querySHA1(SHA1: String): Boolean {
if(mapResult[SHA1] != null) {
return mapResult[SHA1]!!
private fun querySHA1(SHA1: String): Result {
mapResult[SHA1]?.let{
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
}
/**
@ -106,14 +149,44 @@ class ImageLoader @Inject constructor(
* @return sha1 of the image
*/
private fun getSHA1(image: Image): String {
if(mapImageSHA1[image] != null) {
return mapImageSHA1[image]!!
mapImageSHA1[image]?.let{
return it
}
val sha1 = generateModifiedSHA1(image);
mapImageSHA1[image] = 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.
*
@ -133,4 +206,19 @@ class ImageLoader @Inject constructor(
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 fr.free.nrw.commons.contributions.Contribution
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.DepictsDao
@ -12,9 +14,10 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
* 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)
abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao
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.SessionManager;
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.data.DBOpenHelper;
import fr.free.nrw.commons.db.AppDatabase;
@ -68,8 +69,8 @@ public class CommonsApplicationModule {
}
@Provides
public ImageFileLoader providesImageFileLoader() {
return new ImageFileLoader(this.applicationContext);
public ImageFileLoader providesImageFileLoader(Context context) {
return new ImageFileLoader(context);
}
@Provides
@ -250,13 +251,21 @@ public class CommonsApplicationModule {
}
/**
* Get the reference of DepictsDao class
* Get the reference of DepictsDao class.
*/
@Provides
public DepictsDao providesDepictDao(AppDatabase appDatabase) {
return appDatabase.DepictsDao();
}
/**
* Get the reference of UploadedStatus class.
*/
@Provides
public UploadedStatusDao providesUploadedStatusDao(AppDatabase appDatabase) {
return appDatabase.UploadedStatusDao();
}
@Provides
public ContentResolver providesContentResolver(Context context){
return context.getContentResolver();

View file

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

View file

@ -106,7 +106,8 @@ public class UploadModel {
final UploadItem uploadItem = new UploadItem(
Uri.parse(uploadableFile.getFilePath()),
uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate,
createdTimestampSource);
createdTimestampSource,
uploadableFile.getContentUri());
if (place != null) {
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.Contribution
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.media.MediaClient
import fr.free.nrw.commons.upload.FileUtilsWrapper
import fr.free.nrw.commons.upload.StashUploadState
import fr.free.nrw.commons.upload.UploadClient
import fr.free.nrw.commons.upload.UploadResult
@ -48,12 +51,18 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
@Inject
lateinit var contributionDao: ContributionDao
@Inject
lateinit var uploadedStatusDao: UploadedStatusDao
@Inject
lateinit var uploadClient: UploadClient
@Inject
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_ID = 101
@ -383,6 +392,20 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
.blockingGet()
contributionFromUpload.dateModified=Date()
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 {