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

Implement proper permission handling using SAF (fixes #1160) #1170

Merged
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4af6553
add comment
Catfriend1 Jun 24, 2018
473b91a
WIP
Catfriend1 Jun 24, 2018
e77c053
WIP
Catfriend1 Jun 24, 2018
70e3f8e
revert unneccessary RestApi changes
Catfriend1 Jun 24, 2018
98543cd
WIP
Catfriend1 Jun 24, 2018
fc4141a
WIP
Catfriend1 Jun 24, 2018
325aaa2
WIP
Catfriend1 Jun 24, 2018
74cd321
WIP - UriUtil
Catfriend1 Jun 24, 2018
ee7e3bd
WIP
Catfriend1 Jun 24, 2018
1f34e68
WIP
Catfriend1 Jun 24, 2018
220e908
WIP
Catfriend1 Jun 24, 2018
bc3edec
WIP - using reflection
Catfriend1 Jun 24, 2018
93d42e5
WIP
Catfriend1 Jun 24, 2018
c30b803
address https://github.com/syncthing/syncthing-android/issues/1160
Catfriend1 Jun 24, 2018
22f6a51
WIP
Catfriend1 Jun 24, 2018
2b19402
remove debug folder creation
Catfriend1 Jun 24, 2018
00f32c9
add constant for .stfolder marker
Catfriend1 Jun 24, 2018
96ad90a
WIP - canWrite test
Catfriend1 Jun 25, 2018
f1f427b
WIP - proper permission detection (fixes #1160)
Catfriend1 Jun 25, 2018
5eafc22
add Util#runShellCommand
Catfriend1 Jun 26, 2018
75a8d05
correct FolderActivity UI flow
Catfriend1 Jun 26, 2018
3a978ad
fix NPE in onCreate
Catfriend1 Jun 26, 2018
647b511
Explain why we can or cannot have folder write access
Catfriend1 Jun 26, 2018
f287ec0
Reorder accessExplanationView
Catfriend1 Jun 26, 2018
029f039
Safety check - don't allow selection of root directory /
Catfriend1 Jun 26, 2018
bd2e6bb
Show helpful message to the user if an invalid location has
Catfriend1 Jun 26, 2018
a5634a7
Fix string comparison
Catfriend1 Jun 26, 2018
e2eacef
WIP
Catfriend1 Jun 29, 2018
9906514
WIP - add documents folder selection handling
Catfriend1 Jun 29, 2018
c786e35
WIP - better way to get the case of /.../documents/... right
Catfriend1 Jun 29, 2018
6ce1793
WIP
Catfriend1 Jun 30, 2018
b534eea
WIP
Catfriend1 Jun 30, 2018
89d74c1
Show popup on entrance of the SAF UI to inform the user
Catfriend1 Jun 30, 2018
891e9e1
Merge branch 'master' of https://github.com/syncthing/syncthing-andro…
Catfriend1 Jul 1, 2018
32d2b3a
WIP
Catfriend1 Jul 1, 2018
9ee829a
Fix UI glitch after an invalid folder location has been selected
Catfriend1 Jul 1, 2018
67492bc
Only create .stfolder for "sendonly" folder
Jul 5, 2018
fdbc45b
Add constants for folder types
Jul 5, 2018
b3bf0b5
Add import of Constants
Catfriend1 Jul 5, 2018
463d9cc
Review: Update folder type constants for v0.14.49
Catfriend1 Jul 5, 2018
bb2de5c
Merge branch 'master' of https://github.com/syncthing/syncthing-andro…
Catfriend1 Jul 8, 2018
02ee891
Merge branch 'master' of https://github.com/syncthing/syncthing-andro…
Catfriend1 Jul 8, 2018
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.nutomic.syncthingandroid.activities;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
Expand All @@ -8,6 +10,7 @@
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.provider.DocumentFile;
import android.support.v7.widget.SwitchCompat;
import android.text.Editable;
import android.text.TextUtils;
Expand All @@ -31,6 +34,7 @@
import com.nutomic.syncthingandroid.model.Folder;
import com.nutomic.syncthingandroid.service.RestApi;
import com.nutomic.syncthingandroid.service.SyncthingService;
import com.nutomic.syncthingandroid.util.FileUtils;
import com.nutomic.syncthingandroid.util.TextWatcherAdapter;
import com.nutomic.syncthingandroid.util.Util;

Expand Down Expand Up @@ -63,20 +67,25 @@ public class FolderActivity extends SyncthingActivity
public static final String EXTRA_DEVICE_ID =
"com.nutomic.syncthingandroid.activities.FolderActivity.DEVICE_ID";

private static final String TAG = "EditFolderFragment";
private static final String TAG = "FolderActivity";

private static final String IS_SHOWING_DELETE_DIALOG = "DELETE_FOLDER_DIALOG_STATE";
private static final String IS_SHOW_DISCARD_DIALOG = "DISCARD_FOLDER_DIALOG_STATE";

private static final int FILE_VERSIONING_DIALOG_REQUEST = 3454;
private static final int CHOOSE_FOLDER_REQUEST = 3459;

private static final String FOLDER_MARKER_NAME = ".stfolder";
private static final String IGNORE_FILE_NAME = ".stignore";

private Folder mFolder;
// Contains SAF readwrite access URI on API level >= Build.VERSION_CODES.LOLLIPOP (21)
private Uri mFolderUri = null;

private EditText mLabelView;
private EditText mIdView;
private TextView mPathView;
private TextView mAccessExplanationView;
private SwitchCompat mFolderMasterView;
private SwitchCompat mFolderFileWatcher;
private SwitchCompat mFolderPaused;
Expand All @@ -97,8 +106,8 @@ public class FolderActivity extends SyncthingActivity
@Override
public void afterTextChanged(Editable s) {
mFolder.label = mLabelView.getText().toString();
mFolder.id = mIdView.getText().toString();
mFolder.path = mPathView.getText().toString();
mFolder.id = mIdView.getText().toString();;
// mPathView must not be handled here as it's handled by {@link onActivityResult}
mFolderNeedsToUpdate = true;
}
};
Expand Down Expand Up @@ -145,6 +154,7 @@ public void onCreate(Bundle savedInstanceState) {
mLabelView = findViewById(R.id.label);
mIdView = findViewById(R.id.id);
mPathView = findViewById(R.id.directoryTextView);
mAccessExplanationView = findViewById(R.id.accessExplanationView);
mFolderMasterView = findViewById(R.id.master);
mFolderFileWatcher = findViewById(R.id.fileWatcher);
mFolderPaused = findViewById(R.id.folderPause);
Expand All @@ -153,8 +163,7 @@ public void onCreate(Bundle savedInstanceState) {
mDevicesContainer = findViewById(R.id.devicesContainer);
mEditIgnores = findViewById(R.id.edit_ignores);

mPathView.setOnClickListener(view ->
startActivityForResult(FolderPickerActivity.createIntent(this, mFolder.path, null), FolderPickerActivity.DIRECTORY_REQUEST_CODE));
mPathView.setOnClickListener(view -> onPathViewClick());

findViewById(R.id.versioningContainer).setOnClickListener(v -> showVersioningDialog());
mEditIgnores.setOnClickListener(v -> editIgnores());
Expand All @@ -174,7 +183,11 @@ public void onCreate(Bundle savedInstanceState) {
mEditIgnores.setEnabled(false);
}
else {
prepareEditMode();
// Prepare edit mode.
mIdView.clearFocus();
mIdView.setFocusable(false);
mIdView.setEnabled(false);
mPathView.setEnabled(false);
}

if (savedInstanceState != null){
Expand All @@ -190,6 +203,30 @@ public void onCreate(Bundle savedInstanceState) {
}
}

/**
* Invoked after user clicked on the {@link mPathView} label.
*/
@SuppressLint("InlinedAPI")
private void onPathViewClick() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
startActivityForResult(FolderPickerActivity.createIntent(this, mFolder.path, null),
FolderPickerActivity.DIRECTORY_REQUEST_CODE);
return;
}

// This has to be android.net.Uri as it implements a Parcelable.
android.net.Uri externalFilesDirUri = FileUtils.getExternalFilesDirUri(FolderActivity.this);

// Display storage access framework directory picker UI.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
if (externalFilesDirUri != null) {
intent.putExtra("android.provider.extra.INITIAL_URI", externalFilesDirUri);
}
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
intent.putExtra("android.content.extra.SHOW_ADVANCED", true);
startActivityForResult(intent, CHOOSE_FOLDER_REQUEST);
}

private void editIgnores() {
try {
File ignoreFile = new File(mFolder.path, IGNORE_FILE_NAME);
Expand Down Expand Up @@ -241,7 +278,6 @@ public void onDestroy() {
}
mLabelView.removeTextChangedListener(mTextWatcher);
mIdView.removeTextChangedListener(mTextWatcher);
mPathView.removeTextChangedListener(mTextWatcher);
}

@Override
Expand Down Expand Up @@ -297,6 +333,7 @@ public void onServiceStateChange(SyncthingService.State currentState) {
finish();
return;
}
checkWriteAndUpdateUI();
}
if (getIntent().hasExtra(EXTRA_DEVICE_ID)) {
mFolder.addDevice(getIntent().getStringExtra(EXTRA_DEVICE_ID));
Expand All @@ -321,15 +358,13 @@ private void attemptToApplyVersioningConfig() {
private void updateViewsAndSetListeners() {
mLabelView.removeTextChangedListener(mTextWatcher);
mIdView.removeTextChangedListener(mTextWatcher);
mPathView.removeTextChangedListener(mTextWatcher);
mFolderMasterView.setOnCheckedChangeListener(null);
mFolderFileWatcher.setOnCheckedChangeListener(null);
mFolderPaused.setOnCheckedChangeListener(null);

// Update views
mLabelView.setText(mFolder.label);
mIdView.setText(mFolder.id);
mPathView.setText(mFolder.path);
updateVersioningDescription();
mFolderMasterView.setChecked(Objects.equal(mFolder.type, "readonly"));
mFolderFileWatcher.setChecked(mFolder.fsWatcherEnabled);
Expand All @@ -348,7 +383,6 @@ private void updateViewsAndSetListeners() {
// Keep state updated
mLabelView.addTextChangedListener(mTextWatcher);
mIdView.addTextChangedListener(mTextWatcher);
mPathView.addTextChangedListener(mTextWatcher);
mFolderMasterView.setOnCheckedChangeListener(mCheckedListener);
mFolderFileWatcher.setOnCheckedChangeListener(mCheckedListener);
mFolderPaused.setOnCheckedChangeListener(mCheckedListener);
Expand Down Expand Up @@ -381,7 +415,21 @@ public boolean onOptionsItemSelected(MenuItem item) {
.show();
return true;
}
getApi().addFolder(mFolder);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
mFolderUri != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we only do this for send only folders?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we pre-check if syncthing can write to a folder and only then, the user can move the slider to enable sendreceive mode. If syncthing can write itself, it will create .stfolder correctly. So it is sufficient to do the creation of .stfolder from the java code only for send only folders as you suggested. I'll commit that shortly.

/**
* Normally, syncthing takes care of creating the ".stfolder" marker.
* This fails on newer android versions if the syncthing binary only has
* readonly access on the path and the user tries to configure a
* sendonly folder. To fix this, we'll precreate the marker using java code.
*/
DocumentFile dfFolder = DocumentFile.fromTreeUri(this, mFolderUri);
if (dfFolder != null) {
Log.v(TAG, "Creating new directory " + mFolder.path + File.separator + FOLDER_MARKER_NAME);
dfFolder.createDirectory(FOLDER_MARKER_NAME);
}
}
getApi().createFolder(mFolder);
finish();
return true;
case R.id.remove:
Expand Down Expand Up @@ -417,16 +465,75 @@ private Dialog createDeleteDialog(){

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK && requestCode == FolderPickerActivity.DIRECTORY_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK && requestCode == CHOOSE_FOLDER_REQUEST) {
// This result case only occurs on API level >= Build.VERSION_CODES.LOLLIPOP (21)
mFolderUri = data.getData();
if (mFolderUri == null) {
return;
}
// Get the folder path unix style, e.g. "/storage/0000-0000/DCIM"
String targetPath = FileUtils.getAbsolutePathFromSAFUri(FolderActivity.this, mFolderUri);
if (targetPath != null) {
targetPath = Util.formatPath(targetPath);
}
if (targetPath == null || TextUtils.isEmpty(targetPath) || (targetPath.equals(File.separator))) {
mFolder.path = "";
mFolderUri = null;
checkWriteAndUpdateUI();
// Show message to the user suggesting to select a folder on internal or external storage.
Toast.makeText(this, R.string.toast_invalid_folder_selected, Toast.LENGTH_LONG).show();
return;
}
mFolder.path = FileUtils.cutTrailingSlash(targetPath);
Log.v(TAG, "onActivityResult/CHOOSE_FOLDER_REQUEST: Got directory path '" + mFolder.path + "'");
checkWriteAndUpdateUI();
// Postpone sending the config changes using syncthing REST API.
mFolderNeedsToUpdate = true;
} else if (resultCode == Activity.RESULT_OK && requestCode == FolderPickerActivity.DIRECTORY_REQUEST_CODE) {
mFolder.path = data.getStringExtra(FolderPickerActivity.EXTRA_RESULT_DIRECTORY);
mPathView.setText(mFolder.path);
checkWriteAndUpdateUI();
// Postpone sending the config changes using syncthing REST API.
mFolderNeedsToUpdate = true;
mEditIgnores.setEnabled(true);
} else if (resultCode == Activity.RESULT_OK && requestCode == FILE_VERSIONING_DIALOG_REQUEST) {
updateVersioning(data.getExtras());
}
}

/**
* Prerequisite: mFolder.path must be non-empty
*/
private void checkWriteAndUpdateUI() {
mPathView.setText(mFolder.path);
if (TextUtils.isEmpty(mFolder.path)) {
return;
}

/**
* Check if the permissions we have on that folder is readonly or readwrite.
* Access level readonly: folder can only be configured "sendonly".
* Access level readwrite: folder can be configured "sendonly" or "sendreceive".
*/
Boolean canWrite = Util.nativeBinaryCanWriteToPath(FolderActivity.this, mFolder.path);
if (canWrite) {
/**
* Suggest "readwrite" folder because the user most probably
* intentionally chose a special folder like
* "[storage]/Android/data/com.nutomic.syncthingandroid/files"
* or enabled root mode thus having write access.
*/
mAccessExplanationView.setText(R.string.folder_path_readwrite);
mFolderMasterView.setChecked(false);
mFolderMasterView.setEnabled(true);
mEditIgnores.setEnabled(true);
} else {
// Force "sendonly" folder.
mAccessExplanationView.setText(R.string.folder_path_readonly);
mFolderMasterView.setChecked(true);
mFolderMasterView.setEnabled(false);
mEditIgnores.setEnabled(false);
}
}

private String generateRandomFolderId() {
char[] chars = "abcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
StringBuilder sb = new StringBuilder();
Expand Down Expand Up @@ -457,13 +564,6 @@ private void initFolder() {
mFolder.versioning = new Folder.Versioning();
}

private void prepareEditMode() {
mIdView.clearFocus();
mIdView.setFocusable(false);
mIdView.setEnabled(false);
mPathView.setEnabled(false);
}

private void addEmptyDeviceListView() {
int height = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(WRAP_CONTENT, height);
Expand All @@ -489,7 +589,11 @@ private void addDeviceViewAndSetListener(Device device, LayoutInflater inflater)

private void updateFolder() {
if (!mIsCreateMode) {
getApi().editFolder(mFolder);
/**
* RestApi is guaranteed not to be null as {@link onServiceStateChange}
* immediately finishes this activity if SyncthingService shuts down.
*/
getApi().updateFolder(mFolder);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,14 +281,20 @@ public List<Folder> getFolders() {
return folders;
}

public void addFolder(Folder folder) {
/**
* This is only used for new folder creation, see {@link FolderActivity}.
*/
public void createFolder(Folder folder) {
// Add the new folder to the model.
mConfig.folders.add(folder);
// Send model changes to syncthing, does not require a restart.
sendConfig();
}

public void editFolder(Folder newFolder) {
public void updateFolder(Folder newFolder) {
removeFolderInternal(newFolder.id);
addFolder(newFolder);
mConfig.folders.add(newFolder);
sendConfig();
}

public void removeFolder(String id) {
Expand Down
Loading