这是indexloc提供的服务,不要输入任何密码
Skip to content

Commit a546eeb

Browse files
authored
Merge pull request #476 from tareksander/saf
Implement better support for SAF
2 parents f88a6a5 + 22e8171 commit a546eeb

File tree

4 files changed

+346
-0
lines changed

4 files changed

+346
-0
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@
8282
android:theme="@android:style/Theme.Translucent.NoTitleBar"
8383
android:excludeFromRecents="true"
8484
android:exported="false"/>
85+
<activity android:name=".SAFAPI$SAFActivity"
86+
android:theme="@style/TransparentTheme"
87+
android:exported="false"
88+
android:excludeFromRecents="true"/>
8589
<service android:name=".SpeechToTextAPI$SpeechToTextService"/>
8690
<service android:name=".TextToSpeechAPI$TextToSpeechService" />
8791
<service android:name=".SensorAPI$SensorReaderService"/>
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
package com.termux.api;
2+
3+
import android.content.Context;
4+
import android.content.Intent;
5+
import android.content.UriPermission;
6+
import android.database.Cursor;
7+
import android.net.Uri;
8+
import android.os.Build;
9+
import android.os.Bundle;
10+
import android.os.FileUtils;
11+
import android.provider.DocumentsContract;
12+
import android.util.JsonWriter;
13+
import android.webkit.MimeTypeMap;
14+
15+
import androidx.annotation.Nullable;
16+
import androidx.appcompat.app.AppCompatActivity;
17+
import androidx.documentfile.provider.DocumentFile;
18+
19+
import com.termux.api.util.ResultReturner;
20+
import com.termux.api.util.TermuxApiLogger;
21+
22+
import java.io.FileNotFoundException;
23+
import java.io.IOException;
24+
import java.io.InputStream;
25+
import java.io.OutputStream;
26+
import java.io.PrintWriter;
27+
28+
public class SAFAPI
29+
{
30+
public static class SAFActivity extends AppCompatActivity {
31+
private boolean resultReturned = false;
32+
33+
@Override
34+
protected void onCreate(@Nullable Bundle savedInstanceState) {
35+
super.onCreate(savedInstanceState);
36+
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
37+
i.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
38+
startActivityForResult(i, 0);
39+
}
40+
41+
@Override
42+
protected void onDestroy() {
43+
super.onDestroy();
44+
finishAndRemoveTask();
45+
if (! resultReturned) {
46+
ResultReturner.returnData(this, getIntent(), out -> out.write(""));
47+
resultReturned = true;
48+
}
49+
}
50+
51+
@Override
52+
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
53+
super.onActivityResult(requestCode, resultCode, data);
54+
if (data != null) {
55+
Uri uri = data.getData();
56+
if (uri != null) {
57+
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
58+
resultReturned = true;
59+
ResultReturner.returnData(this, getIntent(), out -> out.println(data.getDataString()));
60+
}
61+
}
62+
finish();
63+
}
64+
}
65+
66+
static void onReceive(TermuxApiReceiver apiReceiver, Context context, Intent intent) {
67+
String method = intent.getStringExtra("safmethod");
68+
if (method == null) {
69+
TermuxApiLogger.error("safmethod extra null");
70+
return;
71+
}
72+
try {
73+
switch (method) {
74+
case "getManagedDocumentTrees":
75+
getManagedDocumentTrees(apiReceiver, context, intent);
76+
break;
77+
case "manageDocumentTree":
78+
manageDocumentTree(context, intent);
79+
break;
80+
case "writeDocument":
81+
writeDocument(apiReceiver, context, intent);
82+
break;
83+
case "createDocument":
84+
createDocument(apiReceiver, context, intent);
85+
break;
86+
case "readDocument":
87+
readDocument(apiReceiver, context, intent);
88+
break;
89+
case "listDirectory":
90+
listDirectory(apiReceiver, context, intent);
91+
break;
92+
case "removeDocument":
93+
removeDocument(apiReceiver, context, intent);
94+
break;
95+
case "statURI":
96+
statURI(apiReceiver, context, intent);
97+
break;
98+
default:
99+
TermuxApiLogger.error("Unrecognized safmethod: " + "'" + method + "'");
100+
}
101+
} catch (Exception e) {
102+
TermuxApiLogger.error("Error in SAFAPI", e);
103+
}
104+
}
105+
106+
private static void getManagedDocumentTrees(TermuxApiReceiver apiReceiver, Context context, Intent intent) {
107+
ResultReturner.returnData(apiReceiver, intent, new ResultReturner.ResultJsonWriter()
108+
{
109+
@Override
110+
public void writeJson(JsonWriter out) throws Exception {
111+
out.beginArray();
112+
for (UriPermission p : context.getContentResolver().getPersistedUriPermissions()) {
113+
statDocument(out, context, treeUriToDocumentUri(p.getUri()));
114+
}
115+
out.endArray();
116+
}
117+
});
118+
}
119+
120+
private static void manageDocumentTree(Context context, Intent intent) {
121+
Intent i = new Intent(context, SAFActivity.class);
122+
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
123+
ResultReturner.copyIntentExtras(intent, i);
124+
context.startActivity(i);
125+
}
126+
127+
private static void writeDocument(TermuxApiReceiver apiReceiver, Context context, Intent intent) {
128+
String uri = intent.getStringExtra("uri");
129+
if (uri == null) {
130+
TermuxApiLogger.error("uri extra null");
131+
return;
132+
}
133+
DocumentFile f = DocumentFile.fromSingleUri(context, Uri.parse(uri));
134+
if (f == null) {
135+
return;
136+
}
137+
writeDocumentFile(apiReceiver, context, intent, f);
138+
}
139+
140+
private static void createDocument(TermuxApiReceiver apiReceiver, Context context, Intent intent) {
141+
String treeURIString = intent.getStringExtra("treeuri");
142+
if (treeURIString == null) {
143+
TermuxApiLogger.error("treeuri extra null");
144+
return;
145+
}
146+
String name = intent.getStringExtra("filename");
147+
if (name == null) {
148+
TermuxApiLogger.error("filename extra null");
149+
return;
150+
}
151+
String mime = intent.getStringExtra("mimetype");
152+
if (mime == null) {
153+
mime = "application/octet-stream";
154+
}
155+
Uri treeURI = Uri.parse(treeURIString);
156+
String id = DocumentsContract.getTreeDocumentId(treeURI);
157+
try {
158+
id = DocumentsContract.getDocumentId(Uri.parse(treeURIString));
159+
} catch (IllegalArgumentException ignored) {}
160+
final String finalMime = mime;
161+
final String finalId = id;
162+
ResultReturner.returnData(apiReceiver, intent, out ->
163+
out.println(DocumentsContract.createDocument(context.getContentResolver(), DocumentsContract.buildDocumentUriUsingTree(treeURI, finalId), finalMime, name).toString())
164+
);
165+
}
166+
167+
private static void readDocument(TermuxApiReceiver apiReceiver, Context context, Intent intent) {
168+
String uri = intent.getStringExtra("uri");
169+
if (uri == null) {
170+
TermuxApiLogger.error("uri extra null");
171+
return;
172+
}
173+
DocumentFile f = DocumentFile.fromSingleUri(context, Uri.parse(uri));
174+
if (f == null) {
175+
return;
176+
}
177+
returnDocumentFile(apiReceiver, context, intent, f);
178+
}
179+
180+
private static void listDirectory(TermuxApiReceiver apiReceiver, Context context, Intent intent) {
181+
String treeURIString = intent.getStringExtra("treeuri");
182+
if (treeURIString == null) {
183+
TermuxApiLogger.error("treeuri extra null");
184+
return;
185+
}
186+
Uri treeURI = Uri.parse(treeURIString);
187+
ResultReturner.returnData(apiReceiver, intent, new ResultReturner.ResultJsonWriter()
188+
{
189+
@Override
190+
public void writeJson(JsonWriter out) throws Exception {
191+
out.beginArray();
192+
String id = DocumentsContract.getTreeDocumentId(treeURI);
193+
try {
194+
id = DocumentsContract.getDocumentId(Uri.parse(treeURIString));
195+
} catch (IllegalArgumentException ignored) {}
196+
try (Cursor c = context.getContentResolver().query(DocumentsContract.buildChildDocumentsUriUsingTree(Uri.parse(treeURIString), id), new String[] {
197+
DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null)) {
198+
while (c.moveToNext()) {
199+
String documentId = c.getString(0);
200+
Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(treeURI, documentId);
201+
statDocument(out, context, documentUri);
202+
}
203+
} catch (UnsupportedOperationException ignored) { }
204+
out.endArray();
205+
}
206+
});
207+
}
208+
209+
private static void statURI(TermuxApiReceiver apiReceiver, Context context, Intent intent) {
210+
String uriString = intent.getStringExtra("uri");
211+
if (uriString == null) {
212+
TermuxApiLogger.error("uri extra null");
213+
return;
214+
}
215+
Uri docUri = treeUriToDocumentUri(Uri.parse(uriString));
216+
ResultReturner.returnData(apiReceiver, intent, new ResultReturner.ResultJsonWriter()
217+
{
218+
@Override
219+
public void writeJson(JsonWriter out) throws Exception {
220+
statDocument(out, context, Uri.parse(docUri.toString()));
221+
}
222+
});
223+
}
224+
225+
226+
private static void removeDocument(TermuxApiReceiver apiReceiver, Context context, Intent intent) {
227+
String uri = intent.getStringExtra("uri");
228+
if (uri == null) {
229+
TermuxApiLogger.error("uri extra null");
230+
return;
231+
}
232+
ResultReturner.returnData(apiReceiver, intent, out -> {
233+
try {
234+
if (DocumentsContract.deleteDocument(context.getContentResolver(), Uri.parse(uri))) {
235+
out.println(0);
236+
} else {
237+
out.println(1);
238+
}
239+
} catch (FileNotFoundException | IllegalArgumentException e ) {
240+
out.println(2);
241+
}
242+
});
243+
}
244+
245+
246+
private static Uri treeUriToDocumentUri(Uri tree) {
247+
String id = DocumentsContract.getTreeDocumentId(tree);
248+
try {
249+
id = DocumentsContract.getDocumentId(tree);
250+
} catch (IllegalArgumentException ignored) {}
251+
return DocumentsContract.buildDocumentUriUsingTree(tree, id);
252+
}
253+
254+
private static void statDocument(JsonWriter out, Context context, Uri uri) throws Exception {
255+
try (Cursor c = context.getContentResolver().query(uri, null, null, null, null)) {
256+
if (c == null || c.getCount() == 0) {
257+
return;
258+
}
259+
c.moveToNext();
260+
out.beginObject();
261+
out.name("name");
262+
out.value(c.getString(c.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)));
263+
out.name("type");
264+
String mime = c.getString(c.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE));
265+
out.value(mime);
266+
out.name("uri");
267+
out.value(uri.toString());
268+
if (! DocumentsContract.Document.MIME_TYPE_DIR.equals(mime)) {
269+
out.name("length");
270+
out.value(c.getInt(c.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE)));
271+
}
272+
out.endObject();
273+
}
274+
}
275+
276+
private static void returnDocumentFile(TermuxApiReceiver apiReceiver, Context context, Intent intent, DocumentFile f) {
277+
ResultReturner.returnData(apiReceiver, intent, new ResultReturner.BinaryOutput()
278+
{
279+
@Override
280+
public void writeResult(OutputStream out) throws Exception {
281+
try (InputStream in = context.getContentResolver().openInputStream(f.getUri())) {
282+
writeInputStreamToOutputStream(in, out);
283+
}
284+
}
285+
});
286+
}
287+
288+
private static void writeDocumentFile(TermuxApiReceiver apiReceiver, Context context, Intent intent, DocumentFile f) {
289+
ResultReturner.returnData(apiReceiver, intent, new ResultReturner.WithInput()
290+
{
291+
@Override
292+
public void writeResult(PrintWriter unused) throws Exception {
293+
try (OutputStream out = context.getContentResolver().openOutputStream(f.getUri(), "rwt")) {
294+
writeInputStreamToOutputStream(in, out);
295+
}
296+
}
297+
});
298+
}
299+
300+
private static void writeInputStreamToOutputStream(InputStream in, OutputStream out) throws IOException {
301+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
302+
FileUtils.copy(in, out);
303+
}
304+
else {
305+
byte[] buffer = new byte[4096];
306+
int read;
307+
while ((read = in.read(buffer)) != -1) {
308+
out.write(buffer, 0, read);
309+
}
310+
}
311+
}
312+
313+
}

