1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2024-12-02 01:01:17 +00:00

Add individual sync conditions for devices (#96)

* SyncConditionsActivity - Rename "folder" to "object" as it can mean a folder or device.

* Implement per-device sync conditions

* Default custom wifi whitelist to "all enabled"

* Update APK version to 0.14.51.7 / 4170

* Add checkbox "use Wi-Fi whitelist" in global run conditions

* Rename variable
This commit is contained in:
Catfriend1 2018-10-16 10:18:15 +02:00 committed by GitHub
parent b84d4da34f
commit f8692f02ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 277 additions and 64 deletions

View file

@ -35,8 +35,8 @@ android {
applicationId "com.github.catfriend1.syncthingandroid" applicationId "com.github.catfriend1.syncthingandroid"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 26 targetSdkVersion 26
versionCode 4169 versionCode 4170
versionName "0.14.51.6" versionName "0.14.51.7"
testApplicationId 'com.github.catfriend1.syncthingandroid.test' testApplicationId 'com.github.catfriend1.syncthingandroid.test'
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
playAccountConfig = playAccountConfigs.defaultAccountConfig playAccountConfig = playAccountConfigs.defaultAccountConfig

View file

@ -1,5 +1,6 @@
package com.nutomic.syncthingandroid; package com.nutomic.syncthingandroid;
import com.nutomic.syncthingandroid.activities.DeviceActivity;
import com.nutomic.syncthingandroid.activities.FirstStartActivity; import com.nutomic.syncthingandroid.activities.FirstStartActivity;
import com.nutomic.syncthingandroid.activities.FolderActivity; import com.nutomic.syncthingandroid.activities.FolderActivity;
import com.nutomic.syncthingandroid.activities.FolderPickerActivity; import com.nutomic.syncthingandroid.activities.FolderPickerActivity;
@ -26,6 +27,7 @@ public interface DaggerComponent {
void inject(SyncthingApp app); void inject(SyncthingApp app);
void inject(MainActivity activity); void inject(MainActivity activity);
void inject(FirstStartActivity activity); void inject(FirstStartActivity activity);
void inject(DeviceActivity activity);
void inject(FolderActivity activity); void inject(FolderActivity activity);
void inject(FolderPickerActivity activity); void inject(FolderPickerActivity activity);
void inject(SyncConditionsActivity activity); void inject(SyncConditionsActivity activity);

View file

@ -4,6 +4,7 @@ import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
@ -24,10 +25,14 @@ import android.widget.Toast;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult; import com.google.zxing.integration.android.IntentResult;
import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.model.Connections; import com.nutomic.syncthingandroid.model.Connections;
import com.nutomic.syncthingandroid.model.Device; import com.nutomic.syncthingandroid.model.Device;
import com.nutomic.syncthingandroid.service.Constants;
import com.nutomic.syncthingandroid.service.RestApi;
import com.nutomic.syncthingandroid.service.SyncthingService; import com.nutomic.syncthingandroid.service.SyncthingService;
import com.nutomic.syncthingandroid.SyncthingApp;
import com.nutomic.syncthingandroid.util.Compression; import com.nutomic.syncthingandroid.util.Compression;
import com.nutomic.syncthingandroid.util.TextWatcherAdapter; import com.nutomic.syncthingandroid.util.TextWatcherAdapter;
import com.nutomic.syncthingandroid.util.Util; import com.nutomic.syncthingandroid.util.Util;
@ -36,6 +41,8 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import static android.text.TextUtils.isEmpty; import static android.text.TextUtils.isEmpty;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
@ -46,7 +53,11 @@ import static com.nutomic.syncthingandroid.util.Compression.METADATA;
/** /**
* Shows device details and allows changing them. * Shows device details and allows changing them.
*/ */
public class DeviceActivity extends SyncthingActivity implements View.OnClickListener { public class DeviceActivity extends SyncthingActivity
implements
View.OnClickListener,
SyncthingActivity.OnServiceConnectedListener,
SyncthingService.OnServiceStateChangeListener {
public static final String EXTRA_NOTIFICATION_ID = public static final String EXTRA_NOTIFICATION_ID =
"com.nutomic.syncthingandroid.activities.DeviceActivity.NOTIFICATION_ID"; "com.nutomic.syncthingandroid.activities.DeviceActivity.NOTIFICATION_ID";
@ -84,10 +95,19 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
private SwitchCompat mDevicePaused; private SwitchCompat mDevicePaused;
private SwitchCompat mCustomSyncConditionsSwitch;
private TextView mCustomSyncConditionsDescription;
private TextView mCustomSyncConditionsDialog;
private TextView mSyncthingVersionView; private TextView mSyncthingVersionView;
private View mCompressionContainer; private View mCompressionContainer;
@Inject
SharedPreferences mPreferences;
private boolean mIsCreateMode; private boolean mIsCreateMode;
private boolean mDeviceNeedsToUpdate; private boolean mDeviceNeedsToUpdate;
@ -154,6 +174,12 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
mDevice.paused = isChecked; mDevice.paused = isChecked;
mDeviceNeedsToUpdate = true; mDeviceNeedsToUpdate = true;
break; break;
case R.id.customSyncConditionsSwitch:
mCustomSyncConditionsDescription.setEnabled(isChecked);
mCustomSyncConditionsDialog.setEnabled(isChecked);
// This is needed to display the "discard changes dialog".
mDeviceNeedsToUpdate = true;
break;
} }
} }
}; };
@ -161,11 +187,12 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
((SyncthingApp) getApplication()).component().inject(this);
setContentView(R.layout.fragment_device); setContentView(R.layout.fragment_device);
mIsCreateMode = getIntent().getBooleanExtra(EXTRA_IS_CREATE, false); mIsCreateMode = getIntent().getBooleanExtra(EXTRA_IS_CREATE, false);
registerOnServiceConnectedListener(this::onServiceConnected);
setTitle(mIsCreateMode ? R.string.add_device : R.string.edit_device); setTitle(mIsCreateMode ? R.string.add_device : R.string.edit_device);
registerOnServiceConnectedListener(this);
mIdContainer = findViewById(R.id.idContainer); mIdContainer = findViewById(R.id.idContainer);
mIdView = findViewById(R.id.id); mIdView = findViewById(R.id.id);
@ -177,9 +204,13 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
mCompressionValueView = findViewById(R.id.compressionValue); mCompressionValueView = findViewById(R.id.compressionValue);
mIntroducerView = findViewById(R.id.introducer); mIntroducerView = findViewById(R.id.introducer);
mDevicePaused = findViewById(R.id.devicePause); mDevicePaused = findViewById(R.id.devicePause);
mCustomSyncConditionsSwitch = findViewById(R.id.customSyncConditionsSwitch);
mCustomSyncConditionsDescription = findViewById(R.id.customSyncConditionsDescription);
mCustomSyncConditionsDialog = findViewById(R.id.customSyncConditionsDialog);
mSyncthingVersionView = findViewById(R.id.syncthingVersion); mSyncthingVersionView = findViewById(R.id.syncthingVersion);
mQrButton.setOnClickListener(this); mQrButton.setOnClickListener(this);
mCustomSyncConditionsDialog.setOnClickListener(view -> onCustomSyncConditionsDialogClick());
mCompressionContainer.setOnClickListener(this); mCompressionContainer.setOnClickListener(this);
if (savedInstanceState != null){ if (savedInstanceState != null){
@ -198,6 +229,19 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
} }
} }
/**
* Invoked after user clicked on the {@link mCustomSyncConditionsDialog} label.
*/
private void onCustomSyncConditionsDialogClick() {
startActivityForResult(
SyncConditionsActivity.createIntent(
this, Constants.PREF_OBJECT_PREFIX_DEVICE + mDevice.deviceID, mDevice.name
),
0
);
return;
}
private void restoreDialogStates(Bundle savedInstanceState) { private void restoreDialogStates(Bundle savedInstanceState) {
if (savedInstanceState.getBoolean(IS_SHOWING_COMPRESSION_DIALOG)){ if (savedInstanceState.getBoolean(IS_SHOWING_COMPRESSION_DIALOG)){
showCompressionDialog(); showCompressionDialog();
@ -257,20 +301,32 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
Util.dismissDialogSafe(mDeleteDialog, this); Util.dismissDialogSafe(mDeleteDialog, this);
} }
private void onServiceConnected() { /**
* Register for service state change events.
*/
@Override
public void onServiceConnected() {
Log.v(TAG, "onServiceConnected"); Log.v(TAG, "onServiceConnected");
SyncthingService syncthingService = (SyncthingService) getService(); SyncthingService syncthingService = (SyncthingService) getService();
syncthingService.getNotificationHandler().cancelConsentNotification(getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 0)); syncthingService.getNotificationHandler().cancelConsentNotification(getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 0));
syncthingService.registerOnServiceStateChangeListener(this::onServiceStateChange); syncthingService.registerOnServiceStateChangeListener(this);
} }
/** /**
* Sets version and current address of the device. * Sets version and current address of the device.
* <p/>
* NOTE: This is only called once on startup, should be called more often to properly display * NOTE: This is only called once on startup, should be called more often to properly display
* version/address changes. * version/address changes.
*/ */
private void onReceiveConnections(Connections connections) { private void onReceiveConnections(Connections connections) {
if (connections == null || connections.connections == null) {
Log.e(TAG, "onReceiveConnections: connections == null || connections.connections == null");
return;
}
if (mDevice == null) {
Log.e(TAG, "onReceiveConnections: mDevice == null");
return;
}
boolean viewsExist = mSyncthingVersionView != null && mCurrentAddressView != null; boolean viewsExist = mSyncthingVersionView != null && mCurrentAddressView != null;
if (viewsExist && connections.connections.containsKey(mDevice.deviceID)) { if (viewsExist && connections.connections.containsKey(mDevice.deviceID)) {
mCurrentAddressView.setVisibility(VISIBLE); mCurrentAddressView.setVisibility(VISIBLE);
@ -280,18 +336,21 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
} }
} }
private void onServiceStateChange(SyncthingService.State currentState) { @Override
public void onServiceStateChange(SyncthingService.State currentState) {
if (currentState != ACTIVE) { if (currentState != ACTIVE) {
finish(); finish();
return; return;
} }
if (!mIsCreateMode) { if (!mIsCreateMode) {
List<Device> devices = getApi().getDevices(false); RestApi restApi = getApi(); // restApi != null because of State.ACTIVE
List<Device> devices = restApi.getDevices(false);
String passedId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
mDevice = null; mDevice = null;
for (Device device : devices) { for (Device currentDevice : devices) {
if (device.deviceID.equals(getIntent().getStringExtra(EXTRA_DEVICE_ID))) { if (currentDevice.deviceID.equals(passedId)) {
mDevice = device; mDevice = currentDevice;
break; break;
} }
} }
@ -300,10 +359,10 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
finish(); finish();
return; return;
} }
if (restApi != null) {
restApi.getConnections(this::onReceiveConnections);
}
} }
getApi().getConnections(this::onReceiveConnections);
updateViewsAndSetListeners(); updateViewsAndSetListeners();
} }
@ -313,6 +372,7 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
mAddressesView.removeTextChangedListener(mAddressesTextWatcher); mAddressesView.removeTextChangedListener(mAddressesTextWatcher);
mIntroducerView.setOnCheckedChangeListener(null); mIntroducerView.setOnCheckedChangeListener(null);
mDevicePaused.setOnCheckedChangeListener(null); mDevicePaused.setOnCheckedChangeListener(null);
mCustomSyncConditionsSwitch.setOnCheckedChangeListener(null);
// Update views // Update views
mIdView.setText(mDevice.deviceID); mIdView.setText(mDevice.deviceID);
@ -322,12 +382,26 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
mIntroducerView.setChecked(mDevice.introducer); mIntroducerView.setChecked(mDevice.introducer);
mDevicePaused.setChecked(mDevice.paused); mDevicePaused.setChecked(mDevice.paused);
// 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_DEVICE + mDevice.deviceID), false
));
}
mCustomSyncConditionsSwitch.setEnabled(!mIsCreateMode);
mCustomSyncConditionsDescription.setEnabled(mCustomSyncConditionsSwitch.isChecked());
mCustomSyncConditionsDialog.setEnabled(mCustomSyncConditionsSwitch.isChecked());
// Keep state updated // Keep state updated
mIdView.addTextChangedListener(mIdTextWatcher); mIdView.addTextChangedListener(mIdTextWatcher);
mNameView.addTextChangedListener(mNameTextWatcher); mNameView.addTextChangedListener(mNameTextWatcher);
mAddressesView.addTextChangedListener(mAddressesTextWatcher); mAddressesView.addTextChangedListener(mAddressesTextWatcher);
mIntroducerView.setOnCheckedChangeListener(mCheckedListener); mIntroducerView.setOnCheckedChangeListener(mCheckedListener);
mDevicePaused.setOnCheckedChangeListener(mCheckedListener); mDevicePaused.setOnCheckedChangeListener(mCheckedListener);
mCustomSyncConditionsSwitch.setOnCheckedChangeListener(mCheckedListener);
} }
@Override @Override
@ -423,11 +497,34 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
/** /**
* Sends the updated device info if in edit mode. * Sends the updated device info if in edit mode.
* Preconditions: mDeviceNeedsToUpdate == true
*/ */
private void updateDevice() { private void updateDevice() {
if (!mIsCreateMode && mDeviceNeedsToUpdate && mDevice != null) { if (mIsCreateMode) {
getApi().editDevice(mDevice); // If we are about to create this folder, we cannot update via restApi.
return;
} }
if (mDevice == null) {
Log.e(TAG, "updateDevice: mDevice == null");
return;
}
// Save device specific preferences.
Log.v(TAG, "updateDevice: mDevice.deviceID = \'" + mDevice.deviceID + "\'");
SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean(
Constants.DYN_PREF_OBJECT_CUSTOM_SYNC_CONDITIONS(Constants.PREF_OBJECT_PREFIX_DEVICE + mDevice.deviceID),
mCustomSyncConditionsSwitch.isChecked()
);
editor.apply();
// Update device via restApi and send the config to REST endpoint.
RestApi restApi = getApi();
if (restApi == null) {
Log.e(TAG, "updateDevice: restApi == null");
return;
}
restApi.updateDevice(mDevice);
} }
private List<String> persistableAddresses(CharSequence userInput) { private List<String> persistableAddresses(CharSequence userInput) {

View file

@ -263,7 +263,6 @@ public class FolderActivity extends SyncthingActivity
/** /**
* Invoked after user clicked on the {@link mCustomSyncConditionsDialog} label. * Invoked after user clicked on the {@link mCustomSyncConditionsDialog} label.
*/ */
@SuppressLint("InlinedAPI")
private void onCustomSyncConditionsDialogClick() { private void onCustomSyncConditionsDialogClick() {
startActivityForResult( startActivityForResult(
SyncConditionsActivity.createIntent( SyncConditionsActivity.createIntent(
@ -367,7 +366,7 @@ public class FolderActivity extends SyncthingActivity
} }
/** /**
* Save current settings in case we are in create mode and they aren't yet stored in the config. * Register for service state change events.
*/ */
@Override @Override
public void onServiceConnected() { public void onServiceConnected() {
@ -701,11 +700,19 @@ public class FolderActivity extends SyncthingActivity
deviceView.setOnCheckedChangeListener(mCheckedListener); deviceView.setOnCheckedChangeListener(mCheckedListener);
} }
/**
* Sends the updated folder info if in edit mode.
* Preconditions: mFolderNeedsToUpdate == true
*/
private void updateFolder() { private void updateFolder() {
if (mIsCreateMode) { if (mIsCreateMode) {
// If we are about to create this folder, we cannot update via restApi. // If we are about to create this folder, we cannot update via restApi.
return; return;
} }
if (mFolder == null) {
Log.e(TAG, "updateFolder: mFolder == null");
return;
}
// Save folder specific preferences. // Save folder specific preferences.
Log.v(TAG, "updateFolder: mFolder.id = \'" + mFolder.id + "\'"); Log.v(TAG, "updateFolder: mFolder.id = \'" + mFolder.id + "\'");
@ -716,18 +723,13 @@ public class FolderActivity extends SyncthingActivity
); );
editor.apply(); editor.apply();
// Update folder via restApi. // Update folder via restApi and send the config to REST endpoint.
RestApi restApi = getApi(); RestApi restApi = getApi();
/**
* RestApi is guaranteed not to be null as {@link onServiceStateChange}
* immediately finishes this activity if SyncthingService shuts down.
*/
/*
if (restApi == null) { if (restApi == null) {
Log.e(TAG, "updateFolder: restApi == null"); Log.e(TAG, "updateFolder: restApi == null");
return; return;
} }
*/
// Update ignore list. // Update ignore list.
String[] ignore = mEditIgnoreListContent.getText().toString().split("\n"); String[] ignore = mEditIgnoreListContent.getText().toString().split("\n");
restApi.postFolderIgnoreList(mFolder.id, ignore); restApi.postFolderIgnoreList(mFolder.id, ignore);

View file

@ -105,6 +105,7 @@ public class SettingsActivity extends SyncthingActivity {
private CheckBoxPreference mRunOnMobileData; private CheckBoxPreference mRunOnMobileData;
private CheckBoxPreference mRunOnWifi; private CheckBoxPreference mRunOnWifi;
private CheckBoxPreference mRunOnMeteredWifi; private CheckBoxPreference mRunOnMeteredWifi;
private CheckBoxPreference mUseWifiWhitelist;
private WifiSsidPreference mWifiSsidWhitelist; private WifiSsidPreference mWifiSsidWhitelist;
private CheckBoxPreference mRunInFlightMode; private CheckBoxPreference mRunInFlightMode;
@ -173,6 +174,8 @@ public class SettingsActivity extends SyncthingActivity {
(CheckBoxPreference) findPreference(Constants.PREF_RUN_ON_WIFI); (CheckBoxPreference) findPreference(Constants.PREF_RUN_ON_WIFI);
mRunOnMeteredWifi = mRunOnMeteredWifi =
(CheckBoxPreference) findPreference(Constants.PREF_RUN_ON_METERED_WIFI); (CheckBoxPreference) findPreference(Constants.PREF_RUN_ON_METERED_WIFI);
mUseWifiWhitelist =
(CheckBoxPreference) findPreference(Constants.PREF_USE_WIFI_SSID_WHITELIST);
mWifiSsidWhitelist = mWifiSsidWhitelist =
(WifiSsidPreference) findPreference(Constants.PREF_WIFI_SSID_WHITELIST); (WifiSsidPreference) findPreference(Constants.PREF_WIFI_SSID_WHITELIST);
mRunInFlightMode = mRunInFlightMode =
@ -225,7 +228,8 @@ public class SettingsActivity extends SyncthingActivity {
Preference appVersion = screen.findPreference("app_version"); Preference appVersion = screen.findPreference("app_version");
mRunOnMeteredWifi.setEnabled(mRunOnWifi.isChecked()); mRunOnMeteredWifi.setEnabled(mRunOnWifi.isChecked());
mWifiSsidWhitelist.setEnabled(mRunOnWifi.isChecked()); mUseWifiWhitelist.setEnabled(mRunOnWifi.isChecked());
mWifiSsidWhitelist.setEnabled(mRunOnWifi.isChecked() && mUseWifiWhitelist.isChecked());
/* Experimental options */ /* Experimental options */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
/* Wakelocks are only valid on Android 5 or lower. */ /* Wakelocks are only valid on Android 5 or lower. */
@ -262,7 +266,7 @@ public class SettingsActivity extends SyncthingActivity {
screen.findPreference(Constants.PREF_POWER_SOURCE).setSummary(mPowerSource.getEntry()); screen.findPreference(Constants.PREF_POWER_SOURCE).setSummary(mPowerSource.getEntry());
String wifiSsidSummary = TextUtils.join(", ", mPreferences.getStringSet(Constants.PREF_WIFI_SSID_WHITELIST, new HashSet<>())); String wifiSsidSummary = TextUtils.join(", ", mPreferences.getStringSet(Constants.PREF_WIFI_SSID_WHITELIST, new HashSet<>()));
screen.findPreference(Constants.PREF_WIFI_SSID_WHITELIST).setSummary(TextUtils.isEmpty(wifiSsidSummary) ? screen.findPreference(Constants.PREF_WIFI_SSID_WHITELIST).setSummary(TextUtils.isEmpty(wifiSsidSummary) ?
getString(R.string.run_on_all_wifi_networks) : getString(R.string.wifi_ssid_whitelist_empty) :
getString(R.string.run_on_whitelisted_wifi_networks, wifiSsidSummary) getString(R.string.run_on_whitelisted_wifi_networks, wifiSsidSummary)
); );
handleSocksProxyPreferenceChange(screen.findPreference(Constants.PREF_SOCKS_PROXY_ADDRESS), mPreferences.getString(Constants.PREF_SOCKS_PROXY_ADDRESS, "")); handleSocksProxyPreferenceChange(screen.findPreference(Constants.PREF_SOCKS_PROXY_ADDRESS), mPreferences.getString(Constants.PREF_SOCKS_PROXY_ADDRESS, ""));
@ -366,12 +370,16 @@ public class SettingsActivity extends SyncthingActivity {
break; break;
case Constants.PREF_RUN_ON_WIFI: case Constants.PREF_RUN_ON_WIFI:
mRunOnMeteredWifi.setEnabled((Boolean) o); mRunOnMeteredWifi.setEnabled((Boolean) o);
mUseWifiWhitelist.setEnabled((Boolean) o);
mWifiSsidWhitelist.setEnabled((Boolean) o && mUseWifiWhitelist.isChecked());
break;
case Constants.PREF_USE_WIFI_SSID_WHITELIST:
mWifiSsidWhitelist.setEnabled((Boolean) o); mWifiSsidWhitelist.setEnabled((Boolean) o);
break; break;
case Constants.PREF_WIFI_SSID_WHITELIST: case Constants.PREF_WIFI_SSID_WHITELIST:
String wifiSsidSummary = TextUtils.join(", ", (Set<String>) o); String wifiSsidSummary = TextUtils.join(", ", (Set<String>) o);
preference.setSummary(TextUtils.isEmpty(wifiSsidSummary) ? preference.setSummary(TextUtils.isEmpty(wifiSsidSummary) ?
getString(R.string.run_on_all_wifi_networks) : getString(R.string.wifi_ssid_whitelist_empty) :
getString(R.string.run_on_whitelisted_wifi_networks, wifiSsidSummary) getString(R.string.run_on_whitelisted_wifi_networks, wifiSsidSummary)
); );
break; break;
@ -386,7 +394,7 @@ public class SettingsActivity extends SyncthingActivity {
case "deviceName": case "deviceName":
Device localDevice = mRestApi.getLocalDevice(); Device localDevice = mRestApi.getLocalDevice();
localDevice.name = (String) o; localDevice.name = (String) o;
mRestApi.editDevice(localDevice); mRestApi.updateDevice(localDevice);
break; break;
case "listenAddresses": case "listenAddresses":
mOptions.listenAddresses = Iterables.toArray(splitter.split((String) o), String.class); mOptions.listenAddresses = Iterables.toArray(splitter.split((String) o), String.class);

View file

@ -60,7 +60,8 @@ public class SyncConditionsActivity extends SyncthingActivity
private SwitchCompat mSyncOnMobileData; private SwitchCompat mSyncOnMobileData;
/** /**
* Shared preferences names for custom per-folder settings. * Shared preferences names for custom object settings.
* Object can e.g. be a folder or device.
*/ */
private String mObjectPrefixAndId; private String mObjectPrefixAndId;
private String mPrefSyncOnWifi; private String mPrefSyncOnWifi;
@ -108,7 +109,7 @@ public class SyncConditionsActivity extends SyncthingActivity
mObjectPrefixAndId = intent.getStringExtra(EXTRA_OBJECT_PREFIX_AND_ID); mObjectPrefixAndId = intent.getStringExtra(EXTRA_OBJECT_PREFIX_AND_ID);
Log.v(TAG, "Prefix is \'" + mObjectPrefixAndId + "\' (" + mObjectReadableName + ")"); Log.v(TAG, "Prefix is \'" + mObjectPrefixAndId + "\' (" + mObjectReadableName + ")");
mPrefSyncOnWifi = Constants.DYN_PREF_OBJECT_SYNC_ON_WIFI(mObjectPrefixAndId); mPrefSyncOnWifi = Constants.DYN_PREF_OBJECT_SYNC_ON_WIFI(mObjectPrefixAndId);
mPrefSyncOnWhitelistedWifi = Constants.DYN_PREF_OBJECT_SYNC_ON_WHITELISTED_WIFI(mObjectPrefixAndId); mPrefSyncOnWhitelistedWifi = Constants.DYN_PREF_OBJECT_USE_WIFI_SSID_WHITELIST(mObjectPrefixAndId);
mPrefSelectedWhitelistSsid = Constants.DYN_PREF_OBJECT_SELECTED_WHITELIST_SSID(mObjectPrefixAndId); mPrefSelectedWhitelistSsid = Constants.DYN_PREF_OBJECT_SELECTED_WHITELIST_SSID(mObjectPrefixAndId);
mPrefSyncOnMeteredWifi = Constants.DYN_PREF_OBJECT_SYNC_ON_METERED_WIFI(mObjectPrefixAndId); mPrefSyncOnMeteredWifi = Constants.DYN_PREF_OBJECT_SYNC_ON_METERED_WIFI(mObjectPrefixAndId);
mPrefSyncOnMobileData = Constants.DYN_PREF_OBJECT_SYNC_ON_MOBILE_DATA(mObjectPrefixAndId); mPrefSyncOnMobileData = Constants.DYN_PREF_OBJECT_SYNC_ON_MOBILE_DATA(mObjectPrefixAndId);
@ -117,14 +118,13 @@ public class SyncConditionsActivity extends SyncthingActivity
* Load global run conditions. * Load global run conditions.
*/ */
Boolean globalRunOnWifiEnabled = mPreferences.getBoolean(Constants.PREF_RUN_ON_WIFI, true); Boolean globalRunOnWifiEnabled = mPreferences.getBoolean(Constants.PREF_RUN_ON_WIFI, true);
Boolean globalWhitelistEnabled = !mPreferences.getStringSet(Constants.PREF_WIFI_SSID_WHITELIST, new HashSet<>())
.isEmpty();
Set<String> globalWhitelistedSsid = mPreferences.getStringSet(Constants.PREF_WIFI_SSID_WHITELIST, new HashSet<>()); Set<String> globalWhitelistedSsid = mPreferences.getStringSet(Constants.PREF_WIFI_SSID_WHITELIST, new HashSet<>());
Boolean globalWhitelistEnabled = mPreferences.getBoolean(Constants.PREF_USE_WIFI_SSID_WHITELIST, false);
Boolean globalRunOnMeteredWifiEnabled = mPreferences.getBoolean(Constants.PREF_RUN_ON_METERED_WIFI, false); Boolean globalRunOnMeteredWifiEnabled = mPreferences.getBoolean(Constants.PREF_RUN_ON_METERED_WIFI, false);
Boolean globalRunOnMobileDataEnabled = mPreferences.getBoolean(Constants.PREF_RUN_ON_MOBILE_DATA, false); Boolean globalRunOnMobileDataEnabled = mPreferences.getBoolean(Constants.PREF_RUN_ON_MOBILE_DATA, false);
/** /**
* Load custom folder preferences. If unset, use global setting as default. * Load custom object preferences. If unset, use global setting as default.
*/ */
mSyncOnWifi.setChecked(globalRunOnWifiEnabled && mPreferences.getBoolean(mPrefSyncOnWifi, globalRunOnWifiEnabled)); mSyncOnWifi.setChecked(globalRunOnWifiEnabled && mPreferences.getBoolean(mPrefSyncOnWifi, globalRunOnWifiEnabled));
mSyncOnWifi.setEnabled(globalRunOnWifiEnabled); mSyncOnWifi.setEnabled(globalRunOnWifiEnabled);
@ -143,7 +143,7 @@ public class SyncConditionsActivity extends SyncthingActivity
mSyncOnMobileData.setOnCheckedChangeListener(mCheckedListener); mSyncOnMobileData.setOnCheckedChangeListener(mCheckedListener);
// Read selected WiFi Ssid whitelist items. // Read selected WiFi Ssid whitelist items.
Set<String> selectedWhitelistedSsid = mPreferences.getStringSet(mPrefSelectedWhitelistSsid, new HashSet<>()); Set<String> selectedWhitelistedSsid = mPreferences.getStringSet(mPrefSelectedWhitelistSsid, globalWhitelistedSsid);
// Removes any network that is no longer part of the global WiFi Ssid whitelist. // Removes any network that is no longer part of the global WiFi Ssid whitelist.
selectedWhitelistedSsid.retainAll(globalWhitelistedSsid); selectedWhitelistedSsid.retainAll(globalWhitelistedSsid);
@ -162,7 +162,7 @@ public class SyncConditionsActivity extends SyncthingActivity
setMarginEnd(params, contentInset); setMarginEnd(params, contentInset);
TextView emptyView = new TextView(mWifiSsidContainer.getContext()); TextView emptyView = new TextView(mWifiSsidContainer.getContext());
emptyView.setGravity(CENTER_VERTICAL); emptyView.setGravity(CENTER_VERTICAL);
emptyView.setText(R.string.wifi_ssid_whitelist_empty); emptyView.setText(R.string.custom_wifi_ssid_whitelist_empty);
mWifiSsidContainer.addView(emptyView, params); mWifiSsidContainer.addView(emptyView, params);
mWifiSsidContainer.setEnabled(false); mWifiSsidContainer.setEnabled(false);
} else { } else {
@ -224,7 +224,7 @@ public class SyncConditionsActivity extends SyncthingActivity
if (mUnsavedChanges) { if (mUnsavedChanges) {
Log.v(TAG, "onPause: mUnsavedChanges == true. Saving prefs ..."); Log.v(TAG, "onPause: mUnsavedChanges == true. Saving prefs ...");
/** /**
* Save custom folder preferences. * Save custom object preferences.
*/ */
SharedPreferences.Editor editor = mPreferences.edit(); SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean(mPrefSyncOnWifi, mSyncOnWifi.isChecked()); editor.putBoolean(mPrefSyncOnWifi, mSyncOnWifi.isChecked());

View file

@ -15,6 +15,7 @@ public class Constants {
public static final String PREF_RUN_ON_MOBILE_DATA = "run_on_mobile_data"; public static final String PREF_RUN_ON_MOBILE_DATA = "run_on_mobile_data";
public static final String PREF_RUN_ON_WIFI = "run_on_wifi"; public static final String PREF_RUN_ON_WIFI = "run_on_wifi";
public static final String PREF_RUN_ON_METERED_WIFI = "run_on_metered_wifi"; public static final String PREF_RUN_ON_METERED_WIFI = "run_on_metered_wifi";
public static final String PREF_USE_WIFI_SSID_WHITELIST = "use_wifi_whitelist";
public static final String PREF_WIFI_SSID_WHITELIST = "wifi_ssid_whitelist"; public static final String PREF_WIFI_SSID_WHITELIST = "wifi_ssid_whitelist";
public static final String PREF_POWER_SOURCE = "power_source"; public static final String PREF_POWER_SOURCE = "power_source";
public static final String PREF_RESPECT_BATTERY_SAVING = "respect_battery_saving"; public static final String PREF_RESPECT_BATTERY_SAVING = "respect_battery_saving";
@ -43,8 +44,8 @@ public class Constants {
return objectPrefixAndId + "_" + PREF_RUN_ON_WIFI; return objectPrefixAndId + "_" + PREF_RUN_ON_WIFI;
} }
public static String DYN_PREF_OBJECT_SYNC_ON_WHITELISTED_WIFI(String objectPrefixAndId) { public static String DYN_PREF_OBJECT_USE_WIFI_SSID_WHITELIST(String objectPrefixAndId) {
return objectPrefixAndId + "_" + "use_wifi_whitelist"; return objectPrefixAndId + "_" + PREF_USE_WIFI_SSID_WHITELIST;
} }
public static String DYN_PREF_OBJECT_SELECTED_WHITELIST_SSID(String objectPrefixAndId) { public static String DYN_PREF_OBJECT_SELECTED_WHITELIST_SSID(String objectPrefixAndId) {

View file

@ -518,7 +518,7 @@ public class RestApi {
}, errorListener); }, errorListener);
} }
public void editDevice(Device newDevice) { public void updateDevice(Device newDevice) {
synchronized (mConfigLock) { synchronized (mConfigLock) {
removeDeviceInternal(newDevice.deviceID); removeDeviceInternal(newDevice.deviceID);
mConfig.devices.add(newDevice); mConfig.devices.add(newDevice);
@ -782,26 +782,58 @@ public class RestApi {
Log.v(TAG, "onSyncPreconditionChanged: Event fired."); Log.v(TAG, "onSyncPreconditionChanged: Event fired.");
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
synchronized (mConfigLock) { synchronized (mConfigLock) {
if (mConfig == null || mConfig.folders == null) { Boolean configChanged = false;
// Check if the config has been loaded.
if (mConfig == null) {
Log.d(TAG, "onSyncPreconditionChanged: mConfig is not ready yet.");
return;
}
// Check if the folders are available from config.
if (mConfig.folders != null) {
for (Folder folder : mConfig.folders) {
Boolean folderCustomSyncConditionsEnabled = sharedPreferences.getBoolean(
Constants.DYN_PREF_OBJECT_CUSTOM_SYNC_CONDITIONS(Constants.PREF_OBJECT_PREFIX_FOLDER + folder.id), false
);
if (folderCustomSyncConditionsEnabled) {
Boolean syncConditionsMet = runConditionMonitor.checkObjectSyncConditions(
Constants.PREF_OBJECT_PREFIX_FOLDER + folder.id
);
Log.v(TAG, "onSyncPreconditionChanged: syncFolder(" + folder.id + ")=" + (syncConditionsMet ? "1" : "0"));
if (folder.paused != !syncConditionsMet) {
folder.paused = !syncConditionsMet;
configChanged = true;
}
}
}
} else {
Log.d(TAG, "onSyncPreconditionChanged: mConfig.folders is not ready yet."); Log.d(TAG, "onSyncPreconditionChanged: mConfig.folders is not ready yet.");
return; return;
} }
Boolean configChanged = false;
for (Folder folder : mConfig.folders) { // Check if the devices are available from config.
Boolean folderCustomSyncConditionsEnabled = sharedPreferences.getBoolean( if (mConfig.devices != null) {
Constants.DYN_PREF_OBJECT_CUSTOM_SYNC_CONDITIONS(Constants.PREF_OBJECT_PREFIX_FOLDER + folder.id), false for (Device device : mConfig.devices) {
); Boolean deviceCustomSyncConditionsEnabled = sharedPreferences.getBoolean(
if (folderCustomSyncConditionsEnabled) { Constants.DYN_PREF_OBJECT_CUSTOM_SYNC_CONDITIONS(Constants.PREF_OBJECT_PREFIX_DEVICE + device.deviceID), false
Boolean syncConditionsMet = runConditionMonitor.checkObjectSyncConditions(
Constants.PREF_OBJECT_PREFIX_FOLDER + folder.id
); );
Log.v(TAG, "onSyncPreconditionChanged: syncFolder(" + folder.id + ")=" + (syncConditionsMet ? "1" : "0")); if (deviceCustomSyncConditionsEnabled) {
if (folder.paused != !syncConditionsMet) { Boolean syncConditionsMet = runConditionMonitor.checkObjectSyncConditions(
folder.paused = !syncConditionsMet; Constants.PREF_OBJECT_PREFIX_DEVICE + device.deviceID
configChanged = true; );
Log.v(TAG, "onSyncPreconditionChanged: syncDevice(" + device.deviceID + ")=" + (syncConditionsMet ? "1" : "0"));
if (device.paused != !syncConditionsMet) {
device.paused = !syncConditionsMet;
configChanged = true;
}
} }
} }
} else {
Log.d(TAG, "onSyncPreconditionChanged: mConfig.devices is not ready yet.");
return;
} }
if (configChanged) { if (configChanged) {
Log.v(TAG, "onSyncPreconditionChanged: Sending changed config ..."); Log.v(TAG, "onSyncPreconditionChanged: Sending changed config ...");
sendConfig(); sendConfig();

View file

@ -228,11 +228,13 @@ public class RunConditionMonitor {
/** /**
* Constants.PREF_WIFI_SSID_WHITELIST * Constants.PREF_WIFI_SSID_WHITELIST
*/ */
private SyncConditionResult checkConditionSyncOnWhitelistedWifi(String prefNameSyncOnWhitelistedWifi) { private SyncConditionResult checkConditionSyncOnWhitelistedWifi(
Set<String> whitelistedWifiSsids = mPreferences.getStringSet(prefNameSyncOnWhitelistedWifi, new HashSet<>()); String prefNameUseWifiWhitelist,
boolean prefWifiWhitelistEnabled = !whitelistedWifiSsids.isEmpty(); String prefNameSelectedWhitelistSsid) {
boolean wifiWhitelistEnabled = mPreferences.getBoolean(prefNameUseWifiWhitelist, false);
Set<String> whitelistedWifiSsids = mPreferences.getStringSet(prefNameSelectedWhitelistSsid, new HashSet<>());
try { try {
if (wifiWhitelistConditionMet(prefWifiWhitelistEnabled, whitelistedWifiSsids)) { if (wifiWhitelistConditionMet(wifiWhitelistEnabled, whitelistedWifiSsids)) {
return new SyncConditionResult(true, "\n" + res.getString(R.string.reason_on_whitelisted_wifi)); return new SyncConditionResult(true, "\n" + res.getString(R.string.reason_on_whitelisted_wifi));
} }
return new SyncConditionResult(false, "\n" + res.getString(R.string.reason_not_on_whitelisted_wifi)); return new SyncConditionResult(false, "\n" + res.getString(R.string.reason_not_on_whitelisted_wifi));
@ -348,7 +350,7 @@ public class RunConditionMonitor {
// Wifi type is allowed. // Wifi type is allowed.
Log.v(TAG, "decideShouldRun: checkConditionSyncOnWifi && checkConditionSyncOnMeteredWifi"); Log.v(TAG, "decideShouldRun: checkConditionSyncOnWifi && checkConditionSyncOnMeteredWifi");
scr = checkConditionSyncOnWhitelistedWifi(Constants.PREF_WIFI_SSID_WHITELIST); scr = checkConditionSyncOnWhitelistedWifi(Constants.PREF_USE_WIFI_SSID_WHITELIST, Constants.PREF_WIFI_SSID_WHITELIST);
mRunDecisionExplanation += scr.explanation; mRunDecisionExplanation += scr.explanation;
if (scr.conditionMet) { if (scr.conditionMet) {
// Wifi is whitelisted. // Wifi is whitelisted.
@ -395,7 +397,10 @@ public class RunConditionMonitor {
// Wifi type is allowed. // Wifi type is allowed.
Log.v(TAG, "checkObjectSyncConditions: checkConditionSyncOnWifi && checkConditionSyncOnMeteredWifi"); Log.v(TAG, "checkObjectSyncConditions: checkConditionSyncOnWifi && checkConditionSyncOnMeteredWifi");
scr = checkConditionSyncOnWhitelistedWifi(Constants.DYN_PREF_OBJECT_SELECTED_WHITELIST_SSID(objectPrefixAndId)); scr = checkConditionSyncOnWhitelistedWifi(
Constants.DYN_PREF_OBJECT_USE_WIFI_SSID_WHITELIST(objectPrefixAndId),
Constants.DYN_PREF_OBJECT_SELECTED_WHITELIST_SSID(objectPrefixAndId)
);
if (scr.conditionMet) { if (scr.conditionMet) {
// Wifi is whitelisted. // Wifi is whitelisted.
Log.v(TAG, "checkObjectSyncConditions: checkConditionSyncOnWifi && checkConditionSyncOnMeteredWifi && checkConditionSyncOnWhitelistedWifi"); Log.v(TAG, "checkObjectSyncConditions: checkConditionSyncOnWifi && checkConditionSyncOnMeteredWifi && checkConditionSyncOnWhitelistedWifi");

View file

@ -1,5 +1,5 @@
Enhancements Enhancements
* Specify sync conditions differently for each folder [NEW] * Specify sync conditions differently for each folder, device [NEW]
* UI explains why syncthing is running (or not) * UI explains why syncthing is running (or not)
* Support in-app editing of folder's ignore list items * Support in-app editing of folder's ignore list items
Fixes Fixes

View file

@ -122,6 +122,59 @@
android:drawableStart="@drawable/ic_pause_circle_outline_black_24dp" android:drawableStart="@drawable/ic_pause_circle_outline_black_24dp"
android:text="@string/pause_device" /> android:text="@string/pause_device" />
<!-- Custom sync conditions -->
<LinearLayout
android:id="@+id/customSyncConditionsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:orientation="vertical"
android:gravity="center_vertical">
<android.support.v7.widget.SwitchCompat
android:id="@+id/customSyncConditionsSwitch"
style="@style/Widget.Syncthing.TextView.Label.Details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="false"
android:drawableLeft="@drawable/ic_autorenew_black_24dp"
android:drawableStart="@drawable/ic_autorenew_black_24dp"
android:text="@string/custom_sync_conditions_title" />
<TextView
android:id="@+id/customSyncConditionsDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="75dp"
android:layout_marginStart="75dp"
android:layout_marginTop="-20dp"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:text="@string/custom_sync_conditions_description"
android:focusable="false"/>
<TextView
android:id="@+id/customSyncConditionsDialog"
style="@style/Widget.Syncthing.TextView.Label.Details"
android:layout_marginLeft="56dp"
android:layout_marginStart="56dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:text="@string/custom_sync_conditions_dialog"/>
<TextView
android:id="@+id/customSyncConditionsCurrent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="75dp"
android:layout_marginStart="75dp"
android:layout_marginTop="-20dp"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:text="@null"
android:focusable="false"/>
</LinearLayout>
<TextView <TextView
android:id="@+id/currentAddress" android:id="@+id/currentAddress"
style="@style/Widget.Syncthing.TextView.Label.Details" style="@style/Widget.Syncthing.TextView.Label.Details"

View file

@ -316,8 +316,10 @@ Please report any problems you encounter via Github.</string>
<string name="run_on_metered_wifi_summary">Run when device is connected to a metered Wi-Fi network e.g. a hotspot or tethered network. Attention: This can consume large portion of your data plan if you sync a lot of data.</string> <string name="run_on_metered_wifi_summary">Run when device is connected to a metered Wi-Fi network e.g. a hotspot or tethered network. Attention: This can consume large portion of your data plan if you sync a lot of data.</string>
<string name="run_on_whitelisted_wifi_title">Run on specified Wi-Fi networks</string> <string name="run_on_whitelisted_wifi_title">Run on specified Wi-Fi networks</string>
<string name="run_on_whitelisted_wifi_networks">Run only on selected Wi-Fi networks: %1$s</string> <string name="specify_wifi_ssid_whitelist">Select Wi-Fi networks</string>
<string name="run_on_whitelisted_wifi_networks">Selected Wi-Fi networks: %1$s</string>
<string name="run_on_all_wifi_networks">Run on all Wi-Fi networks.</string> <string name="run_on_all_wifi_networks">Run on all Wi-Fi networks.</string>
<string name="wifi_ssid_whitelist_empty">No Wi-Fi networks specified. Click to specify networks.</string>
<string name="sync_only_wifi_ssids_wifi_turn_on_wifi">Please turn on Wi-Fi to select networks.</string> <string name="sync_only_wifi_ssids_wifi_turn_on_wifi">Please turn on Wi-Fi to select networks.</string>
@ -627,7 +629,7 @@ Please report any problems you encounter via Github.</string>
<!-- Sync Conditions Dialog --> <!-- Sync Conditions Dialog -->
<string name="wifi_ssid_whitelist_empty">No WiFi SSID\'s whitelisted. Please specify some in the settings.</string> <string name="custom_wifi_ssid_whitelist_empty">No WiFi SSID\'s whitelisted. Please specify some in the settings.</string>
<!-- SyncthingService --> <!-- SyncthingService -->

View file

@ -12,23 +12,34 @@
android:title="@string/run_conditions_title" android:title="@string/run_conditions_title"
android:summary="@string/run_conditions_summary"/> android:summary="@string/run_conditions_summary"/>
<!-- Sync on WiFi -->
<CheckBoxPreference <CheckBoxPreference
android:key="run_on_wifi" android:key="run_on_wifi"
android:title="@string/run_on_wifi_title" android:title="@string/run_on_wifi_title"
android:summary="@string/run_on_wifi_summary" android:summary="@string/run_on_wifi_summary"
android:defaultValue="true" /> android:defaultValue="true" />
<!-- Sync on metered WiFi -->
<CheckBoxPreference <CheckBoxPreference
android:key="run_on_metered_wifi" android:key="run_on_metered_wifi"
android:title="@string/run_on_metered_wifi_title" android:title="@string/run_on_metered_wifi_title"
android:summary="@string/run_on_metered_wifi_summary" android:summary="@string/run_on_metered_wifi_summary"
android:defaultValue="false" /> android:defaultValue="false" />
<!-- Use WiFi Ssid whitelist -->
<CheckBoxPreference
android:key="use_wifi_whitelist"
android:title="@string/run_on_whitelisted_wifi_title"
android:summary="@string/run_on_whitelisted_wifi_summary"
android:defaultValue="false" />
<!-- Select whitelisted WiFi Ssid -->
<com.nutomic.syncthingandroid.views.WifiSsidPreference <com.nutomic.syncthingandroid.views.WifiSsidPreference
android:key="wifi_ssid_whitelist" android:key="wifi_ssid_whitelist"
android:title="@string/run_on_whitelisted_wifi_title" android:title="@string/specify_wifi_ssid_whitelist"
android:summary="@null" /> android:summary="@null" />
<!-- Sync on mobile data -->
<CheckBoxPreference <CheckBoxPreference
android:key="run_on_mobile_data" android:key="run_on_mobile_data"
android:title="@string/run_on_mobile_data_title" android:title="@string/run_on_mobile_data_title"