package com.nutomic.syncthingandroid.activities;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
import android.util.Log;
import android.widget.Toast;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.model.Config;
import com.nutomic.syncthingandroid.model.Device;
import com.nutomic.syncthingandroid.model.Options;
import com.nutomic.syncthingandroid.service.RestApi;
import com.nutomic.syncthingandroid.service.SyncthingService;
import com.nutomic.syncthingandroid.views.WifiSsidPreference;
import java.security.InvalidParameterException;
import java.util.List;
import eu.chainfire.libsuperuser.Shell;
public class SettingsActivity extends SyncthingActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new SettingsFragment())
.commit();
}
public static class SettingsFragment extends PreferenceFragment
implements SyncthingActivity.OnServiceConnectedListener,
SyncthingService.OnApiChangeListener, Preference.OnPreferenceChangeListener,
Preference.OnPreferenceClickListener {
private static final String TAG = "SettingsFragment";
private static final String KEY_STTRACE = "sttrace";
private static final String KEY_EXPORT_CONFIG = "export_config";
private static final String KEY_IMPORT_CONFIG = "import_config";
private static final String KEY_STRESET = "streset";
private CheckBoxPreference mAlwaysRunInBackground;
private CheckBoxPreference mSyncOnlyCharging;
private CheckBoxPreference mSyncOnlyWifi;
private WifiSsidPreference mSyncOnlyOnSSIDs;
private EditTextPreference mDeviceName;
private EditTextPreference mListenAddresses;
private EditTextPreference mMaxRecvKbps;
private EditTextPreference mMaxSendKbps;
private CheckBoxPreference mNatEnabled;
private CheckBoxPreference mLocalAnnounceEnabled;
private CheckBoxPreference mGlobalAnnounceEnabled;
private CheckBoxPreference mRelaysEnabled;
private EditTextPreference mGlobalAnnounceServers;
private EditTextPreference mAddress;
private EditTextPreference mUser;
private EditTextPreference mPassword;
private CheckBoxPreference mUrAccepted;
private CheckBoxPreference mUseRoot;
private Preference mSyncthingVersion;
private SyncthingService mSyncthingService;
private RestApi mApi;
private Options mOptions;
private Config.Gui mGui;
/**
* Loads layout, sets version from Rest API.
*
* Manual target API as we manually check if ActionBar is available (for ActionBar back button).
*/
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
((SyncthingActivity) getActivity()).registerOnServiceConnectedListener(this);
addPreferencesFromResource(R.xml.app_settings);
PreferenceScreen screen = getPreferenceScreen();
mAlwaysRunInBackground =
(CheckBoxPreference) findPreference(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND);
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);
mSyncOnlyCharging.setEnabled(mAlwaysRunInBackground.isChecked());
mSyncOnlyWifi.setEnabled(mAlwaysRunInBackground.isChecked());
mSyncOnlyOnSSIDs.setEnabled(mSyncOnlyWifi.isChecked());
mDeviceName = (EditTextPreference) findPreference("deviceName");
mListenAddresses = (EditTextPreference) findPreference("listenAddresses");
mMaxRecvKbps = (EditTextPreference) findPreference("maxRecvKbps");
mMaxSendKbps = (EditTextPreference) findPreference("maxSendKbps");
mNatEnabled = (CheckBoxPreference) findPreference("natEnabled");
mLocalAnnounceEnabled = (CheckBoxPreference) findPreference("localAnnounceEnabled");
mGlobalAnnounceEnabled = (CheckBoxPreference) findPreference("globalAnnounceEnabled");
mRelaysEnabled = (CheckBoxPreference) findPreference("relaysEnabled");
mGlobalAnnounceServers = (EditTextPreference) findPreference("globalAnnounceServers");
mAddress = (EditTextPreference) findPreference("address");
mUser = (EditTextPreference) findPreference("user");
mPassword = (EditTextPreference) findPreference("password");
mUrAccepted = (CheckBoxPreference) findPreference("urAccepted");
Preference exportConfig = findPreference("export_config");
Preference importConfig = findPreference("import_config");
Preference stTrace = findPreference("sttrace");
Preference stReset = findPreference("streset");
mUseRoot = (CheckBoxPreference) findPreference(SyncthingService.PREF_USE_ROOT);
Preference useWakelock = findPreference(SyncthingService.PREF_USE_WAKE_LOCK);
Preference foregroundService = findPreference("run_as_foreground_service");
Preference useTor = findPreference("use_tor");
mSyncthingVersion = findPreference("syncthing_version");
Preference appVersion = screen.findPreference("app_version");
mSyncOnlyOnSSIDs.setEnabled(mSyncOnlyWifi.isChecked());
setPreferenceCategoryChangeListener(findPreference("category_run_conditions"), this);
setPreferenceCategoryChangeListener(
findPreference("category_syncthing_options"), this::onSyncthingPreferenceChange);
exportConfig.setOnPreferenceClickListener(this);
importConfig.setOnPreferenceClickListener(this);
stTrace.setOnPreferenceChangeListener(this);
stReset.setOnPreferenceClickListener(this);
mUseRoot.setOnPreferenceChangeListener(this);
useWakelock.setOnPreferenceChangeListener(this::onRequireRestart);
foregroundService.setOnPreferenceChangeListener(this::onRequireRestart);
useTor.setOnPreferenceChangeListener(this::onRequireRestart);
try {
appVersion.setSummary(getActivity().getPackageManager()
.getPackageInfo(getActivity().getPackageName(), 0).versionName);
} catch (PackageManager.NameNotFoundException e) {
Log.d(TAG, "Failed to get app version name");
}
}
@Override
public void onServiceConnected() {
if (getActivity() == null)
return;
mSyncthingService = ((SyncthingActivity) getActivity()).getService();
mSyncthingService.registerOnApiChangeListener(this);
if (mSyncthingService.getApi().isConfigLoaded()) {
mGui = mSyncthingService.getApi().getGui();
mOptions = mSyncthingService.getApi().getOptions();
}
}
@Override
public void onApiChange(SyncthingService.State currentState) {
boolean syncthingActive = currentState == SyncthingService.State.ACTIVE;
boolean enableAllPrefs = syncthingActive && mSyncthingService.getApi().isConfigLoaded();
PreferenceScreen ps = getPreferenceScreen();
for (int i = 0; i < ps.getPreferenceCount(); i++) {
Preference p = ps.getPreference(i);
p.setEnabled(enableAllPrefs || "category_run_conditions".equals(p.getKey()));
}
if (!enableAllPrefs)
return;
mApi = mSyncthingService.getApi();
mSyncthingVersion.setSummary(mApi.getVersion());
mOptions = mApi.getOptions();
mGui = mApi.getGui();
Joiner joiner = Joiner.on(", ");
mDeviceName.setText(mApi.getLocalDevice().name);
mListenAddresses.setText(joiner.join(mOptions.listenAddresses));
mMaxRecvKbps.setText(Integer.toString(mOptions.maxRecvKbps));
mMaxSendKbps.setText(Integer.toString(mOptions.maxSendKbps));
mNatEnabled.setChecked(mOptions.natEnabled);
mLocalAnnounceEnabled.setChecked(mOptions.localAnnounceEnabled);
mGlobalAnnounceEnabled.setChecked(mOptions.globalAnnounceEnabled);
mRelaysEnabled.setChecked(mOptions.relaysEnabled);
mGlobalAnnounceServers.setText(joiner.join(mOptions.globalAnnounceServers));
mAddress.setText(mGui.address);
mUser.setText(mGui.user);
mPassword.setText(mGui.password);
mUrAccepted.setChecked(mOptions.getUsageReportValue() == Options.USAGE_REPORTING_ACCEPTED);
}
@Override
public void onDestroy() {
super.onDestroy();
if (mSyncthingService != null)
mSyncthingService.unregisterOnApiChangeListener(this);
}
private void setPreferenceCategoryChangeListener(
Preference category, Preference.OnPreferenceChangeListener listener) {
PreferenceScreen ps = (PreferenceScreen) category;
for (int i = 0; i < ps.getPreferenceCount(); i++) {
Preference p = ps.getPreference(i);
p.setOnPreferenceChangeListener(listener);
}
}
public boolean onSyncthingPreferenceChange(Preference preference, Object o) {
Splitter splitter = Splitter.on(",").trimResults().omitEmptyStrings();
switch (preference.getKey()) {
case "deviceName":
Device localDevice = mApi.getLocalDevice();
localDevice.name = (String) o;
mApi.editDevice(localDevice);
break;
case "listenAddresses":
mOptions.listenAddresses = Iterables.toArray(splitter.split((String) o), String.class);
break;
case "maxRecvKbps": mOptions.maxRecvKbps = Integer.parseInt((String) o); break;
case "maxSendKbps": mOptions.maxRecvKbps = Integer.parseInt((String) o); break;
case "natEnabled": mOptions.natEnabled = (boolean) o; break;
case "localAnnounceEnabled": mOptions.localAnnounceEnabled = (boolean) o; break;
case "globalAnnounceEnabled": mOptions.globalAnnounceEnabled = (boolean) o; break;
case "relaysEnabled": mOptions.relaysEnabled = (boolean) o; break;
case "globalAnnounceServers":
mOptions.globalAnnounceServers = Iterables.toArray(splitter.split((String) o), String.class);
break;
case "address": mGui.address = (String) o; break;
case "user": mGui.user = (String) o; break;
case "password": mGui.password = (String) o; break;
case "urAccepted":
mOptions.urAccepted = ((boolean) o)
? Options.USAGE_REPORTING_ACCEPTED
: Options.USAGE_REPORTING_DENIED;
break;
default: throw new InvalidParameterException();
}
mApi.editSettings(mGui, mOptions, getActivity());
return true;
}
public boolean onRequireRestart(Preference preference, Object o) {
mSyncthingService.getApi().showRestartDialog(getActivity());
return true;
}
/**
* Sends the updated value to {@link }RestApi}, and sets it as the summary
* for EditTextPreference.
*/
@Override
public boolean onPreferenceChange(Preference preference, Object o) {
switch (preference.getKey()) {
case SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND:
boolean value = (Boolean) o;
mAlwaysRunInBackground.setSummary((value)
? R.string.always_run_in_background_enabled
: 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);
}
break;
case SyncthingService.PREF_SYNC_ONLY_WIFI:
mSyncOnlyOnSSIDs.setEnabled((Boolean) o);
break;
case KEY_STTRACE:
if (((String) o).matches("[0-9a-z, ]*"))
mSyncthingService.getApi().showRestartDialog(getActivity());
else {
Toast.makeText(getActivity(), R.string.toast_invalid_sttrace, Toast.LENGTH_SHORT)
.show();
return false;
}
break;
}
return true;
}
@Override
public boolean onPreferenceClick(Preference preference) {
switch (preference.getKey()) {
case SyncthingService.PREF_USE_ROOT:
if (mUseRoot.isChecked()) {
// Only check preference after root was granted.
mUseRoot.setChecked(false);
new TestRootTask().execute();
} else {
new Thread(new ChownFilesRunnable()).start();
mSyncthingService.getApi().showRestartDialog(getActivity());
}
return true;
case KEY_EXPORT_CONFIG:
new AlertDialog.Builder(getActivity())
.setMessage(R.string.dialog_confirm_export)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
mSyncthingService.exportConfig();
Toast.makeText(getActivity(),
getString(R.string.config_export_successful,
SyncthingService.EXPORT_PATH), Toast.LENGTH_LONG).show();
})
.setNegativeButton(android.R.string.no, null)
.show();
return true;
case KEY_IMPORT_CONFIG:
new AlertDialog.Builder(getActivity())
.setMessage(R.string.dialog_confirm_import)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
if (mSyncthingService.importConfig()) {
Toast.makeText(getActivity(),
getString(R.string.config_imported_successful),
Toast.LENGTH_SHORT).show();
// No need to restart, as we shutdown to import the config, and
// then have to start Syncthing again.
} else {
Toast.makeText(getActivity(),
getString(R.string.config_import_failed,
SyncthingService.EXPORT_PATH), Toast.LENGTH_LONG).show();
}
})
.setNegativeButton(android.R.string.no, null)
.show();
return true;
case KEY_STRESET:
final Intent intent = new Intent(getActivity(), SyncthingService.class)
.setAction(SyncthingService.ACTION_RESET);
new AlertDialog.Builder(getActivity())
.setTitle(R.string.streset_title)
.setMessage(R.string.streset_question)
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
getActivity().startService(intent);
Toast.makeText(getActivity(), R.string.streset_done, Toast.LENGTH_LONG).show();
})
.setNegativeButton(android.R.string.no, (dialogInterface, i) -> {
})
.show();
return true;
default:
return false;
}
}
/**
* Enables or disables {@link #mUseRoot} preference depending whether root is available.
*/
private class TestRootTask extends AsyncTask<Void, Void, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
return Shell.SU.available();
}
@Override
protected void onPostExecute(Boolean haveRoot) {
if (haveRoot) {
mSyncthingService.getApi().showRestartDialog(getActivity());
mUseRoot.setChecked(true);
} else {
Toast.makeText(getActivity(), R.string.toast_root_denied, Toast.LENGTH_SHORT)
.show();
}
}
}
/**
* Changes the owner of syncthing files so they can be accessed without root.
*/
private class ChownFilesRunnable implements Runnable {
@Override
public void run() {
String f = getActivity().getFilesDir().getAbsolutePath();
List<String> out = Shell.SU.run("chown -R --reference=" + f + " " + f);
Log.i(TAG, "Changed owner of syncthing files, output: " + out);
}
}
}
}