如何实现一个DocumentProvider
前言
假如你做了一个云盘类的app,或者可以保存用户导入的配置。用户在未来肯定需要获取这些文件,一个办法是写一个Activity,有如一个文件管理软件一样把他们列出来。不止实现麻烦,还有一个问题是用户必须进入app 才能访问。
有一个解决方案是实现一个DocumentProvider。只需实现相应方法,不必自己管理ui ,并且可以和其他app 交互,更具通用性。
步骤
DocumentProvider 继承自Content Provider。
-
首先在Manifest 中注册这个Provider。
<provider android:name=".StorageProvider" android:authorities="com.storyteller_f.ping.documents" android:grantUriPermissions="true" android:exported="true" android:permission="android.permission.MANAGE_DOCUMENTS"> <intent-filter> <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> </intent-filter> </provider> -
创建这个Provider
class StorageProvider : DocumentsProvider() { companion object { private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf( DocumentsContract.Root.COLUMN_ROOT_ID, DocumentsContract.Root.COLUMN_MIME_TYPES, //省略... ) private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf( DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, //省略... ) } override fun onCreate(): Boolean { return true } } -
重写queryRoot
在我们的需求下只有一种情况,访问的路径应该是/data/data/packagename/,不过如果一个app 支持多用户,那就应该有多个目录。所以需要一个方法告知文件管理应用我们的app 有多少个
根。比如/data/data/packagename/user1 和/data/data/packagename/user2,不过我们当前不考虑这个问题。override fun queryRoots(projection: Array<out String>?): Cursor { Log.d(TAG, "queryRoots() called with: projection = $projection") val flags = DocumentsContract.Root.FLAG_LOCAL_ONLY or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD return MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION).apply { newRow().apply { add(DocumentsContract.Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID) add(DocumentsContract.Root.COLUMN_MIME_TYPES, DocumentsContract.Document.MIME_TYPE_DIR) add(DocumentsContract.Root.COLUMN_FLAGS, flags) add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher_foreground) add(DocumentsContract.Root.COLUMN_TITLE, context?.getString(R.string.app_name)) add(DocumentsContract.Root.COLUMN_SUMMARY, "your data") add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, "/") } } }返回值是Cursor 就像一个数据库连接查询数据时一样,不过我们没有操作数据库,返回的数据是文件系统。所以我们返回一个MatrixCursor。
返回的数据没有什么真实数据,仅仅作为一个入口。
此时打开Android 系统的文件管理,就能看到了
此时点击的话就会崩溃,因为还没有重写queryDocument
-
重写queryDocument
/** * Return metadata for the single requested document. You should avoid * making network requests to keep this request fast. * * @param documentId the document to return. * @param projection list of {@link Document} columns to put into the * cursor. If {@code null} all supported columns should be * included. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. */ public abstract Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException;这个方法才是真正返回数据的地方,上面返回root 更像是盘符。
在上面我们定义了root 的 DocumentId
add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, "/")的,当用户点击我们的DocumentProvider,系统就会请求我们的queryDocument,并且传入的documentId就是“/”;override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor { Log.d(TAG, "queryDocument() called with: documentId = $documentId, projection = ${projection?.joinToString()}") return MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION).apply { val root = context?.filesDir?.parentFile ?: return@apply val document = documentId ?: "/" val file = File(root, document) newRow().apply { fileRow(file, root) } } }fileRow 实现在文章末尾
-
重写getChildDocument
override fun queryChildDocuments(parentDocumentId: String?, projection: Array<out String>?, sortOrder: String?): Cursor { Log.d(TAG, "queryChildDocuments() called with: parentDocumentId = $parentDocumentId, projection = $projection, sortOrder = $sortOrder") return MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION).apply { handleChild(parentDocumentId) } }我们只需要根据parentDocumentId 来搜索就可以。就像一个遍历出一个文件夹的子文件夹和子文件一样。
private fun MatrixCursor.handleChild(documentId: String?) { if (documentId != null) { context?.let { val root = it.filesDir.parentFile ?: return@let File(root, documentId).listFiles()?.forEach { newRow().apply { fileRow(it, root) } } } } } -
除了上面介绍的基本功能,还可以继承打开document,创建document,显示document thumbnail 等功能。
-
打开document
override fun openDocument(documentId: String?, mode: String?, signal: CancellationSignal?): ParcelFileDescriptor { Log.d(TAG, "openDocument() called with: documentId = $documentId, mode = $mode, signal = $signal") val accessMode: Int = ParcelFileDescriptor.parseMode(mode) val isWrite: Boolean = mode?.contains("w") ?: false val lc = context lc ?: throw Exception() val root = lc.filesDir?.parentFile val file = File(root, documentId ?: throw Exception()) return if (isWrite) { val handler = Handler(lc.mainLooper ?: throw Exception()) // 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) } } -
显示缩略图
sizeHint 是我们这个缩略图期望的大小,我们的缩略图应该尽可能接近。不过这不太可能,还有一条必须满足的是返回的缩略图不能超过期望的2倍,我们就按照这个标准来。
override fun openDocumentThumbnail(documentId: String?, sizeHint: Point?, signal: CancellationSignal?): AssetFileDescriptor { }在前面的getChildDocument,我们给mp4的添加了一个flag
FLAG_SUPPORTS_THUMBNAIL,如果不添加这个flag,是不会请求这个方法。返回值是AssetFileDescriptor,用起来跟ParcelFileDescriptor 差不多(大概吧)。
val thumbFile = File(root, "cache/.storage-provider/.thumbnail/$hash.jpg") if (!thumbFile.exists()) { thumbFile.parentFile?.mkdirs() thumbFile.createNewFile() val fileOutputStream = FileOutputStream(thumbFile) val frameAtTime = firstFrame(root, documentId) var preHeight = frameAtTime.height var preWidth = frameAtTime.width while (preHeight >= sizeHint.y * 2 && preWidth >= sizeHint.x * 2) { preHeight /= 2 preWidth /= 2 } val scale = frameAtTime.scale(preWidth, preHeight) scale.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream) } return AssetFileDescriptor(ParcelFileDescriptor.open(thumbFile, ParcelFileDescriptor.parseMode("r")), 0, AssetFileDescriptor.UNKNOWN_LENGTH)hash 是根据路径生成的,如果缩略图存在,就不创建了,如果不存在,获取第一帧的图片
-
代码
代码可以在这里看 github.com/storyteller…
private fun MatrixCursor.RowBuilder.fileRow(it: File, root: File) {
val subDocumentId = it.absolutePath.substring(root.absolutePath.length)
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, subDocumentId)
val type = if (it.isFile) {
MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension)
} else {
DocumentsContract.Document.MIME_TYPE_DIR
}
add(DocumentsContract.Document.COLUMN_MIME_TYPE, type)
val copyFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
DocumentsContract.Document.FLAG_SUPPORTS_COPY
} else {
0
}
val thumbnailFlag = if (it.extension == "mp4") {
DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL
} else 0
add(DocumentsContract.Document.COLUMN_FLAGS, copyFlag or thumbnailFlag)
add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, it.name)
add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, it.lastModified())
val size = if (it.isFile) {
it.length()
} else 0
add(DocumentsContract.Document.COLUMN_SIZE, size)
}