package com.nutomic.syncthingandroid.fragments; 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.support.v4.app.NavUtils; 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.TreeSet; import eu.chainfire.libsuperuser.Shell; public class SettingsFragment extends PreferenceFragment implements SyncthingActivity.OnServiceConnectedListener, SyncthingService.OnApiChangeListener, Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener { private static final String TAG = "SettingsFragment"; private static final String SYNCTHING_OPTIONS_KEY = "syncthing_options"; private static final String SYNCTHING_GUI_KEY = "syncthing_gui"; private static final String DEVICE_NAME_KEY = "deviceName"; private static final String USAGE_REPORT_ACCEPTED = "urAccepted"; private static final String ADDRESS = "address"; private static final String USER = "user"; // Note that this preference is seperate from the syncthing config value. While Syncthing // stores a password hash, we store the plaintext password in the Android preferences. private static final String PASSWORD = "web_gui_password"; private static final String EXPORT_CONFIG = "export_config"; private static final String IMPORT_CONFIG = "import_config"; private static final String STTRACE = "sttrace"; private static final String SYNCTHING_RESET = "streset"; private static final String SYNCTHING_VERSION_KEY = "syncthing_version"; private static final String APP_VERSION_KEY = "app_version"; private CheckBoxPreference mAlwaysRunInBackground; private CheckBoxPreference mSyncOnlyCharging; private CheckBoxPreference mSyncOnlyWifi; private WifiSsidPreference mSyncOnlyOnSSIDs; private CheckBoxPreference mUseRoot; private CheckBoxPreference mKeepWakelock; private PreferenceScreen mOptionsScreen; private PreferenceScreen mGuiScreen; private SyncthingService mSyncthingService; @Override public void onApiChange(SyncthingService.State currentState) { boolean enabled = currentState == SyncthingService.State.ACTIVE; mOptionsScreen.setEnabled(enabled); mGuiScreen.setEnabled(enabled); mUseRoot.setEnabled(enabled); mKeepWakelock.setEnabled(enabled); if (currentState == SyncthingService.State.ACTIVE) { Preference syncthingVersion = getPreferenceScreen().findPreference(SYNCTHING_VERSION_KEY); syncthingVersion.setSummary(mSyncthingService.getApi().getVersion()); RestApi api = mSyncthingService.getApi(); for (int i = 0; i < mOptionsScreen.getPreferenceCount(); i++) { final Preference pref = mOptionsScreen.getPreference(i); pref.setOnPreferenceChangeListener(SettingsFragment.this); String value; switch (pref.getKey()) { case DEVICE_NAME_KEY: value = api.getLocalDevice().name; break; case USAGE_REPORT_ACCEPTED: int setting = api.getUsageReportAccepted(); value = Boolean.toString(setting == RestApi.USAGE_REPORTING_ACCEPTED); break; default: value = api.getValue(RestApi.TYPE_OPTIONS, pref.getKey()); } applyPreference(pref, value); } Preference address = mGuiScreen.findPreference(ADDRESS); address.setOnPreferenceChangeListener(this); applyPreference(address, api.getValue(RestApi.TYPE_GUI, ADDRESS)); Preference user = mGuiScreen.findPreference(USER); user.setOnPreferenceChangeListener(this); applyPreference(user, api.getValue(RestApi.TYPE_GUI, USER)); Preference password = mGuiScreen.findPreference(PASSWORD); password.setOnPreferenceChangeListener(this); } } /** * Applies the given value to the preference. * * If pref is an EditTextPreference, setText is used and the value shown as summary. If pref is * a CheckBoxPreference, setChecked is used (by parsing value as Boolean). */ private void applyPreference(Preference pref, String value) { if (pref instanceof EditTextPreference) { ((EditTextPreference) pref).setText(value); } else if (pref instanceof CheckBoxPreference) { ((CheckBoxPreference) pref).setChecked(Boolean.parseBoolean(value)); } } /** * 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); final 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); mSyncOnlyOnSSIDs.setDefaultValue(new TreeSet<String>()); // default to empty list mUseRoot = (CheckBoxPreference) findPreference(SyncthingService.PREF_USE_ROOT); mKeepWakelock = (CheckBoxPreference) findPreference(SyncthingService.PREF_USE_WAKE_LOCK); Preference appVersion = screen.findPreference(APP_VERSION_KEY); mOptionsScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_OPTIONS_KEY); mGuiScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_GUI_KEY); Preference sttrace = findPreference(STTRACE); try { appVersion.setSummary(getActivity().getPackageManager() .getPackageInfo(getActivity().getPackageName(), 0).versionName); } catch (PackageManager.NameNotFoundException e) { Log.d(TAG, "Failed to get app version name"); } mAlwaysRunInBackground.setOnPreferenceChangeListener(this); mSyncOnlyCharging.setOnPreferenceChangeListener(this); mSyncOnlyWifi.setOnPreferenceChangeListener(this); mSyncOnlyOnSSIDs.setOnPreferenceChangeListener(this); mUseRoot.setOnPreferenceClickListener(this); mKeepWakelock.setOnPreferenceClickListener(this); screen.findPreference(EXPORT_CONFIG).setOnPreferenceClickListener(this); screen.findPreference(IMPORT_CONFIG).setOnPreferenceClickListener(this); screen.findPreference(SYNCTHING_RESET).setOnPreferenceClickListener(this); sttrace.setOnPreferenceChangeListener(this); } /** * 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().requireRestart(getActivity()); mUseRoot.setChecked(true); } else { Toast.makeText(getActivity(), R.string.toast_root_denied, Toast.LENGTH_SHORT) .show(); } } } @Override public void onServiceConnected() { mSyncthingService = ((SyncthingActivity) getActivity()).getService(); mSyncthingService.registerOnApiChangeListener(this); } @Override public void onDestroy() { super.onDestroy(); mSyncthingService.unregisterOnApiChangeListener(this); } /** * Handles ActionBar back selected. */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: NavUtils.navigateUpFromSameTask(getActivity()); return true; default: return super.onOptionsItemSelected(item); } } /** * Sends the updated value to {@link }RestApi}, and sets it as the summary * for EditTextPreference. */ @Override public boolean onPreferenceChange(Preference preference, Object o) { boolean requireRestart = false; if (preference.equals(mAlwaysRunInBackground)) { boolean value = (Boolean) o; preference.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); } } else if (preference.equals(mSyncOnlyWifi)) { mSyncOnlyOnSSIDs.setEnabled((Boolean) o); } else if (preference.getKey().equals(DEVICE_NAME_KEY)) { RestApi.Device old = mSyncthingService.getApi().getLocalDevice(); RestApi.Device updated = new RestApi.Device(); updated.addresses = old.addresses; updated.compression = old.compression; updated.deviceID = old.deviceID; updated.introducer = old.introducer; updated.name = (String) o; mSyncthingService.getApi().editDevice(updated, getActivity(), null); } else if (preference.getKey().equals(USAGE_REPORT_ACCEPTED)) { int setting = ((Boolean) o) ? RestApi.USAGE_REPORTING_ACCEPTED : RestApi.USAGE_REPORTING_DENIED; mSyncthingService.getApi().setUsageReportAccepted(setting, getActivity()); } else if (mOptionsScreen.findPreference(preference.getKey()) != null) { boolean isArray = preference.getKey().equals("listenAddress") || preference.getKey().equals("globalAnnounceServers"); mSyncthingService.getApi().setValue(RestApi.TYPE_OPTIONS, preference.getKey(), o, isArray, getActivity()); } else if (preference.getKey().equals(ADDRESS)) { mSyncthingService.getApi().setValue( RestApi.TYPE_GUI, preference.getKey(), o, false, getActivity()); } else if (preference.getKey().equals(USER)) { mSyncthingService.getApi().setValue( RestApi.TYPE_GUI, preference.getKey(), o, false, getActivity()); } else if (preference.getKey().equals(PASSWORD)) { mSyncthingService.getApi().setValue( RestApi.TYPE_GUI, "password", o, false, getActivity()); } // Avoid any code injection. if (preference.getKey().equals(STTRACE)) { if (((String) o).matches("[0-9a-z, ]*")) requireRestart = true; else { Toast.makeText(getActivity(), R.string.toast_invalid_sttrace, Toast.LENGTH_SHORT).show(); return false; } } if (requireRestart) mSyncthingService.getApi().requireRestart(getActivity()); return true; } /** * 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); } } @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().requireRestart(getActivity()); } return true; case SyncthingService.PREF_USE_WAKE_LOCK: mSyncthingService.getApi().requireRestart(getActivity()); return true; case 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 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 SYNCTHING_RESET: 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; } } }