Skip to content

Commit 77544f8

Browse files
feat. added document provider (Closes #9)
1 parent 702ab1c commit 77544f8

File tree

3 files changed

+354
-1
lines changed

3 files changed

+354
-1
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
33
<application>
4+
<provider
5+
android:name="com.rk.AlpineDocumentProvider"
6+
android:authorities="${applicationId}.documents"
7+
android:exported="true"
8+
android:grantUriPermissions="true"
9+
android:icon="@mipmap/ic_launcher"
10+
android:permission="android.permission.MANAGE_DOCUMENTS">
11+
<intent-filter>
12+
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
13+
</intent-filter>
14+
</provider>
415
<!-- <service android:name="com.rk.runner.runners.web.html.HtmlRunner$DevTools"/> -->
516
</application>
617
</manifest>
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
package com.rk
2+
3+
import android.content.ComponentName
4+
import android.content.Context
5+
import android.content.pm.PackageManager
6+
import android.content.res.AssetFileDescriptor
7+
import android.database.Cursor
8+
import android.database.MatrixCursor
9+
import android.graphics.Point
10+
import android.os.CancellationSignal
11+
import android.os.ParcelFileDescriptor
12+
import android.provider.DocumentsContract
13+
import android.provider.DocumentsProvider
14+
import android.util.Log
15+
import android.webkit.MimeTypeMap
16+
import com.rk.libcommons.alpineHomeDir
17+
import com.rk.resources.getString
18+
import com.rk.resources.strings
19+
import java.io.File
20+
import java.io.FileNotFoundException
21+
import java.io.IOException
22+
import java.util.Collections
23+
import java.util.LinkedList
24+
import java.util.Locale
25+
import com.rk.terminal.R
26+
27+
class AlpineDocumentProvider : DocumentsProvider() {
28+
override fun queryRoots(projection: Array<String>?): Cursor {
29+
val result = MatrixCursor(
30+
projection
31+
?: DEFAULT_ROOT_PROJECTION
32+
)
33+
val applicationName = "ReTerminal"
34+
35+
val row = result.newRow()
36+
row.add(DocumentsContract.Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR))
37+
row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR))
38+
row.add(DocumentsContract.Root.COLUMN_SUMMARY, null)
39+
row.add(
40+
DocumentsContract.Root.COLUMN_FLAGS,
41+
DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_SEARCH or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
42+
)
43+
row.add(DocumentsContract.Root.COLUMN_TITLE, applicationName)
44+
row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES)
45+
row.add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.freeSpace)
46+
row.add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher)
47+
return result
48+
}
49+
50+
@Throws(FileNotFoundException::class)
51+
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
52+
val result = MatrixCursor(
53+
projection
54+
?: DEFAULT_DOCUMENT_PROJECTION
55+
)
56+
includeFile(result, documentId, null)
57+
return result
58+
}
59+
60+
@Throws(FileNotFoundException::class)
61+
override fun queryChildDocuments(
62+
parentDocumentId: String,
63+
projection: Array<String>?,
64+
sortOrder: String? // Changed from non-nullable to nullable
65+
): Cursor {
66+
val result = MatrixCursor(
67+
projection
68+
?: DEFAULT_DOCUMENT_PROJECTION
69+
)
70+
val parent = getFileForDocId(parentDocumentId)
71+
val files = parent.listFiles()
72+
if (files != null) {
73+
for (file in files) {
74+
includeFile(result, null, file)
75+
}
76+
} else {
77+
Log.e("DocumentsProvider", "Unable to list files in $parentDocumentId")
78+
}
79+
return result
80+
}
81+
82+
@Throws(FileNotFoundException::class)
83+
override fun openDocument(
84+
documentId: String,
85+
mode: String,
86+
signal: CancellationSignal?
87+
): ParcelFileDescriptor {
88+
val file = getFileForDocId(documentId)
89+
val accessMode = ParcelFileDescriptor.parseMode(mode)
90+
return ParcelFileDescriptor.open(file, accessMode)
91+
}
92+
93+
@Throws(FileNotFoundException::class)
94+
override fun openDocumentThumbnail(
95+
documentId: String,
96+
sizeHint: Point,
97+
signal: CancellationSignal
98+
): AssetFileDescriptor {
99+
val file = getFileForDocId(documentId)
100+
val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
101+
return AssetFileDescriptor(pfd, 0, file.length())
102+
}
103+
104+
override fun onCreate(): Boolean {
105+
return true
106+
}
107+
108+
@Throws(FileNotFoundException::class)
109+
override fun createDocument(
110+
parentDocumentId: String,
111+
mimeType: String,
112+
displayName: String
113+
): String {
114+
val parent = getFileForDocId(parentDocumentId)
115+
var newFile = File(parent, displayName)
116+
var noConflictId = 2
117+
while (newFile.exists()) {
118+
newFile = File(parent, displayName + " (" + noConflictId++ + ")")
119+
}
120+
try {
121+
val succeeded = if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) {
122+
newFile.mkdir()
123+
} else {
124+
newFile.createNewFile()
125+
}
126+
if (!succeeded) {
127+
throw FileNotFoundException("Failed to create document with id " + newFile.absolutePath)
128+
}
129+
} catch (e: IOException) {
130+
throw FileNotFoundException("Failed to create document with id " + newFile.absolutePath)
131+
}
132+
return getDocIdForFile(newFile)
133+
}
134+
135+
@Throws(FileNotFoundException::class)
136+
override fun deleteDocument(documentId: String) {
137+
val file = getFileForDocId(documentId)
138+
if (!file.delete()) {
139+
throw FileNotFoundException("Failed to delete document with id $documentId")
140+
}
141+
}
142+
143+
@Throws(FileNotFoundException::class)
144+
override fun getDocumentType(documentId: String): String {
145+
val file = getFileForDocId(documentId)
146+
return getMimeType(file)
147+
}
148+
149+
@Throws(FileNotFoundException::class)
150+
override fun querySearchDocuments(
151+
rootId: String,
152+
query: String,
153+
projection: Array<String>?
154+
): Cursor {
155+
val result = MatrixCursor(
156+
projection
157+
?: DEFAULT_DOCUMENT_PROJECTION
158+
)
159+
val parent = getFileForDocId(rootId)
160+
161+
// This example implementation searches file names for the query and doesn't rank search
162+
// results, so we can stop as soon as we find a sufficient number of matches. Other
163+
// implementations might rank results and use other data about files, rather than the file
164+
// name, to produce a match.
165+
val pending = LinkedList<File>()
166+
pending.add(parent)
167+
168+
val MAX_SEARCH_RESULTS = 50
169+
while (!pending.isEmpty() && result.count < MAX_SEARCH_RESULTS) {
170+
val file = pending.removeFirst()
171+
// Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g. search
172+
// through the whole SD card).
173+
var isInsideHome: Boolean
174+
try {
175+
isInsideHome = file.canonicalPath.startsWith(alpineHomeDir().canonicalPath)
176+
} catch (e: IOException) {
177+
isInsideHome = true
178+
}
179+
if (isInsideHome) {
180+
if (file.isDirectory) {
181+
file.listFiles()?.let { Collections.addAll(pending, *it) }
182+
} else {
183+
if (file.name.lowercase(Locale.getDefault()).contains(query)) {
184+
includeFile(result, null, file)
185+
}
186+
}
187+
}
188+
}
189+
190+
return result
191+
}
192+
193+
override fun isChildDocument(parentDocumentId: String, documentId: String): Boolean {
194+
return documentId.startsWith(parentDocumentId)
195+
}
196+
197+
/**
198+
* Add a representation of a file to a cursor.
199+
*
200+
* @param result the cursor to modify
201+
* @param docId the document ID representing the desired file (may be null if given file)
202+
* @param file the File object representing the desired file (may be null if given docID)
203+
*/
204+
@Throws(FileNotFoundException::class)
205+
private fun includeFile(result: MatrixCursor, docId: String?, file: File?) {
206+
var docId = docId
207+
var file = file
208+
if (docId == null) {
209+
docId = getDocIdForFile(file!!)
210+
} else {
211+
file = getFileForDocId(docId)
212+
}
213+
214+
var flags = 0
215+
if (file.isDirectory) {
216+
if (file.canWrite()) flags =
217+
flags or DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
218+
} else if (file.canWrite()) {
219+
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_WRITE
220+
}
221+
if (file.parentFile?.canWrite() == true) flags =
222+
flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
223+
224+
val displayName = file.name
225+
val mimeType = getMimeType(file)
226+
if (mimeType.startsWith("image/")) flags =
227+
flags or DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL
228+
229+
val row = result.newRow()
230+
row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, docId)
231+
row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, displayName)
232+
row.add(DocumentsContract.Document.COLUMN_SIZE, file.length())
233+
row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, mimeType)
234+
row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified())
235+
row.add(DocumentsContract.Document.COLUMN_FLAGS, flags)
236+
row.add(DocumentsContract.Document.COLUMN_ICON, R.mipmap.ic_launcher)
237+
}
238+
239+
companion object {
240+
fun isDocumentProviderEnabled(context: Context): Boolean {
241+
val componentName = ComponentName(context, AlpineDocumentProvider::class.java)
242+
val state = context.packageManager.getComponentEnabledSetting(componentName)
243+
return state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED ||
244+
state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
245+
}
246+
247+
fun setDocumentProviderEnabled(context: Context, enabled: Boolean) {
248+
if (isDocumentProviderEnabled(context) == enabled) {
249+
return
250+
}
251+
val componentName = ComponentName(context, AlpineDocumentProvider::class.java)
252+
val newState = if (enabled)
253+
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
254+
else
255+
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
256+
257+
context.packageManager.setComponentEnabledSetting(
258+
componentName,
259+
newState,
260+
PackageManager.DONT_KILL_APP
261+
)
262+
}
263+
264+
265+
private const val ALL_MIME_TYPES = "*/*"
266+
267+
private val BASE_DIR = alpineHomeDir()
268+
269+
// The default columns to return information about a root if no specific
270+
// columns are requested in a query.
271+
private val DEFAULT_ROOT_PROJECTION = arrayOf(
272+
DocumentsContract.Root.COLUMN_ROOT_ID,
273+
DocumentsContract.Root.COLUMN_MIME_TYPES,
274+
DocumentsContract.Root.COLUMN_FLAGS,
275+
DocumentsContract.Root.COLUMN_ICON,
276+
DocumentsContract.Root.COLUMN_TITLE,
277+
DocumentsContract.Root.COLUMN_SUMMARY,
278+
DocumentsContract.Root.COLUMN_DOCUMENT_ID,
279+
DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
280+
)
281+
282+
// The default columns to return information about a document if no specific
283+
// columns are requested in a query.
284+
private val DEFAULT_DOCUMENT_PROJECTION = arrayOf(
285+
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
286+
DocumentsContract.Document.COLUMN_MIME_TYPE,
287+
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
288+
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
289+
DocumentsContract.Document.COLUMN_FLAGS,
290+
DocumentsContract.Document.COLUMN_SIZE
291+
)
292+
293+
/**
294+
* Get the document id given a file. This document id must be consistent across time as other
295+
* applications may save the ID and use it to reference documents later.
296+
*
297+
* The reverse of @{link #getFileForDocId}.
298+
*/
299+
private fun getDocIdForFile(file: File): String {
300+
return file.absolutePath
301+
}
302+
303+
/**
304+
* Get the file given a document id (the reverse of [.getDocIdForFile]).
305+
*/
306+
@Throws(FileNotFoundException::class)
307+
private fun getFileForDocId(docId: String): File {
308+
val f = File(docId)
309+
if (!f.exists()) throw FileNotFoundException(f.absolutePath + " not found")
310+
return f
311+
}
312+
313+
private fun getMimeType(file: File): String {
314+
if (file.isDirectory) {
315+
return DocumentsContract.Document.MIME_TYPE_DIR
316+
} else {
317+
val name = file.name
318+
val lastDot = name.lastIndexOf('.')
319+
if (lastDot >= 0) {
320+
val extension = name.substring(lastDot + 1).lowercase(Locale.getDefault())
321+
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
322+
if (mime != null) return mime
323+
}
324+
return "application/octet-stream"
325+
}
326+
}
327+
}
328+
}

core/main/src/main/java/com/rk/libcommons/FileUtil.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
package com.rk.libcommons
22

3+
import android.content.Context
34
import java.io.File
5+
import com.rk.terminal.BuildConfig
6+
7+
private fun getFilesDir(): File{
8+
return if (application == null){
9+
if (BuildConfig.DEBUG){
10+
File("/data/data/com.rk.terminal.debug/files")
11+
}else{
12+
File("/data/data/com.rk.terminal/files")
13+
}
14+
}else{
15+
application!!.filesDir
16+
}
17+
}
418

519
fun localDir(): File {
6-
return File(application!!.filesDir.parentFile, "local").also {
20+
return File(getFilesDir().parentFile, "local").also {
721
if (!it.exists()) {
822
it.mkdirs()
923
}

0 commit comments

Comments
 (0)