Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

easy fix to allow selecting the root SD card volume #195

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@ You should have received a copy of the GNU General Public License
along with SwiFTP. If not, see <http://www.gnu.org/licenses/>.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!--Permissions for the Android below 11 (R)-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--Permission for the Android 11 (R) and above-->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!--Android 10 only requires android:requestLegacyExternalStorage="true"-->

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
Expand Down
4 changes: 1 addition & 3 deletions app/src/main/java/be/ppareit/swiftp/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ public static Date parseDate(String time) throws ParseException {
* Uses an override for when File fails to work during a test as the user sets up the app.
* */
public static boolean useScopedStorage() {
if (sp == null) sp = PreferenceManager.getDefaultSharedPreferences(App.getAppContext());
overrideSDKVer = sp.getBoolean("OverrideScopedStorageMinimum", false);
return Build.VERSION.SDK_INT >= 30 || overrideSDKVer;
return false;
}

public static void reGetStorageOverride() {
Expand Down
172 changes: 44 additions & 128 deletions app/src/main/java/be/ppareit/swiftp/gui/PreferenceFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
Expand All @@ -41,12 +42,14 @@
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.preference.TwoStatePreference;
import android.provider.Settings;
import android.text.util.Linkify;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import androidx.documentfile.provider.DocumentFile;

import net.vrallev.android.cat.Cat;
Expand All @@ -70,10 +73,8 @@
*/
public class PreferenceFragment extends android.preference.PreferenceFragment {

private static final int ACCESS_COARSE_LOCATION_REQUEST_CODE = 14;
private static final int ACTION_OPEN_DOCUMENT_TREE = 42;
private static final int STORAGE_PERMISSION_CODE = 100;

private DynamicMultiSelectListPreference mAutoconnectListPref;
private Handler mHandler = new Handler();

@Override
Expand All @@ -85,12 +86,6 @@ public void onCreate(Bundle savedInstanceState) {
updateRunningState();
runningPref.setOnPreferenceChangeListener((preference, newValue) -> {
if ((Boolean) newValue) {
if (Util.useScopedStorage() && FsSettings.getExternalStorageUri() == null) {
// Seems like we are on a system that requires scoped storage,
// but scoped storage has not yet been configured, force it now
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, ACTION_OPEN_DOCUMENT_TREE);
}
FsService.start();
} else {
FsService.stop();
Expand Down Expand Up @@ -160,8 +155,18 @@ public void onCreate(Bundle savedInstanceState) {
}
writeExternalStoragePref.setOnPreferenceChangeListener((preference, newValue) -> {
if ((boolean) newValue) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, ACTION_OPEN_DOCUMENT_TREE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
//Android is 11(R) or above
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
Uri uri = Uri.fromParts("package", this.getActivity().getPackageName(), null);
intent.setData(uri);
startActivityForResult(intent, STORAGE_PERMISSION_CODE);
} else {
//Android is below 11(R)
String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE};
requestPermissions(permissions, STORAGE_PERMISSION_CODE);
}

return false;
} else {
FsSettings.setExternalStorageUri(null);
Expand Down Expand Up @@ -216,24 +221,30 @@ public void onCreate(Bundle savedInstanceState) {

}

@RequiresApi(api = Build.VERSION_CODES.M)
private void requestAccessCoarseLocationPermission() {
String[] permissions = new String[]{Manifest.permission.ACCESS_COARSE_LOCATION};
requestPermissions(permissions, ACCESS_COARSE_LOCATION_REQUEST_CODE);
}

@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == ACCESS_COARSE_LOCATION_REQUEST_CODE) {
if (permissions[0].equals(Manifest.permission.ACCESS_COARSE_LOCATION)
&& grantResults[0] == PackageManager.PERMISSION_DENIED) {
mAutoconnectListPref.getDialog().cancel();
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == STORAGE_PERMISSION_CODE) {
if (grantResults.length >= 2) {
//check each permission if granted or not
boolean write = grantResults[0] == PackageManager.PERMISSION_GRANTED;
boolean read = grantResults[1] == PackageManager.PERMISSION_GRANTED;
if (write && read) {
//External Storage Permission granted
final CheckBoxPreference writeExternalStoragePref = findPref("writeExternalStorage");
writeExternalStoragePref.setEnabled(false);
writeExternalStoragePref.setChecked(true);
} else {
//External Storage Permission denied...
Toast.makeText(this.getContext(), "Something went wrong !", Toast.LENGTH_LONG).show();
}
}
}
}


@Override
public void onResume() {
super.onResume();
Expand All @@ -259,121 +270,26 @@ public void onPause() {

@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
Cat.d("onActivityResult called");
if (requestCode == ACTION_OPEN_DOCUMENT_TREE && resultCode == Activity.RESULT_OK) {
if (resultData == null) return;
Uri treeUri = resultData.getData();
if (treeUri == null) return;
String path = treeUri.getPath();
Cat.d("Action Open Document Tree on path " + path);
// *************************************
// The order following here is critical. They must stay ordered as they are.
setPermissionToUseExternalStorage(treeUri);
tryToUpgradeToScopedStorage(treeUri);
scopedStorageChrootOverride(treeUri);
}
}

private void setPermissionToUseExternalStorage(Uri treeUri) {
final CheckBoxPreference writeExternalStoragePref = findPref("writeExternalStorage");
if (isNotExternalStorage(treeUri)) {
writeExternalStoragePref.setChecked(false);
} else {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (removeAllUriPermissions(treeUri)) {
FsSettings.setExternalStorageUri(treeUri.toString());
getActivity().getContentResolver()
.takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
//here we will handle the result of our intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
//Android is 11(R) or above
if (Environment.isExternalStorageManager()){
//Manage External Storage Permission is granted
final CheckBoxPreference writeExternalStoragePref = findPref("writeExternalStorage");
writeExternalStoragePref.setEnabled(false);
writeExternalStoragePref.setChecked(true);
} catch (SecurityException e) {
// Harden code against crash: May reach here by adding exact same picker location but
// being removed at same time.
}
}
}

private boolean isNotExternalStorage(Uri treeUri) {
String folder = FileUtil.cleanupUriStoragePath(treeUri);
if (folder != null && folder.contains(":")) {
// Just get rid of the "primary:" part to get what we want (the user selected path/folder)
try {
folder = folder.substring(folder.indexOf(":") + 1);
} catch (IndexOutOfBoundsException e) {
folder = "";
else{
//Manage External Storage Permission is denied....
Toast.makeText(this.getContext(), "Something went wrong !", Toast.LENGTH_LONG).show();
}
}
return folder == null || folder.isEmpty();
}

/*
* If user is on older SDK, check if File can rw and if not then move to newer storage use.
* As we don't know what older SDK will have a problem where or not.
* Could just assume with this use but a check is fast.
* */
private void tryToUpgradeToScopedStorage(Uri treeUri) {
if (!Util.useScopedStorage()) {
DocumentFile df = FileUtil.getDocumentFileFromUri(treeUri);
if (df == null) return;
final String a11Path = FileUtil.getUriStoragePathFullFromDocumentFile(df, "");
if (a11Path == null) return;
File root = new File(a11Path);
if (!root.canRead() || !root.canWrite()) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(App.getAppContext());
// Fix: Use commit; override in next method using useScopedStorage() needs it.
sp.edit().putBoolean("OverrideScopedStorageMinimum", true).commit();
}
else{
//Android is below 11(R)
}
}

/*
* Override all and put path as chroot. Previously user chosen. On Android 11+ it is only
* decided now by the picker if its to be allowed on the Play Store unless allowance was made.
* */
private void scopedStorageChrootOverride(Uri treeUri) {
if (Util.useScopedStorage()) {
DocumentFile df = FileUtil.getDocumentFileFromUri(treeUri);
if (df == null) return;
final String scopedStoragePath = FileUtil.getUriStoragePathFullFromDocumentFile(df, "");
if (scopedStoragePath == null) return;
List<FtpUser> userList = FsSettings.getUsers();
for (int i = 0; i < userList.size(); i++) {
if (userList.get(i) == null) continue;
FtpUser entry = new FtpUser(userList.get(i).getUsername(), userList.get(i).getPassword(), scopedStoragePath);
FsSettings.modifyUser(userList.get(i).getUsername(), entry);
}
}
}

/*
* Clean up URI list since there's only one folder. They have a way of collecting on changes
* which causes an issue. More so only can use one.
* */
private boolean removeAllUriPermissions(Uri treeUri) {
List<UriPermission> oldList = App.getAppContext().getContentResolver().getPersistedUriPermissions();
if (oldList.size() == 0) return true;
// check against current and don't remove if only and same as it won't re-give same.
if (oldList.size() == 1) {
Uri uri = oldList.get(0).getUri();
if (uri != null) {
final String path = uri.getPath();
if (path != null) if (path.equals(treeUri.getPath())) return false;
}
}
// Release all
for (UriPermission uriToRemove : oldList) {
if (uriToRemove == null) continue;
getActivity().getContentResolver()
.releasePersistableUriPermission(uriToRemove.getUri(),
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
return true;
}

/**
* Update the summary for the users. When there are no users, ask to add at least one user.
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.0'
classpath 'com.android.tools.build:gradle:8.0.2'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
Expand Down