Skip to content

Commit 80705fc

Browse files
feat. alpine document provider (Acode-Foundation#1444)
* feat. alpine document provider * feat. use /public for decument provider
1 parent 8b3d6e4 commit 80705fc

File tree

4 files changed

+358
-9
lines changed

4 files changed

+358
-9
lines changed

src/plugins/terminal/plugin.xml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,28 @@
1818
</feature>
1919
</config-file>
2020
<config-file parent="/*" target="AndroidManifest.xml" />
21-
<source-file src="src/android/Executor.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
2221

22+
<source-file src="src/android/Executor.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
23+
<source-file src="src/android/AlpineDocumentProvider.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
2324

2425
<source-file src="scripts/init-sandbox.sh" target-dir="assets"/>
2526
<source-file src="scripts/init-alpine.sh" target-dir="assets"/>
2627

27-
<config-file target="AndroidManifest.xml" parent="/manifest">
28+
<config-file target="AndroidManifest.xml" parent="/manifest"></config-file>
2829

29-
</config-file>
30+
<config-file target="AndroidManifest.xml" parent="./application">
31+
<provider
32+
android:name="com.foxdebug.acode.rk.exec.terminal.AlpineDocumentProvider"
33+
android:authorities="${applicationId}.documents"
34+
android:exported="true"
35+
android:grantUriPermissions="true"
36+
android:icon="@mipmap/ic_launcher"
37+
android:permission="android.permission.MANAGE_DOCUMENTS">
38+
<intent-filter>
39+
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
40+
</intent-filter>
41+
</provider>
42+
</config-file>
3043

3144

3245
</platform>

src/plugins/terminal/scripts/init-sandbox.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ else
2525
fi
2626

2727

28-
$PROOT --link2symlink -L --sysvipc --kill-on-exit -b $PREFIX:$PREFIX -b /data:/data -b /system:/system -b /vendor:/vendor -b /sdcard:/sdcard -b /storage:/storage -S $PREFIX/alpine /bin/sh $PREFIX/init-alpine.sh "$@"
28+
$PROOT --link2symlink -L --sysvipc --kill-on-exit -b $PREFIX:$PREFIX -b /data:/data -b /system:/system -b /vendor:/vendor -b /sdcard:/sdcard -b /storage:/storage -b $PREFIX/public:/public -S $PREFIX/alpine /bin/sh $PREFIX/init-alpine.sh "$@"
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
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+
}

www/index.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,17 +165,17 @@
165165

166166
<title>Acode</title>
167167
<!--styles-->
168-
<link rel="stylesheet" href="./css/build/263.css">
169-
<link rel="stylesheet" href="./css/build/31.css">
170-
<link rel="stylesheet" href="./css/build/439.css">
171-
<link rel="stylesheet" href="./css/build/490.css">
172-
<link rel="stylesheet" href="./css/build/626.css">
173168
<link rel="stylesheet" href="./css/build/about.css">
174169
<link rel="stylesheet" href="./css/build/customTheme.css">
175170
<link rel="stylesheet" href="./css/build/donate.css">
176171
<link rel="stylesheet" href="./css/build/fileBrowser.css">
177172
<link rel="stylesheet" href="./css/build/main.css">
178173
<link rel="stylesheet" href="./css/build/plugins.css">
174+
<link rel="stylesheet" href="./css/build/src_pages_quickTools_quickTools_js.css">
175+
<link rel="stylesheet" href="./css/build/src_sidebarApps_extensions_index_js.css">
176+
<link rel="stylesheet" href="./css/build/src_sidebarApps_files_index_js.css">
177+
<link rel="stylesheet" href="./css/build/src_sidebarApps_notification_index_js.css">
178+
<link rel="stylesheet" href="./css/build/src_sidebarApps_searchInFiles_index_js.css">
179179
<link rel="stylesheet" href="./css/build/themeSetting.css">
180180
<!--styles_end-->
181181
</head>

0 commit comments

Comments
 (0)