Skip to content
Merged
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
27 changes: 27 additions & 0 deletions app/src/main/java/be/ppareit/swiftp/FsSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.List;

import be.ppareit.swiftp.server.FtpUser;
import be.ppareit.swiftp.utils.FileUtil;

public class FsSettings {

Expand Down Expand Up @@ -110,6 +111,31 @@ public static boolean allowAnonymous() {
}

public static File getDefaultChrootDir() {
// Get the path from the app's MANAGE USERS chroot folder UI text field that the user will use during setup.
String subFix = null;
if (Util.useScopedStorage()) {
// The app's MANAGE USERS chroot folder UI selection cannot select the sd card at least on Android 11+.
// The picker does all that's needed there so that use should be switched with ADVANCED SETTINGS >
// WRITE EXTERNAL picker or just make it invisible when on A11+ ? Could also just pull open the same
// picker on both with A11+ or something else. It also presents possible conflicts with the Uri path
// eg "/storage/sd card/" verses "/sd card/Test/".
String s = FileUtil.cleanupUriStoragePath(FileUtil.getTreeUri());
if (s != null && !s.contains("primary:")) {
final String chroot = FileUtil.getSdCardBaseFolderScopedStorage();
// Need to return eg "/storage" for sd card and "/storage/emulated/0" for internal.
if (chroot != null && !chroot.isEmpty()) return new File(chroot);
// otherwise just get the other path from below.
} else if (s != null && s.contains("primary:")) {
// Fix for issue seen on Android 8.0:
// Had to implement over below as the below chroot is forced to
// getExternalStorageDirectory() when actual chroot may include further sub dirs.
subFix = s.replace("primary:", "");
// At the moment, have to do it below, as a StackOverflow is happening on the test device
// here with any additional code for an unknown reason.
}
}

// Original below incorrectly returns "/storage/emulated/0" for sd card with Android 11+
File chrootDir;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
chrootDir = Environment.getExternalStorageDirectory();
Expand All @@ -123,6 +149,7 @@ public static File getDefaultChrootDir() {
// but this will probably not be what the user wants
return App.getAppContext().getFilesDir();
}
if (subFix != null) return new File(chrootDir, subFix);
return chrootDir;
}

Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/be/ppareit/swiftp/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
package be.ppareit.swiftp;

import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;

Expand All @@ -34,6 +36,8 @@

abstract public class Util {
final static String TAG = Util.class.getSimpleName();
private static SharedPreferences sp = null;
private static boolean overrideSDKVer = false;

public static byte byteOfInt(int value, int which) {
int shift = which * 8;
Expand Down Expand Up @@ -108,4 +112,18 @@ public static Date parseDate(String time) throws ParseException {
SimpleDateFormat df = createSimpleDateFormat();
return df.parse(time);
}

/*
* Implemented mainly for Android 11+ where its forced but can work on earlier Android versions.
* 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;
}

public static void reGetStorageOverride() {
sp = null;
}
}
119 changes: 110 additions & 9 deletions app/src/main/java/be/ppareit/swiftp/gui/PreferenceFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.UriPermission;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.Uri;
Expand All @@ -36,6 +38,7 @@
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.preference.TwoStatePreference;
import android.text.util.Linkify;
Expand All @@ -44,9 +47,11 @@

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.documentfile.provider.DocumentFile;

import net.vrallev.android.cat.Cat;

import java.io.File;
import java.net.InetAddress;
import java.util.List;

Expand All @@ -55,7 +60,9 @@
import be.ppareit.swiftp.FsService;
import be.ppareit.swiftp.FsSettings;
import be.ppareit.swiftp.R;
import be.ppareit.swiftp.Util;
import be.ppareit.swiftp.server.FtpUser;
import be.ppareit.swiftp.utils.FileUtil;

/**
* This is the main activity for swiftp, it enables the user to start the server service
Expand Down Expand Up @@ -248,26 +255,120 @@ public void onPause() {
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);
}
}

final CheckBoxPreference writeExternalStoragePref = findPref("writeExternalStorage");
if (!":".equals(path.substring(path.length() - 1)) || path.contains("primary")) {
writeExternalStoragePref.setChecked(false);
} else {
FsSettings.setExternalStorageUri(treeUri.toString());
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) {
getActivity().getContentResolver()
.takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (removeAllUriPermissions(treeUri)) {
FsSettings.setExternalStorageUri(treeUri.toString());
getActivity().getContentResolver()
.takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
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 = "";
}
}
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();
}
}
}

/*
* 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 a11Path = FileUtil.getUriStoragePathFullFromDocumentFile(df, "");
if (a11Path == 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(), a11Path);
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.
* When there is one user, display helpful message about user/password. When there are
Expand Down
41 changes: 35 additions & 6 deletions app/src/main/java/be/ppareit/swiftp/gui/UserEditFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@

import android.app.AlertDialog;
import android.app.Fragment;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import android.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
Expand All @@ -16,8 +20,10 @@

import java.io.File;

import be.ppareit.swiftp.App;
import be.ppareit.swiftp.FsSettings;
import be.ppareit.swiftp.R;
import be.ppareit.swiftp.Util;
import be.ppareit.swiftp.server.FtpUser;

public class UserEditFragment extends Fragment {
Expand Down Expand Up @@ -45,9 +51,19 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
chroot.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus)
return;
showFolderPicker(chroot);
if (Util.useScopedStorage()) {
alertUsersToScopedStorage();
} else {
showFolderPicker(chroot);
}
});
chroot.setOnClickListener(v -> {
if (Util.useScopedStorage()) {
alertUsersToScopedStorage();
} else {
showFolderPicker(chroot);
}
});
chroot.setOnClickListener(view -> showFolderPicker(chroot));

if (item != null) {
username.setText(item.getUsername());
Expand Down Expand Up @@ -81,10 +97,15 @@ private void showFolderPicker(TextView chrootView) {
AlertDialog folderPicker = new FolderPickerDialogBuilder(getActivity(), startDir)
.setSelectedButton(R.string.select, path -> {
final File root = new File(path);
if (!root.canRead()) {
showToast(R.string.notice_cant_read_write);
} else if (!root.canWrite()) {
showToast(R.string.notice_cant_write);
if (!root.canRead() || !root.canWrite()) {
Log.e("pre", "using scoped...");
alertUsersToScopedStorage();
} else {
Log.e("pre", "FILE CAN READ & WRITE...");
// Disable the storage minimum override if eg internal use has rw capability over sd card.
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(App.getAppContext());
sp.edit().remove("OverrideScopedStorageMinimum").apply();
Util.reGetStorageOverride();
}
chrootView.setText(path);
})
Expand Down Expand Up @@ -113,4 +134,12 @@ private void showToast(int errorResId) {
interface OnEditFinishedListener {
void onEditActionFinished(FtpUser oldItem, FtpUser newItem);
}

// Android 11+ must use the other picker only. Alert users to go there.
// eg Android 8.0 sd card may be required also determined by check above.
private void alertUsersToScopedStorage() {
final String s = getString(R.string.advanced_settings_label) + " > " +
getString(R.string.writeExternalStorage_label);
Toast.makeText(App.getAppContext(), s, Toast.LENGTH_SHORT).show();
}
}
Loading