mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 12:53:55 +01:00
[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:
parent
7a6b24470e
commit
8e9d289628
9 changed files with 296 additions and 38 deletions
|
|
@ -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
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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,22 +42,28 @@ 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 mapImageSHA1: HashMap<Image, String> = HashMap()
|
||||
private var mapHolderImage : HashMap<ImageViewHolder, Image> = HashMap()
|
||||
private var mapResult: HashMap<String, Result> = HashMap()
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -105,15 +148,45 @@ class ImageLoader @Inject constructor(
|
|||
*
|
||||
* @return sha1 of the image
|
||||
*/
|
||||
private fun getSHA1(image: Image): String{
|
||||
if(mapImageSHA1[image] != null) {
|
||||
return mapImageSHA1[image]!!
|
||||
private fun getSHA1(image: Image): String {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue