Skip to content

Commit 38c6e50

Browse files
committed
Resolve content URLs from Android file browser to normal paths
1 parent 13daf7a commit 38c6e50

File tree

4 files changed

+140
-0
lines changed

4 files changed

+140
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -788,5 +788,7 @@ to code contained in Git repositories included as Git submodule (which contain t
788788
* All other icons found in this repository are taken from the [KDE/Breeze](https://invent.kde.org/frameworks/breeze-icons) project.
789789
* Code under `tray/gui/quick` originates from [Kirigami](https://invent.kde.org/frameworks/kirigami). The comments at the beginning of those
790790
files state the original authors/contributors.
791+
* Parts of `tray/android/src/io/github/martchus/syncthingtray/Util.java` are based on
792+
[com.nutomic.syncthingandroid.util](https://github.com/Catfriend1/syncthing-android/blob/main/app/src/main/java/com/nutomic/syncthingandroid/util/FileUtils.java).
791793

792794
None of these icons have been (intentionally) modified so no copyright for modifications is asserted.

tray/android/src/io/github/martchus/syncthingtray/Activity.java

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import org.qtproject.qt.android.bindings.QtActivity;
1616

17+
import io.github.martchus.syncthingtray.Util;
18+
1719
public class Activity extends QtActivity {
1820

1921
public boolean performHapticFeedback() {
@@ -55,4 +57,8 @@ public boolean openPath(String path) {
5557
}
5658
return true;
5759
}
60+
61+
public String resolveUri(String uri) {
62+
return Util.getAbsolutePathFromStorageAccessFrameworkUri(this, Uri.parse(uri));
63+
}
5864
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* This code is based on com.nutomic.syncthingandroid.util from
3+
* https://github.com/Catfriend1/syncthing-android/blob/main/app/src/main/java/com/nutomic/syncthingandroid/util/FileUtils.java
4+
*/
5+
6+
package io.github.martchus.syncthingtray;
7+
8+
import android.annotation.SuppressLint;
9+
import android.content.Context;
10+
import android.net.Uri;
11+
import android.os.Environment;
12+
import android.os.storage.StorageManager;
13+
import android.provider.DocumentsContract;
14+
import android.util.Log;
15+
16+
import java.io.File;
17+
import java.lang.IllegalArgumentException;
18+
import java.lang.reflect.Array;
19+
import java.lang.reflect.Method;
20+
21+
public class Util {
22+
23+
private static final String TAG = "Util";
24+
private static final String DOWNLOADS_VOLUME_NAME = "downloads";
25+
private static final String PRIMARY_VOLUME_NAME = "primary";
26+
private static final String HOME_VOLUME_NAME = "home";
27+
28+
private Util() {
29+
}
30+
31+
public static String getAbsolutePathFromStorageAccessFrameworkUri(Context context, final Uri uri) {
32+
try {
33+
return getAbsolutePathFromUri(context, DocumentsContract.getTreeDocumentId(uri));
34+
} catch (IllegalArgumentException e) {
35+
return getAbsolutePathFromUri(context, DocumentsContract.getDocumentId(uri));
36+
}
37+
}
38+
39+
private static String getAbsolutePathFromUri(Context context, final String documentId) {
40+
// determine volumeId, e.g. "home", "documents"
41+
String volumeId = getVolumeIdFromDocument(documentId);
42+
if (volumeId == null) {
43+
return "";
44+
}
45+
46+
// handle Uri referring to internal or external storage.
47+
String volumePath = getVolumePath(volumeId, context);
48+
if (volumePath == null) {
49+
return File.separator;
50+
}
51+
if (volumePath.endsWith(File.separator)) {
52+
volumePath = volumePath.substring(0, volumePath.length() - 1);
53+
}
54+
String documentPath = getPathFromDocument(documentId);
55+
if (documentPath.endsWith(File.separator)) {
56+
documentPath = documentPath.substring(0, documentPath.length() - 1);
57+
}
58+
if (documentPath.length() > 0) {
59+
if (documentPath.startsWith(File.separator)) {
60+
return volumePath + documentPath;
61+
} else {
62+
return volumePath + File.separator + documentPath;
63+
}
64+
} else {
65+
return volumePath;
66+
}
67+
}
68+
69+
@SuppressLint("ObsoleteSdkInt")
70+
private static String getVolumePath(final String volumeId, Context context) {
71+
try {
72+
if (HOME_VOLUME_NAME.equals(volumeId)) {
73+
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath();
74+
}
75+
if (DOWNLOADS_VOLUME_NAME.equals(volumeId)) {
76+
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
77+
}
78+
79+
StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
80+
Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
81+
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
82+
Method getUuid = storageVolumeClazz.getMethod("getUuid");
83+
Method getPath = storageVolumeClazz.getMethod("getPath");
84+
Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
85+
Object result = getVolumeList.invoke(mStorageManager);
86+
87+
final int length = Array.getLength(result);
88+
for (int i = 0; i < length; i++) {
89+
Object storageVolumeElement = Array.get(result, i);
90+
String uuid = (String) getUuid.invoke(storageVolumeElement);
91+
Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
92+
Boolean isPrimaryVolume = (primary && PRIMARY_VOLUME_NAME.equals(volumeId));
93+
Boolean isExternalVolume = ((uuid != null) && uuid.equals(volumeId));
94+
Log.d(TAG, "Found volume with uuid='" + uuid +
95+
"', volumeId='" + volumeId +
96+
"', primary=" + primary +
97+
", isPrimaryVolume=" + isPrimaryVolume +
98+
", isExternalVolume=" + isExternalVolume
99+
);
100+
if (isPrimaryVolume || isExternalVolume) {
101+
return (String) getPath.invoke(storageVolumeElement);
102+
}
103+
}
104+
} catch (Exception e) {
105+
Log.w(TAG, "Ran into exception when determining volume path for " + volumeId, e);
106+
}
107+
Log.d(TAG, "Unable to determine volume path for " + volumeId);
108+
if (PRIMARY_VOLUME_NAME.equals(volumeId)) {
109+
return Environment.getExternalStorageDirectory().getAbsolutePath();
110+
}
111+
return "/storage/" + volumeId;
112+
}
113+
114+
private static String getVolumeIdFromDocument(final String documentId) {
115+
final String[] split = documentId.split(":");
116+
return split.length > 0 ? split[0] : null;
117+
}
118+
119+
private static String getPathFromDocument(final String documentId) {
120+
final String[] split = documentId.split(":");
121+
return ((split.length >= 2) && (split[1] != null)) ? split[1] : File.separator;
122+
}
123+
}

tray/gui/app.cpp

+9
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,16 @@ bool App::showToast(const QString &message)
328328

329329
QString App::resolveUrl(const QUrl &url)
330330
{
331+
#ifdef Q_OS_ANDROID
332+
const auto urlString = url.toString(QUrl::FullyEncoded);
333+
const auto path = QJniObject(QNativeInterface::QAndroidApplication::context()).callMethod<jstring>("resolveUri", urlString).toString();
334+
if (path.isEmpty()) {
335+
showToast(tr("Unable to resolve URL \"%1\".").arg(urlString));
336+
}
337+
return path.isEmpty() ? urlString : path;
338+
#else
331339
return url.path();
340+
#endif
332341
}
333342

334343
void App::invalidateStatus()

0 commit comments

Comments
 (0)