app/src/main/java/com/termux/api/TermuxApiReceiver.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ private void doWork(Context context, Intent intent) {
208208
case "WifiEnable":
209209
WifiAPI.onReceiveWifiEnable(this, context, intent);
210210
break;
211+
case "SAF":
212+
SAFAPI.onReceive(this, context, intent);
213+
break;
211214
default:
212215
TermuxApiLogger.error("Unrecognized 'api_method' extra: '" + apiMethod + "'");
213216
}

app/src/main/java/com/termux/api/util/ResultReturner.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.io.ByteArrayOutputStream;
1414
import java.io.FileDescriptor;
1515
import java.io.InputStream;
16+
import java.io.OutputStream;
1617
import java.io.PrintWriter;
1718
import java.nio.charset.StandardCharsets;
1819

@@ -44,6 +45,27 @@ public void setInput(InputStream inputStream) throws Exception {
4445
this.in = inputStream;
4546
}
4647
}
48+
49+
/**
50+
* Possible subclass of {@link ResultWriter} when the output is binary data instead of text.
51+
*/
52+
public static abstract class BinaryOutput implements ResultWriter {
53+
private OutputStream out;
54+
55+
public void setOutput(OutputStream outputStream) {
56+
this.out = outputStream;
57+
}
58+
59+
public abstract void writeResult(OutputStream out) throws Exception;
60+
61+
/**
62+
* writeResult with a PrintWriter is marked as final and overwritten, so you don't accidentally use it
63+
*/
64+
public final void writeResult(PrintWriter unused) throws Exception {
65+
writeResult(out);
66+
out.flush();
67+
}
68+
}
4769

4870
/**
4971
* Possible marker interface for a {@link ResultWriter} when input is to be read from stdin.
@@ -122,6 +144,10 @@ public static void returnData(Object context, final Intent intent, final ResultW
122144
outputSocket.connect(new LocalSocketAddress(outputSocketAdress));
123145
try (PrintWriter writer = new PrintWriter(outputSocket.getOutputStream())) {
124146
if (resultWriter != null) {
147+
if (resultWriter instanceof BinaryOutput) {
148+
BinaryOutput bout = (BinaryOutput) resultWriter;
149+
bout.setOutput(outputSocket.getOutputStream());
150+
}
125151
if (resultWriter instanceof WithInput) {
126152
try (LocalSocket inputSocket = new LocalSocket()) {
127153
String inputSocketAdress = intent.getStringExtra(SOCKET_INPUT_EXTRA);

0 commit comments

Comments
 (0)