Skip to content

Commit

Permalink
[feature] Support sticker DocumentsProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
SkyD666 committed Mar 24, 2024
1 parent cc7a16d commit 6240a0a
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 4 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ android {
minSdk = 24
targetSdk = 34
versionCode = 61
versionName = "2.2-alpha04"
versionName = "2.2-alpha05"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@
android:resource="@xml/file_paths" />
</provider>

<provider
android:name=".model.provider.StickerProvider"
android:authorities="${applicationId}.documents"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>

<receiver
android:name=".model.broadcast.ShareSheetActionsReceiver"
android:enabled="true" />
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/skyd/rays/config/StickerConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import java.io.File
val STICKER_DIR: String = File(appContext.filesDir.path, "Sticker").path

val TEMP_STICKER_DIR: File = File(appContext.cacheDir, "TempSticker")
val PROVIDER_THUMBNAIL_DIR: File = File(TEMP_STICKER_DIR, "Provider/Thumbnail")

val IMPORT_FILES_DIR: File = File(appContext.cacheDir, "ImportFiles")
val EXPORT_FILES_DIR: File = File(appContext.cacheDir, "ExportFiles")
20 changes: 19 additions & 1 deletion app/src/main/java/com/skyd/rays/model/db/dao/TagDao.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.skyd.rays.model.db.dao

import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.MapColumn
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import com.skyd.rays.model.bean.TAG_TABLE_NAME
import com.skyd.rays.model.bean.TagBean
import com.skyd.rays.model.bean.TagBean.Companion.CREATE_TIME_COLUMN
Expand All @@ -14,6 +19,19 @@ interface TagDao {
@Query("SELECT * FROM $TAG_TABLE_NAME")
fun getTagList(): List<TagBean>

@Transaction
@Query(
"""
SELECT $STICKER_UUID_COLUMN, group_concat($TAG_COLUMN, ', ') AS CONCAT_TAG
FROM $TAG_TABLE_NAME
WHERE $STICKER_UUID_COLUMN IN (:stickerUuids)
GROUP BY $STICKER_UUID_COLUMN
"""
)
fun getTagStringMap(stickerUuids: List<String>): Map<
@MapColumn(columnName = STICKER_UUID_COLUMN) String,
@MapColumn(columnName = "CONCAT_TAG") String>

@Transaction
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addTags(tags: List<TagBean>)
Expand Down
21 changes: 19 additions & 2 deletions app/src/main/java/com/skyd/rays/model/db/dao/sticker/StickerDao.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package com.skyd.rays.model.db.dao.sticker

import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.MapColumn
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.sqlite.db.SupportSQLiteQuery
import com.skyd.rays.appContext
import com.skyd.rays.config.STICKER_DIR
Expand All @@ -26,7 +32,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import java.io.File
import java.util.*
import java.util.UUID

@Dao
interface StickerDao {
Expand Down Expand Up @@ -56,6 +62,17 @@ interface StickerDao {
@Query("SELECT * FROM $STICKER_TABLE_NAME WHERE $UUID_COLUMN LIKE :stickerUuid")
fun getStickerWithTags(stickerUuid: String): StickerWithTags?

@Transaction
@Query(
"""
SELECT $UUID_COLUMN, ${StickerBean.TITLE_COLUMN} FROM $STICKER_TABLE_NAME
WHERE $UUID_COLUMN IN (:stickerUuids)
"""
)
fun getStickerTitles(stickerUuids: List<String>): Map<
@MapColumn(columnName = UUID_COLUMN) String,
@MapColumn(columnName = StickerBean.TITLE_COLUMN) String>

@Transaction
@Query("SELECT * FROM $STICKER_TABLE_NAME WHERE $UUID_COLUMN LIKE :stickerUuid")
fun getStickerWithTagsFlow(stickerUuid: String): Flow<StickerWithTags?>
Expand Down
243 changes: 243 additions & 0 deletions app/src/main/java/com/skyd/rays/model/provider/StickerProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package com.skyd.rays.model.provider

import android.content.res.AssetFileDescriptor
import android.database.Cursor
import android.database.MatrixCursor
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Point
import android.os.CancellationSignal
import android.os.Handler
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract.Document
import android.provider.DocumentsContract.Document.MIME_TYPE_DIR
import android.provider.DocumentsContract.Root
import android.provider.DocumentsProvider
import android.util.Log
import com.skyd.rays.R
import com.skyd.rays.config.PROVIDER_THUMBNAIL_DIR
import com.skyd.rays.config.STICKER_DIR
import com.skyd.rays.model.db.dao.TagDao
import com.skyd.rays.model.db.dao.sticker.StickerDao
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException


class StickerProvider : DocumentsProvider() {
private val entryPoint: StickerProviderEntryPoint
get() = EntryPointAccessors.fromApplication(
context!!,
StickerProviderEntryPoint::class.java
)

override fun onCreate(): Boolean = true

override fun queryRoots(projection: Array<out String>?): Cursor {
// Use a MatrixCursor to build a cursor
// with either the requested fields, or the default
// projection if "projection" is null.
val result = MatrixCursor(projection ?: DEFAULT_ROOT_COLUMNS)

result.newRow().apply {
add(Root.COLUMN_ROOT_ID, StickerProvider::class.java.name + ".id")

add(Root.COLUMN_SUMMARY, context!!.getString(R.string.stickers))

// FLAG_SUPPORTS_CREATE means at least one directory under the root supports
// creating documents. FLAG_SUPPORTS_RECENTS means your application's most
// 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_TITLE, context!!.getString(R.string.app_name))

// This document id cannot change after it's shared.
add(Root.COLUMN_DOCUMENT_ID, STICKER_DIR)

// 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_ICON, R.mipmap.ic_launcher)
}

return result
}

override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean {
if (parentDocumentId == null || documentId == null)
return false

return documentId.startsWith(parentDocumentId)
}

override fun queryDocument(documentId: String, projection: Array<out String>?): Cursor {
// Create a cursor with the requested projection, or the default projection.
return MatrixCursor(projection ?: DEFAULT_DOCUMENT_COLUMNS).apply {
includeFile(this, documentId, getDisplayMap(arrayOf(File(documentId))))
}
}

override fun queryChildDocuments(
parentDocumentId: String?,
projection: Array<out String>?,
sortOrder: String?
): Cursor {
return MatrixCursor(projection ?: DEFAULT_DOCUMENT_COLUMNS).apply {
val parent = File(parentDocumentId ?: STICKER_DIR)
val listFiles = parent.listFiles().orEmpty()
val displayMap = getDisplayMap(listFiles)

listFiles.forEach { file ->
includeFile(this, file.path, displayMap)
}
}
}

private fun getDisplayMap(listFiles: Array<out File>): MutableMap<String, String> {
val titles = entryPoint.stickerDao().getStickerTitles(listFiles.map { it.name })
.toMutableMap()

entryPoint.tagDao().getTagStringMap(
titles.filter { it.value.isBlank() }.map { it.key }
).forEach { (t, u) ->
titles[t] = u.ifBlank { t }
}

return titles
}

private fun includeFile(
cursor: MatrixCursor,
path: String?,
displayMap: MutableMap<String, String>
) {
path ?: return
val file = File(path)
val row = cursor.newRow()
row.add(Document.COLUMN_DOCUMENT_ID, path)
if (file.isFile) {
row.add(Document.COLUMN_MIME_TYPE, "image/*")
row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_THUMBNAIL)
row.add(Document.COLUMN_DISPLAY_NAME, displayMap[file.name] ?: file.name)
} else if (file.isDirectory) {
row.add(Document.COLUMN_MIME_TYPE, MIME_TYPE_DIR)
row.add(Document.COLUMN_DISPLAY_NAME, file.name)
}
row.add(Document.COLUMN_SIZE, file.length())
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified())
}

override fun openDocument(
documentId: String,
mode: String,
signal: CancellationSignal?
): ParcelFileDescriptor {
// It's OK to do network operations in this method to download the document,
// as long as you periodically check the CancellationSignal. If you have an
// extremely large file to transfer from the network, a better solution may
// be pipes or sockets (see ParcelFileDescriptor for helper methods).

val file = File(documentId)
val accessMode: Int = ParcelFileDescriptor.parseMode(mode)

val isWrite: Boolean = mode.contains("w")
return if (isWrite) {
val handler = Handler(context!!.mainLooper)
// Attach a close listener if the document is opened in write mode.
try {
ParcelFileDescriptor.open(file, accessMode, handler) {
// Update the file with the cloud server. The client is done writing.
Log.i(
TAG,
"A file with id $documentId has been closed! Time to update the server."
)
}
} catch (e: IOException) {
throw FileNotFoundException(
"Failed to open document with id $documentId and mode $mode"
)
}
} else {
ParcelFileDescriptor.open(file, accessMode)
}
}

override fun openDocumentThumbnail(
documentId: String,
sizeHint: Point,
signal: CancellationSignal?
): AssetFileDescriptor {
val stickerFile = File(documentId)
val thumbFile = File(PROVIDER_THUMBNAIL_DIR, stickerFile.name)
if (!thumbFile.exists()) {
thumbFile.parentFile?.mkdirs()
thumbFile.createNewFile()
FileOutputStream(thumbFile).use { fos ->
BitmapFactory.Options().apply {
inJustDecodeBounds = true
BitmapFactory.decodeFile(stickerFile.path, this)
// thumbnail images should not
// more than double the size specified by the sizeHint
val preHeight = outHeight / 2
val preWidth = outWidth / 2
inSampleSize = 1
while (preHeight / inSampleSize >= sizeHint.y ||
preWidth / inSampleSize >= sizeHint.x
) {
inSampleSize = inSampleSize shl 1
}
inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeFile(stickerFile.path, this)
bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos)
}
}
}

return AssetFileDescriptor(
ParcelFileDescriptor.open(
thumbFile,
ParcelFileDescriptor.MODE_READ_ONLY
),
0,
AssetFileDescriptor.UNKNOWN_LENGTH
)
}

@EntryPoint
@InstallIn(SingletonComponent::class)
interface StickerProviderEntryPoint {
fun stickerDao(): StickerDao
fun tagDao(): TagDao
}

companion object {
const val TAG = "StickerProvider"

private val DEFAULT_ROOT_COLUMNS = arrayOf(
Root.COLUMN_ROOT_ID,
Root.COLUMN_MIME_TYPES,
Root.COLUMN_FLAGS,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES
)

private val DEFAULT_DOCUMENT_COLUMNS = arrayOf(
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE
)
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -320,4 +320,5 @@
<string name="blur_stickers_screen_tip">如果表情包的标题或标签字符串包含模糊关键词,那么该表情包将被模糊显示。</string>
<string name="blur_stickers_screen_radius">模糊半径</string>
<string name="loading">加载中…</string>
<string name="stickers">表情包</string>
</resources>
1 change: 1 addition & 0 deletions app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -320,4 +320,5 @@
<string name="blur_stickers_screen_tip">如果表情包的標題或標籤字串包含模糊關鍵字,則表情包將顯示為模糊。</string>
<string name="blur_stickers_screen_radius">模糊半徑</string>
<string name="loading">載入中…</string>
<string name="stickers">表情包</string>
</resources>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -332,4 +332,5 @@
<string name="blur_stickers_screen_tip">If the title or label string of a sticker contains the blur keywords, then the sticker will be displayed blurred.</string>
<string name="blur_stickers_screen_radius">Blur radius</string>
<string name="loading">Loading…</string>
<string name="stickers">Stickers</string>
</resources>

0 comments on commit 6240a0a

Please sign in to comment.