diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 17fad9d8175..1df7a1e3d04 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -213,36 +213,6 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> - - - - - > + protected abstract fun getAllCategoriesInternal(): Flow> + + fun getAllCategories(): Flow> = + getAllCategoriesInternal().map { entities -> entities.map { fromEntity(it) } } + + private fun toEntity(model: BookmarksCategoryModal): BookmarkCategoryRoomEntity = + BookmarkCategoryRoomEntity(categoryName = model.categoryName) + private fun fromEntity(entity: BookmarkCategoryRoomEntity): BookmarksCategoryModal = + BookmarksCategoryModal(categoryName = entity.categoryName) } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoryRoomEntity.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoryRoomEntity.kt new file mode 100644 index 00000000000..1c57450b59f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoryRoomEntity.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.bookmarks.category + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Room entity for bookmarked category in DB + */ +@Entity(tableName = "bookmarks_categories") +data class BookmarkCategoryRoomEntity( + @PrimaryKey val categoryName: String +) diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarksCategoryModal.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarksCategoryModal.kt index ab679611f0a..41b7de6ac30 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarksCategoryModal.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarksCategoryModal.kt @@ -1,15 +1,11 @@ package fr.free.nrw.commons.bookmarks.category -import androidx.room.Entity -import androidx.room.PrimaryKey - /** * Data class representing bookmarked category in DB * * @property categoryName * @constructor Create empty Bookmarks category modal */ -@Entity(tableName = "bookmarks_categories") data class BookmarksCategoryModal( - @PrimaryKey val categoryName: String + val categoryName: String ) diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.kt index d1a9ef785e0..f4e9b87e57b 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.kt @@ -8,16 +8,14 @@ import javax.inject.Singleton * Handles loading bookmarked items from Database */ @Singleton -class BookmarkItemsController @Inject constructor() { - @JvmField - @Inject - var bookmarkItemsDao: BookmarkItemsDao? = null - +class BookmarkItemsController @Inject constructor( + val bookmarkItemsRoomDao: BookmarkItemsRoomDao +) { /** * Load from DB the bookmarked items * @return a list of DepictedItem objects. */ fun loadFavoritesItems(): List { - return bookmarkItemsDao?.getAllBookmarksItems() ?: emptyList() + return bookmarkItemsRoomDao.getAllBookmarksItems().blockingGet() } } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsRoomDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsRoomDao.kt index 11fd77b9c9a..b394dbc1a9c 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsRoomDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsRoomDao.kt @@ -5,20 +5,84 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import fr.free.nrw.commons.category.CategoryItem +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import fr.free.nrw.commons.utils.arrayToString +import fr.free.nrw.commons.utils.stringToArray +import io.reactivex.Completable import io.reactivex.Single @Dao -interface BookmarkItemsRoomDao { +abstract class BookmarkItemsRoomDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(depictedItem: BookmarkItemsRoomEntity) + abstract fun insert(depictedItem: BookmarkItemsRoomEntity): Completable @Delete - fun delete(depictedItem: BookmarkItemsRoomEntity) + abstract fun delete(depictedItem: BookmarkItemsRoomEntity): Completable @Query("SELECT * FROM bookmarksItems") - fun getAll(): Single> + abstract fun getAll(): Single> @Query("SELECT EXISTS (SELECT 1 FROM bookmarksItems WHERE item_id = :itemId)") - fun findBookmarkItem(itemId: String?): Boolean + abstract fun findBookmarkItem(itemId: String?): Single + + fun getAllBookmarksItems(): Single> { + return getAll().map { entities -> + entities.map { fromEntity(it) } + } + } + + fun updateBookmarkItem(depictedItem: DepictedItem): Single { + return findBookmarkItem(depictedItem.id).flatMap { exists -> + if (exists) { + delete(toEntity(depictedItem)).andThen(Single.just(false)) + } else { + insert(toEntity(depictedItem)).andThen(Single.just(true)) + } + } + } + + private fun fromEntity(entity: BookmarkItemsRoomEntity): DepictedItem { + return DepictedItem( + entity.name, + entity.description, + entity.imageUrl, + stringToArray(entity.instanceOfs), + convertToCategoryItems( + stringToArray(entity.categoryNames), + stringToArray(entity.categoryDescriptions), + stringToArray(entity.categoryThumbnails) + ), + entity.isSelected, + entity.id + ) + } + + private fun toEntity(depictedItem: DepictedItem): BookmarkItemsRoomEntity { + return BookmarkItemsRoomEntity( + depictedItem.name, + depictedItem.description, + depictedItem.imageUrl, + arrayToString(depictedItem.instanceOfs) ?: "", + arrayToString(depictedItem.commonsCategories.map { it.name }) ?: "", + arrayToString(depictedItem.commonsCategories.map { it.description ?: "" }) ?: "", + arrayToString(depictedItem.commonsCategories.map { it.thumbnail ?: "" }) ?: "", + depictedItem.isSelected, + depictedItem.id + ) + } + + private fun convertToCategoryItems( + categoryNameList: List, + categoryDescriptionList: List, + categoryThumbnailList: List + ): List = categoryNameList.mapIndexed { index, name -> + CategoryItem( + name = name, + description = categoryDescriptionList.getOrNull(index), + thumbnail = categoryThumbnailList.getOrNull(index), + isSelected = false + ) + } } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.kt index 2fa65b2d942..ba94eb77502 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.kt @@ -46,10 +46,10 @@ abstract class BookmarkLocationsDao { val exists = findBookmarkLocation(bookmarkLocation.name) if (exists) { - deleteBookmarkLocation(bookmarkLocation.toBookmarksLocations()) + deleteBookmarkLocation(toEntity(bookmarkLocation)) NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, false) } else { - addBookmarkLocation(bookmarkLocation.toBookmarksLocations()) + addBookmarkLocation(toEntity(bookmarkLocation)) NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, true) } @@ -60,6 +60,51 @@ abstract class BookmarkLocationsDao { * Fetches all bookmark locations as `Place` objects. */ suspend fun getAllBookmarksLocationsPlace(): List { - return getAllBookmarksLocations().map { it.toPlace() } + return getAllBookmarksLocations().map { fromEntity(it) } + } + + fun toEntity(place: Place): BookmarksLocations { + return BookmarksLocations( + locationName = place.name, + locationLanguage = place.language, + locationDescription = place.longDescription, + locationCategory = place.category, + locationLat = place.location.latitude, + locationLong = place.location.longitude, + locationLabelText = place.label?.text ?: "", + locationLabelIcon = place.label?.icon, + locationImageUrl = place.pic, + locationWikipediaLink = place.siteLinks.wikipediaLink.toString(), + locationWikidataLink = place.siteLinks.wikidataLink.toString(), + locationCommonsLink = place.siteLinks.commonsLink.toString(), + locationPic = place.pic, + locationExists = place.exists + ) + } + + private fun fromEntity(entity: BookmarksLocations): Place { + val location = fr.free.nrw.commons.location.LatLng( + entity.locationLat, + entity.locationLong, + 1F + ) + + val builder = fr.free.nrw.commons.nearby.Sitelinks.Builder().apply { + setWikipediaLink(entity.locationWikipediaLink) + setWikidataLink(entity.locationWikidataLink) + setCommonsLink(entity.locationCommonsLink) + } + + return Place( + entity.locationLanguage, + entity.locationName, + fr.free.nrw.commons.nearby.Label.fromText(entity.locationLabelText), + entity.locationDescription, + location, + entity.locationCategory, + builder.build(), + entity.locationPic, + entity.locationExists + ) } } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarksLocations.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarksLocations.kt index 66d670169a7..7d3db2c0597 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarksLocations.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarksLocations.kt @@ -1,72 +1,28 @@ -package fr.free.nrw.commons.bookmarks.locations - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import fr.free.nrw.commons.location.LatLng -import fr.free.nrw.commons.nearby.Label -import fr.free.nrw.commons.nearby.Place -import fr.free.nrw.commons.nearby.Sitelinks - -@Entity(tableName = "bookmarks_locations") -data class BookmarksLocations( - @PrimaryKey @ColumnInfo(name = "location_name") val locationName: String, - @ColumnInfo(name = "location_language") val locationLanguage: String, - @ColumnInfo(name = "location_description") val locationDescription: String, - @ColumnInfo(name = "location_lat") val locationLat: Double, - @ColumnInfo(name = "location_long") val locationLong: Double, - @ColumnInfo(name = "location_category") val locationCategory: String, - @ColumnInfo(name = "location_label_text") val locationLabelText: String, - @ColumnInfo(name = "location_label_icon") val locationLabelIcon: Int?, - @ColumnInfo(name = "location_image_url") val locationImageUrl: String, - @ColumnInfo(name = "location_wikipedia_link") val locationWikipediaLink: String, - @ColumnInfo(name = "location_wikidata_link") val locationWikidataLink: String, - @ColumnInfo(name = "location_commons_link") val locationCommonsLink: String, - @ColumnInfo(name = "location_pic") val locationPic: String, - @ColumnInfo(name = "location_exists") val locationExists: Boolean -) - -fun BookmarksLocations.toPlace(): Place { - val location = LatLng( - locationLat, - locationLong, - 1F - ) - - val builder = Sitelinks.Builder().apply { - setWikipediaLink(locationWikipediaLink) - setWikidataLink(locationWikidataLink) - setCommonsLink(locationCommonsLink) - } - - return Place( - locationLanguage, - locationName, - Label.fromText(locationLabelText), - locationDescription, - location, - locationCategory, - builder.build(), - locationPic, - locationExists - ) -} - -fun Place.toBookmarksLocations(): BookmarksLocations { - return BookmarksLocations( - locationName = name, - locationLanguage = language, - locationDescription = longDescription, - locationCategory = category, - locationLat = location.latitude, - locationLong = location.longitude, - locationLabelText = label?.text ?: "", - locationLabelIcon = label?.icon, - locationImageUrl = pic, - locationWikipediaLink = siteLinks.wikipediaLink.toString(), - locationWikidataLink = siteLinks.wikidataLink.toString(), - locationCommonsLink = siteLinks.commonsLink.toString(), - locationPic = pic, - locationExists = exists - ) -} \ No newline at end of file +package fr.free.nrw.commons.bookmarks.locations + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.Label +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.nearby.Sitelinks + +@Entity(tableName = "bookmarks_locations") +data class BookmarksLocations( + @PrimaryKey @ColumnInfo(name = "location_name") val locationName: String, + @ColumnInfo(name = "location_language") val locationLanguage: String, + @ColumnInfo(name = "location_description") val locationDescription: String, + @ColumnInfo(name = "location_lat") val locationLat: Double, + @ColumnInfo(name = "location_long") val locationLong: Double, + @ColumnInfo(name = "location_category") val locationCategory: String, + @ColumnInfo(name = "location_label_text") val locationLabelText: String, + @ColumnInfo(name = "location_label_icon") val locationLabelIcon: Int?, + @ColumnInfo(name = "location_image_url") val locationImageUrl: String, + @ColumnInfo(name = "location_wikipedia_link") val locationWikipediaLink: String, + @ColumnInfo(name = "location_wikidata_link") val locationWikidataLink: String, + @ColumnInfo(name = "location_commons_link") val locationCommonsLink: String, + @ColumnInfo(name = "location_pic") val locationPic: String, + @ColumnInfo(name = "location_exists") val locationExists: Boolean +) + \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/models/Bookmark.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/models/Bookmark.kt index 630889c011f..f7bf9b4dd67 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/models/Bookmark.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/models/Bookmark.kt @@ -5,22 +5,7 @@ import android.net.Uri class Bookmark( mediaName: String?, mediaCreator: String?, - /** - * Gets or Sets the content URI - marking this bookmark as already saved in the database - * @return content URI - * contentUri the content URI - */ - var contentUri: Uri?, ) { - /** - * Gets the media name - * @return the media name - */ val mediaName: String = mediaName ?: "" - - /** - * Gets media creator - * @return creator name - */ val mediaCreator: String = mediaCreator ?: "" } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.kt index 5ee88d973c5..42ecbec032e 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.kt @@ -11,7 +11,7 @@ import javax.inject.Singleton @Singleton class BookmarkPicturesController @Inject constructor( private val mediaClient: MediaClient, - private val bookmarkDao: BookmarkPicturesDao + private val bookmarkDao: BookmarkPicturesRoomDao ) { private var currentBookmarks: List = listOf() @@ -20,7 +20,7 @@ class BookmarkPicturesController @Inject constructor( * @return a list of bookmarked Media object */ fun loadBookmarkedPictures(): Single> { - val bookmarks = bookmarkDao.getAllBookmarks() + val bookmarks = bookmarkDao.getAllBookmarks().blockingGet() currentBookmarks = bookmarks return Observable.fromIterable(bookmarks).flatMap { mediaClient.getMedia(it.mediaName) @@ -30,7 +30,7 @@ class BookmarkPicturesController @Inject constructor( } fun needRefreshBookmarkedPictures(): Boolean { - val bookmarks = bookmarkDao.getAllBookmarks() + val bookmarks = bookmarkDao.getAllBookmarks().blockingGet() return bookmarks.size != currentBookmarks.size } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt index 00c8e3228b4..c7d45482d97 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt @@ -86,11 +86,7 @@ class BookmarkPicturesDao @Inject constructor( private fun deleteBookmark(bookmark: Bookmark) { val db = clientProvider.get() try { - if (bookmark.contentUri == null) { - throw RuntimeException("tried to delete item with no content URI") - } else { - db.delete(bookmark.contentUri!!, null, null) - } + db.delete(BASE_URI, "$COLUMN_MEDIA_NAME=?", arrayOf(bookmark.mediaName)) } catch (e: RemoteException) { throw RuntimeException(e) } finally { @@ -133,7 +129,7 @@ class BookmarkPicturesDao @Inject constructor( fileName = "" } return Bookmark( - fileName, cursor.getString(COLUMN_CREATOR), uriForName(fileName) + fileName, cursor.getString(COLUMN_CREATOR) ) } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesRoomDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesRoomDao.kt index 3fb743ed8fe..a4f12fcc717 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesRoomDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesRoomDao.kt @@ -5,20 +5,52 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import fr.free.nrw.commons.bookmarks.models.Bookmark +import io.reactivex.Completable import io.reactivex.Single @Dao -interface BookmarkPicturesRoomDao { +abstract class BookmarkPicturesRoomDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(bookmark: BookmarkPictureRoomEntity) + abstract fun insert(bookmark: BookmarkPictureRoomEntity): Completable @Delete - fun delete(bookmark: BookmarkPictureRoomEntity) + abstract fun delete(bookmark: BookmarkPictureRoomEntity): Completable @Query("SELECT * FROM bookmarks") - fun getAll(): Single> + abstract fun getAll(): Single> @Query("SELECT EXISTS (SELECT 1 FROM bookmarks WHERE media_name = :mediaName)") - fun findBookmarkByName(mediaName: String): Boolean + abstract fun findBookmarkByName(mediaName: String): Single + + fun getAllBookmarks(): Single> { + return getAll().map { entities -> + entities.map { fromEntity(it) } + } + } + + fun findBookmark(bookmark: Bookmark?): Single { + if (bookmark?.mediaName == null) return Single.just(false) + return findBookmarkByName(bookmark.mediaName!!) + } + + fun updateBookmark(bookmark: Bookmark): Single { + val entity = toEntity(bookmark) + return findBookmarkByName(bookmark.mediaName!!).flatMap { exists -> + if (exists) { + delete(entity).andThen(Single.just(false)) + } else { + insert(entity).andThen(Single.just(true)) + } + } + } + + private fun toEntity(bookmark: Bookmark): BookmarkPictureRoomEntity { + return BookmarkPictureRoomEntity(bookmark.mediaName!!, bookmark.mediaCreator!!) + } + + private fun fromEntity(entity: BookmarkPictureRoomEntity): Bookmark { + return Bookmark(entity.mediaName, entity.mediaCreator) + } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt index 47147944c67..d3f26c1e0ba 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt @@ -15,11 +15,10 @@ import javax.inject.Inject /** * The model class for categories in upload */ -class CategoriesModel - @Inject +class CategoriesModel @Inject constructor( private val categoryClient: CategoryClient, - private val categoryDao: CategoryDao, + private val categoryDao: CategoryRoomDao, private val gpsCategoryModel: GpsCategoryModel, ) { private val selectedCategories: MutableList = mutableListOf() @@ -72,20 +71,15 @@ class CategoriesModel * @param item */ fun updateCategoryCount(item: CategoryItem) { - var category = categoryDao.find(item.name) - - // Newly used category... - if (category == null) { - category = Category( - null, item.name, - item.description, - item.thumbnail, - Date(), - 0 - ) - } + val category = Category( + item.name, + item.description, + item.thumbnail, + Date(), + 0 + ) category.incTimesUsed() - categoryDao.save(category) + categoryDao.save(category).blockingAwait() } /** @@ -112,7 +106,7 @@ class CategoriesModel categoriesFromDepiction(selectedDepictions), gpsCategoryModel.categoriesFromLocation, titleCategories(imageTitleList), - Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT)), + Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT).blockingGet()), Function4(::combine), ) } else { diff --git a/app/src/main/java/fr/free/nrw/commons/category/Category.kt b/app/src/main/java/fr/free/nrw/commons/category/Category.kt index e4bfb957a29..3cbbaa02485 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/Category.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/Category.kt @@ -4,7 +4,8 @@ import android.net.Uri import java.util.Date data class Category( - var contentUri: Uri? = null, +// @Deprecated("Required for legacy ContentProvider DAO compatibility only") +// var contentUri: Uri? = null, val name: String? = null, val description: String? = null, val thumbnail: String? = null, diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.kt index 28e77f2c029..ba653b20939 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.kt @@ -1,139 +1,139 @@ -package fr.free.nrw.commons.category - -import android.annotation.SuppressLint -import android.content.ContentProviderClient -import android.content.ContentValues -import android.database.Cursor -import android.os.RemoteException -import fr.free.nrw.commons.category.CategoryTable.ALL_FIELDS -import fr.free.nrw.commons.category.CategoryTable.COLUMN_DESCRIPTION -import fr.free.nrw.commons.category.CategoryTable.COLUMN_ID -import fr.free.nrw.commons.category.CategoryTable.COLUMN_LAST_USED -import fr.free.nrw.commons.category.CategoryTable.COLUMN_NAME -import fr.free.nrw.commons.category.CategoryTable.COLUMN_THUMBNAIL -import fr.free.nrw.commons.category.CategoryTable.COLUMN_TIMES_USED - -import java.util.ArrayList -import java.util.Date -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Provider - -class CategoryDao @Inject constructor( - @Named("category") private val clientProvider: Provider -) { - - fun save(category: Category) { - val db = clientProvider.get() - try { - if (category.contentUri == null) { - category.contentUri = db.insert( - CategoryContentProvider.BASE_URI, - toContentValues(category) - ) - } else { - db.update( - category.contentUri!!, - toContentValues(category), - null, - null - ) - } - } catch (e: RemoteException) { - throw RuntimeException(e) - } finally { - db.release() - } - } - - /** - * Find persisted category in database, based on its name. - * - * @param name Category's name - * @return category from database, or null if not found - */ - fun find(name: String): Category? { - var cursor: Cursor? = null - val db = clientProvider.get() - try { - cursor = db.query( - CategoryContentProvider.BASE_URI, - ALL_FIELDS, - "${COLUMN_NAME}=?", - arrayOf(name), - null - ) - if (cursor != null && cursor.moveToFirst()) { - return fromCursor(cursor) - } - } catch (e: RemoteException) { - throw RuntimeException(e) - } finally { - cursor?.close() - db.release() - } - return null - } - - /** - * Retrieve recently-used categories, ordered by descending date. - * - * @return a list containing recent categories - */ - fun recentCategories(limit: Int): List { - val items = ArrayList() - var cursor: Cursor? = null - val db = clientProvider.get() - try { - cursor = db.query( - CategoryContentProvider.BASE_URI, - ALL_FIELDS, - null, - emptyArray(), - "$COLUMN_LAST_USED DESC" - ) - while (cursor != null && cursor.moveToNext() && cursor.position < limit) { - val category = fromCursor(cursor) - if (category.name != null) { - items.add( - CategoryItem( - category.name, - category.description, - category.thumbnail, - false - ) - ) - } - } - } catch (e: RemoteException) { - throw RuntimeException(e) - } finally { - cursor?.close() - db.release() - } - return items - } - - @SuppressLint("Range") - fun fromCursor(cursor: Cursor): Category { - // Hardcoding column positions! - return Category( - CategoryContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(COLUMN_ID))), - cursor.getString(cursor.getColumnIndex(COLUMN_NAME)), - cursor.getString(cursor.getColumnIndex(COLUMN_DESCRIPTION)), - cursor.getString(cursor.getColumnIndex(COLUMN_THUMBNAIL)), - Date(cursor.getLong(cursor.getColumnIndex(COLUMN_LAST_USED))), - cursor.getInt(cursor.getColumnIndex(COLUMN_TIMES_USED)) - ) - } - - private fun toContentValues(category: Category): ContentValues { - return ContentValues().apply { - put(COLUMN_NAME, category.name) - put(COLUMN_DESCRIPTION, category.description) - put(COLUMN_THUMBNAIL, category.thumbnail) - put(COLUMN_LAST_USED, category.lastUsed?.time) - put(COLUMN_TIMES_USED, category.timesUsed) - } - } -} +//package fr.free.nrw.commons.category +// +//import android.annotation.SuppressLint +//import android.content.ContentProviderClient +//import android.content.ContentValues +//import android.database.Cursor +//import android.os.RemoteException +//import fr.free.nrw.commons.category.CategoryTable.ALL_FIELDS +//import fr.free.nrw.commons.category.CategoryTable.COLUMN_DESCRIPTION +//import fr.free.nrw.commons.category.CategoryTable.COLUMN_ID +//import fr.free.nrw.commons.category.CategoryTable.COLUMN_LAST_USED +//import fr.free.nrw.commons.category.CategoryTable.COLUMN_NAME +//import fr.free.nrw.commons.category.CategoryTable.COLUMN_THUMBNAIL +//import fr.free.nrw.commons.category.CategoryTable.COLUMN_TIMES_USED +// +//import java.util.ArrayList +//import java.util.Date +//import javax.inject.Inject +//import javax.inject.Named +//import javax.inject.Provider +// +//class CategoryDao @Inject constructor( +// @Named("category") private val clientProvider: Provider +//) { +// +// fun save(category: Category) { +// val db = clientProvider.get() +// try { +// if (category.contentUri == null) { +// category.contentUri = db.insert( +// CategoryContentProvider.BASE_URI, +// toContentValues(category) +// ) +// } else { +// db.update( +// category.contentUri!!, +// toContentValues(category), +// null, +// null +// ) +// } +// } catch (e: RemoteException) { +// throw RuntimeException(e) +// } finally { +// db.release() +// } +// } +// +// /** +// * Find persisted category in database, based on its name. +// * +// * @param name Category's name +// * @return category from database, or null if not found +// */ +// fun find(name: String): Category? { +// var cursor: Cursor? = null +// val db = clientProvider.get() +// try { +// cursor = db.query( +// CategoryContentProvider.BASE_URI, +// ALL_FIELDS, +// "${COLUMN_NAME}=?", +// arrayOf(name), +// null +// ) +// if (cursor != null && cursor.moveToFirst()) { +// return fromCursor(cursor) +// } +// } catch (e: RemoteException) { +// throw RuntimeException(e) +// } finally { +// cursor?.close() +// db.release() +// } +// return null +// } +// +// /** +// * Retrieve recently-used categories, ordered by descending date. +// * +// * @return a list containing recent categories +// */ +// fun recentCategories(limit: Int): List { +// val items = ArrayList() +// var cursor: Cursor? = null +// val db = clientProvider.get() +// try { +// cursor = db.query( +// CategoryContentProvider.BASE_URI, +// ALL_FIELDS, +// null, +// emptyArray(), +// "$COLUMN_LAST_USED DESC" +// ) +// while (cursor != null && cursor.moveToNext() && cursor.position < limit) { +// val category = fromCursor(cursor) +// if (category.name != null) { +// items.add( +// CategoryItem( +// category.name, +// category.description, +// category.thumbnail, +// false +// ) +// ) +// } +// } +// } catch (e: RemoteException) { +// throw RuntimeException(e) +// } finally { +// cursor?.close() +// db.release() +// } +// return items +// } +// +// @SuppressLint("Range") +// fun fromCursor(cursor: Cursor): Category { +// // Hardcoding column positions! +// return Category( +// CategoryContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(COLUMN_ID))), +// cursor.getString(cursor.getColumnIndex(COLUMN_NAME)), +// cursor.getString(cursor.getColumnIndex(COLUMN_DESCRIPTION)), +// cursor.getString(cursor.getColumnIndex(COLUMN_THUMBNAIL)), +// Date(cursor.getLong(cursor.getColumnIndex(COLUMN_LAST_USED))), +// cursor.getInt(cursor.getColumnIndex(COLUMN_TIMES_USED)) +// ) +// } +// +// private fun toContentValues(category: Category): ContentValues { +// return ContentValues().apply { +// put(COLUMN_NAME, category.name) +// put(COLUMN_DESCRIPTION, category.description) +// put(COLUMN_THUMBNAIL, category.thumbnail) +// put(COLUMN_LAST_USED, category.lastUsed?.time) +// put(COLUMN_TIMES_USED, category.timesUsed) +// } +// } +//} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.kt index d0ee8d53cd9..3cfd3d37404 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.kt @@ -2,6 +2,7 @@ package fr.free.nrw.commons.category import android.os.Parcelable import kotlinx.parcelize.Parcelize +import java.util.Date @Parcelize data class CategoryItem( diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryRoomDao.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryRoomDao.kt index ad2d8152ff7..896c701e704 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryRoomDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryRoomDao.kt @@ -1,27 +1,65 @@ package fr.free.nrw.commons.category -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* import io.reactivex.Completable import io.reactivex.Single +import java.util.* @Dao -interface CategoryRoomDao { +abstract class CategoryRoomDao { @Query("SELECT * FROM categories ORDER BY last_used DESC LIMIT :limit") - fun getAll(limit: Int): Single> + abstract fun getAll(limit: Int): Single> @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(category: CategoryRoomEntity) + abstract fun insert(category: CategoryRoomEntity): Single + + @Update + abstract fun update(category: CategoryRoomEntity): Completable @Query("SELECT * FROM categories WHERE name = :name") - fun findEntity(name: String): CategoryRoomEntity? + abstract fun findEntity(name: String): Single> @Query("SELECT EXISTS (SELECT 1 FROM categories WHERE name = :name)") - fun findCategory(name: String): Boolean + abstract fun findCategory(name: String): Single @Query("DELETE FROM categories") - fun deleteAll(): Completable + abstract fun deleteAll(): Completable + + fun recentCategories(limit: Int): Single> { + return getAll(limit).map { entities -> + entities.map { fromEntity(it) } + } + } + + fun save(category: Category): Completable { + return findEntity(category.name ?: "").flatMapCompletable { entities -> + if (entities.isNotEmpty()) { + update(toEntity(category, entities[0].id)) + } else { + insert(toEntity(category)).ignoreElement() + } + } + } + + fun toEntity( + category: Category, + id: Long = 0 + ): CategoryRoomEntity = CategoryRoomEntity( + id = id, + name = category.name ?: "", + description = category.description, + thumbnail = category.thumbnail, + lastUsed = category.lastUsed ?: Date(), + timesUsed = category.timesUsed + ) + + private fun fromEntity( + entity: CategoryRoomEntity + ): CategoryItem = CategoryItem( + name = entity.name, + description = entity.description, + thumbnail = entity.thumbnail, + isSelected = false, + ) } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt index d623730abf5..c40c9284c64 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt @@ -2,9 +2,6 @@ package fr.free.nrw.commons.contributions import android.net.Uri import android.os.Parcelable -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.PrimaryKey import fr.free.nrw.commons.Media import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.upload.UploadItem @@ -16,11 +13,10 @@ import kotlinx.parcelize.Parcelize import java.io.File import java.util.Date -@Entity(tableName = "contribution") @Parcelize data class Contribution constructor( - @Embedded(prefix = "media_") val media: Media, - @PrimaryKey val pageId: String = media.pageId, + val media: Media, + val pageId: String = media.pageId, var state: Int = 0, var transferred: Long = 0, val decimalCoords: String? = null, diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.kt index 50faa134086..3d638bfacef 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.kt @@ -16,10 +16,16 @@ import java.util.Calendar @Dao abstract class ContributionDao { @Query("SELECT * FROM contribution order by media_dateUploaded DESC") - abstract fun fetchContributions(): DataSource.Factory + protected abstract fun fetchContributionsInternal(): DataSource.Factory + + fun fetchContributions(): DataSource.Factory = + fetchContributionsInternal().map { fromEntity(it) } @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveSynchronous(contribution: Contribution) + protected abstract fun saveSynchronousInternal(contribution: ContributionRoomEntity) + + fun saveSynchronous(contribution: Contribution) = + saveSynchronousInternal(toEntity(contribution)) fun save(contribution: Contribution): Completable { return Completable @@ -42,10 +48,16 @@ abstract class ContributionDao { } @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun save(contribution: List): Single> + protected abstract fun saveInternal(contribution: List): Single> + + fun save(contribution: List): Single> = + saveInternal(contribution.map { toEntity(it) }) @Delete - abstract fun deleteSynchronous(contribution: Contribution) + protected abstract fun deleteSynchronousInternal(contribution: ContributionRoomEntity) + + fun deleteSynchronous(contribution: Contribution) = + deleteSynchronousInternal(toEntity(contribution)) /** * Deletes contributions with specific states from the database. @@ -74,35 +86,38 @@ abstract class ContributionDao { } @Query("SELECT * from contribution WHERE media_filename=:fileName") - abstract fun getContributionWithTitle(fileName: String): List + protected abstract fun getContributionWithTitleInternal(fileName: String): List + + fun getContributionWithTitle(fileName: String): List = + getContributionWithTitleInternal(fileName).map { fromEntity(it) } @Query("SELECT * from contribution WHERE pageId=:pageId") - abstract fun getContribution(pageId: String): Contribution + protected abstract fun getContributionInternal(pageId: String): ContributionRoomEntity + + fun getContribution(pageId: String): Contribution = + fromEntity(getContributionInternal(pageId)) @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC") - abstract fun getContribution(states: List): Single> + protected abstract fun getContributionInternal(states: List): Single> + + fun getContribution(states: List): Single> = + getContributionInternal(states).map { entities -> entities.map { fromEntity(it) } } - /** - * Gets contributions with specific states in descending order by the date they were uploaded. - * - * @param states The states of the contributions to fetch. - * @return A DataSource factory for paginated contributions with the specified states. - */ @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC") - abstract fun getContributions( + protected abstract fun getContributionsInternal( states: List - ): DataSource.Factory + ): DataSource.Factory + + fun getContributions(states: List): DataSource.Factory = + getContributionsInternal(states).map { fromEntity(it) } - /** - * Gets contributions with specific states in ascending order by the date the upload started. - * - * @param states The states of the contributions to fetch. - * @return A DataSource factory for paginated contributions with the specified states. - */ @Query("SELECT * from contribution WHERE state IN (:states) order by dateUploadStarted ASC") - abstract fun getContributionsSortedByDateUploadStarted( + protected abstract fun getContributionsSortedByDateUploadStartedInternal( states: List - ): DataSource.Factory + ): DataSource.Factory + + fun getContributionsSortedByDateUploadStarted(states: List): DataSource.Factory = + getContributionsSortedByDateUploadStartedInternal(states).map { fromEntity(it) } @Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)") abstract fun getPendingUploads(toUpdateStates: IntArray): Single @@ -112,14 +127,11 @@ abstract class ContributionDao { abstract fun deleteAll() @Update - abstract fun updateSynchronous(contribution: Contribution) + protected abstract fun updateSynchronousInternal(contribution: ContributionRoomEntity) + + fun updateSynchronous(contribution: Contribution) = + updateSynchronousInternal(toEntity(contribution)) - /** - * Updates the state of contributions with specific states. - * - * @param states The current states of the contributions to update. - * @param newState The new state to set. - */ @Query("UPDATE contribution SET state = :newState WHERE state IN (:states)") abstract fun updateContributionsState(states: List, newState: Int) @@ -130,19 +142,62 @@ abstract class ContributionDao { } } - - - /** - * Updates the state of contributions with specific states asynchronously. - * - * @param states The current states of the contributions to update. - * @param newState The new state to set. - * @return A Completable indicating the result of the operation. - */ fun updateContributionsWithStates(states: List, newState: Int): Completable { return Completable .fromAction { updateContributionsState(states, newState) } } + + private fun toEntity(contribution: Contribution): ContributionRoomEntity = + ContributionRoomEntity( + media = contribution.media, + pageId = contribution.pageId, + state = contribution.state, + transferred = contribution.transferred, + decimalCoords = contribution.decimalCoords, + dateCreatedSource = contribution.dateCreatedSource, + wikidataPlace = contribution.wikidataPlace, + chunkInfo = contribution.chunkInfo, + errorInfo = contribution.errorInfo, + depictedItems = contribution.depictedItems, + mimeType = contribution.mimeType, + localUri = contribution.localUri, + dataLength = contribution.dataLength, + dateCreated = contribution.dateCreated, + dateCreatedString = contribution.dateCreatedString, + dateModified = contribution.dateModified, + dateUploadStarted = contribution.dateUploadStarted, + hasInvalidLocation = contribution.hasInvalidLocation, + contentUri = contribution.contentUri, + countryCode = contribution.countryCode, + imageSHA1 = contribution.imageSHA1, + retries = contribution.retries + ) + + private fun fromEntity(entity: ContributionRoomEntity): Contribution = + Contribution( + media = entity.media, + pageId = entity.pageId, + state = entity.state, + transferred = entity.transferred, + decimalCoords = entity.decimalCoords, + dateCreatedSource = entity.dateCreatedSource, + wikidataPlace = entity.wikidataPlace, + chunkInfo = entity.chunkInfo, + errorInfo = entity.errorInfo, + depictedItems = entity.depictedItems, + mimeType = entity.mimeType, + localUri = entity.localUri, + dataLength = entity.dataLength, + dateCreated = entity.dateCreated, + dateCreatedString = entity.dateCreatedString, + dateModified = entity.dateModified, + dateUploadStarted = entity.dateUploadStarted, + hasInvalidLocation = entity.hasInvalidLocation, + contentUri = entity.contentUri, + countryCode = entity.countryCode, + imageSHA1 = entity.imageSHA1, + retries = entity.retries + ) } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionRoomEntity.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionRoomEntity.kt new file mode 100644 index 00000000000..5808e352047 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionRoomEntity.kt @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.contributions + +import android.net.Uri +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.upload.WikidataPlace +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import java.util.Date + +/** + * Room entity for the contribution table. + */ +@Entity(tableName = "contribution") +data class ContributionRoomEntity( + @Embedded(prefix = "media_") val media: Media, + @PrimaryKey val pageId: String, + val state: Int, + val transferred: Long, + val decimalCoords: String?, + val dateCreatedSource: String?, + val wikidataPlace: WikidataPlace?, + val chunkInfo: ChunkInfo?, + val errorInfo: String?, + val depictedItems: List, + val mimeType: String?, + val localUri: Uri?, + val dataLength: Long, + val dateCreated: Date?, + val dateCreatedString: String?, + val dateModified: Date?, + val dateUploadStarted: Date?, + val hasInvalidLocation: Int, + val contentUri: Uri?, + val countryCode: String?, + val imageSHA1: String?, + val retries: Int +) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatus.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatus.kt index b3ef36318d9..27bf70bc929 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatus.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatus.kt @@ -1,16 +1,11 @@ package fr.free.nrw.commons.customselector.database -import androidx.room.Entity -import androidx.room.PrimaryKey - /** * Entity class for Not For Upload status. */ -@Entity(tableName = "images_not_for_upload_table") data class NotForUploadStatus( /** * Original image sha1. */ - @PrimaryKey val imageSHA1: String, ) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt index b75a6e1d4f6..b26a72f4a40 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt @@ -5,6 +5,7 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import fr.free.nrw.commons.customselector.database.NotForUploadStatusRoomEntity /** * Dao class for Not For Upload @@ -15,19 +16,32 @@ abstract class NotForUploadStatusDao { * Insert into Not For Upload status. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun insert(notForUploadStatus: NotForUploadStatus) + protected abstract fun insertInternal(notForUploadStatus: NotForUploadStatusRoomEntity) + + suspend fun insert(notForUploadStatus: NotForUploadStatus) { + insertInternal(toEntity(notForUploadStatus)) + } /** * Delete Not For Upload status entry. */ @Delete - abstract suspend fun delete(notForUploadStatus: NotForUploadStatus) + protected abstract fun deleteInternal(notForUploadStatus: NotForUploadStatusRoomEntity) + + suspend fun delete(notForUploadStatus: NotForUploadStatus) { + deleteInternal(toEntity(notForUploadStatus)) + } /** * Query Not For Upload status with image sha1. */ @Query("SELECT * FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus? + protected abstract fun getFromImageSHA1Internal(imageSHA1: String): NotForUploadStatusRoomEntity? + + suspend fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus? { + val entity = getFromImageSHA1Internal(imageSHA1) + return if (entity != null) fromEntity(entity) else null + } /** * Asynchronous image sha1 query. @@ -50,4 +64,14 @@ abstract class NotForUploadStatusDao { */ @Query("SELECT COUNT() FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") abstract suspend fun find(imageSHA1: String): Int + + private fun toEntity(notForUploadStatus: NotForUploadStatus): NotForUploadStatusRoomEntity = + NotForUploadStatusRoomEntity( + imageSHA1 = notForUploadStatus.imageSHA1 + ) + + private fun fromEntity(entity: NotForUploadStatusRoomEntity): NotForUploadStatus = + NotForUploadStatus( + imageSHA1 = entity.imageSHA1 + ) } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusRoomEntity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusRoomEntity.kt new file mode 100644 index 00000000000..87322ef14ec --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusRoomEntity.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.customselector.database + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Room entity for the images_not_for_upload_table. + */ +@Entity(tableName = "images_not_for_upload_table") +data class NotForUploadStatusRoomEntity( + @PrimaryKey + val imageSHA1: String +) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt index 7f635ed954d..b3717796191 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt @@ -1,19 +1,14 @@ package fr.free.nrw.commons.customselector.database -import androidx.room.Entity -import androidx.room.Index -import androidx.room.PrimaryKey import java.util.Date /** * 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). diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt index 378af5b8db7..0412e780d3a 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt @@ -7,6 +7,7 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update import java.util.Calendar +import fr.free.nrw.commons.customselector.database.UploadedStatusRoomEntity /** * UploadedStatusDao for Custom Selector. @@ -17,31 +18,53 @@ abstract class UploadedStatusDao { * Insert into uploaded status. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun insert(uploadedStatus: UploadedStatus) + protected abstract fun insertInternal(uploadedStatus: UploadedStatusRoomEntity) + + suspend fun insert(uploadedStatus: UploadedStatus) { + insertInternal(toEntity(uploadedStatus)) + } /** * Update uploaded status entry. */ @Update - abstract suspend fun update(uploadedStatus: UploadedStatus) + protected abstract fun updateInternal(uploadedStatus: UploadedStatusRoomEntity) + + suspend fun update(uploadedStatus: UploadedStatus) { + updateInternal(toEntity(uploadedStatus)) + } /** * Delete uploaded status entry. */ @Delete - abstract suspend fun delete(uploadedStatus: UploadedStatus) + protected abstract fun deleteInternal(uploadedStatus: UploadedStatusRoomEntity) + + suspend fun delete(uploadedStatus: UploadedStatus) { + deleteInternal(toEntity(uploadedStatus)) + } /** * Query uploaded status with image sha1. */ @Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun getFromImageSHA1(imageSHA1: String): UploadedStatus? + protected abstract fun getFromImageSHA1Internal(imageSHA1: String): UploadedStatusRoomEntity? + + suspend fun getFromImageSHA1(imageSHA1: String): UploadedStatus? { + val entity = getFromImageSHA1Internal(imageSHA1) + return if (entity != null) fromEntity(entity) else null + } /** * Query uploaded status with modified image sha1. */ @Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ") - abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus? + protected abstract fun getFromModifiedImageSHA1Internal(modifiedImageSHA1: String): UploadedStatusRoomEntity? + + suspend fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus? { + val entity = getFromModifiedImageSHA1Internal(modifiedImageSHA1) + return if (entity != null) fromEntity(entity) else null + } /** * Asynchronous insert into uploaded status table. @@ -75,4 +98,22 @@ abstract class UploadedStatusDao { * Asynchronous image sha1 query. */ suspend fun getUploadedFromImageSHA1(imageSHA1: String): UploadedStatus? = getFromImageSHA1(imageSHA1) + + private fun toEntity(uploadedStatus: UploadedStatus): UploadedStatusRoomEntity = + UploadedStatusRoomEntity( + imageSHA1 = uploadedStatus.imageSHA1, + modifiedImageSHA1 = uploadedStatus.modifiedImageSHA1, + imageResult = uploadedStatus.imageResult, + modifiedImageResult = uploadedStatus.modifiedImageResult, + lastUpdated = uploadedStatus.lastUpdated + ) + + private fun fromEntity(entity: UploadedStatusRoomEntity): UploadedStatus = + UploadedStatus( + imageSHA1 = entity.imageSHA1, + modifiedImageSHA1 = entity.modifiedImageSHA1, + imageResult = entity.imageResult, + modifiedImageResult = entity.modifiedImageResult, + lastUpdated = entity.lastUpdated + ) } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusRoomEntity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusRoomEntity.kt new file mode 100644 index 00000000000..2267406fca7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusRoomEntity.kt @@ -0,0 +1,19 @@ +package fr.free.nrw.commons.customselector.database + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import java.util.Date + +/** + * Room entity for the uploaded_table. + */ +@Entity(tableName = "uploaded_table", indices = [Index(value = ["modifiedImageSHA1"], unique = true)]) +data class UploadedStatusRoomEntity( + @PrimaryKey + val imageSHA1: String, + val modifiedImageSHA1: String, + var imageResult: Boolean, + var modifiedImageResult: Boolean, + var lastUpdated: Date? = null +) diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt index f354482f3b6..612fab17ce7 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt +++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt @@ -4,7 +4,7 @@ import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao -import fr.free.nrw.commons.bookmarks.category.BookmarksCategoryModal +import fr.free.nrw.commons.bookmarks.category.BookmarkCategoryRoomEntity import fr.free.nrw.commons.bookmarks.items.BookmarkItemsRoomDao import fr.free.nrw.commons.bookmarks.items.BookmarkItemsRoomEntity import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao @@ -13,21 +13,21 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPictureRoomEntity import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesRoomDao import fr.free.nrw.commons.category.CategoryRoomDao import fr.free.nrw.commons.category.CategoryRoomEntity -import fr.free.nrw.commons.contributions.Contribution +import fr.free.nrw.commons.contributions.ContributionRoomEntity import fr.free.nrw.commons.contributions.ContributionDao -import fr.free.nrw.commons.customselector.database.NotForUploadStatus -import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao -import fr.free.nrw.commons.customselector.database.UploadedStatus +import fr.free.nrw.commons.customselector.database.UploadedStatusRoomEntity import fr.free.nrw.commons.customselector.database.UploadedStatusDao +import fr.free.nrw.commons.customselector.database.NotForUploadStatusRoomEntity +import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao import fr.free.nrw.commons.explore.recentsearches.RecentSearchRoomEntity import fr.free.nrw.commons.explore.recentsearches.RecentSearchesRoomDao -import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.nearby.PlaceRoomEntity import fr.free.nrw.commons.nearby.PlaceDao import fr.free.nrw.commons.recentlanguages.RecentLanguageRoomEntity import fr.free.nrw.commons.recentlanguages.RecentLanguagesRoomDao +import fr.free.nrw.commons.review.ReviewRoomEntity import fr.free.nrw.commons.review.ReviewDao -import fr.free.nrw.commons.review.ReviewEntity -import fr.free.nrw.commons.upload.depicts.Depicts +import fr.free.nrw.commons.upload.depicts.DepictsRoomEntity import fr.free.nrw.commons.upload.depicts.DepictsDao /** @@ -35,10 +35,10 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao * */ @Database( - entities = [Contribution::class, Depicts::class, - UploadedStatus::class, NotForUploadStatus::class, - ReviewEntity::class, Place::class, - BookmarksCategoryModal::class, BookmarksLocations::class, + entities = [ContributionRoomEntity::class, DepictsRoomEntity::class, + UploadedStatusRoomEntity::class, NotForUploadStatusRoomEntity::class, + ReviewRoomEntity::class, PlaceRoomEntity::class, + BookmarkCategoryRoomEntity::class, BookmarksLocations::class, BookmarkPictureRoomEntity::class, BookmarkItemsRoomEntity::class, CategoryRoomEntity::class, RecentLanguageRoomEntity::class, RecentSearchRoomEntity::class ], diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index b1f1b7f9b8d..25b270f115e 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -18,7 +18,7 @@ import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException import fr.free.nrw.commons.databinding.ActivityDescriptionEditBinding import fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_DESCRIPTION_AND_CAPTION import fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT -import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao +import fr.free.nrw.commons.recentlanguages.RecentLanguagesRoomDao import fr.free.nrw.commons.settings.Prefs import fr.free.nrw.commons.theme.BaseActivity import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomInsets @@ -70,7 +70,7 @@ class DescriptionEditActivity : private var progressDialog: ProgressDialog? = null @Inject - lateinit var recentLanguagesDao: RecentLanguagesDao + lateinit var recentLanguagesDao: RecentLanguagesRoomDao private lateinit var binding: ActivityDescriptionEditBinding diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt index 9e569982f7e..d95a5b9ee33 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt @@ -38,7 +38,6 @@ import javax.inject.Singleton ActivityBuilderModule::class, FragmentBuilderModule::class, ServiceBuilderModule::class, - ContentProviderBuilderModule::class, UploadModule::class, ContributionsModule::class, ContributionsProvidesModule::class, diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt index 346bb0ef1b9..d4a32f427e6 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt @@ -1,7 +1,6 @@ package fr.free.nrw.commons.di import android.app.Activity -import android.content.ContentProviderClient import android.content.ContentResolver import android.content.Context import android.database.sqlite.SQLiteDatabase @@ -13,12 +12,16 @@ import androidx.sqlite.db.SupportSQLiteDatabase import com.google.gson.Gson import dagger.Module import dagger.Provides -import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.R import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsRoomDao import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesRoomDao +import fr.free.nrw.commons.category.CategoryRoomDao import fr.free.nrw.commons.contributions.ContributionDao +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesRoomDao +import fr.free.nrw.commons.recentlanguages.RecentLanguagesRoomDao import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao import fr.free.nrw.commons.customselector.database.UploadedStatusDao import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader @@ -90,50 +93,6 @@ open class CommonsApplicationModule(private val applicationContext: Context) { context.getString(R.string.license_name_cc_by_sa_four) to Prefs.Licenses.CC_BY_SA_4 ) - /** - * Provides an instance of CategoryContentProviderClient i.e. the categories - * that are there in local storage - */ - @Provides - @Named("category") - open fun provideCategoryContentProviderClient(context: Context): ContentProviderClient? = - context.contentResolver.acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY) - - @Provides - @Named("recentsearch") - fun provideRecentSearchContentProviderClient(context: Context): ContentProviderClient? = - context.contentResolver.acquireContentProviderClient(BuildConfig.RECENT_SEARCH_AUTHORITY) - - @Provides - @Named("contribution") - open fun provideContributionContentProviderClient(context: Context): ContentProviderClient? = - context.contentResolver.acquireContentProviderClient(BuildConfig.CONTRIBUTION_AUTHORITY) - - @Provides - @Named("modification") - open fun provideModificationContentProviderClient(context: Context): ContentProviderClient? = - context.contentResolver.acquireContentProviderClient(BuildConfig.MODIFICATION_AUTHORITY) - - @Provides - @Named("bookmarks") - fun provideBookmarkContentProviderClient(context: Context): ContentProviderClient? = - context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY) - - @Provides - @Named("bookmarksItem") - fun provideBookmarkItemContentProviderClient(context: Context): ContentProviderClient? = - context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_ITEMS_AUTHORITY) - - /** - * This method is used to provide instance of RecentLanguagesContentProvider - * which provides content of recent used languages from database - * @param context Context - * @return returns RecentLanguagesContentProvider - */ - @Provides - @Named("recent_languages") - fun provideRecentLanguagesContentProviderClient(context: Context): ContentProviderClient? = - context.contentResolver.acquireContentProviderClient(BuildConfig.RECENT_LANGUAGE_AUTHORITY) /** * Provides a Json store instance(JsonKvStore) which keeps @@ -157,6 +116,7 @@ open class CommonsApplicationModule(private val applicationContext: Context) { open fun provideLocationServiceManager(context: Context): LocationServiceManager = LocationServiceManager(context) + @Provides @Singleton open fun provideDBOpenHelper(context: Context): DBOpenHelper = @@ -238,6 +198,26 @@ open class CommonsApplicationModule(private val applicationContext: Context) { fun providesBookmarkCategoriesDao (appDatabase: AppDatabase): BookmarkCategoriesDao = appDatabase.bookmarkCategoriesDao() + @Provides + fun providesCategoryRoomDao(appDatabase: AppDatabase): CategoryRoomDao = + appDatabase.categoryRoomDao() + + @Provides + fun providesBookmarkPicturesRoomDao(appDatabase: AppDatabase): BookmarkPicturesRoomDao = + appDatabase.bookmarkPicturesRoomDao() + + @Provides + fun providesBookmarkItemsRoomDao(appDatabase: AppDatabase): BookmarkItemsRoomDao = + appDatabase.bookmarkItemsRoomDao() + + @Provides + fun providesRecentLanguagesRoomDao(appDatabase: AppDatabase): RecentLanguagesRoomDao = + appDatabase.recentLanguagesRoomDao() + + @Provides + fun providesRecentSearchesRoomDao(appDatabase: AppDatabase): RecentSearchesRoomDao = + appDatabase.recentSearchesRoomDao() + @Provides fun providesContentResolver(context: Context): ContentResolver = context.contentResolver diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt index 0d7dfd21803..16b4bc0f72a 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt @@ -15,7 +15,7 @@ import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment import fr.free.nrw.commons.explore.media.SearchMediaFragment import fr.free.nrw.commons.explore.models.RecentSearch -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesRoomDao import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment import fr.free.nrw.commons.media.MediaDetailPagerFragment import fr.free.nrw.commons.media.MediaDetailProvider @@ -35,7 +35,7 @@ import javax.inject.Inject class SearchActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallback { @JvmField @Inject - var recentSearchesDao: RecentSearchesDao? = null + var recentSearchesDao: RecentSearchesRoomDao? = null private var searchMediaFragment: SearchMediaFragment? = null private var searchCategoryFragment: SearchCategoryFragment? = null @@ -130,14 +130,7 @@ class SearchActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallba } private fun saveRecentSearch(query: String) { - val recentSearch = recentSearchesDao!!.find(query) - // Newly searched query... - if (recentSearch == null) { - recentSearchesDao!!.save(RecentSearch(null, query, Date())) - } else { - recentSearch.lastSearched = Date() - recentSearchesDao!!.save(recentSearch) - } + recentSearchesDao!!.save(RecentSearch(query, Date())).blockingAwait() } override fun getMediaAtPosition(i: Int): Media? = searchMediaFragment!!.getMediaAtPosition(i) diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt index d025fdfe102..38354bac9b6 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt @@ -13,7 +13,7 @@ import com.google.android.material.snackbar.Snackbar import fr.free.nrw.commons.Media import fr.free.nrw.commons.R import fr.free.nrw.commons.ViewPagerAdapter -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsRoomDao import fr.free.nrw.commons.category.CategoryImagesCallback import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment @@ -38,7 +38,7 @@ import javax.inject.Inject class WikidataItemDetailsActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallback { @JvmField @Inject - var bookmarkItemsDao: BookmarkItemsDao? = null + var bookmarkItemsDao: BookmarkItemsRoomDao? = null @JvmField @Inject @@ -220,7 +220,7 @@ class WikidataItemDetailsActivity : BaseActivity(), MediaDetailProvider, Categor .subscribe(Consumer> { depictedItems: List -> val bookmarkExists = bookmarkItemsDao!!.updateBookmarkItem( depictedItems[0]!! - ) + ).blockingGet() val snackbar = if (bookmarkExists) Snackbar.make( findViewById(R.id.toolbar_layout), @@ -238,7 +238,7 @@ class WikidataItemDetailsActivity : BaseActivity(), MediaDetailProvider, Categor }) ) } else { - val bookmarkExists = bookmarkItemsDao!!.updateBookmarkItem(wikidataItem!!) + val bookmarkExists = bookmarkItemsDao!!.updateBookmarkItem(wikidataItem!!).blockingGet() val snackbar = if (bookmarkExists) Snackbar.make( findViewById(R.id.toolbar_layout), @@ -267,9 +267,9 @@ class WikidataItemDetailsActivity : BaseActivity(), MediaDetailProvider, Categor private fun updateBookmarkState(item: MenuItem) { val isBookmarked: Boolean = if (intent.getStringExtra("fragment") != null) { - bookmarkItemsDao!!.findBookmarkItem(intent.getStringExtra("entityId")) + bookmarkItemsDao!!.findBookmarkItem(intent.getStringExtra("entityId")).blockingGet() } else { - bookmarkItemsDao!!.findBookmarkItem(wikidataItem!!.id) + bookmarkItemsDao!!.findBookmarkItem(wikidataItem!!.id).blockingGet() } item.setIcon(if (isBookmarked) { R.drawable.menu_ic_round_star_filled_24px diff --git a/app/src/main/java/fr/free/nrw/commons/explore/models/RecentSearch.kt b/app/src/main/java/fr/free/nrw/commons/explore/models/RecentSearch.kt index 0f72cac29c1..2c527ec3080 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/models/RecentSearch.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/models/RecentSearch.kt @@ -3,17 +3,13 @@ package fr.free.nrw.commons.explore.models import android.net.Uri import java.util.Date -/** - * Represents a recently searched query - * Example - query = "butterfly" - */ class RecentSearch( /** * The content URI that marks this query as already saved in the database. - * * @property contentUri the content URI */ - var contentUri: Uri?, +// @Deprecated("Required for legacy ContentProvider DAO compatibility only") +// var contentUri: Uri? = null, /** * Gets query name * @return query name diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt index d16d250dd7f..d6707db0bc2 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt @@ -1,188 +1,188 @@ -package fr.free.nrw.commons.explore.recentsearches - -import android.annotation.SuppressLint -import android.content.ContentProviderClient -import android.content.ContentValues -import android.database.Cursor -import android.os.RemoteException -import androidx.core.content.contentValuesOf -import fr.free.nrw.commons.explore.models.RecentSearch -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME -import fr.free.nrw.commons.utils.getInt -import fr.free.nrw.commons.utils.getLong -import fr.free.nrw.commons.utils.getString -import java.util.Date -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Provider - -/** - * This class doesn't execute queries in database directly instead it contains the logic behind - * inserting, deleting, searching data from recent searches database. - */ -class RecentSearchesDao @Inject constructor( - @param:Named("recentsearch") private val clientProvider: Provider -) { - /** - * This method is called on click of media/ categories for storing them in recent searches - * @param recentSearch a recent searches object that is to be added in SqLite DB - */ - fun save(recentSearch: RecentSearch) { - val db = clientProvider.get() - try { - val contentValues = toContentValues(recentSearch) - if (recentSearch.contentUri == null) { - recentSearch.contentUri = db.insert(BASE_URI, contentValues) - } else { - db.update(recentSearch.contentUri!!, contentValues, null, null) - } - } catch (e: RemoteException) { - throw RuntimeException(e) - } finally { - db.release() - } - } - - /** - * This method is called on confirmation of delete recent searches. - * It deletes all recent searches from the database - */ - fun deleteAll() { - var cursor: Cursor? = null - val db = clientProvider.get() - try { - cursor = db.query( - BASE_URI, - ALL_FIELDS, - null, - arrayOf(), - "$COLUMN_LAST_USED DESC" - ) - while (cursor != null && cursor.moveToNext()) { - try { - val recentSearch = find(fromCursor(cursor).query) - if (recentSearch!!.contentUri == null) { - throw RuntimeException("tried to delete item with no content URI") - } else { - db.delete(recentSearch.contentUri!!, null, null) - } - } catch (e: RemoteException) { - throw RuntimeException(e) - } finally { - db.release() - } - } - } catch (e: RemoteException) { - throw RuntimeException(e) - } finally { - cursor?.close() - } - } - - /** - * Deletes a recent search from the database - */ - fun delete(recentSearch: RecentSearch) { - val db = clientProvider.get() - try { - if (recentSearch.contentUri == null) { - throw RuntimeException("tried to delete item with no content URI") - } else { - db.delete(recentSearch.contentUri!!, null, null) - } - } catch (e: RemoteException) { - throw RuntimeException(e) - } finally { - db.release() - } - } - - - /** - * Find persisted search query in database, based on its name. - * @param name Search query Ex- "butterfly" - * @return recently searched query from database, or null if not found - */ - fun find(name: String): RecentSearch? { - var cursor: Cursor? = null - val db = clientProvider.get() - try { - cursor = db.query( - BASE_URI, - ALL_FIELDS, - "$COLUMN_NAME=?", - arrayOf(name), - null - ) - if (cursor != null && cursor.moveToFirst()) { - return fromCursor(cursor) - } - } catch (e: RemoteException) { - // This feels lazy, but to hell with checked exceptions. :) - throw RuntimeException(e) - } finally { - cursor?.close() - db.release() - } - return null - } - - /** - * Retrieve recently-searched queries, ordered by descending date. - * @return a list containing recent searches - */ - fun recentSearches(limit: Int): List { - val items: MutableList = mutableListOf() - var cursor: Cursor? = null - val db = clientProvider.get() - try { - cursor = db.query( - BASE_URI, ALL_FIELDS, - null, arrayOf(), "$COLUMN_LAST_USED DESC" - ) - // fixme add a limit on the original query instead of falling out of the loop? - while (cursor != null && cursor.moveToNext() && cursor.position < limit) { - items.add(fromCursor(cursor).query) - } - } catch (e: RemoteException) { - throw RuntimeException(e) - } finally { - cursor?.close() - db.release() - } - return items - } - - /** - * It creates an Recent Searches object from data stored in the SQLite DB by using cursor - * @param cursor - * @return RecentSearch object - */ - fun fromCursor(cursor: Cursor): RecentSearch { - var query = cursor.getString(COLUMN_NAME) - - if (query == null) { - query = "" - } - - return RecentSearch( - uriForId(cursor.getInt(COLUMN_ID)), - query, - Date(cursor.getLong(COLUMN_LAST_USED)) - ) - } - - /** - * This class contains the database table architechture for recent searches, - * It also contains queries and logic necessary to the create, update, delete this table. - */ - private fun toContentValues(recentSearch: RecentSearch): ContentValues = contentValuesOf( - COLUMN_NAME to recentSearch.query, - COLUMN_LAST_USED to recentSearch.lastSearched.time - ) -} +//package fr.free.nrw.commons.explore.recentsearches +// +//import android.annotation.SuppressLint +//import android.content.ContentProviderClient +//import android.content.ContentValues +//import android.database.Cursor +//import android.os.RemoteException +//import androidx.core.content.contentValuesOf +//import fr.free.nrw.commons.explore.models.RecentSearch +//import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI +//import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId +//import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS +//import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID +//import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED +//import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME +//import fr.free.nrw.commons.utils.getInt +//import fr.free.nrw.commons.utils.getLong +//import fr.free.nrw.commons.utils.getString +//import java.util.Date +//import javax.inject.Inject +//import javax.inject.Named +//import javax.inject.Provider +// +///** +// * This class doesn't execute queries in database directly instead it contains the logic behind +// * inserting, deleting, searching data from recent searches database. +// */ +//class RecentSearchesDao @Inject constructor( +// @param:Named("recentsearch") private val clientProvider: Provider +//) { +// /** +// * This method is called on click of media/ categories for storing them in recent searches +// * @param recentSearch a recent searches object that is to be added in SqLite DB +// */ +// fun save(recentSearch: RecentSearch) { +// val db = clientProvider.get() +// try { +// val contentValues = toContentValues(recentSearch) +// if (recentSearch.contentUri == null) { +// recentSearch.contentUri = db.insert(BASE_URI, contentValues) +// } else { +// db.update(recentSearch.contentUri!!, contentValues, null, null) +// } +// } catch (e: RemoteException) { +// throw RuntimeException(e) +// } finally { +// db.release() +// } +// } +// +// /** +// * This method is called on confirmation of delete recent searches. +// * It deletes all recent searches from the database +// */ +// fun deleteAll() { +// var cursor: Cursor? = null +// val db = clientProvider.get() +// try { +// cursor = db.query( +// BASE_URI, +// ALL_FIELDS, +// null, +// arrayOf(), +// "$COLUMN_LAST_USED DESC" +// ) +// while (cursor != null && cursor.moveToNext()) { +// try { +// val recentSearch = find(fromCursor(cursor).query) +// if (recentSearch!!.contentUri == null) { +// throw RuntimeException("tried to delete item with no content URI") +// } else { +// db.delete(recentSearch.contentUri!!, null, null) +// } +// } catch (e: RemoteException) { +// throw RuntimeException(e) +// } finally { +// db.release() +// } +// } +// } catch (e: RemoteException) { +// throw RuntimeException(e) +// } finally { +// cursor?.close() +// } +// } +// +// /** +// * Deletes a recent search from the database +// */ +// fun delete(recentSearch: RecentSearch) { +// val db = clientProvider.get() +// try { +// if (recentSearch.contentUri == null) { +// throw RuntimeException("tried to delete item with no content URI") +// } else { +// db.delete(recentSearch.contentUri!!, null, null) +// } +// } catch (e: RemoteException) { +// throw RuntimeException(e) +// } finally { +// db.release() +// } +// } +// +// +// /** +// * Find persisted search query in database, based on its name. +// * @param name Search query Ex- "butterfly" +// * @return recently searched query from database, or null if not found +// */ +// fun find(name: String): RecentSearch? { +// var cursor: Cursor? = null +// val db = clientProvider.get() +// try { +// cursor = db.query( +// BASE_URI, +// ALL_FIELDS, +// "$COLUMN_NAME=?", +// arrayOf(name), +// null +// ) +// if (cursor != null && cursor.moveToFirst()) { +// return fromCursor(cursor) +// } +// } catch (e: RemoteException) { +// // This feels lazy, but to hell with checked exceptions. :) +// throw RuntimeException(e) +// } finally { +// cursor?.close() +// db.release() +// } +// return null +// } +// +// /** +// * Retrieve recently-searched queries, ordered by descending date. +// * @return a list containing recent searches +// */ +// fun recentSearches(limit: Int): List { +// val items: MutableList = mutableListOf() +// var cursor: Cursor? = null +// val db = clientProvider.get() +// try { +// cursor = db.query( +// BASE_URI, ALL_FIELDS, +// null, arrayOf(), "$COLUMN_LAST_USED DESC" +// ) +// // fixme add a limit on the original query instead of falling out of the loop? +// while (cursor != null && cursor.moveToNext() && cursor.position < limit) { +// items.add(fromCursor(cursor).query) +// } +// } catch (e: RemoteException) { +// throw RuntimeException(e) +// } finally { +// cursor?.close() +// db.release() +// } +// return items +// } +// +// /** +// * It creates an Recent Searches object from data stored in the SQLite DB by using cursor +// * @param cursor +// * @return RecentSearch object +// */ +// fun fromCursor(cursor: Cursor): RecentSearch { +// var query = cursor.getString(COLUMN_NAME) +// +// if (query == null) { +// query = "" +// } +// +// return RecentSearch( +// uriForId(cursor.getInt(COLUMN_ID)), +// query, +// Date(cursor.getLong(COLUMN_LAST_USED)) +// ) +// } +// +// /** +// * This class contains the database table architechture for recent searches, +// * It also contains queries and logic necessary to the create, update, delete this table. +// */ +// private fun toContentValues(recentSearch: RecentSearch): ContentValues = contentValuesOf( +// COLUMN_NAME to recentSearch.query, +// COLUMN_LAST_USED to recentSearch.lastSearched.time +// ) +//} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt index e7903c9ed7b..89f5d0f7ca2 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt @@ -25,7 +25,7 @@ import javax.inject.Inject class RecentSearchesFragment : CommonsDaggerSupportFragment() { @JvmField @Inject - var recentSearchesDao: RecentSearchesDao? = null + var recentSearchesDao: RecentSearchesRoomDao? = null private var recentSearches: List = emptyList() private lateinit var adapter: ArrayAdapter @@ -37,7 +37,7 @@ class RecentSearchesFragment : CommonsDaggerSupportFragment() { ): View { binding = FragmentSearchHistoryBinding.inflate(inflater, container, false) - recentSearches = recentSearchesDao!!.recentSearches(10) + recentSearches = recentSearchesDao!!.recentSearches(10).blockingGet() if (recentSearches.isEmpty()) { binding!!.recentSearchesDeleteButton.visibility = View.GONE @@ -77,7 +77,7 @@ class RecentSearchesFragment : CommonsDaggerSupportFragment() { } private fun setDeleteRecentPositiveButton(context: Context, dialog: DialogInterface) { - recentSearchesDao!!.deleteAll() + recentSearchesDao!!.deleteAll().blockingAwait() if (binding != null) { binding!!.recentSearchesDeleteButton.visibility = View.GONE binding!!.recentSearchesTextView.setText(R.string.no_recent_searches) @@ -85,7 +85,7 @@ class RecentSearchesFragment : CommonsDaggerSupportFragment() { getContext(), getString(R.string.search_history_deleted), Toast.LENGTH_SHORT ).show() - recentSearches = recentSearchesDao!!.recentSearches(10) + recentSearches = recentSearchesDao!!.recentSearches(10).blockingGet() adapter = ArrayAdapter(context, R.layout.item_recent_searches, recentSearches) binding!!.recentSearchesList.adapter = adapter adapter.notifyDataSetChanged() @@ -109,8 +109,8 @@ class RecentSearchesFragment : CommonsDaggerSupportFragment() { } private fun setDeletePositiveButton(context: Context, dialog: DialogInterface, position: Int) { - recentSearchesDao!!.delete(recentSearchesDao!!.find(recentSearches[position])!!) - recentSearches = recentSearchesDao!!.recentSearches(10) + recentSearchesDao!!.delete(recentSearchesDao!!.find(recentSearches[position]).blockingGet()!!).blockingAwait() + recentSearches = recentSearchesDao!!.recentSearches(10).blockingGet() adapter = ArrayAdapter( context, R.layout.item_recent_searches, recentSearches @@ -135,7 +135,7 @@ class RecentSearchesFragment : CommonsDaggerSupportFragment() { * This method is called when search query is null to update Recent Searches */ fun updateRecentSearches() { - recentSearches = recentSearchesDao!!.recentSearches(10) + recentSearches = recentSearchesDao!!.recentSearches(10).blockingGet() adapter.notifyDataSetChanged() if (recentSearches.isNotEmpty()) { diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesRoomDao.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesRoomDao.kt index 8a11bc50668..b1e2a92bb7f 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesRoomDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesRoomDao.kt @@ -1,24 +1,77 @@ package fr.free.nrw.commons.explore.recentsearches -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* +import fr.free.nrw.commons.explore.models.RecentSearch import io.reactivex.Completable import io.reactivex.Single +import java.util.* @Dao -interface RecentSearchesRoomDao { +abstract class RecentSearchesRoomDao { @Query("SELECT * FROM recent_searches ORDER BY last_used DESC LIMIT :limit") - fun getAll(limit: Int): Single> + abstract fun getAll(limit: Int): Single> @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(recentSearch: RecentSearchRoomEntity) + abstract fun insert(recentSearch: RecentSearchRoomEntity): Single + + @Update + abstract fun update(recentSearch: RecentSearchRoomEntity): Completable @Query("SELECT * FROM recent_searches WHERE name = :query") - fun findEntity(query: String): RecentSearchRoomEntity? + abstract fun findEntity(query: String): Single> @Query("DELETE FROM recent_searches") - fun deleteTable(): Completable + abstract fun deleteTable(): Completable + + @Query("DELETE FROM recent_searches WHERE name = :name") + abstract fun deleteByName(name: String): Completable + + fun recentSearches(limit: Int): Single> { + return getAll(limit).map { entities -> + entities.map { it.query } + } + } + + fun find(query: String): io.reactivex.Maybe { + return findEntity(query).flatMapMaybe { entities -> + val entity = entities.firstOrNull() + if (entity != null) io.reactivex.Maybe.just(fromEntity(entity)) + else io.reactivex.Maybe.empty() + } + } + + fun delete(recentSearch: RecentSearch): Completable { + return deleteByName(recentSearch.query) + } + + fun deleteAll(): Completable { + return deleteTable() + } + + fun save(recentSearch: RecentSearch): Completable { + return findEntity(recentSearch.query).flatMapCompletable { entities -> + if (entities.isNotEmpty()) { + update(toEntity(recentSearch, entities[0].id)) + } else { + insert(toEntity(recentSearch)).ignoreElement() + } + } + } + + private fun toEntity( + recentSearch: RecentSearch, + id: Long = 0 + ): RecentSearchRoomEntity = RecentSearchRoomEntity( + id = id, + query = recentSearch.query, + lastSearched = recentSearch.lastSearched + ) + + private fun fromEntity( + entity: RecentSearchRoomEntity + ): RecentSearch = RecentSearch( + query = entity.query, + lastSearched = entity.lastSearched + ) } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt index d1e1d2aad10..7f0337eb6a6 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt @@ -27,8 +27,8 @@ import fr.free.nrw.commons.Media import fr.free.nrw.commons.R import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.bookmarks.models.Bookmark -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao +//import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesRoomDao import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.MainActivity import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding @@ -58,7 +58,7 @@ class MediaDetailPagerFragment : CommonsDaggerSupportFragment(), OnPageChangeLis MediaDetailFragment.Callback { @JvmField @Inject - var bookmarkDao: BookmarkPicturesDao? = null + var bookmarkDao: BookmarkPicturesRoomDao? = null @JvmField @Inject @@ -174,7 +174,7 @@ class MediaDetailPagerFragment : CommonsDaggerSupportFragment(), OnPageChangeLis val mediaDetailFragment = adapter!!.currentMediaDetailFragment when (item.itemId) { R.id.menu_bookmark_current_image -> { - val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark!!) + val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark!!).blockingGet() val snackbar = if (bookmarkExists) Snackbar.make( requireView(), R.string.add_bookmark, @@ -444,7 +444,6 @@ ${m.pageTitle.canonicalUri}""" bookmark = Bookmark( m.filename, m.getAuthorOrUser(), - BookmarkPicturesContentProvider.uriForName(m.filename!!) ) updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image)) val contributionState = provider.getContributionStateAt(position) @@ -512,7 +511,7 @@ ${m.pageTitle.canonicalUri}""" } private fun updateBookmarkState(item: MenuItem) { - val isBookmarked = bookmarkDao!!.findBookmark(bookmark) + val isBookmarked = bookmarkDao!!.findBookmark(bookmark).blockingGet() if (isBookmarked) { if (removedItems.contains(binding!!.mediaDetailsPager.currentItem)) { removedItems.remove(binding!!.mediaDetailsPager.currentItem) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index 4950074482d..8dfcb2fa2ff 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -5,9 +5,6 @@ import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.room.Embedded; -import androidx.room.Entity; -import androidx.room.PrimaryKey; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.nearby.model.NearbyResultItem; import fr.free.nrw.commons.utils.LocationUtils; @@ -18,23 +15,16 @@ /** * A single geolocated Wikidata item */ -@Entity(tableName = "place") public class Place implements Parcelable { - public String language; public String name; private Label label; private String longDescription; - @Embedded public LatLng location; - @PrimaryKey @NonNull public String entityID; private String category; public String pic; - // exists boolean will tell whether the place exists or not, - // For a place to be existing both destroyed and endTime property should be null but it is also not necessary for a non-existing place to have both properties either one property is enough (in such case that not given property will be considered as null). public Boolean exists; - public String distance; public Sitelinks siteLinks; private boolean isMonument; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java index 269384ffab7..45c83ef462d 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java @@ -22,7 +22,14 @@ public abstract class PlaceDao { * @param place The Place object to be inserted. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - public abstract void saveSynchronous(Place place); + public abstract void saveSynchronous(PlaceRoomEntity place); + + void saveSynchronous(final Place place) { + saveSynchronous(new PlaceRoomEntity(place.getLanguage(), place.getName(), place.getLabel(), + place.getLongDescription(), place.getLocation(), place.entityID, place.getCategory(), + place.pic, place.exists, place.distance, place.siteLinks, + place.isMonument(), place.getThumb())); + } /** * Retrieves a Place object from the database based on the provided entity ID. diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRoomEntity.kt b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRoomEntity.kt new file mode 100644 index 00000000000..5ff248fd356 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRoomEntity.kt @@ -0,0 +1,28 @@ +package fr.free.nrw.commons.nearby + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import fr.free.nrw.commons.location.LatLng + +/** + * Room entity for the place table. + */ +@Entity(tableName = "place") +data class PlaceRoomEntity( + val language: String?, + val name: String?, + val label: Label?, + val longDescription: String?, + @Embedded + val location: LatLng?, + @PrimaryKey + val entityID: String, + val category: String?, + val pic: String?, + val exists: Boolean?, + val distance: String?, + val siteLinks: Sitelinks?, + val isMonument: Boolean, + val thumb: String? +) diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesRoomDao.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesRoomDao.kt index 71243c66484..3b157280155 100644 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesRoomDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesRoomDao.kt @@ -1,23 +1,39 @@ package fr.free.nrw.commons.recentlanguages -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* +import io.reactivex.Completable import io.reactivex.Single @Dao -interface RecentLanguagesRoomDao { +abstract class RecentLanguagesRoomDao { @Query("SELECT * FROM recent_languages ORDER BY rowid DESC") - fun getAll(): Single> + abstract fun getAll(): Single> @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(language: RecentLanguageRoomEntity) + abstract fun insert(language: RecentLanguageRoomEntity): Completable @Query("DELETE FROM recent_languages WHERE language_code = :languageCode") - fun deleteRecentLanguage(languageCode: String) + abstract fun deleteRecentLanguage(languageCode: String): Completable @Query("SELECT EXISTS (SELECT 1 FROM recent_languages WHERE language_code = :languageCode)") - fun findRecentLanguage(languageCode: String): Boolean + abstract fun findRecentLanguage(languageCode: String): Single + + fun getRecentLanguages(): Single> { + return getAll().map { entities -> + entities.map { fromEntity(it) } + } + } + + fun addRecentLanguage(language: Language): Completable { + return insert(toEntity(language)) + } + + private fun toEntity(language: Language): RecentLanguageRoomEntity { + return RecentLanguageRoomEntity(language.languageName, language.languageCode) + } + + private fun fromEntity(entity: RecentLanguageRoomEntity): Language { + return Language(entity.languageName, entity.languageCode) + } } diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.kt index 1dc9b6ae8b5..2a87395d0bf 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.kt @@ -1,23 +1,27 @@ package fr.free.nrw.commons.review -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* /** * Dao interface for reviewed images database */ @Dao -interface ReviewDao { +abstract class ReviewDao { /** - * Inserts reviewed/skipped image identifier into the database + * Inserts reviewed/skipped image identifier into the database internally * - * @param reviewEntity + * @param reviewRoomEntity */ @Insert(onConflict = OnConflictStrategy.IGNORE) - fun insert(reviewEntity: ReviewEntity) + protected abstract fun insertInternal(reviewRoomEntity: ReviewRoomEntity) + + /** + * Public method to insert using domain model + */ + fun insert(reviewImage: ReviewImage) { + insertInternal(toEntity(reviewImage)) + } /** * Checks if the image has already been reviewed/skipped by the user @@ -27,5 +31,8 @@ interface ReviewDao { * @return */ @Query("SELECT EXISTS (SELECT * from `reviewed-images` where imageId = (:imageId))") - fun isReviewedAlready(imageId: String): Boolean + abstract fun isReviewedAlready(imageId: String): Boolean + + private fun toEntity(reviewImage: ReviewImage): ReviewRoomEntity = + ReviewRoomEntity(imageId = reviewImage.imageId) } diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt index 3ad15d8bfcb..72dfc906cc1 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt @@ -132,7 +132,7 @@ class ReviewHelper */ fun addViewedImagesToDB(imageId: String?) { Completable - .fromAction { imageId?.let { ReviewEntity(it) }?.let { dao!!.insert(it) } } + .fromAction { imageId?.let { ReviewImage(it) }?.let { dao!!.insert(it) } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewImage.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewImage.kt new file mode 100644 index 00000000000..2ef83b34bf0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewImage.kt @@ -0,0 +1,8 @@ +package fr.free.nrw.commons.review + +/** + * Domain model to store reviewed/skipped images identifier + */ +data class ReviewImage( + val imageId: String +) diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewRoomEntity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewRoomEntity.kt new file mode 100644 index 00000000000..87e6707c57d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewRoomEntity.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.review + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Room entity for the reviewed-images table. + */ +@Entity(tableName = "reviewed-images") +data class ReviewRoomEntity( + @PrimaryKey + val imageId: String +) diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt index c38ed1ecb8c..9711bcb42dc 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -46,7 +46,7 @@ import fr.free.nrw.commons.location.LocationServiceManager import fr.free.nrw.commons.logging.CommonsLogSender import fr.free.nrw.commons.recentlanguages.Language import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter -import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao +import fr.free.nrw.commons.recentlanguages.RecentLanguagesRoomDao import fr.free.nrw.commons.upload.LanguagesAdapter import fr.free.nrw.commons.utils.DialogUtil import fr.free.nrw.commons.utils.PermissionUtils @@ -67,7 +67,7 @@ class SettingsFragment : PreferenceFragmentCompat() { lateinit var commonsLogSender: CommonsLogSender @Inject - lateinit var recentLanguagesDao: RecentLanguagesDao + lateinit var recentLanguagesDao: RecentLanguagesRoomDao @Inject lateinit var contributionController: ContributionController @@ -342,7 +342,7 @@ class SettingsFragment : PreferenceFragmentCompat() { private fun prepareAppLanguages(keyListPreference: String) { // Gets current language code from shared preferences val languageCode = getCurrentLanguageCode(keyListPreference) - val recentLanguages = recentLanguagesDao.getRecentLanguages() + val recentLanguages = recentLanguagesDao.getRecentLanguages().blockingGet() val selectedLanguages = hashMapOf() if (keyListPreference == "appUiDefaultLanguagePref") { @@ -402,11 +402,11 @@ class SettingsFragment : PreferenceFragmentCompat() { listView.setOnItemClickListener { adapterView, _, position, _ -> val lCode = (adapterView.adapter as LanguagesAdapter).getLanguageCode(position) val languageName = (adapterView.adapter as LanguagesAdapter).getLanguageName(position) - val isExists = recentLanguagesDao.findRecentLanguage(lCode) + val isExists = recentLanguagesDao.findRecentLanguage(lCode).blockingGet() if (isExists) { - recentLanguagesDao.deleteRecentLanguage(lCode) + recentLanguagesDao.deleteRecentLanguage(lCode).blockingAwait() } - recentLanguagesDao.addRecentLanguage(Language(languageName, lCode)) + recentLanguagesDao.addRecentLanguage(Language(languageName, lCode)).blockingAwait() saveLanguageValue(lCode, keyListPreference) val defLocale = createLocale(lCode) if (keyListPreference == "appUiDefaultLanguagePref") { @@ -451,7 +451,7 @@ class SettingsFragment : PreferenceFragmentCompat() { separator?.visibility = View.VISIBLE val recentLanguagesAdapter = RecentLanguagesAdapter( requireActivity(), - recentLanguagesDao.getRecentLanguages(), + recentLanguagesDao.getRecentLanguages().blockingGet(), selectedLanguages ) languageHistoryListView?.adapter = recentLanguagesAdapter @@ -469,11 +469,11 @@ class SettingsFragment : PreferenceFragmentCompat() { ) { val recentLanguageCode = (adapterView.adapter as RecentLanguagesAdapter).getLanguageCode(position) val recentLanguageName = (adapterView.adapter as RecentLanguagesAdapter).getLanguageName(position) - val isExists = recentLanguagesDao.findRecentLanguage(recentLanguageCode) + val isExists = recentLanguagesDao.findRecentLanguage(recentLanguageCode).blockingGet() if (isExists) { - recentLanguagesDao.deleteRecentLanguage(recentLanguageCode) + recentLanguagesDao.deleteRecentLanguage(recentLanguageCode).blockingAwait() } - recentLanguagesDao.addRecentLanguage(Language(recentLanguageName, recentLanguageCode)) + recentLanguagesDao.addRecentLanguage(Language(recentLanguageName, recentLanguageCode)).blockingAwait() saveLanguageValue(recentLanguageCode, keyListPreference) val defLocale = createLocale(recentLanguageCode) if (keyListPreference == "appUiDefaultLanguagePref") { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.kt index b19da15e6b0..9c4f962b03c 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.kt @@ -32,7 +32,7 @@ import fr.free.nrw.commons.R import fr.free.nrw.commons.databinding.RowItemDescriptionBinding import fr.free.nrw.commons.recentlanguages.Language import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter -import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao +import fr.free.nrw.commons.recentlanguages.RecentLanguagesRoomDao import fr.free.nrw.commons.utils.AbstractTextWatcher import timber.log.Timber import java.util.Locale @@ -50,7 +50,7 @@ class UploadMediaDetailAdapter : RecyclerView.Adapter private var selectedVoiceIcon: SelectedVoiceIcon? = null - var recentLanguagesDao: RecentLanguagesDao + var recentLanguagesDao: RecentLanguagesRoomDao var callback: Callback? = null var eventListener: EventListener? = null var items: List @@ -65,7 +65,7 @@ class UploadMediaDetailAdapter : RecyclerView.Adapter ) { uploadMediaDetails = ArrayList() @@ -80,7 +80,7 @@ class UploadMediaDetailAdapter : RecyclerView.Adapter, - recentLanguagesDao: RecentLanguagesDao, + recentLanguagesDao: RecentLanguagesRoomDao, voiceInputResultLauncher: ActivityResultLauncher ) { this.uploadMediaDetails = uploadMediaDetails @@ -298,7 +298,7 @@ class UploadMediaDetailAdapter : RecyclerView.Adapter + protected abstract fun getAllDepictsInternal(): List + + fun getAllDepicts(): List = + getAllDepictsInternal().map { fromEntity(it) } @Query("Select * From depicts_table order by lastUsed DESC LIMIT :n OFFSET 10") - abstract suspend fun getDepictsForDeletion(n: Int): List + protected abstract fun getDepictsForDeletionInternal(n: Int): List - @Delete - abstract suspend fun delete(depicts: Depicts) + fun getDepictsForDeletion(n: Int): List = + getDepictsForDeletionInternal(n).map { fromEntity(it) } /** * Gets all Depicts objects from the database, ordered by lastUsed in descending order. @@ -43,16 +51,6 @@ abstract class DepictsDao { getAllDepicts() } - /** - * Inserts a Depicts object into the database. - * - * @param depictedItem The Depicts object to insert. - */ - fun insertDepict(depictedItem: Depicts) = - CoroutineScope(Dispatchers.IO).launch { - insert(depictedItem) - } - /** * Gets a list of Depicts objects that need to be deleted from the database. * @@ -64,16 +62,26 @@ abstract class DepictsDao { getDepictsForDeletion(n) } - /** - * Deletes a Depicts object from the database. - * - * @param depicts The Depicts object to delete. - */ + @Delete + protected abstract fun deleteInternal(depicts: DepictsRoomEntity) + fun deleteDepicts(depicts: Depicts) = CoroutineScope(Dispatchers.IO).launch { - delete(depicts) + deleteInternal(toEntity(depicts)) } + private fun toEntity(depicts: Depicts): DepictsRoomEntity = + DepictsRoomEntity( + item = depicts.item, + lastUsed = depicts.lastUsed + ) + + private fun fromEntity(entity: DepictsRoomEntity): Depicts = + Depicts( + item = entity.item, + lastUsed = entity.lastUsed + ) + /** * Saves a list of DepictedItems in the DepictsRoomDataBase. */ diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsRoomEntity.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsRoomEntity.kt new file mode 100644 index 00000000000..55535c47d46 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsRoomEntity.kt @@ -0,0 +1,15 @@ +package fr.free.nrw.commons.upload.depicts + +import androidx.room.Entity +import androidx.room.PrimaryKey +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import java.util.Date + +/** + * Room entity for the depicts_table. + */ +@Entity(tableName = "depicts_table") +data class DepictsRoomEntity( + @PrimaryKey val item: DepictedItem, + val lastUsed: Date +) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt index 8c2faff3433..8fc056526c3 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt @@ -34,7 +34,7 @@ import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.locationpicker.LocationPicker import fr.free.nrw.commons.locationpicker.LocationPicker.getCameraPosition import fr.free.nrw.commons.nearby.Place -import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao +import fr.free.nrw.commons.recentlanguages.RecentLanguagesRoomDao import fr.free.nrw.commons.settings.Prefs import fr.free.nrw.commons.upload.ImageCoordinates import fr.free.nrw.commons.upload.SimilarImageDialogFragment @@ -75,7 +75,7 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra lateinit var defaultKvStore: JsonKvStore @Inject - lateinit var recentLanguagesDao: RecentLanguagesDao + lateinit var recentLanguagesDao: RecentLanguagesRoomDao /** * True when user removes location from the current image diff --git a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt index 4ae366535f6..0bc41eef89d 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt @@ -1,8 +1,6 @@ package fr.free.nrw.commons.upload.structure.depictions import android.os.Parcelable -import androidx.room.Entity -import androidx.room.PrimaryKey import fr.free.nrw.commons.category.CategoryItem import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.upload.WikidataItem @@ -25,7 +23,6 @@ const val THUMB_IMAGE_SIZE = "70px" * Model class for Depicted Item in Upload and Explore */ @Parcelize -@Entity data class DepictedItem constructor( override val name: String, val description: String?, @@ -33,7 +30,7 @@ data class DepictedItem constructor( val instanceOfs: List, val commonsCategories: List, var isSelected: Boolean, - @PrimaryKey override val id: String, + override val id: String, ) : WikidataItem, Parcelable { constructor(entity: Entities.Entity) : this( diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt index c0e3bda0831..c338da96414 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt @@ -48,13 +48,6 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu val contributionClient: ContentProviderClient = mock() val modificationClient: ContentProviderClient = mock() val uploadPrefs: JsonKvStore = mock() - - override fun provideCategoryContentProviderClient(context: Context): ContentProviderClient = categoryClient - - override fun provideContributionContentProviderClient(context: Context): ContentProviderClient = contributionClient - - override fun provideModificationContentProviderClient(context: Context): ContentProviderClient = modificationClient - override fun providesDefaultKvStore(context: Context, gson: Gson): JsonKvStore = defaultSharedPreferences override fun provideLocationServiceManager(context: Context): LocationServiceManager = locationServiceManager diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsControllerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsControllerTest.kt index 38bf6b2678e..c413e7ae69f 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsControllerTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsControllerTest.kt @@ -9,11 +9,12 @@ import org.junit.Test import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.MockitoAnnotations +import io.reactivex.Single import java.util.ArrayList class BookmarkItemsControllerTest { @Mock - var bookmarkDao: BookmarkItemsDao? = null + var bookmarkDao: BookmarkItemsRoomDao? = null @InjectMocks lateinit var bookmarkItemsController: BookmarkItemsController @@ -22,7 +23,7 @@ class BookmarkItemsControllerTest { fun setup() { MockitoAnnotations.openMocks(this) whenever(bookmarkDao!!.getAllBookmarksItems()) - .thenReturn(mockBookmarkList) + .thenReturn(Single.just(mockBookmarkList)) } /** diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDaoTest.kt index 4754e82affc..a8491392119 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDaoTest.kt @@ -1,74 +1,39 @@ package fr.free.nrw.commons.bookmarks.items -import android.content.ContentProviderClient -import android.content.ContentValues -import android.database.Cursor -import android.database.MatrixCursor -import android.net.Uri -import android.os.RemoteException -import androidx.sqlite.db.SupportSQLiteDatabase -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.isA -import com.nhaarman.mockitokotlin2.isNull -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +import androidx.room.Room.inMemoryDatabaseBuilder +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 import fr.free.nrw.commons.TestCommonsApplication -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_DESCRIPTION_LIST -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_NAME_LIST -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_THUMBNAIL_LIST -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_DESCRIPTION -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IMAGE -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_INSTANCE_LIST -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IS_SELECTED -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_NAME -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.CREATE_TABLE_STATEMENT -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.DROP_TABLE_STATEMENT -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.onCreate -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.onDelete -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.onUpdate import fr.free.nrw.commons.category.CategoryItem +import fr.free.nrw.commons.db.AppDatabase import fr.free.nrw.commons.upload.structure.depictions.DepictedItem -import org.junit.Assert +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.verifyNoInteractions -import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode -@RunWith(RobolectricTestRunner::class) +@RunWith(AndroidJUnit4::class) @Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) class BookmarkItemsDaoTest { - private val columns = - arrayOf( - COLUMN_NAME, - COLUMN_DESCRIPTION, - COLUMN_IMAGE, - COLUMN_INSTANCE_LIST, - COLUMN_CATEGORIES_NAME_LIST, - COLUMN_CATEGORIES_DESCRIPTION_LIST, - COLUMN_CATEGORIES_THUMBNAIL_LIST, - COLUMN_IS_SELECTED, - COLUMN_ID, - ) - private val client: ContentProviderClient = mock() - private val database: SupportSQLiteDatabase = mock() - private val captor = argumentCaptor() - - private lateinit var testObject: BookmarkItemsDao + private lateinit var bookmarkItemsRoomDao: BookmarkItemsRoomDao + private lateinit var database: AppDatabase private lateinit var exampleItemBookmark: DepictedItem - /** - * Set up Test DepictedItem and BookmarkItemsDao - */ @Before - fun setUp() { + fun createDb() { + database = + inMemoryDatabaseBuilder( + context = ApplicationProvider.getApplicationContext(), + klass = AppDatabase::class.java, + ).allowMainThreadQueries().build() + bookmarkItemsRoomDao = database.bookmarkItemsRoomDao() + exampleItemBookmark = DepictedItem( "itemName", @@ -86,330 +51,49 @@ class BookmarkItemsDaoTest { false, "itemID", ) - testObject = BookmarkItemsDao { client } - } - - @Test - fun createTable() { - onCreate(database) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - - @Test - fun deleteTable() { - onDelete(database) - inOrder(database) { - verify(database).execSQL(DROP_TABLE_STATEMENT) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } } - @Test - fun createFromCursor() { - createCursor(1).let { cursor -> - cursor.moveToFirst() - testObject.fromCursor(cursor).let { - Assert.assertEquals("itemName", it.name) - Assert.assertEquals("itemDescription", it.description) - Assert.assertEquals("itemImageUrl", it.imageUrl) - Assert.assertEquals(listOf("instance"), it.instanceOfs) - Assert.assertEquals( - listOf( - CategoryItem( - "category name", - "category description", - "category thumbnail", - false, - ), - ), - it.commonsCategories, - ) - Assert.assertEquals(false, it.isSelected) - Assert.assertEquals("itemID", it.id) - } - } + @After + fun closeDb() { + database.close() } @Test fun getAllItemsBookmarks() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())) - .thenReturn(createCursor(14)) - - val result = testObject.getAllBookmarksItems() - - Assert.assertEquals(14, (result.size)) - } - - @Test(expected = RuntimeException::class) - fun getAllItemsBookmarksTranslatesExceptions() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow( - RemoteException(""), - ) - testObject.getAllBookmarksItems() - } - - @Test - fun getAllItemsBookmarksReturnsEmptyList_emptyCursor() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())) - .thenReturn(createCursor(0)) - Assert.assertTrue(testObject.getAllBookmarksItems().isEmpty()) - } - - @Test - fun getAllItemsBookmarksReturnsEmptyList_nullCursor() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null) - Assert.assertTrue(testObject.getAllBookmarksItems().isEmpty()) - } - - @Test - fun cursorsAreClosedAfterGetAllItemsBookmarksQuery() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) - - testObject.getAllBookmarksItems() + for (i in 1..5) { + val item = exampleItemBookmark.copy(id = "item$i") + bookmarkItemsRoomDao.updateBookmarkItem(item).blockingGet() + } - verify(mockCursor).close() + val result = bookmarkItemsRoomDao.getAllBookmarksItems().blockingGet() + assertEquals(5, result.size) } @Test fun updateNewItemBookmark() { - whenever(client.insert(any(), any())).thenReturn(Uri.EMPTY) - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null) - - Assert.assertTrue(testObject.updateBookmarkItem(exampleItemBookmark)) - verify(client).insert(eq(BookmarkItemsContentProvider.BASE_URI), captor.capture()) - captor.firstValue.let { cv -> - Assert.assertEquals(9, cv.size()) - Assert.assertEquals( - exampleItemBookmark.name, - cv.getAsString(COLUMN_NAME), - ) - Assert.assertEquals( - exampleItemBookmark.description, - cv.getAsString(COLUMN_DESCRIPTION), - ) - Assert.assertEquals( - exampleItemBookmark.imageUrl, - cv.getAsString(COLUMN_IMAGE), - ) - Assert.assertEquals( - exampleItemBookmark.instanceOfs[0], - cv.getAsString(COLUMN_INSTANCE_LIST), - ) - Assert.assertEquals( - exampleItemBookmark.commonsCategories[0].name, - cv.getAsString(COLUMN_CATEGORIES_NAME_LIST), - ) - Assert.assertEquals( - exampleItemBookmark.commonsCategories[0].description, - cv.getAsString(COLUMN_CATEGORIES_DESCRIPTION_LIST), - ) - Assert.assertEquals( - exampleItemBookmark.commonsCategories[0].thumbnail, - cv.getAsString(COLUMN_CATEGORIES_THUMBNAIL_LIST), - ) - Assert.assertEquals( - exampleItemBookmark.isSelected, - cv.getAsBoolean(COLUMN_IS_SELECTED), - ) - Assert.assertEquals( - exampleItemBookmark.id, - cv.getAsString(COLUMN_ID), - ) - } + assertTrue(bookmarkItemsRoomDao.updateBookmarkItem(exampleItemBookmark).blockingGet()) + assertTrue(bookmarkItemsRoomDao.findBookmarkItem(exampleItemBookmark.id).blockingGet()) } @Test fun updateExistingItemBookmark() { - whenever(client.delete(isA(), isNull(), isNull())).thenReturn(1) - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) + // First insert + bookmarkItemsRoomDao.updateBookmarkItem(exampleItemBookmark).blockingGet() + assertTrue(bookmarkItemsRoomDao.findBookmarkItem(exampleItemBookmark.id).blockingGet()) - Assert.assertFalse(testObject.updateBookmarkItem(exampleItemBookmark)) - verify(client).delete( - eq(BookmarkItemsContentProvider.uriForName(exampleItemBookmark.id)), - isNull(), - isNull(), - ) + // Second update should remove it (toggle behavior) + assertFalse(bookmarkItemsRoomDao.updateBookmarkItem(exampleItemBookmark).blockingGet()) + assertFalse(bookmarkItemsRoomDao.findBookmarkItem(exampleItemBookmark.id).blockingGet()) } @Test fun findExistingItemBookmark() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) - Assert.assertTrue(testObject.findBookmarkItem(exampleItemBookmark.id)) - } - - @Test(expected = RuntimeException::class) - fun findItemBookmarkTranslatesExceptions() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow( - RemoteException(""), - ) - testObject.findBookmarkItem(exampleItemBookmark.id) + bookmarkItemsRoomDao.updateBookmarkItem(exampleItemBookmark).blockingGet() + assertTrue(bookmarkItemsRoomDao.findBookmarkItem(exampleItemBookmark.id).blockingGet()) } @Test - fun findNotExistingItemBookmarkReturnsNull_emptyCursor() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(0)) - Assert.assertFalse(testObject.findBookmarkItem(exampleItemBookmark.id)) + fun findNotExistingItemBookmark() { + assertFalse(bookmarkItemsRoomDao.findBookmarkItem(exampleItemBookmark.id).blockingGet()) } - - @Test - fun findNotExistingItemBookmarkReturnsNull_nullCursor() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null) - Assert.assertFalse(testObject.findBookmarkItem(exampleItemBookmark.id)) - } - - @Test - fun cursorsAreClosedAfterFindItemBookmarkQuery() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) - - testObject.findBookmarkItem(exampleItemBookmark.id) - - verify(mockCursor).close() - } - - @Test - fun migrateTableVersionFrom_v1_to_v2() { - onUpdate(database, 1, 2) - // Table didn't exist before v5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v2_to_v3() { - onUpdate(database, 2, 3) - // Table didn't exist before v5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v3_to_v4() { - onUpdate(database, 3, 4) - // Table didn't exist before v5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v4_to_v5() { - onUpdate(database, 4, 5) - // Table didn't change in version 5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v5_to_v6() { - onUpdate(database, 5, 6) - // Table didn't change in version 6 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v6_to_v7() { - onUpdate(database, 6, 7) - // Table didn't change in version 7 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v7_to_v8() { - onUpdate(database, 7, 8) - // Table didn't change in version 8 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v8_to_v9() { - onUpdate(database, 8, 9) - // Table didn't change in version 9 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v9_to_v10() { - onUpdate(database, 9, 10) - // Table didn't change in version 10 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v10_to_v11() { - onUpdate(database, 10, 11) - // Table didn't change in version 11 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v11_to_v12() { - onUpdate(database, 11, 12) - // Table didn't change in version 12 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v12_to_v13() { - onUpdate(database, 12, 13) - // Table didn't change in version 13 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v13_to_v14() { - onUpdate(database, 13, 14) - // Table didn't change in version 14 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v14_to_v15() { - onUpdate(database, 14, 15) - // Table didn't change in version 15 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v15_to_v16() { - onUpdate(database, 15, 16) - // Table didn't change in version 16 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v16_to_v17() { - onUpdate(database, 16, 17) - // Table didn't change in version 17 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v18_to_v19() { - onUpdate(database, 18, 19) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - - @Test - fun migrateTableVersionFrom_v19_to_v19() { - onUpdate(database, 19, 19) - verifyNoInteractions(database) - } - - private fun createCursor(rowCount: Int) = - MatrixCursor(columns, rowCount).apply { - for (i in 0 until rowCount) { - addRow( - listOf( - "itemName", - "itemDescription", - "itemImageUrl", - "instance", - "category name", - "category description", - "category thumbnail", - false, - "itemID", - ), - ) - } - } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt index 71e7bdd682c..32d922e70cf 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt @@ -1,23 +1,7 @@ package fr.free.nrw.commons.bookmarks.locations -import android.content.ContentProviderClient -import android.content.ContentValues -import android.database.Cursor -import android.database.MatrixCursor -import android.net.Uri -import android.os.RemoteException import androidx.room.Room import androidx.test.core.app.ApplicationProvider -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.isA -import com.nhaarman.mockitokotlin2.isNull -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.db.AppDatabase import fr.free.nrw.commons.location.LatLng @@ -32,8 +16,6 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.verifyNoInteractions import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -47,14 +29,12 @@ class BookMarkLocationDaoTest { private lateinit var examplePlaceBookmark: Place private lateinit var exampleLabel: Label - private lateinit var exampleUri: Uri private lateinit var exampleLocation: LatLng private lateinit var builder: Sitelinks.Builder @Before fun setUp() { exampleLabel = Label.FOREST - exampleUri = Uri.parse("wikimedia/uri") exampleLocation = LatLng(40.0, 51.4, 1f) database = Room.inMemoryDatabaseBuilder( @@ -91,7 +71,7 @@ class BookMarkLocationDaoTest { @Test fun testForAddAndGetAllBookmarkLocations() = runBlocking { - bookmarkLocationsDao.addBookmarkLocation(examplePlaceBookmark.toBookmarksLocations()) + bookmarkLocationsDao.addBookmarkLocation(bookmarkLocationsDao.toEntity(examplePlaceBookmark)) val bookmarks = bookmarkLocationsDao.getAllBookmarksLocations() @@ -103,7 +83,7 @@ class BookMarkLocationDaoTest { @Test fun testFindBookmarkByNameForTrue() = runBlocking { - bookmarkLocationsDao.addBookmarkLocation(examplePlaceBookmark.toBookmarksLocations()) + bookmarkLocationsDao.addBookmarkLocation(bookmarkLocationsDao.toEntity(examplePlaceBookmark)) val exists = bookmarkLocationsDao.findBookmarkLocation(examplePlaceBookmark.name) assertTrue(exists) @@ -111,7 +91,7 @@ class BookMarkLocationDaoTest { @Test fun testFindBookmarkByNameForFalse() = runBlocking { - bookmarkLocationsDao.addBookmarkLocation(examplePlaceBookmark.toBookmarksLocations()) + bookmarkLocationsDao.addBookmarkLocation(bookmarkLocationsDao.toEntity(examplePlaceBookmark)) val exists = bookmarkLocationsDao.findBookmarkLocation("xyz") assertFalse(exists) @@ -119,7 +99,7 @@ class BookMarkLocationDaoTest { @Test fun testDeleteBookmark() = runBlocking { - val bookmarkLocation = examplePlaceBookmark.toBookmarksLocations() + val bookmarkLocation = bookmarkLocationsDao.toEntity(examplePlaceBookmark) bookmarkLocationsDao.addBookmarkLocation(bookmarkLocation) bookmarkLocationsDao.deleteBookmarkLocation(bookmarkLocation) @@ -137,7 +117,7 @@ class BookMarkLocationDaoTest { @Test fun testUpdateBookmarkForFalse() = runBlocking { - val newBookmark = examplePlaceBookmark.toBookmarksLocations() + val newBookmark = bookmarkLocationsDao.toEntity(examplePlaceBookmark) bookmarkLocationsDao.addBookmarkLocation(newBookmark) val exists = bookmarkLocationsDao.updateBookmarkLocation(examplePlaceBookmark) @@ -146,7 +126,7 @@ class BookMarkLocationDaoTest { @Test fun testGetAllBookmarksLocationsPlace() = runBlocking { - val bookmarkLocation = examplePlaceBookmark.toBookmarksLocations() + val bookmarkLocation = bookmarkLocationsDao.toEntity(examplePlaceBookmark) bookmarkLocationsDao.addBookmarkLocation(bookmarkLocation) val bookmarks = bookmarkLocationsDao.getAllBookmarksLocationsPlace() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt index 74d95908e2d..06712d1c8c6 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt @@ -1,233 +1,83 @@ package fr.free.nrw.commons.bookmarks.pictures -import android.content.ContentProviderClient -import android.content.ContentValues -import android.database.Cursor -import android.database.MatrixCursor import android.net.Uri -import android.os.RemoteException -import androidx.sqlite.db.SupportSQLiteDatabase -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.isA -import com.nhaarman.mockitokotlin2.isNull -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +import androidx.room.Room.inMemoryDatabaseBuilder +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.bookmarks.models.Bookmark -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.BASE_URI -import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_CREATOR -import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME -import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.CREATE_TABLE_STATEMENT -import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.DROP_TABLE_STATEMENT -import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.onCreate -import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.onDelete -import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.onUpdate +import fr.free.nrw.commons.db.AppDatabase +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.verifyNoInteractions -import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode -@RunWith(RobolectricTestRunner::class) +@RunWith(AndroidJUnit4::class) @Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) class BookmarkPictureDaoTest { - private val columns = arrayOf(COLUMN_MEDIA_NAME, COLUMN_CREATOR) - private val client: ContentProviderClient = mock() - private val database: SupportSQLiteDatabase = mock() - private val captor = argumentCaptor() - - private lateinit var testObject: BookmarkPicturesDao + private lateinit var bookmarkPicturesRoomDao: BookmarkPicturesRoomDao + private lateinit var database: AppDatabase private lateinit var exampleBookmark: Bookmark @Before - fun setUp() { - exampleBookmark = Bookmark("mediaName", "creatorName", Uri.EMPTY) - testObject = BookmarkPicturesDao { client } - } - - @Test - fun createTable() { - onCreate(database) - verify(database).execSQL(CREATE_TABLE_STATEMENT) + fun createDb() { + database = + inMemoryDatabaseBuilder( + context = ApplicationProvider.getApplicationContext(), + klass = AppDatabase::class.java, + ).allowMainThreadQueries().build() + bookmarkPicturesRoomDao = database.bookmarkPicturesRoomDao() + exampleBookmark = Bookmark("mediaName", "creatorName") } - @Test - fun deleteTable() { - onDelete(database) - inOrder(database) { - verify(database).execSQL(DROP_TABLE_STATEMENT) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - } - - @Test - fun createFromCursor() { - createCursor(1).let { cursor -> - cursor.moveToFirst() - testObject.fromCursor(cursor).let { - assertEquals("mediaName", it.mediaName) - assertEquals("creatorName", it.mediaCreator) - } - } + @After + fun closeDb() { + database.close() } @Test fun getAllBookmarks() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(14)) - - var result = testObject.getAllBookmarks() - - assertEquals(14, (result.size)) - } - - @Test(expected = RuntimeException::class) - fun getAllBookmarksTranslatesExceptions() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException("")) - testObject.getAllBookmarks() - } - - @Test - fun getAllBookmarksReturnsEmptyList_emptyCursor() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(0)) - assertTrue(testObject.getAllBookmarks().isEmpty()) - } - - @Test - fun getAllBookmarksReturnsEmptyList_nullCursor() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null) - assertTrue(testObject.getAllBookmarks().isEmpty()) - } - - @Test - fun cursorsAreClosedAfterGetAllBookmarksQuery() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) - - testObject.getAllBookmarks() + for (i in 1..5) { + bookmarkPicturesRoomDao.updateBookmark(Bookmark("media $i", "creator")).blockingGet() + } - verify(mockCursor).close() + val result = bookmarkPicturesRoomDao.getAllBookmarks().blockingGet() + assertEquals(5, result.size) } @Test fun updateNewBookmark() { - whenever(client.insert(any(), any())).thenReturn(exampleBookmark.contentUri) - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null) - - assertTrue(testObject.updateBookmark(exampleBookmark)) - verify(client).insert(eq(BASE_URI), captor.capture()) - captor.firstValue.let { cv -> - assertEquals(2, cv.size()) - assertEquals(exampleBookmark.mediaName, cv.getAsString(COLUMN_MEDIA_NAME)) - assertEquals(exampleBookmark.mediaCreator, cv.getAsString(COLUMN_CREATOR)) - } + assertTrue(bookmarkPicturesRoomDao.updateBookmark(exampleBookmark).blockingGet()) + assertTrue(bookmarkPicturesRoomDao.findBookmark(exampleBookmark).blockingGet()) } @Test fun updateExistingBookmark() { - whenever(client.delete(isA(), isNull(), isNull())).thenReturn(1) - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) + // First insert + bookmarkPicturesRoomDao.updateBookmark(exampleBookmark).blockingGet() + assertTrue(bookmarkPicturesRoomDao.findBookmark(exampleBookmark).blockingGet()) - assertFalse(testObject.updateBookmark(exampleBookmark)) - verify(client).delete(eq(exampleBookmark.contentUri!!), isNull(), isNull()) + // Second update should remove it (matches legacy behavior) + assertFalse(bookmarkPicturesRoomDao.updateBookmark(exampleBookmark).blockingGet()) + assertFalse(bookmarkPicturesRoomDao.findBookmark(exampleBookmark).blockingGet()) } @Test fun findExistingBookmark() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) - assertTrue(testObject.findBookmark(exampleBookmark)) - } - - @Test(expected = RuntimeException::class) - fun findBookmarkTranslatesExceptions() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException("")) - testObject.findBookmark(exampleBookmark) - } - - @Test - fun findNotExistingBookmarkReturnsNull_emptyCursor() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(0)) - assertFalse(testObject.findBookmark(exampleBookmark)) - } - - @Test - fun findNotExistingBookmarkReturnsNull_nullCursor() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null) - assertFalse(testObject.findBookmark(exampleBookmark)) - } - - @Test - fun cursorsAreClosedAfterFindBookmarkQuery() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) - - testObject.findBookmark(exampleBookmark) - - verify(mockCursor).close() - } - - @Test - fun migrateTableVersionFrom_v1_to_v2() { - onUpdate(database, 1, 2) - // Table didn't exist before v5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v2_to_v3() { - onUpdate(database, 2, 3) - // Table didn't exist before v5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v3_to_v4() { - onUpdate(database, 3, 4) - // Table didn't exist before v5 - verifyNoInteractions(database) + bookmarkPicturesRoomDao.updateBookmark(exampleBookmark).blockingGet() + assertTrue(bookmarkPicturesRoomDao.findBookmark(exampleBookmark).blockingGet()) } @Test - fun migrateTableVersionFrom_v4_to_v5() { - onUpdate(database, 4, 5) - // Table didn't change in version 5 - verifyNoInteractions(database) + fun findNotExistingBookmark() { + assertFalse(bookmarkPicturesRoomDao.findBookmark(exampleBookmark).blockingGet()) } - - @Test - fun migrateTableVersionFrom_v5_to_v6() { - onUpdate(database, 5, 6) - // Table didn't change in version 6 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v6_to_v7() { - onUpdate(database, 6, 7) - // Table didn't change in version 7 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v7_to_v8() { - onUpdate(database, 7, 8) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - - private fun createCursor(rowCount: Int) = - MatrixCursor(columns, rowCount).apply { - for (i in 0 until rowCount) { - addRow(listOf("mediaName", "creatorName")) - } - } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesControllerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesControllerTest.kt index 154a5a9b3da..6f32fc05bc7 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesControllerTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesControllerTest.kt @@ -23,7 +23,7 @@ class BookmarkPicturesControllerTest { var mediaClient: MediaClient? = null @Mock - var bookmarkDao: BookmarkPicturesDao? = null + var bookmarkDao: BookmarkPicturesRoomDao? = null @InjectMocks var bookmarkPicturesController: BookmarkPicturesController? = null @@ -36,7 +36,7 @@ class BookmarkPicturesControllerTest { MockitoAnnotations.initMocks(this) val mockMedia = mockMedia whenever(bookmarkDao!!.getAllBookmarks()) - .thenReturn(mockBookmarkList) + .thenReturn(Single.just(mockBookmarkList)) whenever( mediaClient!!.getMedia( ArgumentMatchers.anyString(), @@ -51,8 +51,8 @@ class BookmarkPicturesControllerTest { private val mockBookmarkList: List private get() { val list = ArrayList() - list.add(Bookmark("File:Test1.jpg", "Maskaravivek", Uri.EMPTY)) - list.add(Bookmark("File:Test2.jpg", "Maskaravivek", Uri.EMPTY)) + list.add(Bookmark("File:Test1.jpg", "Maskaravivek")) + list.add(Bookmark("File:Test2.jpg", "Maskaravivek")) return list } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragmentUnitTests.kt index b1dae0fab79..dd67333fae5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragmentUnitTests.kt @@ -23,6 +23,7 @@ import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.category.GridViewAdapter import fr.free.nrw.commons.createTestClient import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding +import fr.free.nrw.commons.db.InMemoryDatabaseTest import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.profile.ProfileActivity import media @@ -43,7 +44,7 @@ import java.lang.reflect.Method @RunWith(RobolectricTestRunner::class) @Config(sdk = [21], application = TestCommonsApplication::class) @LooperMode(LooperMode.Mode.PAUSED) -class BookmarkPicturesFragmentUnitTests { +class BookmarkPicturesFragmentUnitTests : InMemoryDatabaseTest() { private lateinit var fragment: BookmarkPicturesFragment private lateinit var binding: FragmentBookmarksPicturesBinding @@ -96,7 +97,7 @@ class BookmarkPicturesFragmentUnitTests { binding = FragmentBookmarksPicturesBinding.inflate(LayoutInflater.from(activity)) - val bookmarkDao = BookmarkPicturesDao { client } + val bookmarkDao = roomDatabase.bookmarkPicturesRoomDao() controller = BookmarkPicturesController(mediaClient, bookmarkDao) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt index 21fdba2f575..f4041b8f9b0 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt @@ -21,7 +21,7 @@ import org.mockito.MockitoAnnotations // class for testing CategoriesModel class class CategoriesModelTest { @Mock - internal lateinit var categoryDao: CategoryDao + internal lateinit var categoryDao: CategoryRoomDao @Mock internal lateinit var categoryClient: CategoryClient @@ -35,6 +35,8 @@ class CategoriesModelTest { @Throws(Exception::class) fun setUp() { MockitoAnnotations.openMocks(this) + // Default mock returns for Room DAO reactive methods + whenever(categoryDao.save(any())).thenReturn(io.reactivex.Completable.complete()) categoriesModel = CategoriesModel(categoryClient, categoryDao, gpsCategoryModel) } @@ -139,14 +141,11 @@ class CategoriesModelTest { ), ) whenever(categoryDao.recentCategories(25)).thenReturn( - listOf( - CategoryItem( - "recentCategories", - "", - "", - false, - ), - ), + Single.just( + listOf( + CategoryItem("recentCategories", "", "", false) + ) + ) ) whenever( categoryClient.getCategoriesByName( diff --git a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt index 53d82dabf7a..fab26471b56 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt @@ -1,319 +1,94 @@ package fr.free.nrw.commons.category -import android.content.ContentProviderClient -import android.content.ContentValues -import android.database.Cursor -import android.database.MatrixCursor -import android.os.RemoteException -import androidx.sqlite.db.SupportSQLiteDatabase -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.isA -import com.nhaarman.mockitokotlin2.isNull -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +import androidx.room.Room.inMemoryDatabaseBuilder +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 import fr.free.nrw.commons.TestCommonsApplication -import fr.free.nrw.commons.category.CategoryTable.ALL_FIELDS -import fr.free.nrw.commons.category.CategoryTable.COLUMN_DESCRIPTION -import fr.free.nrw.commons.category.CategoryTable.COLUMN_ID -import fr.free.nrw.commons.category.CategoryTable.COLUMN_LAST_USED -import fr.free.nrw.commons.category.CategoryTable.COLUMN_NAME -import fr.free.nrw.commons.category.CategoryTable.COLUMN_THUMBNAIL -import fr.free.nrw.commons.category.CategoryTable.COLUMN_TIMES_USED -import fr.free.nrw.commons.category.CategoryTable.CREATE_TABLE_STATEMENT -import fr.free.nrw.commons.category.CategoryTable.DROP_TABLE_STATEMENT -import fr.free.nrw.commons.category.CategoryTable.onCreate -import fr.free.nrw.commons.category.CategoryTable.onDelete -import fr.free.nrw.commons.category.CategoryTable.onUpdate -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId +import fr.free.nrw.commons.db.AppDatabase +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.verifyNoInteractions -import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode import java.util.Date -@RunWith(RobolectricTestRunner::class) +@RunWith(AndroidJUnit4::class) @Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) class CategoryDaoTest { - private val columns = - arrayOf( - COLUMN_ID, - COLUMN_NAME, - COLUMN_DESCRIPTION, - COLUMN_THUMBNAIL, - COLUMN_LAST_USED, - COLUMN_TIMES_USED, - ) - private val client: ContentProviderClient = mock() - private val database: SupportSQLiteDatabase = mock() - private val captor = argumentCaptor() - private val queryCaptor = argumentCaptor>() - - private lateinit var testObject: CategoryDao + private lateinit var categoryRoomDao: CategoryRoomDao + private lateinit var database: AppDatabase @Before - fun setUp() { - testObject = CategoryDao { client } - } - - @Test - fun createTable() { - onCreate(database) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - - @Test - fun deleteTable() { - onDelete(database) - inOrder(database) { - verify(database).execSQL(DROP_TABLE_STATEMENT) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } + fun createDb() { + database = + inMemoryDatabaseBuilder( + context = ApplicationProvider.getApplicationContext(), + klass = AppDatabase::class.java, + ).allowMainThreadQueries().build() + categoryRoomDao = database.categoryRoomDao() } - @Test - fun migrateTableVersionFrom_v1_to_v2() { - onUpdate(database, 1, 2) - // Table didn't exist before v5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v2_to_v3() { - onUpdate(database, 2, 3) - // Table didn't exist before v5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v3_to_v4() { - onUpdate(database, 3, 4) - // Table didn't exist before v5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v4_to_v5() { - onUpdate(database, 4, 5) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - - @Test - fun migrateTableVersionFrom_v5_to_v6() { - onUpdate(database, 5, 6) - // Table didn't change in version 6 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v6_to_v7() { - onUpdate(database, 6, 7) - // Table didn't change in version 7 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v7_to_v8() { - onUpdate(database, 7, 8) - // Table didn't change in version 8 - verifyNoInteractions(database) - } - - @Test - fun createFromCursor() { - createCursor(1).let { cursor -> - cursor.moveToFirst() - testObject.fromCursor(cursor).let { - assertEquals(CategoryContentProvider.uriForId(1), it.contentUri) - assertEquals("showImageWithItem", it.name) - assertEquals(123L, it.lastUsed?.time) - assertEquals(2, it.timesUsed) - } - } + @After + fun closeDb() { + database.close() } @Test fun saveExistingCategory() { - createCursor(1).let { - val category = testObject.fromCursor(it.apply { moveToFirst() }) - - testObject.save(category) - - verify(client).update( - eq(category.contentUri)!!, - captor.capture(), - isNull(), - isNull() - ) - captor.firstValue.let { cv -> - assertEquals(5, cv.size()) - assertEquals(category.name, cv.getAsString(COLUMN_NAME)) - assertEquals(category.description, cv.getAsString(COLUMN_DESCRIPTION)) - assertEquals(category.thumbnail, cv.getAsString(COLUMN_THUMBNAIL)) - assertEquals(category.lastUsed?.time, cv.getAsLong(COLUMN_LAST_USED)) - assertEquals(category.timesUsed, cv.getAsInteger(COLUMN_TIMES_USED)) - } - } - } - - @Test - fun saveNewCategory() { - val contentUri = uriForId(111) - whenever(client.insert(isA(), isA())).thenReturn(contentUri) - val category = - Category( - null, - "showImageWithItem", - "description", - "image", - Date(234L), - 1, - ) - - testObject.save(category) - - verify(client).insert(eq(CategoryContentProvider.BASE_URI), captor.capture()) - captor.firstValue.let { cv -> - assertEquals(5, cv.size()) - assertEquals(category.name, cv.getAsString(COLUMN_NAME)) - assertEquals(category.description, cv.getAsString(COLUMN_DESCRIPTION)) - assertEquals(category.thumbnail, cv.getAsString(COLUMN_THUMBNAIL)) - assertEquals(category.lastUsed?.time, cv.getAsLong(COLUMN_LAST_USED)) - assertEquals(category.timesUsed, cv.getAsInteger(COLUMN_TIMES_USED)) - assertEquals(contentUri, category.contentUri) - } - } + // First insert + val category = Category("Test Category", "desc", "thumb", Date(1234), 1) + categoryRoomDao.save(category).blockingAwait() - @Test(expected = RuntimeException::class) - fun testSaveTranslatesRemoteExceptions() { - whenever(client.insert(isA(), isA())).thenThrow(RemoteException("")) - testObject.save(Category()) - } + val foundBefore = categoryRoomDao.findCategory("Test Category").blockingGet() + MatcherAssert.assertThat(foundBefore, CoreMatchers.equalTo(true)) - @Test - fun whenTheresNoDataFindReturnsNull_nullCursor() { - whenever(client.query(any(), any(), any(), any(), any())).thenReturn(null) - assertNull(testObject.find("showImageWithItem")) - } + // Update it + category.timesUsed = 5 + categoryRoomDao.save(category).blockingAwait() - @Test - fun whenTheresNoDataFindReturnsNull_emptyCursor() { - whenever(client.query(any(), any(), any(), any(), any())).thenReturn(createCursor(0)) - assertNull(testObject.find("showImageWithItem")) + // Verify update via entity + val entities = categoryRoomDao.findEntity("Test Category").blockingGet() + assertEquals(5, entities[0].timesUsed) } @Test - fun cursorsAreClosedAfterUse() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) - - testObject.find("showImageWithItem") + fun saveNewCategory() { + val category = Category("New Category", "desc", "thumb", Date(1234), 1) + categoryRoomDao.save(category).blockingAwait() - verify(mockCursor).close() + val items = categoryRoomDao.recentCategories(1).blockingGet() + assertEquals(1, items.size) + assertEquals("New Category", items[0].name) } @Test fun findCategory() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) - - val category = testObject.find("showImageWithItem") - assertNotNull(category) + val category = Category("Category to Find", "desc", "thumb", Date(1234), 1) + categoryRoomDao.save(category).blockingAwait() - assertEquals(CategoryContentProvider.uriForId(1), category?.contentUri) - assertEquals("showImageWithItem", category?.name) - assertEquals("description", category?.description) - assertEquals("image", category?.thumbnail) - assertEquals(123L, category?.lastUsed?.time) - assertEquals(2, category?.timesUsed) + val isFound = categoryRoomDao.findCategory("Category to Find").blockingGet() + MatcherAssert.assertThat(isFound, CoreMatchers.equalTo(true)) - verify(client).query( - eq(CategoryContentProvider.BASE_URI), - eq(ALL_FIELDS), - eq("$COLUMN_NAME=?"), - queryCaptor.capture(), - isNull(), - ) - assertEquals("showImageWithItem", queryCaptor.firstValue[0]) - } - - @Test(expected = RuntimeException::class) - fun findCategoryTranslatesExceptions() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenThrow(RemoteException("")) - testObject.find("showImageWithItem") - } - - @Test(expected = RuntimeException::class) - fun recentCategoriesTranslatesExceptions() { - whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenThrow(RemoteException("")) - testObject.recentCategories(1) - } - - @Test - fun recentCategoriesReturnsEmptyList_nullCursor() { - whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(null) - assertTrue(testObject.recentCategories(1).isEmpty()) - } - - @Test - fun recentCategoriesReturnsEmptyList_emptyCursor() { - whenever(client.query(any(), any(), any(), any(), any())).thenReturn(createCursor(0)) - assertTrue(testObject.recentCategories(1).isEmpty()) - } - - @Test - fun cursorsAreClosedAfterRecentCategoriesQuery() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) - - testObject.recentCategories(1) - - verify(mockCursor).close() - } - - @Test - fun recentCategoriesReturnsLessThanLimit() { - whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(createCursor(1)) - - val result = testObject.recentCategories(10) - - assertEquals(1, result.size) - assertEquals("showImageWithItem", result[0].name) - - verify(client).query( - eq(CategoryContentProvider.BASE_URI), - eq(ALL_FIELDS), - isNull(), - queryCaptor.capture(), - eq("$COLUMN_LAST_USED DESC"), - ) - assertEquals(0, queryCaptor.firstValue.size) + val isNotFound = categoryRoomDao.findCategory("Non existent").blockingGet() + MatcherAssert.assertThat(isNotFound, CoreMatchers.equalTo(false)) } @Test fun recentCategoriesHonorsLimit() { - whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(createCursor(10)) - - val result = testObject.recentCategories(5) + for (i in 1..10) { + val category = Category("Category $i", "desc", "thumb", Date(i * 1000L), 1) + categoryRoomDao.save(category).blockingAwait() + } - assertEquals(5, result.size) + val items = categoryRoomDao.recentCategories(5).blockingGet() + assertEquals(5, items.size) + // Check ordering (LIFO by lastUsed) + assertEquals("Category 10", items[0].name) } - - private fun createCursor(rowCount: Int) = - MatrixCursor(columns, rowCount).apply { - for (i in 0 until rowCount) { - addRow(listOf("1", "showImageWithItem", "description", "image", "123", "2")) - } - } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/db/BookmarkItemsTableTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/db/BookmarkItemsTableTest.kt index e67bcf701f8..01d553eae5a 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/db/BookmarkItemsTableTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/db/BookmarkItemsTableTest.kt @@ -28,7 +28,7 @@ class BookmarkItemsTableTest : InMemoryDatabaseTest() { isSelected = true, id = "Q12345" ) - dao.insert(item) + dao.insert(item).blockingAwait() val allItems = dao.getAll().blockingGet() assertEquals(1, allItems.size) @@ -49,7 +49,7 @@ class BookmarkItemsTableTest : InMemoryDatabaseTest() { VALUES ('Legacy Item', 'Q6789', 'Instance', 'Cat', 'Desc', 'Thumb', 1); """.trimIndent()) - val exists = dao.findBookmarkItem("Q6789") + val exists = dao.findBookmarkItem("Q6789").blockingGet() assertTrue(exists) val allItems = dao.getAll().blockingGet() assertEquals(1, allItems.size) @@ -72,18 +72,18 @@ class BookmarkItemsTableTest : InMemoryDatabaseTest() { isSelected = false, id = "QToDelete" ) - dao.insert(item) + dao.insert(item).blockingAwait() assertRowCount(BookmarkItemsTable.TABLE_NAME, 1) - dao.delete(item) + dao.delete(item).blockingAwait() assertRowCount(BookmarkItemsTable.TABLE_NAME, 0) } @Test fun testClearAllTables() { val dao = roomDatabase.bookmarkItemsRoomDao() - dao.insert(BookmarkItemsRoomEntity("Item 1", null, null, "", "", "", "", false, "Q1")) - dao.insert(BookmarkItemsRoomEntity("Item 2", null, null, "", "", "", "", false, "Q2")) + dao.insert(BookmarkItemsRoomEntity("Item 1", null, null, "", "", "", "", false, "Q1")).blockingAwait() + dao.insert(BookmarkItemsRoomEntity("Item 2", null, null, "", "", "", "", false, "Q2")).blockingAwait() assertRowCount(BookmarkItemsTable.TABLE_NAME, 2) clearAllTables() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/db/BookmarkPictureTableTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/db/BookmarkPictureTableTest.kt index 27535892b44..5ca13e08b70 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/db/BookmarkPictureTableTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/db/BookmarkPictureTableTest.kt @@ -21,7 +21,7 @@ class BookmarkPictureTableTest : InMemoryDatabaseTest() { mediaName = "Test Image", mediaCreator = "Test Creator" ) - dao.insert(bookmark) + dao.insert(bookmark).blockingAwait() val allBookmarks = dao.getAll().blockingGet() assertEquals(1, allBookmarks.size) @@ -38,7 +38,7 @@ class BookmarkPictureTableTest : InMemoryDatabaseTest() { // Insert with legacy SQL db.execSQL("INSERT INTO bookmarks (media_name, media_creator) VALUES ('Legacy Image', 'Legacy Creator');") - val exists = dao.findBookmarkByName("Legacy Image") + val exists: Boolean = dao.findBookmarkByName("Legacy Image").blockingGet() assertTrue(exists) val allBookmarks = dao.getAll().blockingGet() assertEquals(1, allBookmarks.size) @@ -50,18 +50,18 @@ class BookmarkPictureTableTest : InMemoryDatabaseTest() { fun testDeleteBookmark() { val dao = roomDatabase.bookmarkPicturesRoomDao() val bookmark = BookmarkPictureRoomEntity("Image to delete", "Creator") - dao.insert(bookmark) + dao.insert(bookmark).blockingAwait() assertRowCount(BookmarksTable.TABLE_NAME, 1) - dao.delete(bookmark) + dao.delete(bookmark).blockingAwait() assertRowCount(BookmarksTable.TABLE_NAME, 0) } @Test fun testClearAllTables() { val dao = roomDatabase.bookmarkPicturesRoomDao() - dao.insert(BookmarkPictureRoomEntity("Image 1", "Creator 1")) - dao.insert(BookmarkPictureRoomEntity("Image 2", "Creator 2")) + dao.insert(BookmarkPictureRoomEntity("Image 1", "Creator 1")).blockingAwait() + dao.insert(BookmarkPictureRoomEntity("Image 2", "Creator 2")).blockingAwait() assertRowCount(BookmarksTable.TABLE_NAME, 2) clearAllTables() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/db/CategoryTableTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/db/CategoryTableTest.kt index 14e9a14d2df..a541f17f06d 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/db/CategoryTableTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/db/CategoryTableTest.kt @@ -24,9 +24,9 @@ class CategoryTableTest : InMemoryDatabaseTest() { thumbnail = "http://test.com/thumb.jpg", timesUsed = 5 ) - categoryDao.insert(category) + categoryDao.insert(category).blockingGet() - val retrieved = categoryDao.findEntity("Test Category") + val retrieved = categoryDao.findEntity("Test Category").blockingGet().firstOrNull() assertNotNull(retrieved) assertEquals("Test Category", retrieved?.name) assertEquals("Test Description", retrieved?.description) @@ -43,7 +43,7 @@ class CategoryTableTest : InMemoryDatabaseTest() { // Insert with legacy SQL db.execSQL("INSERT INTO categories (name, description, times_used) VALUES ('Nature', 'Nature category', 10);") - val entity = categoryDao.findEntity("Nature") + val entity = categoryDao.findEntity("Nature").blockingGet().firstOrNull() assertNotNull(entity) assertEquals("Nature", entity?.name) assertEquals("Nature category", entity?.description) @@ -55,13 +55,13 @@ class CategoryTableTest : InMemoryDatabaseTest() { fun testUpdateCategory() { val categoryDao = roomDatabase.categoryRoomDao() val category = CategoryRoomEntity(name = "Original Name", timesUsed = 1) - categoryDao.insert(category) + categoryDao.insert(category).blockingGet() - val savedCategory = categoryDao.findEntity("Original Name")!! - val updatedCategory = savedCategory.copy(timesUsed = 2) - categoryDao.insert(updatedCategory) + val savedCategory = categoryDao.findEntity("Original Name").blockingGet().firstOrNull() + val updatedCategory = savedCategory?.copy(timesUsed = 2) + categoryDao.insert(updatedCategory!!).blockingGet() - val retrieved = categoryDao.findEntity("Original Name") + val retrieved = categoryDao.findEntity("Original Name").blockingGet().firstOrNull() assertEquals(2, retrieved?.timesUsed) assertRowCount(CategoryTable.TABLE_NAME, 1) } @@ -69,8 +69,8 @@ class CategoryTableTest : InMemoryDatabaseTest() { @Test fun testClearAllTables() { val categoryDao = roomDatabase.categoryRoomDao() - categoryDao.insert(CategoryRoomEntity(name = "Cat 1")) - categoryDao.insert(CategoryRoomEntity(name = "Cat 2")) + categoryDao.insert(CategoryRoomEntity(name = "Cat 1")).blockingGet() + categoryDao.insert(CategoryRoomEntity(name = "Cat 2")).blockingGet() assertRowCount(CategoryTable.TABLE_NAME, 2) clearAllTables() @@ -80,9 +80,9 @@ class CategoryTableTest : InMemoryDatabaseTest() { @Test fun testFindCategory() { val categoryDao = roomDatabase.categoryRoomDao() - categoryDao.insert(CategoryRoomEntity(name = "Exist")) + categoryDao.insert(CategoryRoomEntity(name = "Exist")).blockingGet() - assertTrue(categoryDao.findCategory("Exist")) - assertTrue(!categoryDao.findCategory("NotExist")) + assertTrue(categoryDao.findCategory("Exist").blockingGet()) + assertTrue(!categoryDao.findCategory("NotExist").blockingGet()) } } \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/db/RecentLanguagesTableTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/db/RecentLanguagesTableTest.kt index 6757b35630c..e225d99a632 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/db/RecentLanguagesTableTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/db/RecentLanguagesTableTest.kt @@ -21,7 +21,7 @@ class RecentLanguagesTableTest : InMemoryDatabaseTest() { languageName = "English", languageCode = "en" ) - dao.insert(language) + dao.insert(language).blockingAwait() val allLanguages = dao.getAll().blockingGet() assertEquals(1, allLanguages.size) @@ -38,7 +38,7 @@ class RecentLanguagesTableTest : InMemoryDatabaseTest() { // Insert with legacy SQL db.execSQL("INSERT INTO recent_languages (language_name, language_code) VALUES ('French', 'fr');") - val exists = dao.findRecentLanguage("fr") + val exists = dao.findRecentLanguage("fr").blockingGet() assertTrue(exists) val allLanguages = dao.getAll().blockingGet() assertEquals(1, allLanguages.size) @@ -49,18 +49,18 @@ class RecentLanguagesTableTest : InMemoryDatabaseTest() { @Test fun testDeleteRecentLanguage() { val dao = roomDatabase.recentLanguagesRoomDao() - dao.insert(RecentLanguageRoomEntity("Spanish", "es")) + dao.insert(RecentLanguageRoomEntity("Spanish", "es")).blockingAwait() assertRowCount(RecentLanguagesTable.TABLE_NAME, 1) - dao.deleteRecentLanguage("es") + dao.deleteRecentLanguage("es").blockingAwait() assertRowCount(RecentLanguagesTable.TABLE_NAME, 0) } @Test fun testClearAllTables() { val dao = roomDatabase.recentLanguagesRoomDao() - dao.insert(RecentLanguageRoomEntity("Language 1", "l1")) - dao.insert(RecentLanguageRoomEntity("Language 2", "l2")) + dao.insert(RecentLanguageRoomEntity("Language 1", "l1")).blockingAwait() + dao.insert(RecentLanguageRoomEntity("Language 2", "l2")).blockingAwait() assertRowCount(RecentLanguagesTable.TABLE_NAME, 2) clearAllTables() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/db/RecentSearchesTableTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/db/RecentSearchesTableTest.kt index b70cca65721..70669936280 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/db/RecentSearchesTableTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/db/RecentSearchesTableTest.kt @@ -23,9 +23,9 @@ class RecentSearchesTableTest : InMemoryDatabaseTest() { query = "Test Search", lastSearched = date ) - dao.insert(search) + dao.insert(search).blockingGet() - val retrieved = dao.findEntity("Test Search") + val retrieved = dao.findEntity("Test Search").blockingGet().firstOrNull() assertNotNull(retrieved) assertEquals("Test Search", retrieved?.query) // Date might lose some precision in DB, but should be close @@ -42,7 +42,7 @@ class RecentSearchesTableTest : InMemoryDatabaseTest() { val now = System.currentTimeMillis() db.execSQL("INSERT INTO recent_searches (name, last_used) VALUES ('Legacy Search', $now);") - val entity = dao.findEntity("Legacy Search") + val entity = dao.findEntity("Legacy Search").blockingGet().firstOrNull() assertNotNull(entity) assertEquals("Legacy Search", entity?.query) assertEquals(now, entity?.lastSearched?.time) @@ -52,8 +52,8 @@ class RecentSearchesTableTest : InMemoryDatabaseTest() { @Test fun testDeleteTable() { val dao = roomDatabase.recentSearchesRoomDao() - dao.insert(RecentSearchRoomEntity(query = "Search 1", lastSearched = Date())) - dao.insert(RecentSearchRoomEntity(query = "Search 2", lastSearched = Date())) + dao.insert(RecentSearchRoomEntity(query = "Search 1", lastSearched = Date())).blockingGet() + dao.insert(RecentSearchRoomEntity(query = "Search 2", lastSearched = Date())).blockingGet() assertRowCount(RecentSearchesTable.TABLE_NAME, 2) dao.deleteTable().blockingAwait() @@ -63,7 +63,7 @@ class RecentSearchesTableTest : InMemoryDatabaseTest() { @Test fun testClearAllTables() { val dao = roomDatabase.recentSearchesRoomDao() - dao.insert(RecentSearchRoomEntity(query = "Search 1", lastSearched = Date())) + dao.insert(RecentSearchRoomEntity(query = "Search 1", lastSearched = Date())).blockingGet() assertRowCount(RecentSearchesTable.TABLE_NAME, 1) clearAllTables() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt index f5e338e8572..7451e55f6c8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt @@ -1,326 +1,93 @@ package fr.free.nrw.commons.explore.recentsearches -import android.content.ContentProviderClient -import android.content.ContentValues -import android.database.Cursor -import android.database.MatrixCursor -import android.os.RemoteException -import androidx.sqlite.db.SupportSQLiteDatabase -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.isA -import com.nhaarman.mockitokotlin2.isNull -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +import androidx.room.Room.inMemoryDatabaseBuilder +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.db.AppDatabase import fr.free.nrw.commons.explore.models.RecentSearch -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.CREATE_TABLE_STATEMENT -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.DROP_TABLE_STATEMENT -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onCreate -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onDelete -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onUpdate +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.verifyNoInteractions -import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode import java.util.Date -@RunWith(RobolectricTestRunner::class) +@RunWith(AndroidJUnit4::class) @Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) class RecentSearchesDaoTest { - private val columns = arrayOf(COLUMN_ID, COLUMN_NAME, COLUMN_LAST_USED) - private val client: ContentProviderClient = mock() - private val database: SupportSQLiteDatabase = mock() - private val captor = argumentCaptor() - private val queryCaptor = argumentCaptor>() - - private lateinit var testObject: RecentSearchesDao + private lateinit var recentSearchesRoomDao: RecentSearchesRoomDao + private lateinit var database: AppDatabase @Before - fun setUp() { - testObject = RecentSearchesDao { client } - } - - /** - * Unit Test for creating a table for recent Searches - */ - @Test - fun createTable() { - onCreate(database) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - - /** - * Unit Test for deleting table for recent Searches - */ - @Test - fun deleteTable() { - onDelete(database) - inOrder(database) { - verify(database).execSQL(DROP_TABLE_STATEMENT) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - } - - /** - * Unit Test for migrating from database version 1 to 2 for recent Searches Table - */ - @Test - fun migrateTableVersionFrom_v1_to_v2() { - onUpdate(database, 1, 2) - // Table didnt exist before v7 - verifyNoInteractions(database) - } - - /** - * Unit Test for migrating from database version 2 to 3 for recent Searches Table - */ - @Test - fun migrateTableVersionFrom_v2_to_v3() { - onUpdate(database, 2, 3) - // Table didnt exist before v7 - verifyNoInteractions(database) - } - - /** - * Unit Test for migrating from database version 3 to 4 for recent Searches Table - */ - @Test - fun migrateTableVersionFrom_v3_to_v4() { - onUpdate(database, 3, 4) - // Table didnt exist before v7 - verifyNoInteractions(database) - } - - /** - * Unit Test for migrating from database version 4 to 5 for recent Searches Table - */ - @Test - fun migrateTableVersionFrom_v4_to_v5() { - onUpdate(database, 4, 5) - // Table didnt exist before v7 - verifyNoInteractions(database) - } - - /** - * Unit Test for migrating from database version 5 to 6 for recent Searches Table - */ - @Test - fun migrateTableVersionFrom_v5_to_v6() { - onUpdate(database, 5, 6) - // Table didnt exist before v7 - verifyNoInteractions(database) - } - - /** - * Unit Test for migrating from database version 6 to 7 for recent Searches Table - */ - @Test - fun migrateTableVersionFrom_v6_to_v7() { - onUpdate(database, 6, 7) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - - /** - * Unit Test for migrating from database version 7 to 8 for recent Searches Table - */ - @Test - fun migrateTableVersionFrom_v7_to_v8() { - onUpdate(database, 7, 8) - // Table didnt change in version 8 - verifyNoInteractions(database) + fun createDb() { + database = + inMemoryDatabaseBuilder( + context = ApplicationProvider.getApplicationContext(), + klass = AppDatabase::class.java, + ).allowMainThreadQueries().build() + recentSearchesRoomDao = database.recentSearchesRoomDao() } - /** - * Unit Test for migrating from creating a row without using ID in recent Searches Table - */ - @Test - fun createFromCursor() { - createCursor(1).let { cursor -> - cursor.moveToFirst() - testObject.fromCursor(cursor).let { - assertEquals(uriForId(1), it.contentUri) - assertEquals("butterfly", it.query) - assertEquals(123, it.lastSearched.time) - } - } + @After + fun closeDb() { + database.close() } - /** - * Unit Test for migrating from updating a row using contentUri in recent Searches Table - */ @Test fun saveExistingQuery() { - createCursor(1).let { - val recentSearch = testObject.fromCursor(it.apply { moveToFirst() }) + // First insert + val recentSearch = RecentSearch("butterfly", Date(123L)) + recentSearchesRoomDao.save(recentSearch).blockingAwait() - testObject.save(recentSearch) + // Update it + val updatedSearch = RecentSearch("butterfly", Date(456L)) + recentSearchesRoomDao.save(updatedSearch).blockingAwait() - verify(client).update(eq(recentSearch.contentUri!!), captor.capture(), isNull(), isNull()) - captor.firstValue.let { cv -> - assertEquals(2, cv.size()) - assertEquals(recentSearch.query, cv.getAsString(COLUMN_NAME)) - assertEquals(recentSearch.lastSearched.time, cv.getAsLong(COLUMN_LAST_USED)) - } - } + // Verify update + val found = recentSearchesRoomDao.find("butterfly").blockingGet() + assertNotNull(found) + assertEquals(456L, found?.lastSearched?.time) } - /** - * Unit Test for migrating from creating a row using ID in recent Searches Table - */ @Test fun saveNewQuery() { - val contentUri = RecentSearchesContentProvider.uriForId(111) - whenever(client.insert(isA(), isA())).thenReturn(contentUri) - val recentSearch = RecentSearch(null, "butterfly", Date(234L)) + val recentSearch = RecentSearch("butterfly", Date(123L)) + recentSearchesRoomDao.save(recentSearch).blockingAwait() - testObject.save(recentSearch) - - verify(client).insert(eq(BASE_URI), captor.capture()) - captor.firstValue.let { cv -> - assertEquals(2, cv.size()) - assertEquals(recentSearch.query, cv.getAsString(COLUMN_NAME)) - assertEquals(recentSearch.lastSearched.time, cv.getAsLong(COLUMN_LAST_USED)) - assertEquals(contentUri, recentSearch.contentUri) - } - } - - /** - * Unit Test for checking translation exceptions in searching a row from DB using recent search query - */ - @Test(expected = RuntimeException::class) - fun findRecentSearchTranslatesExceptions() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenThrow(RemoteException("")) - testObject.find("butterfly") - } - - /** - * Unit Test for checking data if it's not present in searching a row from DB using recent search query - */ - @Test - fun whenTheresNoDataFindReturnsNull_nullCursor() { - whenever(client.query(any(), any(), any(), any(), any())).thenReturn(null) - assertNull(testObject.find("butterfly")) - } - - /** - * Unit Test for checking data if it's not present in searching a row from DB using recent search query - */ - @Test - fun whenTheresNoDataFindReturnsNull_emptyCursor() { - whenever(client.query(any(), any(), any(), any(), any())).thenReturn(createCursor(0)) - assertNull(testObject.find("butterfly")) - } - - /** - * Unit Test for checking if cursor's are closed after use or not - */ - @Test - fun cursorsAreClosedAfterUse() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) - - testObject.find("butterfly") - - verify(mockCursor).close() + val found = recentSearchesRoomDao.find("butterfly").blockingGet() + assertNotNull(found) + assertEquals("butterfly", found?.query) } - /** - * Unit Test for checking search results after searching a row from DB using recent search query - */ @Test fun findRecentSearchQuery() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) - - val recentSearch = testObject.find("butterfly") - assertNotNull(recentSearch) + val recentSearch = RecentSearch("butterfly", Date(123L)) + recentSearchesRoomDao.save(recentSearch).blockingAwait() - assertEquals(uriForId(1), recentSearch?.contentUri) - assertEquals("butterfly", recentSearch?.query) - assertEquals(123L, recentSearch?.lastSearched?.time) + val found = recentSearchesRoomDao.find("butterfly").blockingGet() + assertNotNull(found) + assertEquals("butterfly", found?.query) - verify(client).query( - eq(BASE_URI), - eq(ALL_FIELDS), - eq("$COLUMN_NAME=?"), - queryCaptor.capture(), - isNull(), - ) - assertEquals("butterfly", queryCaptor.firstValue[0]) + val notFound = recentSearchesRoomDao.find("non existent").blockingGet() + assertNull(notFound) } - /** - * Unit Test for checking if cursor's are closed after recent search query or not - */ - @Test - fun cursorsAreClosedAfterRecentSearchQuery() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) - - testObject.recentSearches(1) - - verify(mockCursor).close() - } - - /** - * Unit Test for checking when recent searches returns less than the limit - */ - @Test - fun recentSearchesReturnsLessThanLimit() { - whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(createCursor(1)) - - val result = testObject.recentSearches(10) - - assertEquals(1, result.size) - assertEquals("butterfly", result[0]) - - verify(client).query( - eq(BASE_URI), - eq(ALL_FIELDS), - isNull(), - queryCaptor.capture(), - eq("$COLUMN_LAST_USED DESC"), - ) - assertEquals(0, queryCaptor.firstValue.size) - } - - /** - * Unit Test for checking size or list recieved from recent searches - */ @Test fun recentSearchesHonorsLimit() { - whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(createCursor(10)) - - val result = testObject.recentSearches(5) + for (i in 1..10) { + recentSearchesRoomDao.save(RecentSearch("query $i", Date(i * 1000L))).blockingAwait() + } - assertEquals(5, result.size) + val results = recentSearchesRoomDao.recentSearches(5).blockingGet() + assertEquals(5, results.size) + assertEquals("query 10", results[0]) } - - /** - * Unit Test for creating entries in recent searches database. - * @param rowCount No of rows - */ - private fun createCursor(rowCount: Int) = - MatrixCursor(columns, rowCount).apply { - for (i in 0 until rowCount) { - addRow(listOf("1", "butterfly", "123")) - } - } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt index cd1186fc810..393351f4c80 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt @@ -12,6 +12,9 @@ import fr.free.nrw.commons.OkHttpConnectionFactory import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.contributions.MainActivity import fr.free.nrw.commons.createTestClient +import fr.free.nrw.commons.db.InMemoryDatabaseTest +import fr.free.nrw.commons.explore.SearchActivity +import io.reactivex.Single import org.junit.Assert import org.junit.Before import org.junit.Test @@ -35,7 +38,7 @@ class RecentSearchesFragmentUnitTest { private lateinit var layoutInflater: LayoutInflater @Mock - private lateinit var recentSearchesDao: RecentSearchesDao + private lateinit var recentSearchesDao: RecentSearchesRoomDao @Mock private lateinit var adapter: ArrayAdapter<*> @@ -53,7 +56,7 @@ class RecentSearchesFragmentUnitTest { OkHttpConnectionFactory.CLIENT = createTestClient() - val activity = Robolectric.buildActivity(MainActivity::class.java).create().get() + val activity = Robolectric.buildActivity(SearchActivity::class.java).create().get() fragment = RecentSearchesFragment() fragmentManager = activity.supportFragmentManager val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction() @@ -65,6 +68,11 @@ class RecentSearchesFragmentUnitTest { Whitebox.setInternalState(fragment, "recentSearchesDao", recentSearchesDao) Whitebox.setInternalState(fragment, "adapter", adapter) Whitebox.setInternalState(fragment, "recentSearches", listOf("string")) + + // Default mock returns for Room DAO reactive methods + whenever(recentSearchesDao.recentSearches(10)).thenReturn(Single.just(emptyList())) + whenever(recentSearchesDao.deleteTable()).thenReturn(io.reactivex.Completable.complete()) + whenever(recentSearchesDao.deleteAll()).thenReturn(io.reactivex.Completable.complete()) } @Test @@ -82,7 +90,7 @@ class RecentSearchesFragmentUnitTest { @Test @Throws(Exception::class) fun testOnResume() { - whenever(recentSearchesDao.recentSearches(10)).thenReturn(mutableListOf("search1")) + whenever(recentSearchesDao.recentSearches(10)).thenReturn(Single.just(mutableListOf("search1"))) fragment.onResume() } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/search/SearchActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/search/SearchActivityUnitTests.kt index 00b9c0fd91a..c52ba99bc26 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/search/SearchActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/search/SearchActivityUnitTests.kt @@ -3,6 +3,7 @@ package fr.free.nrw.commons.explore.search import android.content.Context import androidx.fragment.app.FragmentManager import androidx.test.core.app.ApplicationProvider +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.verify import fr.free.nrw.commons.Media import fr.free.nrw.commons.TestCommonsApplication @@ -11,10 +12,10 @@ import fr.free.nrw.commons.explore.SearchActivity import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment import fr.free.nrw.commons.explore.media.SearchMediaFragment -import fr.free.nrw.commons.explore.models.RecentSearch -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesRoomDao import fr.free.nrw.commons.media.MediaDetailPagerFragment +import io.reactivex.Completable import io.reactivex.disposables.CompositeDisposable import org.junit.Assert import org.junit.Before @@ -50,7 +51,7 @@ class SearchActivityUnitTests { private lateinit var viewPagerAdapter: ViewPagerAdapter @Mock - private lateinit var recentSearchesDao: RecentSearchesDao + private lateinit var recentSearchesDao: RecentSearchesRoomDao @Mock private lateinit var searchMediaFragment: SearchMediaFragment @@ -125,11 +126,12 @@ class SearchActivityUnitTests { fun testSaveRecentSearchCaseNull() { val query = "test" Whitebox.setInternalState(activity, "recentSearchesDao", recentSearchesDao) + `when`(recentSearchesDao.save(any())).thenReturn(Completable.complete()) val method: Method = SearchActivity::class.java.getDeclaredMethod("saveRecentSearch", String::class.java) method.isAccessible = true method.invoke(activity, query) - verify(recentSearchesDao).find(query) + verify(recentSearchesDao).save(any()) } @Test @@ -137,12 +139,12 @@ class SearchActivityUnitTests { fun testSaveRecentSearchCaseNonNull() { val query = "test" Whitebox.setInternalState(activity, "recentSearchesDao", recentSearchesDao) - `when`(recentSearchesDao.find(query)).thenReturn(mock(RecentSearch::class.java)) + `when`(recentSearchesDao.save(any())).thenReturn(Completable.complete()) val method: Method = SearchActivity::class.java.getDeclaredMethod("saveRecentSearch", String::class.java) method.isAccessible = true method.invoke(activity, query) - verify(recentSearchesDao).find(query) + verify(recentSearchesDao).save(any()) } @Test @@ -204,6 +206,8 @@ class SearchActivityUnitTests { `when`(searchMediaFragment.isRemoving).thenReturn(false) `when`(searchCategoryFragment.isRemoving).thenReturn(false) + `when`(recentSearchesDao.save(any())).thenReturn(Completable.complete()) + val method: Method = SearchActivity::class.java.getDeclaredMethod( "handleSearch", @@ -211,7 +215,6 @@ class SearchActivityUnitTests { ) method.isAccessible = true method.invoke(activity, query) - verify(recentSearchesDao).find(query) verify(searchDepictionsFragment).onQueryUpdated(query) verify(searchMediaFragment).onQueryUpdated(query) verify(searchCategoryFragment).onQueryUpdated(query) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt index 4c9c9b96bc0..2c7d2c84e9b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt @@ -1,323 +1,79 @@ package fr.free.nrw.commons.recentlanguages -import android.content.ContentProviderClient -import android.content.ContentValues -import android.database.Cursor -import android.database.MatrixCursor -import android.os.RemoteException -import androidx.sqlite.db.SupportSQLiteDatabase -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.inOrder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.whenever +import androidx.room.Room.inMemoryDatabaseBuilder +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 import fr.free.nrw.commons.TestCommonsApplication -import fr.free.nrw.commons.recentlanguages.RecentLanguagesTable.COLUMN_CODE -import fr.free.nrw.commons.recentlanguages.RecentLanguagesTable.COLUMN_NAME -import fr.free.nrw.commons.recentlanguages.RecentLanguagesTable.CREATE_TABLE_STATEMENT -import fr.free.nrw.commons.recentlanguages.RecentLanguagesTable.DROP_TABLE_STATEMENT -import fr.free.nrw.commons.recentlanguages.RecentLanguagesTable.onCreate -import fr.free.nrw.commons.recentlanguages.RecentLanguagesTable.onDelete -import fr.free.nrw.commons.recentlanguages.RecentLanguagesTable.onUpdate -import org.junit.Assert +import fr.free.nrw.commons.db.AppDatabase +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoInteractions -import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode -@RunWith(RobolectricTestRunner::class) +@RunWith(AndroidJUnit4::class) @Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) class RecentLanguagesDaoUnitTest { - private val columns = - arrayOf( - COLUMN_NAME, - COLUMN_CODE, - ) - - private val client: ContentProviderClient = mock() - private val database: SupportSQLiteDatabase = mock() - private val captor = argumentCaptor() - - private lateinit var testObject: RecentLanguagesDao + private lateinit var recentLanguagesRoomDao: RecentLanguagesRoomDao + private lateinit var database: AppDatabase private lateinit var exampleLanguage: Language - /** - * Set up Test Language and RecentLanguagesDao - */ @Before - fun setUp() { + fun createDb() { + database = + inMemoryDatabaseBuilder( + context = ApplicationProvider.getApplicationContext(), + klass = AppDatabase::class.java, + ).allowMainThreadQueries().build() + recentLanguagesRoomDao = database.recentLanguagesRoomDao() exampleLanguage = Language("English", "en") - testObject = RecentLanguagesDao { client } } - @Test - fun createTable() { - onCreate(database) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - - @Test - fun deleteTable() { - onDelete(database) - inOrder(database) { - verify(database).execSQL(DROP_TABLE_STATEMENT) - } - } - - @Test - fun createFromCursor() { - createCursor(1).let { cursor -> - cursor.moveToFirst() - testObject.fromCursor(cursor).let { - Assert.assertEquals("languageName", it.languageName) - Assert.assertEquals("languageCode", it.languageCode) - } - } + @After + fun closeDb() { + database.close() } @Test fun testGetRecentLanguages() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())) - .thenReturn(createCursor(14)) - - val result = testObject.getRecentLanguages() - - Assert.assertEquals(14, (result.size)) - } - - @Test(expected = RuntimeException::class) - fun getGetRecentLanguagesTranslatesExceptions() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow( - RemoteException(""), - ) - testObject.getRecentLanguages() - } - - @Test - fun getGetRecentLanguagesReturnsEmptyList_emptyCursor() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())) - .thenReturn(createCursor(0)) - Assert.assertTrue(testObject.getRecentLanguages().isEmpty()) - } - - @Test - fun getGetRecentLanguagesReturnsEmptyList_nullCursor() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null) - Assert.assertTrue(testObject.getRecentLanguages().isEmpty()) - } - - @Test - fun cursorsAreClosedAfterGetRecentLanguages() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) - - testObject.getRecentLanguages() + for (i in 1..5) { + recentLanguagesRoomDao.addRecentLanguage(Language("Lang$i", "code$i")).blockingAwait() + } - verify(mockCursor).close() + val result = recentLanguagesRoomDao.getRecentLanguages().blockingGet() + assertEquals(5, result.size) } @Test fun findExistingLanguage() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) - Assert.assertTrue(testObject.findRecentLanguage(exampleLanguage.languageCode)) - } - - @Test(expected = RuntimeException::class) - fun findLanguageTranslatesExceptions() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow( - RemoteException(""), - ) - testObject.findRecentLanguage(exampleLanguage.languageCode) - } - - @Test - fun findNotExistingLanguageReturnsNull_emptyCursor() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(0)) - Assert.assertFalse(testObject.findRecentLanguage(exampleLanguage.languageCode)) - } - - @Test - fun findNotExistingLanguageReturnsNull_nullCursor() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null) - Assert.assertFalse(testObject.findRecentLanguage(exampleLanguage.languageCode)) - } - - @Test - fun cursorsAreClosedAfterFindLanguageQuery() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) - - testObject.findRecentLanguage(exampleLanguage.languageCode) - - verify(mockCursor).close() - } - - @Test - fun migrateTableVersionFrom_v1_to_v2() { - onUpdate(database, 1, 2) - // Table didnt exist before v7 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v2_to_v3() { - onUpdate(database, 2, 3) - // Table didnt exist before v7 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v3_to_v4() { - onUpdate(database, 3, 4) - // Table didnt exist before v7 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v4_to_v5() { - onUpdate(database, 4, 5) - // Table didnt exist before v7 - verifyNoInteractions(database) + recentLanguagesRoomDao.addRecentLanguage(exampleLanguage).blockingAwait() + assertTrue(recentLanguagesRoomDao.findRecentLanguage(exampleLanguage.languageCode).blockingGet()) } @Test - fun migrateTableVersionFrom_v5_to_v6() { - onUpdate(database, 5, 6) - // Table didnt exist in version 6 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v6_to_v7() { - onUpdate(database, 6, 7) - // Table didnt exist in version 7 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v7_to_v8() { - onUpdate(database, 7, 8) - // Table didnt exist in version 8 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v8_to_v9() { - onUpdate(database, 8, 9) - // Table didnt exist in version 9 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v9_to_v10() { - onUpdate(database, 9, 10) - // Table didnt exist in version 10 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v10_to_v11() { - onUpdate(database, 10, 11) - // Table didnt exist in version 11 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v11_to_v12() { - onUpdate(database, 11, 12) - // Table didnt exist in version 12 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v12_to_v13() { - onUpdate(database, 12, 13) - // Table didnt exist in version 13 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v13_to_v14() { - onUpdate(database, 13, 14) - // Table didnt exist in version 14 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v14_to_v15() { - onUpdate(database, 14, 15) - // Table didnt exist in version 15 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v15_to_v16() { - onUpdate(database, 15, 16) - // Table didnt exist in version 16 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v16_to_v17() { - onUpdate(database, 16, 17) - // Table didnt exist in version 17 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v18_to_v19() { - onUpdate(database, 18, 19) - // Table didnt exist in version 18 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v19_to_v20() { - onUpdate(database, 19, 20) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - - @Test - fun migrateTableVersionFrom_v20_to_v20() { - onUpdate(database, 20, 20) - verifyNoInteractions(database) + fun findNotExistingLanguage() { + assertFalse(recentLanguagesRoomDao.findRecentLanguage(exampleLanguage.languageCode).blockingGet()) } @Test fun testAddNewLanguage() { - testObject.addRecentLanguage(exampleLanguage) - - verify(client).insert(eq(RecentLanguagesContentProvider.BASE_URI), captor.capture()) - captor.firstValue.let { cv -> - Assert.assertEquals(2, cv.size()) - Assert.assertEquals( - exampleLanguage.languageName, - cv.getAsString(COLUMN_NAME), - ) - Assert.assertEquals( - exampleLanguage.languageCode, - cv.getAsString(COLUMN_CODE), - ) - } + recentLanguagesRoomDao.addRecentLanguage(exampleLanguage).blockingAwait() + val result = recentLanguagesRoomDao.getRecentLanguages().blockingGet() + assertEquals(1, result.size) + assertEquals(exampleLanguage.languageName, result[0].languageName) } @Test fun testDeleteLanguage() { - testObject.addRecentLanguage(exampleLanguage) - testObject.deleteRecentLanguage(exampleLanguage.languageCode) - } + recentLanguagesRoomDao.addRecentLanguage(exampleLanguage).blockingAwait() + assertTrue(recentLanguagesRoomDao.findRecentLanguage(exampleLanguage.languageCode).blockingGet()) - private fun createCursor(rowCount: Int) = - MatrixCursor(columns, rowCount).apply { - for (i in 0 until rowCount) { - addRow(listOf("languageName", "languageCode")) - } - } + recentLanguagesRoomDao.deleteRecentLanguage(exampleLanguage.languageCode).blockingAwait() + assertFalse(recentLanguagesRoomDao.findRecentLanguage(exampleLanguage.languageCode).blockingGet()) + } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewDaoTest.kt index 96aacf6f848..7cf0bd2e6c5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewDaoTest.kt @@ -51,8 +51,8 @@ class ReviewDaoTest { fun insert() { // Insert data val imageId = "1234" - val reviewEntity = ReviewEntity(imageId) - reviewDao.insert(reviewEntity) + val reviewImage = ReviewImage(imageId) + reviewDao.insert(reviewImage) // Check insertion // Covers the case where the image exists in the database diff --git a/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt index acd56c5f4b0..bd64ded5f36 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt @@ -18,7 +18,7 @@ import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.recentlanguages.Language import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter -import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao +import fr.free.nrw.commons.recentlanguages.RecentLanguagesRoomDao import fr.free.nrw.commons.settings.SettingsFragment.Companion.createLocale import org.junit.Assert import org.junit.Assert.assertEquals @@ -47,7 +47,7 @@ class SettingsFragmentUnitTests { private lateinit var context: Context @Mock - private lateinit var recentLanguagesDao: RecentLanguagesDao + private lateinit var recentLanguagesDao: RecentLanguagesRoomDao @Mock private lateinit var recentLanguagesTextView: TextView @@ -87,6 +87,16 @@ class SettingsFragmentUnitTests { "languageHistoryListView", languageHistoryListView, ) + + // Default mock returns for Room DAO reactive methods + whenever(recentLanguagesDao.getRecentLanguages()) + .thenReturn(io.reactivex.Single.just(emptyList())) + whenever(recentLanguagesDao.findRecentLanguage(any())) + .thenReturn(io.reactivex.Single.just(false)) + whenever(recentLanguagesDao.deleteRecentLanguage(any())) + .thenReturn(io.reactivex.Completable.complete()) + whenever(recentLanguagesDao.addRecentLanguage(any())) + .thenReturn(io.reactivex.Completable.complete()) } @Test @@ -165,13 +175,15 @@ class SettingsFragmentUnitTests { Shadows.shadowOf(Looper.getMainLooper()).idle() whenever(recentLanguagesDao.getRecentLanguages()) .thenReturn( - mutableListOf( - Language("English", "en"), - Language("English", "en"), - Language("English", "en"), - Language("English", "en"), - Language("English", "en"), - Language("English", "en"), + io.reactivex.Single.just( + mutableListOf( + Language("English", "en"), + Language("English", "en"), + Language("English", "en"), + Language("English", "en"), + Language("English", "en"), + Language("English", "en"), + ) ), ) val method: Method = @@ -217,7 +229,7 @@ class SettingsFragmentUnitTests { @Throws(Exception::class) fun testOnRecentLanguageClicked() { whenever(recentLanguagesDao.findRecentLanguage(any())) - .thenReturn(true) + .thenReturn(io.reactivex.Single.just(true)) whenever(adapterView.adapter) .thenReturn( RecentLanguagesAdapter( diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt index 7cc59b78dd1..15e6af02059 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt @@ -17,8 +17,9 @@ import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.recentlanguages.Language import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter -import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao +import fr.free.nrw.commons.recentlanguages.RecentLanguagesRoomDao import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment +import io.reactivex.Single import org.junit.Assert import org.junit.Before import org.junit.Test @@ -52,7 +53,7 @@ class UploadMediaDetailAdapterUnitTest { private lateinit var eventListener: UploadMediaDetailAdapter.EventListener @Mock - private lateinit var recentLanguagesDao: RecentLanguagesDao + private lateinit var recentLanguagesDao: RecentLanguagesRoomDao @Mock private lateinit var textView: TextView @@ -78,6 +79,17 @@ class UploadMediaDetailAdapterUnitTest { uploadMediaDetails = mutableListOf(uploadMediaDetail, uploadMediaDetail) activity = Robolectric.buildActivity(UploadActivity::class.java).get() fragment = mock(UploadMediaDetailFragment::class.java) + + // Default mock returns for Room DAO reactive methods + whenever(recentLanguagesDao.getRecentLanguages()) + .thenReturn(Single.just(emptyList())) + whenever(recentLanguagesDao.findRecentLanguage(com.nhaarman.mockitokotlin2.any())) + .thenReturn(Single.just(false)) + whenever(recentLanguagesDao.deleteRecentLanguage(com.nhaarman.mockitokotlin2.any())) + .thenReturn(io.reactivex.Completable.complete()) + whenever(recentLanguagesDao.addRecentLanguage(com.nhaarman.mockitokotlin2.any())) + .thenReturn(io.reactivex.Completable.complete()) + adapter = UploadMediaDetailAdapter(fragment, "", recentLanguagesDao, mockResultLauncher) context = ApplicationProvider.getApplicationContext() Whitebox.setInternalState(adapter, "uploadMediaDetails", uploadMediaDetails) @@ -240,7 +252,7 @@ class UploadMediaDetailAdapterUnitTest { @Throws(Exception::class) fun testOnRecentLanguageClicked() { whenever(recentLanguagesDao.findRecentLanguage(any())) - .thenReturn(true) + .thenReturn(Single.just(true)) whenever(adapterView.adapter) .thenReturn( RecentLanguagesAdapter( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9ae80547be..9504a5b2dd3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,6 +70,7 @@ soloader = "0.10.5" timber = "4.7.1" uiautomator = "2.2.0" workManager = "2.8.1" +roomKtx = "2.8.4" [libraries] # AndroidX Core Dependencies @@ -202,6 +203,7 @@ coordinates2country-android = { module = "io.github.coordinates2country:coordina osmdroid-android = { module = "org.osmdroid:osmdroid-android", version.ref = "osmdroidAndroid" } kotlin-stdlib-jdk7 = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk7", version.ref = "kotlinStdlib" } kotlin-stdlib-jdk8 = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlinStdlib" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }