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