Skip to content

Commit 259901c

Browse files
committed
Add Android Q Support
Signed-off-by: Fung Gwo <fython@163.com>
1 parent 3cb499c commit 259901c

7 files changed

Lines changed: 213 additions & 6 deletions

File tree

app/src/main/java/app/gwo/safenhancer/lite/ProxyCameraActivity.java

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import android.content.ComponentName;
77
import android.content.Context;
88
import android.content.Intent;
9+
import android.content.pm.PackageInfo;
10+
import android.content.pm.PackageManager;
911
import android.net.Uri;
1012
import android.os.Build;
1113
import android.os.Bundle;
@@ -16,10 +18,14 @@
1618
import android.view.LayoutInflater;
1719
import android.view.View;
1820

19-
import java.util.Objects;
21+
import java.io.UnsupportedEncodingException;
22+
import java.net.URLDecoder;
2023

2124
import androidx.annotation.NonNull;
2225
import androidx.annotation.Nullable;
26+
import androidx.documentfile.provider.DocumentFile;
27+
import app.gwo.safenhancer.lite.compat.Optional;
28+
import app.gwo.safenhancer.lite.util.BuildUtils;
2329
import app.gwo.safenhancer.lite.util.DumpUtils;
2430
import app.gwo.safenhancer.lite.util.Settings;
2531
import moe.shizuku.redirectstorage.RedirectPackageInfo;
@@ -91,7 +97,10 @@ private void getExtrasFromCaptureIntent(@NonNull Intent intent) {
9197

9298
if (intent.hasExtra(MediaStore.EXTRA_OUTPUT)) {
9399
mExpectedOutput = intent.getParcelableExtra(MediaStore.EXTRA_OUTPUT);
94-
String referrerPackage = getReferrerPackage();
100+
final String referrerPackage = getReferrerPackage();
101+
102+
boolean isolatedStoragePathProceed = false;
103+
95104
if (mExpectedOutput != null
96105
&& "file".equals(mExpectedOutput.getScheme())
97106
&& referrerPackage != null
@@ -123,6 +132,7 @@ && checkSelfPermission(StorageRedirectManager.PERMISSION) == PERMISSION_GRANTED
123132
mExpectedOutput = Uri.parse(
124133
originalPath.replace(externalRoot, newExternalRoot)
125134
);
135+
isolatedStoragePathProceed = true;
126136
Log.d(TAG, "Original path: " + originalPath + ", external root: " +
127137
externalRoot + ", redirect target: " + rpi.redirectTarget +
128138
", after: " + mExpectedOutput);
@@ -136,6 +146,67 @@ && checkSelfPermission(StorageRedirectManager.PERMISSION) == PERMISSION_GRANTED
136146
}
137147
}
138148

149+
if (mExpectedOutput != null
150+
&& "file".equals(mExpectedOutput.getScheme())
151+
&& referrerPackage != null
152+
&& BuildUtils.isAtLeastQ()
153+
&& !isolatedStoragePathProceed) {
154+
// TODO Check LEGACY_STORAGE app ops
155+
final Uri rootUri = Settings.getInstance().getRootStorageUri().get();
156+
if (rootUri == null) {
157+
// TODO Show warning
158+
Log.w(TAG, "Android Q+ cannot work without Storage Access Framework API.");
159+
} else {
160+
DocumentFile rootFile = DocumentFile.fromTreeUri(this, rootUri);
161+
DocumentFile sandboxRoot = Optional.ofNullable(rootFile)
162+
.map(file -> file.findFile("Android"))
163+
.filter(DocumentFile::isDirectory)
164+
.map(file -> file.findFile("sandbox"))
165+
.filter(DocumentFile::isDirectory)
166+
.map(file -> {
167+
String sandboxDirName = referrerPackage;
168+
try {
169+
PackageInfo pi = getPackageManager()
170+
.getPackageInfo(referrerPackage, 0);
171+
if (pi.sharedUserId != null) {
172+
sandboxDirName = "shared-" + pi.sharedUserId;
173+
}
174+
} catch (PackageManager.NameNotFoundException e) {
175+
e.printStackTrace();
176+
}
177+
return file.findFile(sandboxDirName);
178+
})
179+
.filter(DocumentFile::isDirectory)
180+
.get();
181+
if (sandboxRoot == null) {
182+
sandboxRoot = rootFile;
183+
}
184+
String externalRoot = Environment.getExternalStorageDirectory()
185+
.getAbsolutePath();
186+
String originalPath = mExpectedOutput.toString();
187+
try {
188+
originalPath = URLDecoder.decode(originalPath, "UTF-8");
189+
} catch (UnsupportedEncodingException e) {
190+
e.printStackTrace();
191+
}
192+
originalPath = originalPath.replace("file://" + externalRoot + "/", "");
193+
String[] pathSegs = Optional.ofNullable(originalPath.split("/"))
194+
.orElse(new String[0]);
195+
Optional<DocumentFile> curFile = Optional.ofNullable(sandboxRoot);
196+
for (int i = 0; i < pathSegs.length; i++) {
197+
String path = pathSegs[i];
198+
curFile = curFile.map(file -> file.findFile(path));
199+
if (i != pathSegs.length - 1) {
200+
curFile = curFile.filter(DocumentFile::isDirectory);
201+
if (!curFile.isPresent()) {
202+
break;
203+
}
204+
}
205+
}
206+
mExpectedOutput = curFile.map(DocumentFile::getUri).orElse(mExpectedOutput);
207+
}
208+
}
209+
139210
Log.d(TAG, "Expected output path: " + mExpectedOutput);
140211
}
141212
}

