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+ }
0 commit comments