Skip to content

Commit

Permalink
Allow specifying different sync conditions per folder (fixes #57) (#66)
Browse files Browse the repository at this point in the history
* WIP - #57

* Add sync conditions activity

* Update folder edit layout

Implement SyncConditionsActivity stub

* Add dialog open logic

* Fix UI glitches

* Update APK version to 0.14.51.4 / 4167

* Revert "Update APK version to 0.14.51.4 / 4167"

This reverts commit 47bc4aa.

* Implement WiFi Ssid Whitelist UI

* Read per-folder sync conditions from prefs

* Implement onCheckedChange listeners

* Implement saving object sync conditions

* Remove early draft stuff

* Implement per folder master switch for custom sync conditions

* FolderActivity - More log

* Improve generation of object-specific constants

* Added onSyncPreconditionChanged to RunConditionMonitor

* Fix UI glitch

* Fix non-harming typos

* Refactor "on mobile data" sync condition

* Refactor "on wifi" sync condition

* Refactor "on whitelisted wifi" sync condition

* Refactor "on metered wifi" sync condition

* Refactor RunConditionMonitor#decideShouldRun

* RunConditionMonitor - Rename variables

* Pause/Unpause folders using RunConditionMonitor events

* Implement RunConditionMonitor#SyncConditionResult

* Fix lint - use editor.apply instead of editor.commit

* Updated translations

* Add null check in RestApi

* Fix missing explanation text

* Fix german translation

* Update APK version to 0.14.51.6 / 4169

* Update whatsnew
  • Loading branch information
Catfriend1 authored Oct 14, 2018
1 parent c5a436f commit f6f90c9
Show file tree
Hide file tree
Showing 80 changed files with 1,141 additions and 1,094 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ android {
applicationId "com.github.catfriend1.syncthingandroid"
minSdkVersion 16
targetSdkVersion 26
versionCode 4168
versionName "0.14.51.5"
versionCode 4169
versionName "0.14.51.6"
testApplicationId 'com.github.catfriend1.syncthingandroid.test'
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
playAccountConfig = playAccountConfigs.defaultAccountConfig
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@
android:name="android.support.UI_OPTIONS"
android:value="splitActionBarWhenNarrow" />
</activity>
<activity
android:name=".activities.SyncConditionsActivity"
android:label="@string/custom_sync_conditions_dialog">
<meta-data
android:name="android.support.UI_OPTIONS"
android:value="splitActionBarWhenNarrow" />
</activity>
<activity android:name=".activities.RestartActivity"
android:theme="@style/Theme.Syncthing.Translucent"
android:launchMode="singleTop"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.nutomic.syncthingandroid;

import com.nutomic.syncthingandroid.activities.FirstStartActivity;
import com.nutomic.syncthingandroid.activities.FolderActivity;
import com.nutomic.syncthingandroid.activities.FolderPickerActivity;
import com.nutomic.syncthingandroid.activities.MainActivity;
import com.nutomic.syncthingandroid.activities.SettingsActivity;
import com.nutomic.syncthingandroid.activities.SyncConditionsActivity;
import com.nutomic.syncthingandroid.receiver.AppConfigReceiver;
import com.nutomic.syncthingandroid.service.RunConditionMonitor;
import com.nutomic.syncthingandroid.service.EventProcessor;
Expand All @@ -24,7 +26,9 @@ public interface DaggerComponent {
void inject(SyncthingApp app);
void inject(MainActivity activity);
void inject(FirstStartActivity activity);
void inject(FolderActivity activity);
void inject(FolderPickerActivity activity);
void inject(SyncConditionsActivity activity);
void inject(Languages languages);
void inject(SyncthingService service);
void inject(RunConditionMonitor runConditionMonitor);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
Expand All @@ -19,14 +19,14 @@
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.google.common.base.Objects;
import com.google.gson.Gson;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.model.Device;
Expand All @@ -35,17 +35,19 @@
import com.nutomic.syncthingandroid.service.Constants;
import com.nutomic.syncthingandroid.service.RestApi;
import com.nutomic.syncthingandroid.service.SyncthingService;
import com.nutomic.syncthingandroid.SyncthingApp;
import com.nutomic.syncthingandroid.util.FileUtils;
import com.nutomic.syncthingandroid.util.TextWatcherAdapter;
import com.nutomic.syncthingandroid.util.Util;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Random;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;

import static android.support.v4.view.MarginLayoutParamsCompat.setMarginEnd;
import static android.support.v4.view.MarginLayoutParamsCompat.setMarginStart;
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
Expand Down Expand Up @@ -95,16 +97,22 @@ public class FolderActivity extends SyncthingActivity
private TextView mAccessExplanationView;
private TextView mFolderTypeView;
private TextView mFolderTypeDescriptionView;
private ViewGroup mDevicesContainer;
private SwitchCompat mFolderFileWatcher;
private SwitchCompat mFolderPaused;
private ViewGroup mDevicesContainer;
private SwitchCompat mCustomSyncConditionsSwitch;
private TextView mCustomSyncConditionsDescription;
private TextView mCustomSyncConditionsDialog;
private TextView mPullOrderTypeView;
private TextView mPullOrderDescriptionView;
private TextView mVersioningDescriptionView;
private TextView mVersioningTypeView;
private TextView mEditIgnoreListTitle;
private EditText mEditIgnoreListContent;

@Inject
SharedPreferences mPreferences;

private boolean mIsCreateMode;
private boolean mFolderNeedsToUpdate = false;

Expand Down Expand Up @@ -137,6 +145,12 @@ public void onCheckedChanged(CompoundButton view, boolean isChecked) {
mFolder.paused = isChecked;
mFolderNeedsToUpdate = true;
break;
case R.id.customSyncConditionsSwitch:
mCustomSyncConditionsDescription.setEnabled(isChecked);
mCustomSyncConditionsDialog.setEnabled(isChecked);
// This is needed to display the "discard changes dialog".
mFolderNeedsToUpdate = true;
break;
case R.id.device_toggle:
Device device = (Device) view.getTag();
if (isChecked) {
Expand All @@ -153,6 +167,7 @@ public void onCheckedChanged(CompoundButton view, boolean isChecked) {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((SyncthingApp) getApplication()).component().inject(this);
setContentView(R.layout.fragment_folder);

mIsCreateMode = getIntent().getBooleanExtra(EXTRA_IS_CREATE, false);
Expand All @@ -167,6 +182,9 @@ public void onCreate(Bundle savedInstanceState) {
mFolderTypeDescriptionView = findViewById(R.id.folderTypeDescription);
mFolderFileWatcher = findViewById(R.id.fileWatcher);
mFolderPaused = findViewById(R.id.folderPause);
mCustomSyncConditionsSwitch = findViewById(R.id.customSyncConditionsSwitch);
mCustomSyncConditionsDescription = findViewById(R.id.customSyncConditionsDescription);
mCustomSyncConditionsDialog = findViewById(R.id.customSyncConditionsDialog);
mPullOrderTypeView = findViewById(R.id.pullOrderType);
mPullOrderDescriptionView = findViewById(R.id.pullOrderDescription);
mVersioningDescriptionView = findViewById(R.id.versioningDescription);
Expand All @@ -176,6 +194,7 @@ public void onCreate(Bundle savedInstanceState) {
mEditIgnoreListContent = findViewById(R.id.edit_ignore_list_content);

mPathView.setOnClickListener(view -> onPathViewClick());
mCustomSyncConditionsDialog.setOnClickListener(view -> onCustomSyncConditionsDialogClick());

findViewById(R.id.folderTypeContainer).setOnClickListener(v -> showFolderTypeDialog());
findViewById(R.id.pullOrderContainer).setOnClickListener(v -> showPullOrderDialog());
Expand Down Expand Up @@ -241,6 +260,20 @@ private void onPathViewClick() {
startActivityForResult(intent, CHOOSE_FOLDER_REQUEST);
}

/**
* Invoked after user clicked on the {@link mCustomSyncConditionsDialog} label.
*/
@SuppressLint("InlinedAPI")
private void onCustomSyncConditionsDialogClick() {
startActivityForResult(
SyncConditionsActivity.createIntent(
this, Constants.PREF_OBJECT_PREFIX_FOLDER + mFolder.id, mFolder.label
),
0
);
return;
}

private void showFolderTypeDialog() {
if (TextUtils.isEmpty(mFolder.path)) {
Toast.makeText(this, R.string.folder_path_required, Toast.LENGTH_LONG)
Expand Down Expand Up @@ -407,6 +440,7 @@ private void updateViewsAndSetListeners() {
mIdView.removeTextChangedListener(mTextWatcher);
mFolderFileWatcher.setOnCheckedChangeListener(null);
mFolderPaused.setOnCheckedChangeListener(null);
mCustomSyncConditionsSwitch.setOnCheckedChangeListener(null);

// Update views
mLabelView.setText(mFolder.label);
Expand All @@ -416,8 +450,23 @@ private void updateViewsAndSetListeners() {
updateVersioningDescription();
mFolderFileWatcher.setChecked(mFolder.fsWatcherEnabled);
mFolderPaused.setChecked(mFolder.paused);
List<Device> devicesList = getApi().getDevices(false);
findViewById(R.id.editIgnoresContainer).setVisibility(mIsCreateMode ? View.GONE : View.VISIBLE);

// Update views - custom sync conditions.
mCustomSyncConditionsSwitch.setChecked(false);
if (mIsCreateMode) {
findViewById(R.id.customSyncConditionsContainer).setVisibility(View.GONE);
} else {
mCustomSyncConditionsSwitch.setChecked(mPreferences.getBoolean(
Constants.DYN_PREF_OBJECT_CUSTOM_SYNC_CONDITIONS(Constants.PREF_OBJECT_PREFIX_FOLDER + mFolder.id), false
));
}
mCustomSyncConditionsSwitch.setEnabled(!mIsCreateMode);
mCustomSyncConditionsDescription.setEnabled(mCustomSyncConditionsSwitch.isChecked());
mCustomSyncConditionsDialog.setEnabled(mCustomSyncConditionsSwitch.isChecked());

// Populate devicesList.
List<Device> devicesList = getApi().getDevices(false);
mDevicesContainer.removeAllViews();
if (devicesList.isEmpty()) {
addEmptyDeviceListView();
Expand All @@ -432,6 +481,7 @@ private void updateViewsAndSetListeners() {
mIdView.addTextChangedListener(mTextWatcher);
mFolderFileWatcher.setOnCheckedChangeListener(mCheckedListener);
mFolderPaused.setOnCheckedChangeListener(mCheckedListener);
mCustomSyncConditionsSwitch.setOnCheckedChangeListener(mCheckedListener);
}

@Override
Expand Down Expand Up @@ -652,25 +702,38 @@ private void addDeviceViewAndSetListener(Device device, LayoutInflater inflater)
}

private void updateFolder() {
if (!mIsCreateMode) {
RestApi restApi = getApi();
/**
* RestApi is guaranteed not to be null as {@link onServiceStateChange}
* immediately finishes this activity if SyncthingService shuts down.
*/
/*
if (restApi == null) {
Log.e(TAG, "updateFolder: restApi == null");
return;
}
*/
// Update ignore list.
String[] ignore = mEditIgnoreListContent.getText().toString().split("\n");
restApi.postFolderIgnoreList(mFolder.id, ignore);
if (mIsCreateMode) {
// If we are about to create this folder, we cannot update via restApi.
return;
}

// Update model and send the config to REST endpoint.
restApi.updateFolder(mFolder);
// Save folder specific preferences.
Log.v(TAG, "updateFolder: mFolder.id = \'" + mFolder.id + "\'");
SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean(
Constants.DYN_PREF_OBJECT_CUSTOM_SYNC_CONDITIONS(Constants.PREF_OBJECT_PREFIX_FOLDER + mFolder.id),
mCustomSyncConditionsSwitch.isChecked()
);
editor.apply();

// Update folder via restApi.
RestApi restApi = getApi();
/**
* RestApi is guaranteed not to be null as {@link onServiceStateChange}
* immediately finishes this activity if SyncthingService shuts down.
*/
/*
if (restApi == null) {
Log.e(TAG, "updateFolder: restApi == null");
return;
}
*/
// Update ignore list.
String[] ignore = mEditIgnoreListContent.getText().toString().split("\n");
restApi.postFolderIgnoreList(mFolder.id, ignore);

// Update model and send the config to REST endpoint.
restApi.updateFolder(mFolder);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public static class SettingsFragment extends PreferenceFragment
private Preference mSyncthingVersion;

private SyncthingService mSyncthingService;
private RestApi mApi;
private RestApi mRestApi;

private Options mOptions;
private Config.Gui mGui;
Expand Down Expand Up @@ -312,21 +312,21 @@ public void onServiceConnected() {

@Override
public void onServiceStateChange(SyncthingService.State currentState) {
mApi = mSyncthingService.getApi();
boolean isSyncthingRunning = (mApi != null) &&
mApi.isConfigLoaded() &&
mRestApi = mSyncthingService.getApi();
boolean isSyncthingRunning = (mRestApi != null) &&
mRestApi.isConfigLoaded() &&
(currentState == SyncthingService.State.ACTIVE);
mCategorySyncthingOptions.setEnabled(isSyncthingRunning);

if (!isSyncthingRunning)
return;

mSyncthingVersion.setSummary(mApi.getVersion());
mOptions = mApi.getOptions();
mGui = mApi.getGui();
mSyncthingVersion.setSummary(mRestApi.getVersion());
mOptions = mRestApi.getOptions();
mGui = mRestApi.getGui();

Joiner joiner = Joiner.on(", ");
mDeviceName.setText(mApi.getLocalDevice().name);
mDeviceName.setText(mRestApi.getLocalDevice().name);
mListenAddresses.setText(joiner.join(mOptions.listenAddresses));
mMaxRecvKbps.setText(Integer.toString(mOptions.maxRecvKbps));
mMaxSendKbps.setText(Integer.toString(mOptions.maxSendKbps));
Expand All @@ -337,7 +337,7 @@ public void onServiceStateChange(SyncthingService.State currentState) {
mGlobalAnnounceServers.setText(joiner.join(mOptions.globalAnnounceServers));
mAddress.setText(mGui.address);
mRestartOnWakeup.setChecked(mOptions.restartOnWakeup);
mApi.getSystemStatus(systemStatus ->
mRestApi.getSystemStatus(systemStatus ->
mUrAccepted.setChecked(mOptions.isUsageReportingAccepted(systemStatus.urVersionMax)));
}

Expand Down Expand Up @@ -384,9 +384,9 @@ public boolean onSyncthingPreferenceChange(Preference preference, Object o) {
Splitter splitter = Splitter.on(",").trimResults().omitEmptyStrings();
switch (preference.getKey()) {
case "deviceName":
Device localDevice = mApi.getLocalDevice();
Device localDevice = mRestApi.getLocalDevice();
localDevice.name = (String) o;
mApi.editDevice(localDevice);
mRestApi.editDevice(localDevice);
break;
case "listenAddresses":
mOptions.listenAddresses = Iterables.toArray(splitter.split((String) o), String.class);
Expand Down Expand Up @@ -435,7 +435,7 @@ public boolean onSyncthingPreferenceChange(Preference preference, Object o) {
mOptions.restartOnWakeup = (boolean) o;
break;
case "urAccepted":
mApi.getSystemStatus(systemStatus -> {
mRestApi.getSystemStatus(systemStatus -> {
mOptions.urAccepted = ((boolean) o)
? systemStatus.urVersionMax
: Options.USAGE_REPORTING_DENIED;
Expand All @@ -444,7 +444,7 @@ public boolean onSyncthingPreferenceChange(Preference preference, Object o) {
default: throw new InvalidParameterException();
}

mApi.editSettings(mGui, mOptions);
mRestApi.editSettings(mGui, mOptions);
mPendingConfig = true;
return true;
}
Expand All @@ -454,9 +454,9 @@ public void onStop() {
if (mSyncthingService != null) {
mNotificationHandler.updatePersistentNotification(mSyncthingService);
if (mPendingConfig) {
if (mApi != null &&
if (mRestApi != null &&
mSyncthingService.getCurrentState() != SyncthingService.State.DISABLED) {
mApi.saveConfigAndRestart();
mRestApi.saveConfigAndRestart();
mPendingConfig = false;
}
}
Expand Down Expand Up @@ -556,13 +556,13 @@ public boolean onPreferenceClick(Preference preference) {
new AlertDialog.Builder(getActivity())
.setMessage(R.string.undo_ignored_devices_folders_question)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
if (mApi == null) {
if (mRestApi == null) {
Toast.makeText(getActivity(),
getString(R.string.generic_error) + getString(R.string.syncthing_disabled_title),
Toast.LENGTH_SHORT).show();
return;
}
mApi.undoIgnoredDevicesAndFolders();
mRestApi.undoIgnoredDevicesAndFolders();
mPendingConfig = true;
Toast.makeText(getActivity(),
getString(R.string.undo_ignored_devices_folders_done),
Expand Down
Loading

0 comments on commit f6f90c9

Please sign in to comment.