app/src/main/java/app/gwo/safenhancer/lite/SettingsActivity.java

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package app.gwo.safenhancer.lite;
22

33
import android.Manifest;
4+
import android.annotation.SuppressLint;
5+
import android.app.AlertDialog;
46
import android.content.Intent;
57
import android.content.pm.PackageInfo;
68
import android.content.pm.PackageManager;
@@ -10,14 +12,15 @@
1012
import android.preference.Preference;
1113
import android.preference.PreferenceFragment;
1214
import android.preference.SwitchPreference;
13-
import android.util.Log;
15+
import android.provider.DocumentsContract;
1416
import android.widget.Toast;
1517

16-
import java.util.Arrays;
1718
import java.util.List;
1819

1920
import androidx.annotation.NonNull;
2021
import androidx.annotation.Nullable;
22+
import app.gwo.safenhancer.lite.compat.Optional;
23+
import app.gwo.safenhancer.lite.util.BuildUtils;
2124
import app.gwo.safenhancer.lite.util.Settings;
2225
import moe.shizuku.redirectstorage.StorageRedirectManager;
2326

@@ -54,12 +57,16 @@ public static final class SettingsFragment extends PreferenceFragment {
5457
private static final String KEY_ABOUT_VERSION = "version";
5558
private static final String KEY_ABOUT_GITHUB = "github";
5659
private static final String KEY_SR_API_PERMISSION = "sr_api_permission";
60+
private static final String KEY_Q_ISOLATED_SUPPORT = "q_isolated_support";
5761

5862
private static final int REQUEST_CODE_SR_PERMISSION = 1;
63+
private static final int REQUEST_CODE_OPEN_ROOT_URI = 2;
5964

6065
private Preference mHandledAppsChoose;
6166
private SwitchPreference mSRPermission;
67+
private SwitchPreference mQIsolatedSupport;
6268

69+
@SuppressLint("InlinedApi")
6370
@Override
6471
public void onCreate(@Nullable Bundle savedInstanceState) {
6572
super.onCreate(savedInstanceState);
@@ -92,7 +99,16 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
9299
}, REQUEST_CODE_SR_PERMISSION);
93100
}
94101
} else {
95-
// TODO Jump to settings
102+
try {
103+
Intent intent = new Intent(
104+
android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
105+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
106+
intent.setData(Uri.fromParts("package",
107+
getActivity().getPackageName(), null));
108+
startActivity(intent);
109+
} catch (Exception e) {
110+
e.printStackTrace();
111+
}
96112
}
97113
return false;
98114
});
@@ -103,6 +119,46 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
103119
== PackageManager.PERMISSION_GRANTED
104120
);
105121

