Add restriction for allowed WiFi SSIDs when WiFi-only sync

This commit is contained in:
Martin Carpella 2015-11-30 21:10:42 +01:00
parent 90bfb9f548
commit 73c993dcd9
7 changed files with 231 additions and 3 deletions

View File

@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application

View File

@ -15,16 +15,20 @@ import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.support.v4.app.NavUtils;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Log;
import android.view.MenuItem;
import android.widget.Toast;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.activities.SyncthingActivity;
import com.nutomic.syncthingandroid.preferences.WifiSsidPreference;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import com.nutomic.syncthingandroid.syncthing.SyncthingService;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import eu.chainfire.libsuperuser.Shell;
@ -52,6 +56,7 @@ public class SettingsFragment extends PreferenceFragment
private CheckBoxPreference mAlwaysRunInBackground;
private CheckBoxPreference mSyncOnlyCharging;
private CheckBoxPreference mSyncOnlyWifi;
private WifiSsidPreference mSyncOnlyOnSSIDs;
private CheckBoxPreference mUseRoot;
private PreferenceScreen mOptionsScreen;
private PreferenceScreen mGuiScreen;
@ -126,6 +131,8 @@ public class SettingsFragment extends PreferenceFragment
mSyncOnlyCharging = (CheckBoxPreference)
findPreference(SyncthingService.PREF_SYNC_ONLY_CHARGING);
mSyncOnlyWifi = (CheckBoxPreference) findPreference(SyncthingService.PREF_SYNC_ONLY_WIFI);
mSyncOnlyOnSSIDs = (WifiSsidPreference) findPreference(SyncthingService.PREF_SYNC_ONLY_WIFI_SSIDS);
mSyncOnlyOnSSIDs.setDefaultValue(new TreeSet<String>()); // 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<String>()));
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<String>) 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<String> ssids) {
Set<String> 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.
*/

View File

@ -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&lt;String&gt; 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<String> selected = getSharedPreferences().getStringSet(getKey(), new HashSet<String>());
// 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<String> selected, CharSequence[] all) {
HashSet<CharSequence> 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<WifiConfiguration> 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<WifiConfiguration>() {
@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;
}
}

View File

@ -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;
}
}

View File

@ -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<String> ssids = sp.getStringSet(PREF_SYNC_ONLY_WIFI_SSIDS, new HashSet<String>());
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();
}

View File

@ -238,6 +238,14 @@ Please report any problems you encounter via Github.</string>
<string name="sync_only_wifi">Sync only on wifi</string>
<string name="sync_only_wifi_ssids">Restrict to certain wifi networks</string>
<string name="sync_only_wifi_ssids_all">Sync on all wifi networks</string>
<string name="sync_only_wifi_ssids_values">Sync only while connected to: %1$s</string>
<string name="sync_only_wifi_ssids_wifi_turn_on_wifi">Please turn on WiFi to select networks.</string>
<string name="advanced_folder_picker">Use advanced Folder Picker</string>
<string name="advanced_folder_picker_summary">Select any folder on the device for syncing</string>

View File

@ -19,6 +19,10 @@
android:title="@string/sync_only_wifi"
android:defaultValue="false" />
<com.nutomic.syncthingandroid.preferences.WifiSsidPreference
android:key="sync_only_wifi_ssids_set"
android:title="@string/sync_only_wifi_ssids" />
<CheckBoxPreference
android:key="advanced_folder_picker"
android:title="@string/advanced_folder_picker"