diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c45288a..758683e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ android { minSdk = 24 targetSdk = 34 versionCode = 62 - versionName = "2.2-alpha12" + versionName = "2.2-beta01" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -35,8 +35,6 @@ android { signingConfigs { create("release") { - // You need to specify either an absolute path or include the - // keystore file in the same directory as the build.gradle file. @Suppress("UNCHECKED_CAST") val sign = ((extra["secret"] as Map<*, *>)["sign"] as Map) storeFile = file("../key.jks") @@ -200,6 +198,6 @@ dependencies { implementation("com.github.penfeizhou.android.animation:apng:2.28.0") - debugImplementation("androidx.compose.ui:ui-tooling:1.6.4") - debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.4") + debugImplementation("androidx.compose.ui:ui-tooling:1.6.5") + debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.5") } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/rays/api/strategy/SearchStickersStrategy.kt b/app/src/main/java/com/skyd/rays/api/strategy/SearchStickersStrategy.kt index f1e3f1f..2e24c3c 100644 --- a/app/src/main/java/com/skyd/rays/api/strategy/SearchStickersStrategy.kt +++ b/app/src/main/java/com/skyd/rays/api/strategy/SearchStickersStrategy.kt @@ -1,25 +1,12 @@ package com.skyd.rays.api.strategy import android.content.Intent -import android.os.Parcelable import androidx.core.content.FileProvider import com.skyd.rays.api.ApiStickerWithTags import com.skyd.rays.appContext -import com.skyd.rays.base.BaseBean -import com.skyd.rays.model.bean.StickerBean -import com.skyd.rays.model.bean.StickerWithTags -import com.skyd.rays.model.bean.TagBean import com.skyd.rays.model.respository.SearchRepository import com.skyd.rays.util.stickerUuidToFile -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.flow.first -import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json object SearchStickersStrategy { suspend fun execute( @@ -28,7 +15,7 @@ object SearchStickersStrategy { requestPackage: String, ): List { val result = repo - .requestStickerWithTagsList(keyword.orEmpty()) + .requestStickerWithTagsListFlow(keyword.orEmpty()) .first() .map { val uri = FileProvider.getUriForFile( diff --git a/app/src/main/java/com/skyd/rays/ext/ActivityResultLauncherExt.kt b/app/src/main/java/com/skyd/rays/ext/ActivityResultLauncherExt.kt index 76be177..3333ab6 100644 --- a/app/src/main/java/com/skyd/rays/ext/ActivityResultLauncherExt.kt +++ b/app/src/main/java/com/skyd/rays/ext/ActivityResultLauncherExt.kt @@ -11,7 +11,7 @@ fun ActivityResultLauncher.safeLaunch( runCatching { launch(input) }.onFailure { - // May: No Activity found to handle Intent + // e.g. No Activity found to handle Intent it.printStackTrace() onError.invoke(it) } @@ -25,7 +25,7 @@ fun ActivityResultLauncher.safeLaunch( runCatching { launch(input, options) }.onFailure { - // May: No Activity found to handle Intent + // e.g. No Activity found to handle Intent it.printStackTrace() onError.invoke(it) } diff --git a/app/src/main/java/com/skyd/rays/model/db/dao/sticker/StickerDao.kt b/app/src/main/java/com/skyd/rays/model/db/dao/sticker/StickerDao.kt index 5bc841a..7dc9573 100644 --- a/app/src/main/java/com/skyd/rays/model/db/dao/sticker/StickerDao.kt +++ b/app/src/main/java/com/skyd/rays/model/db/dao/sticker/StickerDao.kt @@ -16,6 +16,7 @@ import com.skyd.rays.model.bean.STICKER_TABLE_NAME import com.skyd.rays.model.bean.StickerBean import com.skyd.rays.model.bean.StickerBean.Companion.CLICK_COUNT_COLUMN import com.skyd.rays.model.bean.StickerBean.Companion.CREATE_TIME_COLUMN +import com.skyd.rays.model.bean.StickerBean.Companion.MODIFY_TIME_COLUMN import com.skyd.rays.model.bean.StickerBean.Companion.SHARE_COUNT_COLUMN import com.skyd.rays.model.bean.StickerBean.Companion.STICKER_MD5_COLUMN import com.skyd.rays.model.bean.StickerBean.Companion.UUID_COLUMN @@ -62,6 +63,21 @@ interface StickerDao { @Query("SELECT * FROM $STICKER_TABLE_NAME WHERE $UUID_COLUMN LIKE :stickerUuid") fun getStickerWithTags(stickerUuid: String): StickerWithTags? + @Transaction + @Query("SELECT * FROM $STICKER_TABLE_NAME ORDER BY $MODIFY_TIME_COLUMN DESC LIMIT :count") + fun getRecentModifiedStickers(count: Int = 15): List + + @Transaction + @Query( + """ + SELECT $UUID_COLUMN, $MODIFY_TIME_COLUMN FROM $STICKER_TABLE_NAME + WHERE $UUID_COLUMN IN (:stickerUuids) + """ + ) + fun getStickerModified(stickerUuids: List): Map< + @MapColumn(columnName = UUID_COLUMN) String, + @MapColumn(columnName = MODIFY_TIME_COLUMN) Long> + @Transaction @Query( """ diff --git a/app/src/main/java/com/skyd/rays/model/provider/StickerProvider.kt b/app/src/main/java/com/skyd/rays/model/provider/StickerProvider.kt index 48284c5..a32c22c 100644 --- a/app/src/main/java/com/skyd/rays/model/provider/StickerProvider.kt +++ b/app/src/main/java/com/skyd/rays/model/provider/StickerProvider.kt @@ -21,6 +21,7 @@ import com.skyd.rays.ext.toDateTimeString import com.skyd.rays.model.db.dao.TagDao import com.skyd.rays.model.db.dao.sticker.MimeTypeDao import com.skyd.rays.model.db.dao.sticker.StickerDao +import com.skyd.rays.model.respository.SearchRepository import com.skyd.rays.util.image.ImageFormatChecker import com.skyd.rays.util.image.format.ImageFormat import dagger.hilt.EntryPoint @@ -61,7 +62,11 @@ class StickerProvider : DocumentsProvider() { // recently used documents will show up in the "Recents" category. // FLAG_SUPPORTS_SEARCH allows users to search all documents the application // shares. - add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY or Root.FLAG_SUPPORTS_IS_CHILD) + add( + Root.COLUMN_FLAGS, + Root.FLAG_LOCAL_ONLY or Root.FLAG_SUPPORTS_IS_CHILD or + Root.FLAG_SUPPORTS_RECENTS or Root.FLAG_SUPPORTS_SEARCH + ) add(Root.COLUMN_TITLE, context!!.getString(R.string.app_name)) @@ -70,7 +75,7 @@ class StickerProvider : DocumentsProvider() { // The child MIME types are used to filter the roots and only present to the // user those roots that contain the desired type somewhere in their file hierarchy. - add(Root.COLUMN_MIME_TYPES, MIME_TYPE_DIR) + add(Root.COLUMN_MIME_TYPES, "image/*") add(Root.COLUMN_ICON, R.mipmap.ic_launcher) } @@ -87,12 +92,14 @@ class StickerProvider : DocumentsProvider() { override fun queryDocument(documentId: String, projection: Array?): Cursor { // Create a cursor with the requested projection, or the default projection. return MatrixCursor(projection ?: DEFAULT_DOCUMENT_COLUMNS).apply { - val files = arrayOf(File(documentId)) + val uuids = listOf(File(documentId).name) + val modifiedMap = getModifiedMap(uuids) includeFile( this, - documentId, - getDisplayMap(files), - getMimeTypeMap(files), + path = documentId, + lastModified = modifiedMap[uuids.first()], + displayMap = getDisplayMap(uuids), + mimeTypeMap = getMimeTypeMap(uuids), ) } } @@ -105,17 +112,29 @@ class StickerProvider : DocumentsProvider() { return MatrixCursor(projection ?: DEFAULT_DOCUMENT_COLUMNS).apply { val parent = File(parentDocumentId ?: STICKER_DIR) val listFiles = parent.listFiles().orEmpty() - val displayMap = getDisplayMap(listFiles) - val mimeTypeMap = getMimeTypeMap(listFiles) + val stickerUuids = listFiles.map { it.name } + val modifiedMap = getModifiedMap(stickerUuids) + val displayMap = getDisplayMap(stickerUuids) + val mimeTypeMap = getMimeTypeMap(stickerUuids) listFiles.forEach { file -> - includeFile(this, file.path, displayMap, mimeTypeMap) + includeFile( + cursor = this, + path = file.path, + lastModified = modifiedMap[file.name], + displayMap = displayMap, + mimeTypeMap = mimeTypeMap, + ) } } } - private fun getDisplayMap(listFiles: Array): MutableMap { - val titles = entryPoint.stickerDao().getStickerTitles(listFiles.map { it.name }) + private fun getModifiedMap(stickerUuids: List): MutableMap { + return entryPoint.stickerDao().getStickerModified(stickerUuids).toMutableMap() + } + + private fun getDisplayMap(stickerUuids: List): MutableMap { + val titles = entryPoint.stickerDao().getStickerTitles(stickerUuids) .toMutableMap() entryPoint.tagDao().getTagStringMap( @@ -127,14 +146,14 @@ class StickerProvider : DocumentsProvider() { return titles } - private fun getMimeTypeMap(listFiles: Array): MutableMap { - return entryPoint.mimeTypeDao().getStickerMimeTypes(listFiles.map { it.name }) - .toMutableMap() + private fun getMimeTypeMap(stickerUuids: List): MutableMap { + return entryPoint.mimeTypeDao().getStickerMimeTypes(stickerUuids).toMutableMap() } private fun includeFile( cursor: MatrixCursor, path: String?, + lastModified: Long? = null, displayMap: Map, mimeTypeMap: MutableMap, ) { @@ -163,7 +182,7 @@ class StickerProvider : DocumentsProvider() { row.add(Document.COLUMN_DISPLAY_NAME, file.name) } row.add(Document.COLUMN_SIZE, file.length()) - row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified()) + row.add(Document.COLUMN_LAST_MODIFIED, lastModified ?: file.lastModified()) } override fun openDocument( @@ -232,6 +251,8 @@ class StickerProvider : DocumentsProvider() { if (reuseBitmap?.get() != null) { inBitmap = reuseBitmap?.get() } + // Only mutable bitmap can be reused. + inMutable = true val bitmap = BitmapFactory.decodeFile(stickerFile.path, this) bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos) @@ -262,9 +283,62 @@ class StickerProvider : DocumentsProvider() { ) } + override fun queryRecentDocuments(rootId: String?, projection: Array?): Cursor { + return MatrixCursor(projection ?: DEFAULT_DOCUMENT_COLUMNS).apply { + val stickers = entryPoint.stickerDao().getRecentModifiedStickers() + .associateBy { it.sticker.uuid } + val listFiles = stickers.values + .map { File(STICKER_DIR, it.sticker.uuid) } + .filter { it.exists() } + .toTypedArray() + val stickerUuids = listFiles.map { it.name } + val displayMap = getDisplayMap(stickerUuids) + val mimeTypeMap = getMimeTypeMap(stickerUuids) + + listFiles.forEach { file -> + includeFile( + cursor = this, + path = file.path, + lastModified = stickers[file.name]?.sticker?.modifyTime, + displayMap = displayMap, + mimeTypeMap = mimeTypeMap, + ) + } + } + } + + override fun querySearchDocuments( + rootId: String?, + query: String?, + projection: Array? + ): Cursor { + return MatrixCursor(projection ?: DEFAULT_DOCUMENT_COLUMNS).apply { + val stickers = entryPoint.searchRepo().requestStickerWithTagsList(query.orEmpty()) + .associateBy { it.sticker.uuid } + val listFiles = stickers.values + .map { File(STICKER_DIR, it.sticker.uuid) } + .filter { it.exists() } + .toTypedArray() + val stickerUuids = listFiles.map { it.name } + val displayMap = getDisplayMap(stickerUuids) + val mimeTypeMap = getMimeTypeMap(stickerUuids) + + listFiles.forEach { file -> + includeFile( + cursor = this, + path = file.path, + lastModified = stickers[file.name]?.sticker?.modifyTime, + displayMap = displayMap, + mimeTypeMap = mimeTypeMap, + ) + } + } + } + @EntryPoint @InstallIn(SingletonComponent::class) interface StickerProviderEntryPoint { + fun searchRepo(): SearchRepository fun stickerDao(): StickerDao fun tagDao(): TagDao fun mimeTypeDao(): MimeTypeDao diff --git a/app/src/main/java/com/skyd/rays/model/respository/SearchRepository.kt b/app/src/main/java/com/skyd/rays/model/respository/SearchRepository.kt index ab6ebf2..a48f7b3 100644 --- a/app/src/main/java/com/skyd/rays/model/respository/SearchRepository.kt +++ b/app/src/main/java/com/skyd/rays/model/respository/SearchRepository.kt @@ -31,18 +31,24 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.runBlocking import javax.inject.Inject class SearchRepository @Inject constructor( private val stickerDao: StickerDao, ) : BaseRepository() { - fun requestStickerWithTagsList(keyword: String): Flow> { + fun requestStickerWithTagsList(keyword: String): List = runBlocking { + stickerDao.getStickerWithTagsList(genSql(k = keyword)).first() + } + + fun requestStickerWithTagsListFlow(keyword: String): Flow> { return flow { emit(genSql(keyword)) } .flowOn(Dispatchers.IO) .flatMapConcat { @@ -268,6 +274,9 @@ class SearchRepository @Inject constructor( appContext, HomeRepositoryEntryPoint::class.java ).searchDomainDao.getSearchDomain(tableName, columnName) }, + intersectSearchBySpace: Boolean = appContext.dataStore.getOrDefault( + IntersectSearchBySpacePreference + ), ): SimpleSQLiteQuery { val useRegexSearch = appContext.dataStore.getOrDefault(UseRegexSearchPreference) if (useRegexSearch) { @@ -277,9 +286,6 @@ class SearchRepository @Inject constructor( } } - // 是否使用多个关键字并集查询 - val intersectSearchBySpace = - appContext.dataStore.getOrDefault(IntersectSearchBySpacePreference) return if (intersectSearchBySpace) { // 以多个连续的空格/制表符/换行符分割 val keywords = k.trim().split("\\s+".toRegex()).toSet() diff --git a/build.gradle.kts b/build.gradle.kts index 61a4556..5e664f9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ buildscript { extra.apply { - set("composeVersion", "1.6.0") - set("md3Version", "1.2.0") + set("composeVersion", "1.6.5") + set("md3Version", "1.2.1") set("accompanistVersion", "0.34.0") set("mlkitRecognitionVersion", "16.0.0") set("roomVersion", "2.6.1")