122+
mQIsolatedSupport = (SwitchPreference) findPreference(KEY_Q_ISOLATED_SUPPORT);
123+
mQIsolatedSupport.setEnabled(BuildUtils.isAtLeastQ());
124+
if (!BuildUtils.isAtLeastQ()) {
125+
mQIsolatedSupport.setSummary(
126+
R.string.isolated_storage_support_for_q_summary_disabled);
127+
}
128+
mQIsolatedSupport.setOnPreferenceChangeListener((pref, newValue) -> {
129+
boolean newBool = (boolean) newValue;
130+
if (newBool) {
131+
new AlertDialog.Builder(getActivity())
132+
.setTitle(R.string.isolated_storage_support_for_q_dialog_title)
133+
.setMessage(R.string.isolated_storage_support_for_q_dialog_message)
134+
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
135+
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
136+
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI,
137+
Uri.parse("content://" +
138+
"com.android.externalstorage.documents" +
139+
"/tree/primary%3A"));
140+
if (intent.resolveActivity(pm) != null) {
141+
startActivityForResult(intent, REQUEST_CODE_OPEN_ROOT_URI);
142+
} else {
143+
// TODO Show error
144+
}
145+
})
146+
.show();
147+
return false;
148+
} else {
149+
Settings.getInstance().setRootStorageUri(null);
150+
return true;
151+
}
152+
});
153+
final Optional<Uri> rootStorageUri = Settings.getInstance().getRootStorageUri();
154+
mQIsolatedSupport.setChecked(rootStorageUri.isPresent());
155+
if (rootStorageUri.isPresent()) {
156+
mQIsolatedSupport.setSummaryOn(getString(
157+
R.string.isolated_storage_support_for_q_summary_checked,
158+
rootStorageUri.get().toString()
159+
));
160+
}
161+
106162
Preference versionPref = findPreference(KEY_ABOUT_VERSION);
107163
String version = "Unknown";
108164
try {
@@ -129,6 +185,21 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
129185
final List<String> result = PackagesSelectorActivity.getResult(data);
130186
Settings.getInstance().setHandledApps(result);
131187
updateHandledAppsSummary(result.size());
188+
} else if (REQUEST_CODE_OPEN_ROOT_URI == requestCode
189+
&& RESULT_OK == resultCode
190+
&& data != null) {
191+
Uri uri = data.getData();
192+
if (uri != null) {
193+
getActivity().getContentResolver().takePersistableUriPermission(uri,
194+
Intent.FLAG_GRANT_READ_URI_PERMISSION |
195+
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
196+
Settings.getInstance().setRootStorageUri(uri);
197+
mQIsolatedSupport.setChecked(true);
198+
mQIsolatedSupport.setSummaryOn(getString(
199+
R.string.isolated_storage_support_for_q_summary_checked,
200+
uri.toString()
201+
));
202+
}
132203
}
133204
}
134205

app/src/main/java/app/gwo/safenhancer/lite/compat/Optional.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ public void ifPresent(@NonNull Consumer<T> callable) {
7070
}
7171
}
7272

73+
public boolean isPresent() {
74+
return value != null;
75+
}
76+
7377
@Nullable
7478
public T get() {
7579
return value;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package app.gwo.safenhancer.lite.util;
2+
3+
import android.os.Build;
4+
5+
public final class BuildUtils {
6+
7+
public static class VERSION_CODE {
8+
9+
public static int Q = 29;
10+
11+
}
12+
13+
public static boolean isAtLeastQ() {
14+
if (Build.VERSION.SDK_INT >= VERSION_CODE.Q) {
15+
return true;
16+
}
17+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && Build.VERSION.PREVIEW_SDK_INT > 0) {
18+
return true;
19+
}
20+
return false;
21+
}
22+
23+
}

