From 56455fc89d8705c8a76d628c20d7f6d7d2b7f4f4 Mon Sep 17 00:00:00 2001 From: Catfriend1 Date: Thu, 24 Jan 2019 20:37:45 +0000 Subject: [PATCH] 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. --- .../activities/DeviceActivity.java | 182 +++++------ .../activities/FolderActivity.java | 286 ++++++++---------- .../syncthingandroid/model/Device.java | 91 ++++++ .../service/EventProcessor.java | 1 + app/src/main/res/menu/device_settings.xml | 2 +- app/src/main/res/menu/folder_settings.xml | 2 +- app/src/main/res/values-de/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 8 files changed, 327 insertions(+), 243 deletions(-) diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java index 99db9d74..4182af96 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java @@ -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 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 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 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 ""; diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java index eed172e8..cc38d3db 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java @@ -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 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 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; } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/model/Device.java b/app/src/main/java/com/nutomic/syncthingandroid/model/Device.java index 251e27c6..9d1aa804 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/model/Device.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/model/Device.java @@ -24,6 +24,8 @@ public class Device { public List pendingFolders; public List ignoredFolders; + // private static final String TAG = "Device"; + /** * Relevant fields for Folder.List "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; + } } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java b/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java index 4432b07b..11eb90e4 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java @@ -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": diff --git a/app/src/main/res/menu/device_settings.xml b/app/src/main/res/menu/device_settings.xml index 7f6a1705..67c9b0a3 100644 --- a/app/src/main/res/menu/device_settings.xml +++ b/app/src/main/res/menu/device_settings.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> diff --git a/app/src/main/res/menu/folder_settings.xml b/app/src/main/res/menu/folder_settings.xml index f590cab7..f6fb1c0a 100644 --- a/app/src/main/res/menu/folder_settings.xml +++ b/app/src/main/res/menu/folder_settings.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ec058cb0..b3e0f918 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -305,6 +305,9 @@ Bitte melden Sie auftretende Probleme via GitHub. Der Gerätename darf nicht leer sein + + Die Geräteadressen sind nicht im richtigen Format. Bitte versuche es erneut. Sieh Dir die Syncthing Doku für weitere Hilfe an. + QR Code scannen diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 30138f01..ae55ef38 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -308,6 +308,9 @@ Please report any problems you encounter via Github. The device name must not be empty + + The device addresses are not formatted correctly. Please retry. Look at the Syncthing docs for more info. + Scan QR Code