summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2024-11-09 17:58:09 +0100
committerJoel Klinghed <the_jk@spawned.biz>2024-11-09 17:58:09 +0100
commitd274ffe5ec33c285034bdae64b532f2a470b9407 (patch)
tree9cc54dff13ce7b2bfcb471c66d3d12f55f4ea417
parentf5aa9ba724fa8bfc2aba3532f5427398329b386a (diff)
documents: Create DocumentTreeTestHelper
For future tests
-rw-r--r--libs/documents/src/test/java/org/the_jk/cleversync/documents/DocumentTreeTest.kt429
-rw-r--r--libs/documents/src/test/java/org/the_jk/cleversync/documents/DocumentTreeTestHelper.kt440
2 files changed, 443 insertions, 426 deletions
diff --git a/libs/documents/src/test/java/org/the_jk/cleversync/documents/DocumentTreeTest.kt b/libs/documents/src/test/java/org/the_jk/cleversync/documents/DocumentTreeTest.kt
index f116f0e..ad6abfa 100644
--- a/libs/documents/src/test/java/org/the_jk/cleversync/documents/DocumentTreeTest.kt
+++ b/libs/documents/src/test/java/org/the_jk/cleversync/documents/DocumentTreeTest.kt
@@ -1,34 +1,14 @@
package org.the_jk.cleversync.documents
-import android.Manifest.permission.MANAGE_DOCUMENTS
-import android.content.Context
-import android.content.pm.ProviderInfo
-import android.database.Cursor
-import android.database.MatrixCursor
-import android.os.Build
-import android.os.CancellationSignal
-import android.os.ParcelFileDescriptor
-import android.provider.DocumentsContract
-import android.provider.DocumentsProvider
-import android.webkit.MimeTypeMap
-import androidx.test.core.app.ApplicationProvider
-import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
-import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
-import org.robolectric.android.controller.ContentProviderController
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowLooper
import org.the_jk.cleversync.TreeAbstractTest
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileNotFoundException
-import java.io.FileOutputStream
-import java.io.IOException
import java.util.concurrent.TimeUnit
@Config(manifest=Config.NONE)
@@ -37,36 +17,16 @@ class DocumentTreeTest : TreeAbstractTest() {
@get:Rule
val folder = TemporaryFolder()
- private lateinit var controller: ContentProviderController<FakeProvider>
+ private val helper = DocumentTreeTestHelper()
@Before
fun setUp() {
- val context = ApplicationProvider.getApplicationContext<Context>()
- val contentResolver = context.contentResolver
- val providerInfo = ProviderInfo()
- providerInfo.authority = "org.the_jk.cleversync.documents.test"
- providerInfo.grantUriPermissions = true
- providerInfo.exported = true
- providerInfo.readPermission = MANAGE_DOCUMENTS
- providerInfo.writePermission = MANAGE_DOCUMENTS
- controller = Robolectric
- .buildContentProvider(FakeProvider::class.java)
-
- controller.get().setBaseDir(folder.root)
- controller.create(providerInfo)
-
- val treeUri = DocumentsContract.buildTreeDocumentUri(providerInfo.authority, ROOT)
- assertThat(DocumentsContract.isTreeUri(treeUri)).isTrue()
-
- tree = DocumentTreeFactory.modifiableTree(
- contentResolver,
- treeUri,
- )
+ tree = helper.openTree(folder)
}
@After
fun tearDown() {
- controller.shutdown()
+ helper.closeTree(tree)
}
override fun idle() {
@@ -74,387 +34,4 @@ class DocumentTreeTest : TreeAbstractTest() {
}
override fun supportSymlinks() = false
-
- private class FakeProvider : DocumentsProvider() {
- private lateinit var baseDir: File
-
- fun setBaseDir(file: File) {
- baseDir = file
- }
-
- override fun onCreate(): Boolean {
- return true
- }
-
- override fun queryRoots(projection: Array<String>?): Cursor {
- val result = MatrixCursor(resolveRootProjection(projection))
- val row = result.newRow()
- row.add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT)
- row.add(DocumentsContract.Root.COLUMN_SUMMARY, "")
- row.add(DocumentsContract.Root.COLUMN_FLAGS,
- DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
- DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD)
- row.add(DocumentsContract.Root.COLUMN_TITLE, "fake")
- row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir))
- row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, getChildMimeTypes())
- row.add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDir.freeSpace)
- row.add(DocumentsContract.Root.COLUMN_ICON, null)
- return result
- }
-
- override fun queryDocument(
- documentId: String?,
- projection: Array<String>?
- ): Cursor {
- val result = MatrixCursor(resolveDocumentProjection(projection))
- includeFile(result, documentId, null)
- return result
- }
-
- override fun queryChildDocuments(
- parentDocumentId: String,
- projection: Array<String>?,
- sortOrder: String?
- ): Cursor {
- val result = MatrixCursor(resolveDocumentProjection(projection))
- val parent = getFileForDocId(parentDocumentId)
- parent.listFiles()?.forEach { file ->
- includeFile(result, null, file)
- }
- return result
- }
-
- override fun openDocument(
- documentId: String,
- mode: String?,
- signal: CancellationSignal?
- ): ParcelFileDescriptor {
- val file = getFileForDocId(documentId)
- val accessMode = ParcelFileDescriptor.parseMode(mode)
- return ParcelFileDescriptor.open(file, accessMode)
- }
-
- override fun isChildDocument(
- parentDocumentId: String,
- documentId: String
- ): Boolean {
- try {
- val parentFile = getFileForDocId(parentDocumentId)
- var childFile: File? = getFileForDocId(documentId)
- // childFile can be any level of descendant of parentFile
- // to return true.
- do {
- if (isChildFile(parentFile, childFile!!)) return true
- childFile = childFile.parentFile
- } while (childFile != null)
- return false
- } catch (_: FileNotFoundException) {
- }
- return false
- }
-
- override fun createDocument(
- parentDocumentId: String,
- mimeType: String?,
- displayName: String,
- ): String {
- val parent = getFileForDocId(parentDocumentId)
- val file = File(parent.path, displayName)
- try {
- var documentWasCreated = false
- if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
- if (file.mkdir()) {
- if (file.setWritable(true) && file.setWritable(true) && file.setExecutable(true)) {
- documentWasCreated = true
- }
- }
- } else {
- if (file.createNewFile()) {
- if (file.setWritable(true) && file.setReadable(true)) {
- documentWasCreated = true
- }
- }
- }
-
- if (!documentWasCreated) {
- throw IOException("Failed to create document with name " +
- displayName +" and documentId " + parentDocumentId)
- }
- } catch (e: IOException) {
- throw IOException("Failed to create document with name " +
- displayName +" and documentId " + parentDocumentId, e)
- }
- return getDocIdForFile(file)
- }
-
- override fun renameDocument(
- documentId: String,
- displayName: String?
- ): String {
- if (displayName == null) {
- throw IOException("Failed to rename document, new name is null")
- }
-
- val sourceFile = getFileForDocId(documentId)
- val sourceParentFile = sourceFile.parentFile
- ?: throw IOException("Failed to rename document. File has no parent.")
- val destFile = File(sourceParentFile.path, displayName)
-
- try {
- if (!sourceFile.renameTo(destFile)) {
- throw IOException("Failed to rename document. Renamed failed.")
- }
- } catch (e: Exception) {
- throw IOException("Failed to rename document.", e)
- }
-
- return getDocIdForFile(destFile)
- }
-
- override fun deleteDocument(documentId: String) {
- val file = getFileForDocId(documentId)
- if (!file.delete()) {
- throw IOException("Failed to delete document with id $documentId")
- }
- }
-
- override fun removeDocument(
- documentId: String,
- parentDocumentId: String,
- ) {
- val parent = getFileForDocId(parentDocumentId)
- val file = getFileForDocId(documentId)
-
- val doesFileParentMatch = isChildFile(parent, file)
-
- if (parent == file || doesFileParentMatch) {
- if (!file.delete()) {
- throw IOException("Failed to delete document with id $documentId")
- }
- } else {
- throw IOException("Failed to delete document with id $documentId")
- }
- }
-
- override fun copyDocument(
- sourceDocumentId: String,
- targetParentDocumentId: String
- ): String {
- val parent = getFileForDocId(targetParentDocumentId)
- val oldFile = getFileForDocId(sourceDocumentId)
- val newFile = File(parent.path, oldFile.name)
-
- try {
- var wasNewFileCreated = false
- if (newFile.createNewFile()) {
- if (newFile.setWritable(true) && newFile.setReadable(true)) {
- wasNewFileCreated = true
- }
- }
-
- if (!wasNewFileCreated) {
- throw IOException("Failed to copy document " + sourceDocumentId +
- ". Could not create new file.")
- }
-
- FileInputStream(oldFile).use { inStream ->
- FileOutputStream(newFile).use { outStream ->
- // Transfer bytes from in to out
- val buf = ByteArray(65536)
- var len: Int
- while ((inStream.read(buf).also { len = it }) > 0) {
- outStream.write(buf, 0, len)
- }
- }
- }
- } catch (e: IOException) {
- throw IOException("Failed to copy document: $sourceDocumentId", e)
- }
- return getDocIdForFile(newFile)
- }
-
- override fun moveDocument(
- sourceDocumentId: String,
- sourceParentDocumentId: String,
- targetParentDocumentId: String,
- ): String {
- try {
- // Copy document, insisting that the parent is correct
- val newDocumentId = copyDocument(sourceDocumentId, sourceParentDocumentId,
- targetParentDocumentId)
- // Remove old document
- removeDocument(sourceDocumentId,sourceParentDocumentId)
- return newDocumentId
- } catch (e: IOException) {
- throw IOException("Failed to move document $sourceDocumentId", e)
- }
- }
-
- override fun getDocumentType(documentId: String): String {
- val file = getFileForDocId(documentId)
- return getTypeForFile(file)
- }
-
- private fun copyDocument(
- sourceDocumentId: String,
- sourceParentDocumentId: String,
- targetParentDocumentId: String,
- ): String {
- if (!isChildDocument(sourceParentDocumentId, sourceDocumentId)) {
- throw IOException(
- "Failed to copy document with id " +
- sourceDocumentId + ". Parent is not: " + sourceParentDocumentId
- )
- }
- return copyDocument(sourceDocumentId, targetParentDocumentId)
- }
-
- private fun isChildFile(parentFile: File, childFile: File): Boolean {
- return parentFile == childFile.parentFile
- }
-
- private fun getDocIdForFile(file: File): String {
- var path = file.absolutePath
-
- // Start at first char of path under root
- val rootPath = baseDir.path
- path = if (rootPath.equals(path)) {
- ""
- } else if (rootPath.endsWith("/")) {
- path.substring(rootPath.length)
- } else {
- path.substring(rootPath.length + 1)
- }
-
- return "$ROOT:$path"
- }
-
- private fun getFileForDocId(docId: String): File {
- var target = baseDir
- if (docId == ROOT) {
- return target
- }
- val splitIndex = docId.indexOf(':', 1)
- if (splitIndex < 0) {
- throw FileNotFoundException("Missing root for $docId")
- } else {
- val path = docId.substring(splitIndex + 1)
- target = File(target, path)
- if (!target.exists()) {
- throw FileNotFoundException("Missing file for $docId at $target")
- }
- return target
- }
- }
-
- private fun includeFile(result: MatrixCursor, inDocId: String?, inFile: File?) {
- val (docId, file) = if (inDocId == null) {
- getDocIdForFile(inFile!!) to inFile
- } else {
- inDocId to getFileForDocId(inDocId)
- }
-
- var flags = 0
-
- if (file.isDirectory) {
- // Add FLAG_DIR_SUPPORTS_CREATE if the file is a writable directory.
- if (file.canWrite()) {
- flags = flags or DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
- }
- }
- if (file.canWrite()) {
- // If the file is writable set FLAG_SUPPORTS_WRITE and
- // FLAG_SUPPORTS_DELETE
- flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_WRITE
- flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- flags =
- flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
- }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- flags =
- flags or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE
- flags =
- flags or DocumentsContract.Document.FLAG_SUPPORTS_MOVE
- flags =
- flags or DocumentsContract.Document.FLAG_SUPPORTS_COPY
- }
- }
-
- val displayName = file.name
- val mimeType = getTypeForFile(file)
-
- val row = result.newRow()
- row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, docId)
- row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, displayName)
- row.add(DocumentsContract.Document.COLUMN_SIZE, file.length())
- row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, mimeType)
- row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified())
- row.add(DocumentsContract.Document.COLUMN_FLAGS, flags)
- row.add(DocumentsContract.Document.COLUMN_ICON, null)
- }
- }
-
- private companion object {
- val DEFAULT_ROOT_PROJECTION = arrayOf(
- DocumentsContract.Root.COLUMN_ROOT_ID,
- DocumentsContract.Root.COLUMN_MIME_TYPES,
- DocumentsContract.Root.COLUMN_FLAGS,
- DocumentsContract.Root.COLUMN_ICON,
- DocumentsContract.Root.COLUMN_TITLE,
- DocumentsContract.Root.COLUMN_SUMMARY,
- DocumentsContract.Root.COLUMN_DOCUMENT_ID,
- DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
- )
-
- val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
- DocumentsContract.Document.COLUMN_DOCUMENT_ID,
- DocumentsContract.Document.COLUMN_MIME_TYPE,
- DocumentsContract.Document.COLUMN_DISPLAY_NAME,
- DocumentsContract.Document.COLUMN_LAST_MODIFIED,
- DocumentsContract.Document.COLUMN_FLAGS,
- DocumentsContract.Document.COLUMN_SIZE
- )
-
- const val ROOT: String = "root"
-
- fun resolveRootProjection(projection: Array<String>?): Array<String> {
- return projection ?: DEFAULT_ROOT_PROJECTION
- }
-
- fun resolveDocumentProjection(projection: Array<String>?): Array<String> {
- return projection ?: DEFAULT_DOCUMENT_PROJECTION
- }
-
- fun getTypeForFile(file: File): String {
- return if (file.isDirectory) {
- DocumentsContract.Document.MIME_TYPE_DIR
- } else {
- getTypeForName(file.name)
- }
- }
-
- fun getTypeForName(name: String): String {
- val lastDot = name.lastIndexOf('.')
- if (lastDot >= 0) {
- val extension = name.substring(lastDot + 1)
- val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
- if (mime != null) {
- return mime
- }
- }
- return "application/octet-stream"
- }
-
- fun getChildMimeTypes(): String {
- val mimeTypes = setOf(
- "image/*",
- "text/*",
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- DocumentsContract.Document.MIME_TYPE_DIR,
- "application/octet-stream",
- )
- return mimeTypes.joinToString("\n")
- }
- }
}
diff --git a/libs/documents/src/test/java/org/the_jk/cleversync/documents/DocumentTreeTestHelper.kt b/libs/documents/src/test/java/org/the_jk/cleversync/documents/DocumentTreeTestHelper.kt
new file mode 100644
index 0000000..5358920
--- /dev/null
+++ b/libs/documents/src/test/java/org/the_jk/cleversync/documents/DocumentTreeTestHelper.kt
@@ -0,0 +1,440 @@
+package org.the_jk.cleversync.documents
+
+import android.Manifest.permission.MANAGE_DOCUMENTS
+import android.content.Context
+import android.content.pm.ProviderInfo
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.os.Build
+import android.os.CancellationSignal
+import android.os.ParcelFileDescriptor
+import android.provider.DocumentsContract
+import android.provider.DocumentsProvider
+import android.webkit.MimeTypeMap
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.rules.TemporaryFolder
+import org.robolectric.Robolectric
+import org.robolectric.android.controller.ContentProviderController
+import org.the_jk.cleversync.io.ModifiableTree
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+
+class DocumentTreeTestHelper {
+ private lateinit var controller: ContentProviderController<FakeProvider>
+
+ fun openTree(folder: TemporaryFolder): ModifiableTree {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val contentResolver = context.contentResolver
+ val providerInfo = ProviderInfo()
+ providerInfo.authority = "org.the_jk.cleversync.documents.test"
+ providerInfo.grantUriPermissions = true
+ providerInfo.exported = true
+ providerInfo.readPermission = MANAGE_DOCUMENTS
+ providerInfo.writePermission = MANAGE_DOCUMENTS
+ controller = Robolectric
+ .buildContentProvider(FakeProvider::class.java)
+
+ controller.get().setBaseDir(folder.root)
+ controller.create(providerInfo)
+
+ val treeUri = DocumentsContract.buildTreeDocumentUri(providerInfo.authority, ROOT)
+ assertThat(DocumentsContract.isTreeUri(treeUri)).isTrue()
+
+ return DocumentTreeFactory.modifiableTree(
+ contentResolver,
+ treeUri,
+ )
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ fun closeTree(tree: ModifiableTree) {
+ controller.shutdown()
+ }
+
+ private class FakeProvider : DocumentsProvider() {
+ private lateinit var baseDir: File
+
+ fun setBaseDir(file: File) {
+ baseDir = file
+ }
+
+ override fun onCreate(): Boolean {
+ return true
+ }
+
+ override fun queryRoots(projection: Array<String>?): Cursor {
+ val result = MatrixCursor(resolveRootProjection(projection))
+ val row = result.newRow()
+ row.add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT)
+ row.add(DocumentsContract.Root.COLUMN_SUMMARY, "")
+ row.add(DocumentsContract.Root.COLUMN_FLAGS,
+ DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
+ DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD)
+ row.add(DocumentsContract.Root.COLUMN_TITLE, "fake")
+ row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir))
+ row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, getChildMimeTypes())
+ row.add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDir.freeSpace)
+ row.add(DocumentsContract.Root.COLUMN_ICON, null)
+ return result
+ }
+
+ override fun queryDocument(
+ documentId: String?,
+ projection: Array<String>?
+ ): Cursor {
+ val result = MatrixCursor(resolveDocumentProjection(projection))
+ includeFile(result, documentId, null)
+ return result
+ }
+
+ override fun queryChildDocuments(
+ parentDocumentId: String,
+ projection: Array<String>?,
+ sortOrder: String?
+ ): Cursor {
+ val result = MatrixCursor(resolveDocumentProjection(projection))
+ val parent = getFileForDocId(parentDocumentId)
+ parent.listFiles()?.forEach { file ->
+ includeFile(result, null, file)
+ }
+ return result
+ }
+
+ override fun openDocument(
+ documentId: String,
+ mode: String?,
+ signal: CancellationSignal?
+ ): ParcelFileDescriptor {
+ val file = getFileForDocId(documentId)
+ val accessMode = ParcelFileDescriptor.parseMode(mode)
+ return ParcelFileDescriptor.open(file, accessMode)
+ }
+
+ override fun isChildDocument(
+ parentDocumentId: String,
+ documentId: String
+ ): Boolean {
+ try {
+ val parentFile = getFileForDocId(parentDocumentId)
+ var childFile: File? = getFileForDocId(documentId)
+ // childFile can be any level of descendant of parentFile
+ // to return true.
+ do {
+ if (isChildFile(parentFile, childFile!!)) return true
+ childFile = childFile.parentFile
+ } while (childFile != null)
+ return false
+ } catch (_: FileNotFoundException) {
+ }
+ return false
+ }
+
+ override fun createDocument(
+ parentDocumentId: String,
+ mimeType: String?,
+ displayName: String,
+ ): String {
+ val parent = getFileForDocId(parentDocumentId)
+ val file = File(parent.path, displayName)
+ try {
+ var documentWasCreated = false
+ if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
+ if (file.mkdir()) {
+ if (file.setWritable(true) && file.setWritable(true) && file.setExecutable(true)) {
+ documentWasCreated = true
+ }
+ }
+ } else {
+ if (file.createNewFile()) {
+ if (file.setWritable(true) && file.setReadable(true)) {
+ documentWasCreated = true
+ }
+ }
+ }
+
+ if (!documentWasCreated) {
+ throw IOException("Failed to create document with name " +
+ displayName +" and documentId " + parentDocumentId)
+ }
+ } catch (e: IOException) {
+ throw IOException("Failed to create document with name " +
+ displayName +" and documentId " + parentDocumentId, e)
+ }
+ return getDocIdForFile(file)
+ }
+
+ override fun renameDocument(
+ documentId: String,
+ displayName: String?
+ ): String {
+ if (displayName == null) {
+ throw IOException("Failed to rename document, new name is null")
+ }
+
+ val sourceFile = getFileForDocId(documentId)
+ val sourceParentFile = sourceFile.parentFile
+ ?: throw IOException("Failed to rename document. File has no parent.")
+ val destFile = File(sourceParentFile.path, displayName)
+
+ try {
+ if (!sourceFile.renameTo(destFile)) {
+ throw IOException("Failed to rename document. Renamed failed.")
+ }
+ } catch (e: Exception) {
+ throw IOException("Failed to rename document.", e)
+ }
+
+ return getDocIdForFile(destFile)
+ }
+
+ override fun deleteDocument(documentId: String) {
+ val file = getFileForDocId(documentId)
+ if (!file.delete()) {
+ throw IOException("Failed to delete document with id $documentId")
+ }
+ }
+
+ override fun removeDocument(
+ documentId: String,
+ parentDocumentId: String,
+ ) {
+ val parent = getFileForDocId(parentDocumentId)
+ val file = getFileForDocId(documentId)
+
+ val doesFileParentMatch = isChildFile(parent, file)
+
+ if (parent == file || doesFileParentMatch) {
+ if (!file.delete()) {
+ throw IOException("Failed to delete document with id $documentId")
+ }
+ } else {
+ throw IOException("Failed to delete document with id $documentId")
+ }
+ }
+
+ override fun copyDocument(
+ sourceDocumentId: String,
+ targetParentDocumentId: String
+ ): String {
+ val parent = getFileForDocId(targetParentDocumentId)
+ val oldFile = getFileForDocId(sourceDocumentId)
+ val newFile = File(parent.path, oldFile.name)
+
+ try {
+ var wasNewFileCreated = false
+ if (newFile.createNewFile()) {
+ if (newFile.setWritable(true) && newFile.setReadable(true)) {
+ wasNewFileCreated = true
+ }
+ }
+
+ if (!wasNewFileCreated) {
+ throw IOException("Failed to copy document " + sourceDocumentId +
+ ". Could not create new file.")
+ }
+
+ FileInputStream(oldFile).use { inStream ->
+ FileOutputStream(newFile).use { outStream ->
+ // Transfer bytes from in to out
+ val buf = ByteArray(65536)
+ var len: Int
+ while ((inStream.read(buf).also { len = it }) > 0) {
+ outStream.write(buf, 0, len)
+ }
+ }
+ }
+ } catch (e: IOException) {
+ throw IOException("Failed to copy document: $sourceDocumentId", e)
+ }
+ return getDocIdForFile(newFile)
+ }
+
+ override fun moveDocument(
+ sourceDocumentId: String,
+ sourceParentDocumentId: String,
+ targetParentDocumentId: String,
+ ): String {
+ try {
+ // Copy document, insisting that the parent is correct
+ val newDocumentId = copyDocument(sourceDocumentId, sourceParentDocumentId,
+ targetParentDocumentId)
+ // Remove old document
+ removeDocument(sourceDocumentId,sourceParentDocumentId)
+ return newDocumentId
+ } catch (e: IOException) {
+ throw IOException("Failed to move document $sourceDocumentId", e)
+ }
+ }
+
+ override fun getDocumentType(documentId: String): String {
+ val file = getFileForDocId(documentId)
+ return getTypeForFile(file)
+ }
+
+ private fun copyDocument(
+ sourceDocumentId: String,
+ sourceParentDocumentId: String,
+ targetParentDocumentId: String,
+ ): String {
+ if (!isChildDocument(sourceParentDocumentId, sourceDocumentId)) {
+ throw IOException(
+ "Failed to copy document with id " +
+ sourceDocumentId + ". Parent is not: " + sourceParentDocumentId
+ )
+ }
+ return copyDocument(sourceDocumentId, targetParentDocumentId)
+ }
+
+ private fun isChildFile(parentFile: File, childFile: File): Boolean {
+ return parentFile == childFile.parentFile
+ }
+
+ private fun getDocIdForFile(file: File): String {
+ var path = file.absolutePath
+
+ // Start at first char of path under root
+ val rootPath = baseDir.path
+ path = if (rootPath.equals(path)) {
+ ""
+ } else if (rootPath.endsWith("/")) {
+ path.substring(rootPath.length)
+ } else {
+ path.substring(rootPath.length + 1)
+ }
+
+ return "$ROOT:$path"
+ }
+
+ private fun getFileForDocId(docId: String): File {
+ var target = baseDir
+ if (docId == ROOT) {
+ return target
+ }
+ val splitIndex = docId.indexOf(':', 1)
+ if (splitIndex < 0) {
+ throw FileNotFoundException("Missing root for $docId")
+ } else {
+ val path = docId.substring(splitIndex + 1)
+ target = File(target, path)
+ if (!target.exists()) {
+ throw FileNotFoundException("Missing file for $docId at $target")
+ }
+ return target
+ }
+ }
+
+ private fun includeFile(result: MatrixCursor, inDocId: String?, inFile: File?) {
+ val (docId, file) = if (inDocId == null) {
+ getDocIdForFile(inFile!!) to inFile
+ } else {
+ inDocId to getFileForDocId(inDocId)
+ }
+
+ var flags = 0
+
+ if (file.isDirectory) {
+ // Add FLAG_DIR_SUPPORTS_CREATE if the file is a writable directory.
+ if (file.canWrite()) {
+ flags = flags or DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
+ }
+ }
+ if (file.canWrite()) {
+ // If the file is writable set FLAG_SUPPORTS_WRITE and
+ // FLAG_SUPPORTS_DELETE
+ flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_WRITE
+ flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ flags =
+ flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ flags =
+ flags or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE
+ flags =
+ flags or DocumentsContract.Document.FLAG_SUPPORTS_MOVE
+ flags =
+ flags or DocumentsContract.Document.FLAG_SUPPORTS_COPY
+ }
+ }
+
+ val displayName = file.name
+ val mimeType = getTypeForFile(file)
+
+ val row = result.newRow()
+ row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, docId)
+ row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, displayName)
+ row.add(DocumentsContract.Document.COLUMN_SIZE, file.length())
+ row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, mimeType)
+ row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified())
+ row.add(DocumentsContract.Document.COLUMN_FLAGS, flags)
+ row.add(DocumentsContract.Document.COLUMN_ICON, null)
+ }
+ }
+
+ private companion object {
+ val DEFAULT_ROOT_PROJECTION = arrayOf(
+ DocumentsContract.Root.COLUMN_ROOT_ID,
+ DocumentsContract.Root.COLUMN_MIME_TYPES,
+ DocumentsContract.Root.COLUMN_FLAGS,
+ DocumentsContract.Root.COLUMN_ICON,
+ DocumentsContract.Root.COLUMN_TITLE,
+ DocumentsContract.Root.COLUMN_SUMMARY,
+ DocumentsContract.Root.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
+ )
+
+ val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Document.COLUMN_MIME_TYPE,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ DocumentsContract.Document.COLUMN_LAST_MODIFIED,
+ DocumentsContract.Document.COLUMN_FLAGS,
+ DocumentsContract.Document.COLUMN_SIZE
+ )
+
+ const val ROOT: String = "root"
+
+ fun resolveRootProjection(projection: Array<String>?): Array<String> {
+ return projection ?: DEFAULT_ROOT_PROJECTION
+ }
+
+ fun resolveDocumentProjection(projection: Array<String>?): Array<String> {
+ return projection ?: DEFAULT_DOCUMENT_PROJECTION
+ }
+
+ fun getTypeForFile(file: File): String {
+ return if (file.isDirectory) {
+ DocumentsContract.Document.MIME_TYPE_DIR
+ } else {
+ getTypeForName(file.name)
+ }
+ }
+
+ fun getTypeForName(name: String): String {
+ val lastDot = name.lastIndexOf('.')
+ if (lastDot >= 0) {
+ val extension = name.substring(lastDot + 1)
+ val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
+ if (mime != null) {
+ return mime
+ }
+ }
+ return "application/octet-stream"
+ }
+
+ fun getChildMimeTypes(): String {
+ val mimeTypes = setOf(
+ "image/*",
+ "text/*",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ DocumentsContract.Document.MIME_TYPE_DIR,
+ "application/octet-stream",
+ )
+ return mimeTypes.joinToString("\n")
+ }
+ }
+}