1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2025-01-07 10:42:07 +00:00

Device create/edit dialog: Check address input, fix losing changes on screen rotation (#270)

* Add comments

* Add model/Device#checkDeviceAddresses

* Add checkDeviceAddresses to device create dialog

* FolderActivity: Consolidate createDiscardDialog into showDiscardDialog

* folder_settings: R.id.create => R.id.save

* device_settings: R.id.create => R.id.save

* Edit device dialog: Offer back+discard and save action

* Add deviceNeedsToUpdate to savedInstanceState (fixes #271)

Remove dependency SyncthingService.OnServiceStateChangeListener

* Fix folder settings validation doesn't take place when editing folder (fixes #273) (fixes #265)

* Conditional label of the create/save button

* Remove workaround for rotation: mVersioning

* Fix folder.type is reset from SendOnly to Send&Receive on recreation of FolderActivity while creating a new folder (fixes #274)

* Fix typo

* Fix typo

* Fix typo

* Review - Relocate code

* Move null checks to the beginning of onSave

* Updated de translation

* Add ListenAddressesChanged to EventProcessor

as unhandled event.
This commit is contained in:
Catfriend1 2019-01-24 20:37:45 +00:00 committed by GitHub
parent b2864cc3c5
commit 56455fc89d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 327 additions and 243 deletions

View file

@ -53,9 +53,7 @@ import static com.nutomic.syncthingandroid.util.Compression.METADATA;
/**
* Shows device details and allows changing them.
*/
public class DeviceActivity extends SyncthingActivity
implements
SyncthingService.OnServiceStateChangeListener {
public class DeviceActivity extends SyncthingActivity {
public static final String EXTRA_NOTIFICATION_ID =
"com.github.catfriend1.syncthingandroid.activities.DeviceActivity.NOTIFICATION_ID";
@ -207,19 +205,47 @@ public class DeviceActivity extends SyncthingActivity
mCompressionContainer.setOnClickListener(view -> onCompressionContainerClick());
mCustomSyncConditionsDialog.setOnClickListener(view -> onCustomSyncConditionsDialogClick());
if (savedInstanceState != null){
if (mDevice == null) {
mDevice = new Gson().fromJson(savedInstanceState.getString("device"), Device.class);
}
restoreDialogStates(savedInstanceState);
}
findViewById(R.id.editDeviceIdContainer).setVisibility(mIsCreateMode ? View.VISIBLE : View.GONE);
mShowDeviceIdContainer.setVisibility(!mIsCreateMode ? View.VISIBLE : View.GONE);
if (mIsCreateMode) {
if (mDevice == null) {
if (savedInstanceState != null) {
Log.d(TAG, "Retrieving state from savedInstanceState ...");
mDevice = new Gson().fromJson(savedInstanceState.getString("device"), Device.class);
mDeviceNeedsToUpdate = savedInstanceState.getBoolean("deviceNeedsToUpdate");
restoreDialogStates(savedInstanceState);
} else {
// Fresh init of the edit or create mode.
if (mIsCreateMode) {
Log.d(TAG, "Initializing create mode ...");
initDevice();
mDeviceNeedsToUpdate = true;
} else {
// Edit mode.
String passedId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
Log.d(TAG, "Initializing edit mode: deviceID=" + passedId);
RestApi restApi = getApi();
List<Device> devices = mConfig.getDevices(restApi, false);
mDevice = null;
for (Device currentDevice : devices) {
if (currentDevice.deviceID.equals(passedId)) {
mDevice = currentDevice;
break;
}
}
if (mDevice == null) {
Log.w(TAG, "Device not found in API update, maybe it was deleted?");
finish();
return;
}
if (restApi != null) {
restApi.getConnections(this::onReceiveConnections);
}
mDeviceNeedsToUpdate = false;
}
}
updateViewsAndSetListeners();
if (mIsCreateMode) {
mEditDeviceId.requestFocus();
} else {
getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN);
@ -236,10 +262,8 @@ public class DeviceActivity extends SyncthingActivity
showDeleteDialog();
}
if (mIsCreateMode){
if (savedInstanceState.getBoolean(IS_SHOWING_DISCARD_DIALOG)){
showDiscardDialog();
}
if (savedInstanceState.getBoolean(IS_SHOWING_DISCARD_DIALOG)){
showDiscardDialog();
}
}
@ -252,37 +276,11 @@ public class DeviceActivity extends SyncthingActivity
SyncthingServiceBinder syncthingServiceBinder = (SyncthingServiceBinder) iBinder;
SyncthingService syncthingService = (SyncthingService) syncthingServiceBinder.getService();
syncthingService.getNotificationHandler().cancelConsentNotification(getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 0));
syncthingService.registerOnServiceStateChangeListener(DeviceActivity.this);
}
@Override
public void onServiceStateChange(SyncthingService.State currentState) {
if (!mIsCreateMode) {
RestApi restApi = getApi();
List<Device> devices = mConfig.getDevices(restApi, false);
String passedId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
mDevice = null;
for (Device currentDevice : devices) {
if (currentDevice.deviceID.equals(passedId)) {
mDevice = currentDevice;
break;
}
}
if (mDevice == null) {
Log.w(TAG, "Device not found in API update, maybe it was deleted?");
finish();
return;
}
if (restApi != null) {
restApi.getConnections(this::onReceiveConnections);
}
}
updateViewsAndSetListeners();
}
@Override
public void onBackPressed() {
if (mIsCreateMode) {
if (mDeviceNeedsToUpdate) {
showDiscardDialog();
}
else {
@ -290,24 +288,12 @@ public class DeviceActivity extends SyncthingActivity
}
}
@Override
public void onPause() {
super.onPause();
// We don't want to update every time a TextView's character changes,
// so we hold off until the view stops being visible to the user.
if (mDeviceNeedsToUpdate) {
updateDevice();
}
}
@Override
public void onDestroy() {
super.onDestroy();
SyncthingService syncthingService = getService();
if (syncthingService != null) {
syncthingService.getNotificationHandler().cancelConsentNotification(getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 0));
syncthingService.unregisterOnServiceStateChangeListener(DeviceActivity.this);
}
mEditDeviceId.removeTextChangedListener(mIdTextWatcher);
mNameView.removeTextChangedListener(mNameTextWatcher);
@ -321,10 +307,10 @@ public class DeviceActivity extends SyncthingActivity
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("device", new Gson().toJson(mDevice));
if (mIsCreateMode){
outState.putBoolean(IS_SHOWING_DISCARD_DIALOG, mDiscardDialog != null && mDiscardDialog.isShowing());
Util.dismissDialogSafe(mDiscardDialog, this);
}
outState.putBoolean("deviceNeedsToUpdate", mDeviceNeedsToUpdate);
outState.putBoolean(IS_SHOWING_DISCARD_DIALOG, mDiscardDialog != null && mDiscardDialog.isShowing());
Util.dismissDialogSafe(mDiscardDialog, this);
outState.putBoolean(IS_SHOWING_COMPRESSION_DIALOG, mCompressionDialog != null && mCompressionDialog.isShowing());
Util.dismissDialogSafe(mCompressionDialog, this);
@ -405,7 +391,7 @@ public class DeviceActivity extends SyncthingActivity
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.create).setVisible(mIsCreateMode);
menu.findItem(R.id.save).setTitle(mIsCreateMode ? R.string.create : R.string.save_title);
menu.findItem(R.id.share_device_id).setVisible(!mIsCreateMode);
menu.findItem(R.id.remove).setVisible(!mIsCreateMode);
return true;
@ -414,24 +400,8 @@ public class DeviceActivity extends SyncthingActivity
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.create:
if (isEmpty(mDevice.deviceID)) {
Toast.makeText(this, R.string.device_id_required, Toast.LENGTH_LONG)
.show();
return true;
}
if (!mDevice.checkDeviceID()) {
Toast.makeText(this, R.string.device_id_invalid, Toast.LENGTH_LONG)
.show();
return true;
}
if (isEmpty(mDevice.name)) {
Toast.makeText(this, R.string.device_name_required, Toast.LENGTH_LONG)
.show();
return true;
}
mConfig.addDevice(getApi(), mDevice);
finish();
case R.id.save:
onSave();
return true;
case R.id.share_device_id:
shareDeviceId(this, mDevice.deviceID);
@ -491,23 +461,51 @@ public class DeviceActivity extends SyncthingActivity
mDevice.introducedBy = "";
}
/**
* Sends the updated device info if in edit mode.
* Preconditions: mDeviceNeedsToUpdate == true
*/
private void updateDevice() {
if (mIsCreateMode) {
// If we are about to create this device, we cannot update via restApi.
private void onSave() {
if (mDevice == null) {
Log.e(TAG, "onSave: mDevice == null");
return;
}
if (mDevice == null) {
Log.e(TAG, "updateDevice: mDevice == null");
// Validate fields.
if (isEmpty(mDevice.deviceID)) {
Toast.makeText(this, R.string.device_id_required, Toast.LENGTH_LONG)
.show();
return;
}
if (!mDevice.checkDeviceID()) {
Toast.makeText(this, R.string.device_id_invalid, Toast.LENGTH_LONG)
.show();
return;
}
if (isEmpty(mDevice.name)) {
Toast.makeText(this, R.string.device_name_required, Toast.LENGTH_LONG)
.show();
return;
}
if (!mDevice.checkDeviceAddresses()) {
Toast.makeText(this, R.string.device_addresses_invalid, Toast.LENGTH_LONG)
.show();
return;
}
if (mIsCreateMode) {
Log.v(TAG, "onSave: Adding device with ID = \'" + mDevice.deviceID + "\'");
mConfig.addDevice(getApi(), mDevice);
finish();
return;
}
// Edit mode.
if (!mDeviceNeedsToUpdate) {
// We've got nothing to save.
finish();
return;
}
// Log.v(TAG, "deviceID=" + mDevice.deviceID + ", introducedBy=" + mDevice.introducedBy);
// Save device specific preferences.
Log.v(TAG, "updateDevice: mDevice.deviceID = \'" + mDevice.deviceID + "\'");
Log.v(TAG, "onSave: Updating device with ID = \'" + mDevice.deviceID + "\'");
SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean(
Constants.DYN_PREF_OBJECT_CUSTOM_SYNC_CONDITIONS(Constants.PREF_OBJECT_PREFIX_DEVICE + mDevice.deviceID),
@ -517,8 +515,13 @@ public class DeviceActivity extends SyncthingActivity
// Update device using RestApi or ConfigXml.
mConfig.updateDevice(getApi(), mDevice);
finish();
return;
}
/**
* Converts text line to addresses array.
*/
private List<String> persistableAddresses(CharSequence userInput) {
if (isEmpty(userInput)) {
return DYNAMIC_ADDRESS;
@ -543,6 +546,9 @@ public class DeviceActivity extends SyncthingActivity
return Arrays.asList(input.split(", "));
}
/**
* Converts addresses array to a text line.
*/
private String displayableAddresses() {
if (mDevice.addresses == null) {
return "";

View file

@ -61,8 +61,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
/**
* Shows folder details and allows changing them.
*/
public class FolderActivity extends SyncthingActivity
implements SyncthingService.OnServiceStateChangeListener {
public class FolderActivity extends SyncthingActivity {
public static final String EXTRA_NOTIFICATION_ID =
"com.github.catfriend1.syncthingandroid.activities.FolderActivity.NOTIFICATION_ID";
@ -125,8 +124,6 @@ public class FolderActivity extends SyncthingActivity
private Dialog mDeleteDialog;
private Dialog mDiscardDialog;
private Folder.Versioning mVersioning;
private final TextWatcher mTextWatcher = new TextWatcherAdapter() {
@Override
public void afterTextChanged(Editable s) {
@ -221,34 +218,72 @@ public class FolderActivity extends SyncthingActivity
findViewById(R.id.pullOrderContainer).setOnClickListener(v -> showPullOrderDialog());
findViewById(R.id.versioningContainer).setOnClickListener(v -> showVersioningDialog());
if (mIsCreateMode) {
if (savedInstanceState != null) {
mFolder = new Gson().fromJson(savedInstanceState.getString("mFolder"), Folder.class);
mFolderUri = savedInstanceState.getParcelable("mFolderUri");
}
if (mFolder == null) {
if (savedInstanceState != null) {
Log.d(TAG, "Retrieving state from savedInstanceState ...");
mFolder = new Gson().fromJson(savedInstanceState.getString("mFolder"), Folder.class);
mFolderNeedsToUpdate = savedInstanceState.getBoolean("mFolderNeedsToUpdate");
mIgnoreListNeedsToUpdate = savedInstanceState.getBoolean("mIgnoreListNeedsToUpdate");
mFolderUri = savedInstanceState.getParcelable("mFolderUri");
restoreDialogStates(savedInstanceState);
} else {
// Fresh init of the edit or create mode.
if (mIsCreateMode) {
Log.d(TAG, "Initializing create mode ...");
initFolder();
mFolderNeedsToUpdate = true;
} else {
// Edit mode.
String passedId = getIntent().getStringExtra(EXTRA_FOLDER_ID);
Log.d(TAG, "Initializing edit mode: folder.id=" + passedId);
RestApi restApi = getApi();
List<Folder> folders = mConfig.getFolders(restApi);
mFolder = null;
for (Folder currentFolder : folders) {
if (currentFolder.id.equals(passedId)) {
mFolder = currentFolder;
break;
}
}
if (mFolder == null) {
Log.w(TAG, "Folder not found in API update, maybe it was deleted?");
finish();
return;
}
mConfig.getFolderIgnoreList(restApi, mFolder, this::onReceiveFolderIgnoreList);
mFolderNeedsToUpdate = false;
}
// If the extra is set, we should automatically share the current folder with the given device.
if (getIntent().hasExtra(EXTRA_DEVICE_ID)) {
Device device = new Device();
device.deviceID = getIntent().getStringExtra(EXTRA_DEVICE_ID);
mFolder.addDevice(device);
mFolderNeedsToUpdate = true;
}
}
if (mIsCreateMode) {
mEditIgnoreListTitle.setEnabled(false);
mEditIgnoreListContent.setEnabled(false);
}
else {
// Prepare edit mode.
} else {
// Edit mode.
mIdView.setFocusable(false);
mIdView.setEnabled(false);
mPathView.setFocusable(false);
mPathView.setEnabled(false);
}
checkWriteAndUpdateUI();
updateViewsAndSetListeners();
// Open keyboard on label view in edit mode.
mLabelView.requestFocus();
}
if (savedInstanceState != null) {
if (savedInstanceState.getBoolean(IS_SHOWING_DELETE_DIALOG)) {
showDeleteDialog();
} else if (savedInstanceState.getBoolean(IS_SHOW_DISCARD_DIALOG)) {
showDiscardDialog();
}
private void restoreDialogStates(Bundle savedInstanceState) {
if (savedInstanceState.getBoolean(IS_SHOWING_DELETE_DIALOG)) {
showDeleteDialog();
} else if (savedInstanceState.getBoolean(IS_SHOW_DISCARD_DIALOG)) {
showDiscardDialog();
}
}
@ -345,7 +380,7 @@ public class FolderActivity extends SyncthingActivity
@Override
public void onBackPressed() {
if (mIsCreateMode) {
if (mFolderNeedsToUpdate) {
showDiscardDialog();
}
else {
@ -353,25 +388,12 @@ public class FolderActivity extends SyncthingActivity
}
}
@Override
public void onPause() {
super.onPause();
// We don't want to update every time a TextView's character changes,
// so we hold off until the view stops being visible to the user.
if (mFolderNeedsToUpdate) {
Log.v(TAG, "onPause: mFolderNeedsToUpdate=true, mIgnoreListNeedsToUpdate=" + Boolean.toString(mIgnoreListNeedsToUpdate));
updateFolder();
}
}
@Override
public void onDestroy() {
super.onDestroy();
SyncthingService syncthingService = getService();
if (syncthingService != null) {
syncthingService.getNotificationHandler().cancelConsentNotification(getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 0));
syncthingService.unregisterOnServiceStateChangeListener(FolderActivity.this);
}
mLabelView.removeTextChangedListener(mTextWatcher);
mIdView.removeTextChangedListener(mTextWatcher);
@ -381,17 +403,16 @@ public class FolderActivity extends SyncthingActivity
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("mFolder", new Gson().toJson(mFolder));
outState.putBoolean("mFolderNeedsToUpdate", mFolderNeedsToUpdate);
outState.putBoolean("mIgnoreListNeedsToUpdate", mIgnoreListNeedsToUpdate);
outState.putParcelable("mFolderUri", mFolderUri);
outState.putBoolean(IS_SHOWING_DELETE_DIALOG, mDeleteDialog != null && mDeleteDialog.isShowing());
Util.dismissDialogSafe(mDeleteDialog, this);
outState.putBoolean(IS_SHOW_DISCARD_DIALOG, mDiscardDialog != null && mDiscardDialog.isShowing());
Util.dismissDialogSafe(mDiscardDialog, this);
if (mIsCreateMode) {
outState.putString("mFolder", new Gson().toJson(mFolder));
outState.putParcelable("mFolderUri", mFolderUri);
}
}
/**
@ -403,47 +424,6 @@ public class FolderActivity extends SyncthingActivity
SyncthingServiceBinder syncthingServiceBinder = (SyncthingServiceBinder) iBinder;
SyncthingService syncthingService = (SyncthingService) syncthingServiceBinder.getService();
syncthingService.getNotificationHandler().cancelConsentNotification(getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 0));
syncthingService.registerOnServiceStateChangeListener(FolderActivity.this);
}
@Override
public void onServiceStateChange(SyncthingService.State currentState) {
if (mFolderNeedsToUpdate) {
Log.d(TAG, "onServiceStateChange: Suppressing reload of folder config as changes were made to that folder in the meantime.");
return;
}
if (!mIsCreateMode) {
Log.d(TAG, "onServiceStateChange: (Re)loading folder config ...");
RestApi restApi = getApi();
List<Folder> folders = mConfig.getFolders(restApi);
String passedId = getIntent().getStringExtra(EXTRA_FOLDER_ID);
mFolder = null;
for (Folder currentFolder : folders) {
if (currentFolder.id.equals(passedId)) {
mFolder = currentFolder;
break;
}
}
if (mFolder == null) {
Log.w(TAG, "Folder not found in API update, maybe it was deleted?");
finish();
return;
}
mConfig.getFolderIgnoreList(restApi, mFolder, this::onReceiveFolderIgnoreList);
}
// If the extra is set, we should automatically share the current folder with the given device.
if (getIntent().hasExtra(EXTRA_DEVICE_ID)) {
Device device = new Device();
device.deviceID = getIntent().getStringExtra(EXTRA_DEVICE_ID);
mFolder.addDevice(device);
mFolderNeedsToUpdate = true;
}
checkWriteAndUpdateUI();
attemptToApplyVersioningConfig();
updateViewsAndSetListeners();
}
private void onReceiveFolderIgnoreList(FolderIgnoreList folderIgnoreList) {
@ -456,16 +436,6 @@ public class FolderActivity extends SyncthingActivity
mEditIgnoreListContent.addTextChangedListener(mIgnoreListContentTextWatcher);
}
// If the FolderActivity gets recreated after the VersioningDialogActivity is closed, then the result from the VersioningDialogActivity will be received before
// the mFolder variable has been recreated, so the versioning config will be stored in the mVersioning variable until the mFolder variable has been
// recreated in the onServiceStateChange(). This has been observed to happen after the screen orientation has changed while the VersioningDialogActivity was open.
private void attemptToApplyVersioningConfig() {
if (mFolder != null && mVersioning != null){
mFolder.versioning = mVersioning;
mVersioning = null;
}
}
private void updateViewsAndSetListeners() {
mLabelView.removeTextChangedListener(mTextWatcher);
mIdView.removeTextChangedListener(mTextWatcher);
@ -528,7 +498,7 @@ public class FolderActivity extends SyncthingActivity
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.create).setVisible(mIsCreateMode);
menu.findItem(R.id.save).setTitle(mIsCreateMode ? R.string.create : R.string.save_title);
menu.findItem(R.id.remove).setVisible(!mIsCreateMode);
return true;
}
@ -536,39 +506,8 @@ public class FolderActivity extends SyncthingActivity
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.create:
if (TextUtils.isEmpty(mFolder.id)) {
Toast.makeText(this, R.string.folder_id_required, Toast.LENGTH_LONG)
.show();
return true;
}
if (TextUtils.isEmpty(mFolder.label)) {
Toast.makeText(this, R.string.folder_label_required, Toast.LENGTH_LONG)
.show();
return true;
}
if (TextUtils.isEmpty(mFolder.path)) {
Toast.makeText(this, R.string.folder_path_required, Toast.LENGTH_LONG)
.show();
return true;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
mFolderUri != null &&
mFolder.type.equals(Constants.FOLDER_TYPE_SEND_ONLY)) {
/**
* 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);
}
}
mConfig.addFolder(getApi(), mFolder);
finish();
case R.id.save:
onSave();
return true;
case R.id.remove:
showDeleteDialog();
@ -662,8 +601,12 @@ public class FolderActivity extends SyncthingActivity
* 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.
* Default from {@link #initFolder} was already set in {@link #onCreate}.
* mFolder.type = Constants.FOLDER_TYPE_SEND_RECEIVE;
* We won't set it again here as this would cause user selection to be reset on
* screen rotation - as we don't know if we restored the activity or created
* a fresh one.
*/
mFolder.type = Constants.FOLDER_TYPE_SEND_RECEIVE;
updateFolderTypeDescription();
} else {
mEditIgnoreListTitle.setEnabled(true);
@ -694,6 +637,9 @@ public class FolderActivity extends SyncthingActivity
return sb.toString();
}
/**
* Init a new folder in mIsCreateMode, used in {@link #onCreate}.
*/
private void initFolder() {
mFolder = new Folder();
mFolder.id = (getIntent().hasExtra(EXTRA_FOLDER_ID))
@ -708,7 +654,7 @@ public class FolderActivity extends SyncthingActivity
*/
mFolder.rescanIntervalS = 3600;
mFolder.paused = false;
mFolder.type = Constants.FOLDER_TYPE_SEND_RECEIVE;
mFolder.type = Constants.FOLDER_TYPE_SEND_RECEIVE; // Default for {@link #checkWriteAndUpdateUI}.
mFolder.versioning = new Folder.Versioning();
}
@ -735,24 +681,60 @@ public class FolderActivity extends SyncthingActivity
deviceView.setOnCheckedChangeListener(mCheckedListener);
}
/**
* Sends the updated folder info if in edit mode.
* Preconditions:
* mFolderNeedsToUpdate == true
* mIgnoreListNeedsToUpdate == true (Optional)
*/
private void updateFolder() {
if (mIsCreateMode) {
// If we are about to create this folder, we cannot update via restApi.
private void onSave() {
if (mFolder == null) {
Log.e(TAG, "onSave: mFolder == null");
return;
}
if (mFolder == null) {
Log.e(TAG, "updateFolder: mFolder == null");
// Validate fields.
if (TextUtils.isEmpty(mFolder.id)) {
Toast.makeText(this, R.string.folder_id_required, Toast.LENGTH_LONG)
.show();
return;
}
if (TextUtils.isEmpty(mFolder.label)) {
Toast.makeText(this, R.string.folder_label_required, Toast.LENGTH_LONG)
.show();
return;
}
if (TextUtils.isEmpty(mFolder.path)) {
Toast.makeText(this, R.string.folder_path_required, Toast.LENGTH_LONG)
.show();
return;
}
if (mIsCreateMode) {
Log.v(TAG, "onSave: Adding folder with ID = \'" + mFolder.id + "\'");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
mFolderUri != null &&
mFolder.type.equals(Constants.FOLDER_TYPE_SEND_ONLY)) {
/**
* 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, "onSave: Creating new directory " + mFolder.path + File.separator + FOLDER_MARKER_NAME);
dfFolder.createDirectory(FOLDER_MARKER_NAME);
}
}
mConfig.addFolder(getApi(), mFolder);
finish();
return;
}
// Edit mode.
if (!mFolderNeedsToUpdate) {
// We've got nothing to save.
finish();
return;
}
// Save folder specific preferences.
Log.v(TAG, "updateFolder: mFolder.id = \'" + mFolder.id + "\'");
Log.v(TAG, "onSave: Updating folder with ID = \'" + mFolder.id + "\'");
SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean(
Constants.DYN_PREF_OBJECT_CUSTOM_SYNC_CONDITIONS(Constants.PREF_OBJECT_PREFIX_FOLDER + mFolder.id),
@ -770,41 +752,39 @@ public class FolderActivity extends SyncthingActivity
// Update folder using RestApi or ConfigXml.
mConfig.updateFolder(restApi, mFolder);
finish();
return;
}
private void showDiscardDialog(){
mDiscardDialog = createDiscardDialog();
mDiscardDialog.show();
}
private Dialog createDiscardDialog() {
return new AlertDialog.Builder(this)
mDiscardDialog = new AlertDialog.Builder(this)
.setMessage(R.string.dialog_discard_changes)
.setPositiveButton(android.R.string.ok, (dialog, which) -> finish())
.setNegativeButton(android.R.string.cancel, null)
.create();
mDiscardDialog.show();
}
private void updateVersioning(Bundle arguments) {
if (mFolder != null){
mVersioning = mFolder.versioning;
} else {
mVersioning = new Folder.Versioning();
if (mFolder == null) {
Log.e(TAG, "updateVersioning: mFolder == null");
return;
}
if (mFolder.versioning == null) {
mFolder.versioning = new Folder.Versioning();
}
String type = arguments.getString("type");
arguments.remove("type");
if (type.equals("none")){
mVersioning = new Folder.Versioning();
if (type.equals("none")) {
mFolder.versioning = new Folder.Versioning();
} else {
for (String key : arguments.keySet()) {
mVersioning.params.put(key, arguments.getString(key));
mFolder.versioning.params.put(key, arguments.getString(key));
}
mVersioning.type = type;
mFolder.versioning.type = type;
}
attemptToApplyVersioningConfig();
updateVersioningDescription();
mFolderNeedsToUpdate = true;
}

View file

@ -24,6 +24,8 @@ public class Device {
public List<PendingFolder> pendingFolders;
public List<IgnoredFolder> ignoredFolders;
// private static final String TAG = "Device";
/**
* Relevant fields for Folder.List<Device> "shared-with-device" model,
* handled by {@link ConfigRouter#updateFolder and ConfigXml#updateFolder}
@ -112,4 +114,93 @@ public class Device {
return false;
}
}
/**
* Returns if device.addresses elements are correctly formatted.
* See https://docs.syncthing.net/users/config.html#device-element for what is correct.
* It can be improved in the future because it doesn't catch all mistakes a user can do.
* It catches the most common mistakes.
*/
public Boolean checkDeviceAddresses() {
if (this.addresses == null) {
return false;
}
for (String address : this.addresses) {
// Log.v(TAG, "address=(" + address + ")");
if (address.equals("dynamic")) {
continue;
}
/**
* RegEx documentation:
*
* - Matching
* tcp://127.0.0.1:4000
* tcp4://127.0.0.1:4000
* tcp6://127.0.0.1:4000
* tcp4://127.0.0.1
* tcp://[2001:db8::23:42]
* tcp://[2001:db8::23:42]:12345
* tcp://myserver
* tcp://myserver:12345
*
* - Not-Matching
* tcp8://127.0.0.1
* udp4://127.0.0.1
*/
if (!address.matches("^tcp([46])?://.*$")) {
// Log.v(TAG, "Invalid protocol.");
return false;
}
// Separate protocol from address and port.
String[] addressSplit = address.split("://");
if (addressSplit.length == 1) {
// There's only the protocol given, nothing more.
// Log.v(TAG, "There's only the protocol given, nothing more.");
return false;
}
else if (addressSplit.length == 2) {
// Check if the address ends with ":" or "]:"
if (addressSplit[addressSplit.length-1].endsWith(":") ||
addressSplit[addressSplit.length-1].endsWith("]:")) {
// The address ends with ":". Will match "tcp://myserver:"
// Log.v(TAG, "address ends with \":\" or \"]:\". Will match \"tcp://myserver:\".");
return false;
}
// Check if there's a "hostname:port" number given in the part after "://".
String[] hostnamePortSplit = addressSplit[addressSplit.length-1].split(":");
if (hostnamePortSplit.length > 1) {
// Check if the hostname or IP address given before the port is empty.
if (TextUtils.isEmpty(hostnamePortSplit[0])) {
// Empty hostname or IP address before the port. Will match "tcp://:4000"
// Log.v(TAG, "Empty hostname or IP address before the port.");
return false;
}
// Check if there's a port number given in the last part.
String potentialPort = hostnamePortSplit[hostnamePortSplit.length-1];
if (!potentialPort.endsWith("]")) {
// It's not the end of an IPv6 address and likely a port number.
// Log.v(TAG, "... potentialPort=(" + potentialPort + ")");
Integer port = 0;
try {
port = Integer.parseInt(potentialPort);
} catch (Exception e) {
}
if (port < 1 || port > 65535) {
// Invalid port number.
// Log.v(TAG, "Invalid port number.");
return false;
}
}
}
} else {
// Protocol is given more than one time. Will match "tcp://tcp://"
return false;
}
}
return true;
}
}

View file

@ -162,6 +162,7 @@ public class EventProcessor implements Runnable, RestApi.OnReceiveEventListener
case "FolderSummary":
case "FolderWatchStateChanged":
case "ItemStarted":
case "ListenAddressesChanged":
case "LocalIndexUpdated":
case "LoginAttempt":
case "RemoteDownloadProgress":

View file

@ -4,7 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/create"
android:id="@+id/save"
android:title="@string/add"
android:icon="@drawable/ic_done_white_24dp"
app:showAsAction="always|withText" />

View file

@ -4,7 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/create"
android:id="@+id/save"
android:icon="@drawable/ic_done_white_24dp"
android:title="@string/create"
app:showAsAction="always|withText" />

View file

@ -305,6 +305,9 @@ Bitte melden Sie auftretende Probleme via GitHub.</string>
<!-- Toast shown when trying to create a device with an empty name -->
<string name="device_name_required">Der Gerätename darf nicht leer sein</string>
<!-- Toast shown when trying to create a device with invalid addresses -->
<string name="device_addresses_invalid">Die Geräteadressen sind nicht im richtigen Format. Bitte versuche es erneut. Sieh Dir die Syncthing Doku für weitere Hilfe an.</string>
<!-- Content description for device ID qr code icon -->
<string name="scan_qr_code_description">QR Code scannen</string>

View file

@ -308,6 +308,9 @@ Please report any problems you encounter via Github.</string>
<!-- Toast shown when trying to create a device with an empty name -->
<string name="device_name_required">The device name must not be empty</string>
<!-- Toast shown when trying to create a device with invalid addresses -->
<string name="device_addresses_invalid">The device addresses are not formatted correctly. Please retry. Look at the Syncthing docs for more info.</string>
<!-- Content description for device ID qr code icon -->
<string name="scan_qr_code_description">Scan QR Code</string>