diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index add94cb9..9bf714de 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
+
()); // default to empty list
mUseRoot = (CheckBoxPreference) findPreference(SyncthingService.PREF_USE_ROOT);
Preference appVersion = screen.findPreference(APP_VERSION_KEY);
mOptionsScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_OPTIONS_KEY);
@@ -144,6 +151,7 @@ public class SettingsFragment extends PreferenceFragment
mAlwaysRunInBackground.setOnPreferenceChangeListener(this);
mSyncOnlyCharging.setOnPreferenceChangeListener(this);
mSyncOnlyWifi.setOnPreferenceChangeListener(this);
+ mSyncOnlyOnSSIDs.setOnPreferenceChangeListener(this);
mUseRoot.setOnPreferenceClickListener(this);
screen.findPreference(EXPORT_CONFIG).setOnPreferenceClickListener(this);
screen.findPreference(IMPORT_CONFIG).setOnPreferenceClickListener(this);
@@ -153,8 +161,9 @@ public class SettingsFragment extends PreferenceFragment
sttrace.setOnPreferenceChangeListener(this);
// Force summary update and wifi/charging preferences enable/disable.
- onPreferenceChange(mAlwaysRunInBackground, mAlwaysRunInBackground.isChecked());
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ onPreferenceChange(mAlwaysRunInBackground, mAlwaysRunInBackground.isChecked());
+ onPreferenceChange(mSyncOnlyOnSSIDs, sp.getStringSet("sync_only_wifi_ssids_set", new TreeSet()));
user.setSummary(sp.getString("gui_user", ""));
sttrace.setSummary(sp.getString("sttrace", ""));
}
@@ -236,11 +245,19 @@ public class SettingsFragment extends PreferenceFragment
: R.string.always_run_in_background_disabled);
mSyncOnlyCharging.setEnabled(value);
mSyncOnlyWifi.setEnabled(value);
+ mSyncOnlyOnSSIDs.setEnabled(mSyncOnlyWifi.isChecked());
// Uncheck items when disabled, so it is clear they have no effect.
if (!value) {
mSyncOnlyCharging.setChecked(false);
mSyncOnlyWifi.setChecked(false);
}
+ } else if (preference.equals(mSyncOnlyWifi)) {
+ mSyncOnlyOnSSIDs.setEnabled((Boolean) o);
+ } else if (preference.equals(mSyncOnlyOnSSIDs)) {
+ String ssids = formatWifiNameList((Set) o);
+ mSyncOnlyOnSSIDs.setSummary(ssids.isEmpty()
+ ? getString(R.string.sync_only_wifi_ssids_all)
+ : getString(R.string.sync_only_wifi_ssids_values, ssids));
} else if (preference.getKey().equals(DEVICE_NAME_KEY)) {
RestApi.Device old = mSyncthingService.getApi().getLocalDevice();
RestApi.Device updated = new RestApi.Device();
@@ -297,6 +314,14 @@ public class SettingsFragment extends PreferenceFragment
return true;
}
+ private String formatWifiNameList(Set ssids) {
+ Set formatted = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+ for (String ssid : ssids) {
+ formatted.add(ssid.replaceFirst("^\"", "").replaceFirst("\"$", ""));
+ }
+ return TextUtils.join(", ", formatted);
+ }
+
/**
* Changes the owner of syncthing files so they can be accessed without root.
*/
diff --git a/src/main/java/com/nutomic/syncthingandroid/preferences/WifiSsidPreference.java b/src/main/java/com/nutomic/syncthingandroid/preferences/WifiSsidPreference.java
new file mode 100644
index 00000000..7fe3407d
--- /dev/null
+++ b/src/main/java/com/nutomic/syncthingandroid/preferences/WifiSsidPreference.java
@@ -0,0 +1,129 @@
+package com.nutomic.syncthingandroid.preferences;
+
+import android.content.Context;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.preference.MultiSelectListPreference;
+import android.util.AttributeSet;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import com.nutomic.syncthingandroid.R;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * MultiSelectListPreference which allows the user to select on which WiFi networks (based on SSID)
+ * syncing should be allowed.
+ *
+ * Setting can be "All networks" (none selected), or selecting individual networks.
+ *
+ * Due to restrictions in Android, it is possible/likely, that the list of saved WiFi networks
+ * cannot be retrieved if the WiFi is turned off. In this case, an explanation is shown.
+ *
+ * The preference is stored as Set<String> where an empty set represents
+ * "all networks allowed".
+ *
+ * SSIDs are formatted according to the naming convention of WifiManager, i.e. they have the
+ * surrounding double-quotes (") for UTF-8 names, or they are hex strings (if not quoted).
+ */
+public class WifiSsidPreference extends MultiSelectListPreference {
+
+ public WifiSsidPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public WifiSsidPreference(Context context) {
+ super(context);
+ }
+
+ /**
+ * Show the dialog if WiFi is available and configured networks can be loaded.
+ * Otherwise will display a Toast requesting to turn on WiFi.
+ *
+ * On opening of the dialog, will also remove any SSIDs from the set that have been removed
+ * by the user in the WiFi manager. This change will be persisted only if the user changes
+ * any other setting
+ */
+ @Override
+ protected void showDialog(Bundle state) {
+ WifiConfiguration[] networks = loadConfiguredNetworksSorted();
+ if (networks != null) {
+ Set selected = getSharedPreferences().getStringSet(getKey(), new HashSet());
+ // from JavaDoc: Note that you must not modify the set instance returned by this call.
+ // therefore required to make a defensive copy of the elements
+ selected = new HashSet<>(selected);
+ CharSequence[] all = extractSsid(networks, false);
+ filterRemovedNetworks(selected, all);
+ setEntries(extractSsid(networks, true)); // display without surrounding quotes
+ setEntryValues(all); // the value of the entry is the SSID "as is"
+ setValues(selected); // the currently selected values (without meanwhile deleted networks)
+ super.showDialog(state);
+ } else {
+ Toast.makeText(getContext(), R.string.sync_only_wifi_ssids_wifi_turn_on_wifi, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ /**
+ * Removes any network that is no longer saved on the device. Otherwise it will never be
+ * removed from the allowed set by MultiSelectListPreference.
+ */
+ private void filterRemovedNetworks(Set selected, CharSequence[] all) {
+ HashSet availableNetworks = new HashSet<>(Arrays.asList(all));
+ selected.retainAll(availableNetworks);
+ }
+
+ /**
+ * Converts WiFi configuration to it's string representation, using the SSID.
+ *
+ * It can also remove surrounding quotes which indicate that the SSID is an UTF-8
+ * string and not a Hex-String, if the strings are intended to be displayed to the
+ * user, who will not expect the quotes.
+ *
+ * @param configs the objects to convert
+ * @param stripQuotes if to remove surrounding quotes
+ * @return the formatted SSID of the wifi configurations
+ */
+ private CharSequence[] extractSsid(WifiConfiguration[] configs, boolean stripQuotes) {
+ CharSequence[] result = new CharSequence[configs.length];
+ for (int i = 0; i < configs.length; i++) {
+ // WiFi SSIDs can either be UTF-8 (encapsulated in '"') or hex-strings
+ if (stripQuotes)
+ result[i] = configs[i].SSID.replaceFirst("^\"", "").replaceFirst("\"$", "");
+ else
+ result[i] = configs[i].SSID;
+ }
+ return result;
+ }
+
+ /**
+ * Load the configured WiFi networks, sort them by SSID.
+ *
+ * @return a sorted array of WifiConfiguration, or null, if data cannot be retrieved
+ */
+ private WifiConfiguration[] loadConfiguredNetworksSorted() {
+ WifiManager wifiManager = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
+ if (wifiManager != null) {
+ List configuredNetworks = wifiManager.getConfiguredNetworks();
+ // if WiFi is turned off, getConfiguredNetworks returns null on many devices
+ if (configuredNetworks != null) {
+ WifiConfiguration[] result = configuredNetworks.toArray(new WifiConfiguration[configuredNetworks.size()]);
+ Arrays.sort(result, new Comparator() {
+ @Override
+ public int compare(WifiConfiguration lhs, WifiConfiguration rhs) {
+ return lhs.SSID.compareToIgnoreCase(rhs.SSID);
+ }
+ });
+ return result;
+ }
+ }
+ // WiFi is turned off or device doesn't have WiFi
+ return null;
+ }
+
+}
diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/DeviceStateHolder.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/DeviceStateHolder.java
index 14803da9..9c028ae0 100644
--- a/src/main/java/com/nutomic/syncthingandroid/syncthing/DeviceStateHolder.java
+++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/DeviceStateHolder.java
@@ -5,6 +5,8 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
import android.os.BatteryManager;
/**
@@ -26,17 +28,25 @@ public class DeviceStateHolder extends BroadcastReceiver {
*/
public static final String EXTRA_IS_CHARGING = "is_charging";
+ private Context mContext;
+
private boolean mIsWifiConnected = false;
+ private String mWifiSsid;
+
private boolean mIsCharging = false;
@TargetApi(16)
public DeviceStateHolder(Context context) {
+ mContext = context;
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
mIsWifiConnected = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI).isConnected();
if (android.os.Build.VERSION.SDK_INT >= 16 && cm.isActiveNetworkMetered())
mIsWifiConnected = false;
+ if (mIsWifiConnected) {
+ updateWifiSsid();
+ }
}
/**
@@ -61,5 +71,25 @@ public class DeviceStateHolder extends BroadcastReceiver {
public void update(Intent intent) {
mIsWifiConnected = intent.getBooleanExtra(EXTRA_HAS_WIFI, mIsWifiConnected);
mIsCharging = intent.getBooleanExtra(EXTRA_IS_CHARGING, mIsCharging);
+
+ if (mIsWifiConnected) {
+ updateWifiSsid();
+ } else {
+ mWifiSsid = null;
+ }
+ }
+
+ public void updateWifiSsid() {
+ mWifiSsid = null;
+ WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+ // may be null, if WiFi has been turned off in meantime
+ if (wifiInfo != null) {
+ mWifiSsid = wifiInfo.getSSID();
+ }
+ }
+
+ public String getWifiSsid() {
+ return mWifiSsid;
}
}
diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java
index 0ab48604..f431da82 100644
--- a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java
+++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java
@@ -39,6 +39,7 @@ import java.security.SecureRandom;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
+import java.util.Set;
/**
* Holds the native syncthing instance and provides an API to access it.
@@ -92,6 +93,7 @@ public class SyncthingService extends Service implements
public static final String PREF_ALWAYS_RUN_IN_BACKGROUND = "always_run_in_background";
public static final String PREF_SYNC_ONLY_WIFI = "sync_only_wifi";
+ public static final String PREF_SYNC_ONLY_WIFI_SSIDS = "sync_only_wifi_ssids_set";
public static final String PREF_SYNC_ONLY_CHARGING = "sync_only_charging";
public static final String PREF_USE_ROOT = "use_root";
private static final String PREF_NOTIFICATION_TYPE = "notification_type";
@@ -213,7 +215,7 @@ public class SyncthingService extends Service implements
boolean prefStopNotCharging = prefs.getBoolean(PREF_SYNC_ONLY_CHARGING, false);
shouldRun = (mDeviceStateHolder.isCharging() || !prefStopNotCharging) &&
- (mDeviceStateHolder.isWifiConnected() || !prefStopMobileData);
+ (!prefStopMobileData || isAllowedWifiConnected());
}
// Start syncthing.
@@ -261,6 +263,34 @@ public class SyncthingService extends Service implements
onApiChange();
}
+ private boolean isAllowedWifiConnected() {
+ boolean wifiConnected = mDeviceStateHolder.isWifiConnected();
+ if (wifiConnected) {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ Set ssids = sp.getStringSet(PREF_SYNC_ONLY_WIFI_SSIDS, new HashSet());
+ if (ssids.isEmpty()) {
+ Log.d(TAG, "All SSIDs allowed for syncing");
+ return true;
+ } else {
+ String ssid = mDeviceStateHolder.getWifiSsid();
+ if (ssid != null) {
+ if (ssids.contains(ssid)) {
+ Log.d(TAG, "SSID " + ssid + " found in whitelist");
+ return true;
+ }
+ Log.i(TAG, "SSID " + ssid + " not whitelisted");
+ return false;
+ } else {
+ // Don't know the SSID (yet) (should not happen?!), so not allowing
+ Log.w(TAG, "SSID unknown (yet), cannot check SSID whitelist. Disallowing sync.");
+ return false;
+ }
+ }
+ }
+ Log.d(TAG, "Wifi not connected");
+ return false;
+ }
+
/**
* Shows or hides the persistent notification based on running state and
* {@link #PREF_NOTIFICATION_TYPE}.
@@ -290,7 +320,8 @@ public class SyncthingService extends Service implements
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(PREF_NOTIFICATION_TYPE))
updateNotification();
- else if (key.equals(PREF_SYNC_ONLY_CHARGING) || key.equals(PREF_SYNC_ONLY_WIFI))
+ else if (key.equals(PREF_SYNC_ONLY_CHARGING) || key.equals(PREF_SYNC_ONLY_WIFI)
+ || key.equals(PREF_SYNC_ONLY_WIFI_SSIDS))
updateState();
}
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index bba5f32e..1dff15d4 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -238,6 +238,14 @@ Please report any problems you encounter via Github.
Sync only on wifi
+ Restrict to certain wifi networks
+
+ Sync on all wifi networks
+
+ Sync only while connected to: %1$s
+
+ Please turn on WiFi to select networks.
+
Use advanced Folder Picker
Select any folder on the device for syncing
diff --git a/src/main/res/xml/app_settings.xml b/src/main/res/xml/app_settings.xml
index 5ddec148..dbc01dbe 100644
--- a/src/main/res/xml/app_settings.xml
+++ b/src/main/res/xml/app_settings.xml
@@ -19,6 +19,10 @@
android:title="@string/sync_only_wifi"
android:defaultValue="false" />
+
+