app/src/main/java/app/gwo/safenhancer/lite/util/Settings.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import android.content.ComponentName;
44
import android.content.Context;
55
import android.content.SharedPreferences;
6+
import android.net.Uri;
67
import android.text.TextUtils;
78

89
import java.util.ArrayList;
@@ -14,13 +15,15 @@
1415
import androidx.annotation.Nullable;
1516
import app.gwo.safenhancer.lite.Constants;
1617
import app.gwo.safenhancer.lite.compat.CollectionsCompat;
18+
import app.gwo.safenhancer.lite.compat.Optional;
1719

1820
public final class Settings {
1921

2022
public static final String PREF_NAME = "settings";
2123

2224
public static final String KEY_PREFERRED_CAMERA = "preferred_camera";
2325
public static final String KEY_HANDLED_APPS = "handled_apps";
26+
public static final String KEY_ROOT_STORAGE_URI = "root_storage_uri";
2427

2528
private volatile static Settings sInstance = null;
2629

@@ -105,4 +108,17 @@ public void setHandledApps(@Nullable List<String> handledApps) {
105108
}
106109
}
107110

111+
@NonNull
112+
public Optional<Uri> getRootStorageUri() {
113+
return Optional.ofNullable(mPrefs.getString(KEY_ROOT_STORAGE_URI, null)).map(Uri::parse);
114+
}
115+
116+
public void setRootStorageUri(@Nullable Uri rootUri) {
117+
if (rootUri == null) {
118+
mPrefs.edit().remove(KEY_ROOT_STORAGE_URI).apply();
119+
} else {
120+
mPrefs.edit().putString(KEY_ROOT_STORAGE_URI, rootUri.toString()).apply();
121+
}
122+
}
123+
108124
}

app/src/main/res/values/strings.xml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,30 @@ If they use their own interface only, it will have no effect even if apps are ch
3030
If apps handled by SAFEnhancer is controlled by Storage Redirect and you cannot choose images
3131
normally, you need enable this feature.
3232
]]></string>
33+
<string name="isolated_storage_support_for_q">Enable Stock Isolated storage support</string>
34+
<string name="isolated_storage_support_for_q_summary_disabled"><![CDATA[
35+
This feature is enabled for Android Q+ devices only.
36+
]]></string>
37+
<string name="isolated_storage_support_for_q_summary_enabled"><![CDATA[
38+
If your device is running on Android Q+, you may need to enable this feature to fix problems
39+
that some apps cannot receive pictures normally.
40+
]]></string>
41+
<string name="isolated_storage_support_for_q_summary_checked"><![CDATA[
42+
You have enabled this feature. Current root documents tree uri: %s
43+
]]></string>
44+
<string name="isolated_storage_support_for_q_dialog_title">Enable support</string>
45+
<string name="isolated_storage_support_for_q_dialog_message"><![CDATA[
46+
Now we will open Documents UI to pick root storage. Please be sure to choose the real root storage!
47+
]]></string>
3348

3449
<string name="action_camera">Camera</string>
3550
<string name="action_documents">Documents</string>
3651

3752
<string name="about_title">About</string>
3853
<string name="version_format">%1$s (%2$d)</string>
3954
<string name="github_title">GitHub repository</string>
40-
<string name="github_url" translatable="false">https://github.com/fython-tools/DocUIProxy-Android</string>
55+
<string name="github_url" translatable="false"><![CDATA[
56+
https://github.com/fython-tools/DocUIProxy-Android
57+
]]></string>
4158

4259
</resources>

app/src/main/res/xml/settings_screen.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
android:title="@string/isolated_storage_support_for_sr"
2828
android:summary="@string/isolated_storage_support_for_sr_summary"/>
2929

30+
<SwitchPreference android:key="q_isolated_support"
31+
android:title="@string/isolated_storage_support_for_q"
32+
android:summaryOff="@string/isolated_storage_support_for_q_summary_enabled"
33+
android:summaryOn="@string/isolated_storage_support_for_q_summary_checked"/>
34+
3035
</PreferenceCategory>
3136

3237
<PreferenceCategory android:title="@string/about_title">

0 commit comments

Comments
 (0)