1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2024-11-26 22:31:16 +00:00

Merge pull request #369 from Zillode/https

TLS and v0.11.0 Support
This commit is contained in:
Zillode 2015-05-05 19:43:35 +02:00
commit 7c54b762af
28 changed files with 631 additions and 305 deletions

@ -1 +1 @@
Subproject commit 3cc4cb0a0b71908ae2d6392f14457e7ca6712278
Subproject commit b35958d024175609a9e07934cdb1bedd3243939c

View file

@ -1,3 +1,8 @@
# Fix appcompat-v7 v21.0.0 causing crash on Samsung devices with Android v4.2.2 (https://code.google.com/p/android/issues/detail?id=78377)
-keep class !android.support.v7.internal.view.menu.MenuBuilder, !android.support.v7.internal.view.menu.SubMenuBuilder, android.support.v7.** { *; }
-keep interface android.support.v7.** { *; }
# Enable reflective access to mX509Certificate
-keepclassmembers class android.net.http.SslCertificate {
private final X509Certificate mX509Certificate;
}

View file

@ -43,7 +43,7 @@ public class GetTaskTest extends AndroidTestCase {
@MediumTest
public void testGetNoParams() throws IOException, InterruptedException {
new GetTask() {
new GetTask("") {
@Override
protected void onPostExecute(String s) {
assertEquals(RESPONSE, s);
@ -57,7 +57,7 @@ public class GetTaskTest extends AndroidTestCase {
@MediumTest
public void testGetParams() throws IOException, InterruptedException {
new GetTask() {
new GetTask("") {
@Override
protected void onPostExecute(String s) {
assertEquals(RESPONSE, s);

View file

@ -33,8 +33,10 @@ public class PollWebGuiAvailableTaskTest extends AndroidTestCase {
new SyncthingRunnable(new MockContext(null),
getContext().getApplicationInfo().dataDir + "/" + SyncthingService.BINARY_NAME);
String httpsCertPath = getContext().getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE;
final CountDownLatch latch = new CountDownLatch(1);
new PollWebGuiAvailableTask() {
new PollWebGuiAvailableTask(httpsCertPath) {
@Override
protected void onPostExecute(Void aVoid) {
latch.countDown();

View file

@ -17,31 +17,29 @@ import java.util.concurrent.TimeUnit;
public class RestApiTest extends AndroidTestCase {
private SyncthingRunnable mSyncthing;
private ConfigXml mConfig;
private RestApi mApi;
@Override
protected void setUp() throws Exception {
super.setUp();
mSyncthing = new SyncthingRunnable(new MockContext(null),
new SyncthingRunnable(new MockContext(null),
getContext().getApplicationInfo().dataDir + "/" + SyncthingService.BINARY_NAME);
mConfig = new ConfigXml(new MockContext(getContext()));
mConfig.changeDefaultFolder();
ConfigXml config = new ConfigXml(new MockContext(getContext()));
config.changeDefaultFolder();
String httpsCertPath = getContext().getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE;
final CountDownLatch latch = new CountDownLatch(2);
new PollWebGuiAvailableTask() {
new PollWebGuiAvailableTask(httpsCertPath) {
@Override
protected void onPostExecute(Void aVoid) {
mApi.onWebGuiAvailable();
latch.countDown();
}
}.execute(mConfig.getWebGuiUrl());
mApi = new RestApi(getContext(), mConfig.getWebGuiUrl(), mConfig.getApiKey(),
}.execute(config.getWebGuiUrl());
mApi = new RestApi(getContext(), config.getWebGuiUrl(), config.getApiKey(),
new RestApi.OnApiAvailableListener() {
@Override
public void onApiAvailable() {
@ -54,8 +52,6 @@ public class RestApiTest extends AndroidTestCase {
@Override
protected void tearDown() throws Exception {
super.tearDown();
final CountDownLatch latch = new CountDownLatch(1);
SyncthingRunnable.killSyncthing();
ConfigXml.getConfigFile(new MockContext(getContext())).delete();
}

View file

@ -36,7 +36,7 @@ public class ConfigXmlTest extends AndroidTestCase {
@SmallTest
public void testGetWebGuiUrl() {
assertTrue(mConfig.getWebGuiUrl().startsWith("http://127.0.0.1:"));
assertTrue(mConfig.getWebGuiUrl().startsWith("https://127.0.0.1:"));
}
/**

View file

@ -25,13 +25,13 @@ public class DevicesAdapterTest extends AndroidTestCase {
super.setUp();
mAdapter = new DevicesAdapter(getContext());
mDevice.Addresses = "127.0.0.1:12345";
mDevice.Name = "the device";
mDevice.DeviceID = "123-456-789";
mDevice.addresses = "127.0.0.1:12345";
mDevice.name = "the device";
mDevice.deviceID = "123-456-789";
mConnection.Completion = 100;
mConnection.InBits = 1048576;
mConnection.OutBits = 1073741824;
mConnection.completion = 100;
mConnection.inBits = 1048576;
mConnection.outBits = 1073741824;
}
@ -40,7 +40,7 @@ public class DevicesAdapterTest extends AndroidTestCase {
mAdapter.add(Arrays.asList(mDevice));
View v = mAdapter.getView(0, null, null);
assertEquals(mDevice.Name, ((TextView) v.findViewById(R.id.name)).getText());
assertEquals(mDevice.name, ((TextView) v.findViewById(R.id.name)).getText());
assertEquals(getContext().getString(R.string.device_disconnected),
((TextView) v.findViewById(R.id.status)).getText().toString());
assertFalse(((TextView) v.findViewById(R.id.status)).getText().equals(""));
@ -52,7 +52,7 @@ public class DevicesAdapterTest extends AndroidTestCase {
public void testGetViewConnections() {
mAdapter.add(Arrays.asList(mDevice));
mAdapter.onReceiveConnections(
new HashMap<String, RestApi.Connection>() {{ put(mDevice.DeviceID, mConnection); }});
new HashMap<String, RestApi.Connection>() {{ put(mDevice.deviceID, mConnection); }});
View v = mAdapter.getView(0, null, null);
assertEquals(getContext().getString(R.string.device_up_to_date),

View file

@ -44,8 +44,8 @@ public class FolderObserverTest extends AndroidTestCase
private RestApi.Folder createFolder(String id) {
RestApi.Folder r = new RestApi.Folder();
r.Path = mTestFolder.getAbsolutePath();
r.ID = id;
r.path = mTestFolder.getAbsolutePath();
r.id = id;
return r;
}

View file

@ -25,12 +25,12 @@ public class FoldersAdapterTest extends AndroidTestCase {
super.setUp();
mAdapter = new FoldersAdapter(getContext());
mFolder.Path = "/my/dir/";
mFolder.ID = "id 123";
mFolder.Invalid = "all good";
mFolder.DeviceIds = new ArrayList<>();
mFolder.ReadOnly = false;
mFolder.Versioning = new RestApi.Versioning();
mFolder.path = "/my/dir/";
mFolder.id = "id 123";
mFolder.invalid = "all good";
mFolder.deviceIds = new ArrayList<>();
mFolder.readOnly = false;
mFolder.versioning = new RestApi.Versioning();
mModel.state = "idle";
mModel.localFiles = 50;
@ -43,15 +43,15 @@ public class FoldersAdapterTest extends AndroidTestCase {
public void testGetViewNoModel() {
mAdapter.add(Arrays.asList(mFolder));
View v = mAdapter.getView(0, null, null);
assertEquals(mFolder.ID, ((TextView) v.findViewById(R.id.id)).getText());
assertEquals(mFolder.Path, ((TextView) v.findViewById(R.id.directory)).getText());
assertEquals(mFolder.Invalid, ((TextView) v.findViewById(R.id.invalid)).getText());
assertEquals(mFolder.id, ((TextView) v.findViewById(R.id.id)).getText());
assertEquals(mFolder.path, ((TextView) v.findViewById(R.id.directory)).getText());
assertEquals(mFolder.invalid, ((TextView) v.findViewById(R.id.invalid)).getText());
}
@MediumTest
public void testGetViewModel() {
mAdapter.add(Arrays.asList(mFolder));
mAdapter.onReceiveModel(mFolder.ID, mModel);
mAdapter.onReceiveModel(mFolder.id, mModel);
View v = mAdapter.getView(0, null, null);
assertFalse(((TextView) v.findViewById(R.id.state)).getText().toString().equals(""));
String items = ((TextView) v.findViewById(R.id.items)).getText().toString();

View file

@ -2,30 +2,95 @@ package com.nutomic.syncthingandroid.activities;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.graphics.Bitmap;
import android.net.http.SslCertificate;
import android.net.http.SslError;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.webkit.HttpAuthHandler;
import android.webkit.SslErrorHandler;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import com.nutomic.syncthingandroid.syncthing.SyncthingService;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
/**
* Holds a WebView that shows the web ui of the local syncthing instance.
*/
public class WebGuiActivity extends SyncthingActivity implements SyncthingService.OnWebGuiAvailableListener {
public class WebGuiActivity extends SyncthingActivity
implements SyncthingService.OnWebGuiAvailableListener {
private static final String TAG = "WebGuiActivity";
private WebView mWebView;
private View mLoadingView;
private X509Certificate mCaCert;
/**
* Hides the loading screen and shows the WebView once it is fully loaded.
*/
private final WebViewClient mWebViewClient = new WebViewClient() {
/**
* Catch (self-signed) SSL errors and test if they correspond to Syncthing's certificate.
*/
@Override
public void onReceivedSslError (WebView view, SslErrorHandler handler, SslError error) {
try {
int sdk = android.os.Build.VERSION.SDK_INT;
if (sdk >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
// The mX509Certificate field is not available for ICS- devices
Log.w(TAG, "Skipping certificate check for devices <ICS");
handler.proceed();
return;
}
// Use reflection to access the private mX509Certificate field of SslCertificate
SslCertificate sslCert = error.getCertificate();
Field f = sslCert.getClass().getDeclaredField("mX509Certificate");
f.setAccessible(true);
X509Certificate cert = (X509Certificate)f.get(sslCert);
if (cert == null) {
Log.w(TAG, "X509Certificate reference invalid");
handler.cancel();
return;
}
cert.verify(mCaCert.getPublicKey());
handler.proceed();
} catch (NoSuchFieldException|IllegalAccessException|CertificateException|
NoSuchAlgorithmException|InvalidKeyException|NoSuchProviderException|
SignatureException e) {
Log.w(TAG, e);
handler.cancel();
}
}
public void onReceivedHttpAuthRequest (WebView view, HttpAuthHandler handler, String host, String realm) {
handler.proceed(getService().getApi().getGuiUser(), getService().getApi().getGuiPassword());
}
@Override
public void onPageFinished(WebView view, String url) {
mWebView.setVisibility(View.VISIBLE);
@ -50,6 +115,8 @@ public class WebGuiActivity extends SyncthingActivity implements SyncthingServic
ProgressBar pb = (ProgressBar) mLoadingView.findViewById(R.id.progress);
pb.setIndeterminate(true);
loadCaCert();
mWebView = (WebView) findViewById(R.id.webview);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.setWebViewClient(mWebViewClient);
@ -61,12 +128,31 @@ public class WebGuiActivity extends SyncthingActivity implements SyncthingServic
getService().registerOnWebGuiAvailableListener(WebGuiActivity.this);
}
/**
* Loads and shows WebView, hides loading view.
*/
@Override
public void onWebGuiAvailable() {
mWebView.loadUrl(getService().getWebGuiUrl());
}
/**
* Reads the SyncthingService.HTTPS_CERT_FILE Ca Cert key and loads it in memory
*/
private void loadCaCert() {
InputStream inStream = null;
try {
String httpsCertPath = getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE;
inStream = new FileInputStream(httpsCertPath);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
mCaCert = (X509Certificate)
cf.generateCertificate(inStream);
} catch (FileNotFoundException|CertificateException e) {
throw new IllegalArgumentException("Untrusted Certificate", e);
} finally {
try {
if (inStream != null)
inStream.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
}

View file

@ -99,12 +99,12 @@ public class DeviceSettingsFragment extends PreferenceFragment implements
}
if (mDevice == null) {
mDevice = new RestApi.Device();
mDevice.Name = "";
mDevice.DeviceID = "";
mDevice.Addresses = "dynamic";
mDevice.Compression = "always";
mDevice.Introducer = false;
((EditTextPreference) mDeviceId).setText(mDevice.DeviceID);
mDevice.name = "";
mDevice.deviceID = "";
mDevice.addresses = "dynamic";
mDevice.compression = "always";
mDevice.introducer = false;
((EditTextPreference) mDeviceId).setText(mDevice.deviceID);
}
}
}
@ -144,7 +144,7 @@ public class DeviceSettingsFragment extends PreferenceFragment implements
getActivity().setTitle(R.string.edit_device);
List<RestApi.Device> devices = mSyncthingService.getApi().getDevices(false);
for (int i = 0; i < devices.size(); i++) {
if (devices.get(i).DeviceID.equals(
if (devices.get(i).deviceID.equals(
getActivity().getIntent().getStringExtra(EXTRA_NODE_ID))) {
device = devices.get(i);
break;
@ -161,14 +161,14 @@ public class DeviceSettingsFragment extends PreferenceFragment implements
mSyncthingService.getApi().getConnections(DeviceSettingsFragment.this);
mDeviceId.setSummary(mDevice.DeviceID);
mName.setText((mDevice.Name));
mName.setSummary(mDevice.Name);
mAddresses.setText(mDevice.Addresses);
mAddresses.setSummary(mDevice.Addresses);
mCompression.setValue(mDevice.Compression);
mCompression.setSummary(mDevice.Compression);
mIntroducer.setChecked(mDevice.Introducer);
mDeviceId.setSummary(mDevice.deviceID);
mName.setText((mDevice.name));
mName.setSummary(mDevice.name);
mAddresses.setText(mDevice.addresses);
mAddresses.setSummary(mDevice.addresses);
mCompression.setValue(mDevice.compression);
mCompression.setSummary(mDevice.compression);
mIntroducer.setChecked(mDevice.introducer);
}
@Override
@ -188,12 +188,12 @@ public class DeviceSettingsFragment extends PreferenceFragment implements
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.create:
if (mDevice.DeviceID.equals("")) {
if (mDevice.deviceID.equals("")) {
Toast.makeText(getActivity(), R.string.device_id_required, Toast.LENGTH_LONG)
.show();
return true;
}
if (mDevice.Name.equals("")) {
if (mDevice.name.equals("")) {
Toast.makeText(getActivity(), R.string.device_name_required, Toast.LENGTH_LONG)
.show();
return true;
@ -201,7 +201,7 @@ public class DeviceSettingsFragment extends PreferenceFragment implements
mSyncthingService.getApi().editDevice(mDevice, getActivity(), this);
return true;
case R.id.share_device_id:
RestApi.shareDeviceId(getActivity(), mDevice.DeviceID);
RestApi.shareDeviceId(getActivity(), mDevice.deviceID);
return true;
case R.id.delete:
new AlertDialog.Builder(getActivity())
@ -234,23 +234,23 @@ public class DeviceSettingsFragment extends PreferenceFragment implements
pref.setSummary((String) o);
}
if (preference.equals(mDeviceId)) {
mDevice.DeviceID = (String) o;
mDevice.deviceID = (String) o;
deviceUpdated();
return true;
} else if (preference.equals(mName)) {
mDevice.Name = (String) o;
mDevice.name = (String) o;
deviceUpdated();
return true;
} else if (preference.equals(mAddresses)) {
mDevice.Addresses = (String) o;
mDevice.addresses = (String) o;
deviceUpdated();
return true;
} else if (preference.equals(mCompression)) {
mDevice.Compression = (String) o;
mDevice.compression = (String) o;
deviceUpdated();
return true;
} else if (preference.equals(mIntroducer)) {
mDevice.Introducer = (Boolean) o;
mDevice.introducer = (Boolean) o;
deviceUpdated();
return true;
}
@ -260,7 +260,7 @@ public class DeviceSettingsFragment extends PreferenceFragment implements
@Override
public boolean onPreferenceClick(Preference preference) {
if (preference.equals(mDeviceId)) {
mSyncthingService.getApi().copyDeviceId(mDevice.DeviceID);
mSyncthingService.getApi().copyDeviceId(mDevice.deviceID);
return true;
}
return false;
@ -276,9 +276,9 @@ public class DeviceSettingsFragment extends PreferenceFragment implements
public void onReceiveConnections(Map<String, RestApi.Connection> connections) {
if (mVersion == null || mCurrentAddress == null)
return;
if (connections.containsKey(mDevice.DeviceID)) {
mVersion.setSummary(connections.get(mDevice.DeviceID).ClientVersion);
mCurrentAddress.setSummary(connections.get(mDevice.DeviceID).Address);
if (connections.containsKey(mDevice.deviceID)) {
mVersion.setSummary(connections.get(mDevice.deviceID).clientVersion);
mCurrentAddress.setSummary(connections.get(mDevice.deviceID).address);
}
}
@ -308,14 +308,14 @@ public class DeviceSettingsFragment extends PreferenceFragment implements
}
/**
* Receives value of scanned QR code and sets it as device ID.
* Receives value of scanned QR code and sets it as device id.
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == SCAN_QR_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
mDevice.DeviceID = data.getStringExtra("SCAN_RESULT");
((EditTextPreference) mDeviceId).setText(mDevice.DeviceID);
mDeviceId.setSummary(mDevice.DeviceID);
mDevice.deviceID = data.getStringExtra("SCAN_RESULT");
((EditTextPreference) mDeviceId).setText(mDevice.deviceID);
mDeviceId.setSummary(mDevice.deviceID);
}
}

View file

@ -96,7 +96,7 @@ public class DevicesFragment extends ListFragment implements SyncthingService.On
Intent intent = new Intent(getActivity(), SettingsActivity.class);
intent.setAction(SettingsActivity.ACTION_NODE_SETTINGS_FRAGMENT);
intent.putExtra(SettingsActivity.EXTRA_IS_CREATE, false);
intent.putExtra(DeviceSettingsFragment.EXTRA_NODE_ID, mAdapter.getItem(i).DeviceID);
intent.putExtra(DeviceSettingsFragment.EXTRA_NODE_ID, mAdapter.getItem(i).deviceID);
startActivity(intent);
}

View file

@ -207,9 +207,9 @@ public class DrawerFragment extends Fragment implements RestApi.OnReceiveSystemI
*/
@Override
public void onReceiveConnections(Map<String, RestApi.Connection> connections) {
RestApi.Connection c = connections.get(RestApi.LOCAL_DEVICE_CONNECTIONS);
mDownload.setText(RestApi.readableTransferRate(mActivity, c.InBits));
mUpload.setText(RestApi.readableTransferRate(mActivity, c.OutBits));
RestApi.Connection c = connections.get(RestApi.TOTAL_STATS);
mDownload.setText(RestApi.readableTransferRate(mActivity, c.inBits));
mUpload.setText(RestApi.readableTransferRate(mActivity, c.outBits));
}
/**

View file

@ -101,11 +101,11 @@ public class FolderSettingsFragment extends PreferenceFragment
}
if (mFolder == null) {
mFolder = new RestApi.Folder();
mFolder.ID = "";
mFolder.Path = "";
mFolder.RescanIntervalS = 259200; // Scan every 3 days (in case inotify dropped some changes)
mFolder.DeviceIds = new ArrayList<>();
mFolder.Versioning = new RestApi.Versioning();
mFolder.id = "";
mFolder.path = "";
mFolder.rescanIntervalS = 259200; // Scan every 3 days (in case inotify dropped some changes)
mFolder.deviceIds = new ArrayList<>();
mFolder.versioning = new RestApi.Versioning();
}
}
}
@ -133,7 +133,7 @@ public class FolderSettingsFragment extends PreferenceFragment
getActivity().setTitle(R.string.edit_folder);
List<RestApi.Folder> folders = mSyncthingService.getApi().getFolders();
for (int i = 0; i < folders.size(); i++) {
if (folders.get(i).ID.equals(
if (folders.get(i).id.equals(
getActivity().getIntent().getStringExtra(EXTRA_REPO_ID))) {
folder = folders.get(i);
break;
@ -147,29 +147,29 @@ public class FolderSettingsFragment extends PreferenceFragment
mFolder = folder;
}
mFolderId.setText(mFolder.ID);
mFolderId.setSummary(mFolder.ID);
mDirectory.setSummary(mFolder.Path);
mFolderMaster.setChecked(mFolder.ReadOnly);
mFolderId.setText(mFolder.id);
mFolderId.setSummary(mFolder.id);
mDirectory.setSummary(mFolder.path);
mFolderMaster.setChecked(mFolder.readOnly);
List<RestApi.Device> devicesList = mSyncthingService.getApi().getDevices(false);
for (RestApi.Device n : devicesList) {
ExtendedCheckBoxPreference cbp = new ExtendedCheckBoxPreference(getActivity(), n);
// Calling addPreference later causes it to change the checked state.
mDevices.addPreference(cbp);
cbp.setTitle(n.Name);
cbp.setTitle(n.name);
cbp.setKey(KEY_NODE_SHARED);
cbp.setOnPreferenceChangeListener(FolderSettingsFragment.this);
cbp.setChecked(false);
for (String n2 : mFolder.DeviceIds) {
if (n2.equals(n.DeviceID)) {
for (String n2 : mFolder.deviceIds) {
if (n2.equals(n.deviceID)) {
cbp.setChecked(true);
}
}
}
mVersioning.setChecked(mFolder.Versioning instanceof RestApi.SimpleVersioning);
mVersioning.setChecked(mFolder.versioning instanceof RestApi.SimpleVersioning);
if (mVersioning.isChecked()) {
mVersioningKeep.setText(mFolder.Versioning.getParams().get("keep"));
mVersioningKeep.setSummary(mFolder.Versioning.getParams().get("keep"));
mVersioningKeep.setText(mFolder.versioning.getParams().get("keep"));
mVersioningKeep.setSummary(mFolder.versioning.getParams().get("keep"));
mVersioningKeep.setEnabled(true);
} else {
mVersioningKeep.setEnabled(false);
@ -204,12 +204,12 @@ public class FolderSettingsFragment extends PreferenceFragment
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.create:
if (mFolder.ID.length() > 64 || !mFolder.ID.matches("[a-zA-Z0-9-_\\.]+")) {
if (mFolder.id.length() > 64 || !mFolder.id.matches("[a-zA-Z0-9-_\\.]+")) {
Toast.makeText(getActivity(), R.string.folder_id_invalid, Toast.LENGTH_LONG)
.show();
return true;
}
if (mFolder.Path.equals("")) {
if (mFolder.path.equals("")) {
Toast.makeText(getActivity(), R.string.folder_path_required, Toast.LENGTH_LONG)
.show();
return true;
@ -252,27 +252,27 @@ public class FolderSettingsFragment extends PreferenceFragment
}
if (preference.equals(mFolderId)) {
mFolder.ID = (String) o;
mFolder.id = (String) o;
folderUpdated();
return true;
} else if (preference.equals(mDirectory)) {
mFolder.Path = (String) o;
mFolder.path = (String) o;
folderUpdated();
return true;
} else if (preference.equals(mFolderMaster)) {
mFolder.ReadOnly = (Boolean) o;
mFolder.readOnly = (Boolean) o;
folderUpdated();
return true;
} else if (preference.getKey().equals(KEY_NODE_SHARED)) {
ExtendedCheckBoxPreference pref = (ExtendedCheckBoxPreference) preference;
RestApi.Device device = (RestApi.Device) pref.getObject();
if ((Boolean) o) {
mFolder.DeviceIds.add(device.DeviceID);
mFolder.deviceIds.add(device.deviceID);
} else {
Iterator<String> it = mFolder.DeviceIds.iterator();
Iterator<String> it = mFolder.deviceIds.iterator();
while (it.hasNext()) {
String n = it.next();
if (n.equals(device.DeviceID)) {
if (n.equals(device.deviceID)) {
it.remove();
}
}
@ -283,23 +283,23 @@ public class FolderSettingsFragment extends PreferenceFragment
mVersioningKeep.setEnabled((Boolean) o);
if ((Boolean) o) {
RestApi.SimpleVersioning v = new RestApi.SimpleVersioning();
mFolder.Versioning = v;
mFolder.versioning = v;
v.setParams(5);
mVersioningKeep.setText("5");
mVersioningKeep.setSummary("5");
} else {
mFolder.Versioning = new RestApi.Versioning();
mFolder.versioning = new RestApi.Versioning();
}
folderUpdated();
return true;
} else if (preference.equals(mVersioningKeep)) {
try {
((RestApi.SimpleVersioning) mFolder.Versioning)
((RestApi.SimpleVersioning) mFolder.versioning)
.setParams(Integer.parseInt((String) o));
folderUpdated();
return true;
} catch (NumberFormatException e) {
Log.w(TAG, "Invalid versioning option: "+ o);
Log.w(TAG, "invalid versioning option: "+ o);
}
}
@ -310,8 +310,8 @@ public class FolderSettingsFragment extends PreferenceFragment
public boolean onPreferenceClick(Preference preference) {
if (preference.equals(mDirectory)) {
Intent intent = new Intent(getActivity(), FolderPickerActivity.class);
if (mFolder.Path.length() > 0) {
intent.putExtra(FolderPickerActivity.EXTRA_INITIAL_DIRECTORY, mFolder.Path);
if (mFolder.path.length() > 0) {
intent.putExtra(FolderPickerActivity.EXTRA_INITIAL_DIRECTORY, mFolder.path);
}
startActivityForResult(intent, DIRECTORY_REQUEST_CODE);
} else if (preference.equals(mDevices) &&
@ -325,8 +325,8 @@ public class FolderSettingsFragment extends PreferenceFragment
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK && requestCode == DIRECTORY_REQUEST_CODE) {
mFolder.Path = data.getStringExtra(FolderPickerActivity.EXTRA_RESULT_DIRECTORY);
mDirectory.setSummary(mFolder.Path);
mFolder.path = data.getStringExtra(FolderPickerActivity.EXTRA_RESULT_DIRECTORY);
mDirectory.setSummary(mFolder.path);
folderUpdated();
}
}

View file

@ -96,7 +96,7 @@ public class FoldersFragment extends ListFragment implements SyncthingService.On
Intent intent = new Intent(getActivity(), SettingsActivity.class)
.setAction(SettingsActivity.ACTION_REPO_SETTINGS_FRAGMENT)
.putExtra(SettingsActivity.EXTRA_IS_CREATE, false)
.putExtra(FolderSettingsFragment.EXTRA_REPO_ID, mAdapter.getItem(i).ID);
.putExtra(FolderSettingsFragment.EXTRA_REPO_ID, mAdapter.getItem(i).id);
startActivity(intent);
}
@ -106,7 +106,7 @@ public class FoldersFragment extends ListFragment implements SyncthingService.On
@Override
public boolean onItemLongClick(AdapterView<?> adapterView, View view, int i, long l) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.parse(mAdapter.getItem(i).Path);
Uri uri = Uri.parse(mAdapter.getItem(i).path);
intent.setDataAndType(uri, "*/*");
startActivity(intent);
return true;

View file

@ -29,12 +29,11 @@ public class SettingsFragment extends PreferenceFragment
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 DEVICE_NAME_KEY = "deviceName";
private static final String USAGE_REPORT_ACCEPTED = "urAccepted";
private static final String ADDRESS = "address";
private static final String GUI_USER = "gui_user";
private static final String GUI_PASSWORD = "gui_password";
private static final String USER_TLS = "UseTLS";
private static final String EXPORT_CONFIG = "export_config";
private static final String IMPORT_CONFIG = "import_config";
private static final String STTRACE = "sttrace";
@ -70,7 +69,7 @@ public class SettingsFragment extends PreferenceFragment
String value;
switch (pref.getKey()) {
case DEVICE_NAME_KEY:
value = api.getLocalDevice().Name;
value = api.getLocalDevice().name;
break;
case USAGE_REPORT_ACCEPTED:
String v = api.getValue(RestApi.TYPE_OPTIONS, pref.getKey());
@ -84,9 +83,6 @@ public class SettingsFragment extends PreferenceFragment
Preference address = mGuiScreen.findPreference(ADDRESS);
applyPreference(address, api.getValue(RestApi.TYPE_GUI, ADDRESS));
Preference tls = mGuiScreen.findPreference(USER_TLS);
applyPreference(tls, api.getValue(RestApi.TYPE_GUI, USER_TLS));
}
}
@ -194,7 +190,7 @@ public class SettingsFragment extends PreferenceFragment
o = Integer.parseInt((String) o);
o = o.toString();
} catch (NumberFormatException e) {
Log.w(TAG, "Invalid number: " + o);
Log.w(TAG, "invalid number: " + o);
return false;
}
}
@ -212,21 +208,21 @@ public class SettingsFragment extends PreferenceFragment
} 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;
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)) {
mSyncthingService.getApi().setValue(RestApi.TYPE_OPTIONS, preference.getKey(),
((Boolean) o) ? 1 : 0, false, getActivity());
} else if (mOptionsScreen.findPreference(preference.getKey()) != null) {
boolean isArray = preference.getKey().equals("ListenAddress") ||
preference.getKey().equals("GlobalAnnServers");
boolean isArray = preference.getKey().equals("listenAddress") ||
preference.getKey().equals("globalAnnServers");
mSyncthingService.getApi().setValue(RestApi.TYPE_OPTIONS, preference.getKey(), o,
isArray, getActivity());
} else if (preference.getKey().equals(ADDRESS) || preference.getKey().equals(USER_TLS)) {
} else if (preference.getKey().equals(ADDRESS)) {
mSyncthingService.getApi().setValue(
RestApi.TYPE_GUI, preference.getKey(), o, false, getActivity());
}

View file

@ -3,13 +3,14 @@ package com.nutomic.syncthingandroid.syncthing;
import android.os.AsyncTask;
import android.util.Log;
import com.nutomic.syncthingandroid.util.Https;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
@ -28,17 +29,23 @@ public class GetTask extends AsyncTask<String, Void, String> {
private static final String TAG = "GetTask";
public static final String URI_CONFIG = "/rest/config";
public static final String URI_CONFIG = "/rest/system/config";
public static final String URI_VERSION = "/rest/version";
public static final String URI_VERSION = "/rest/system/version";
public static final String URI_SYSTEM = "/rest/system";
public static final String URI_SYSTEM = "/rest/system/status";
public static final String URI_CONNECTIONS = "/rest/connections";
public static final String URI_CONNECTIONS = "/rest/system/connections";
public static final String URI_MODEL = "/rest/model";
public static final String URI_MODEL = "/rest/db/status";
public static final String URI_DEVICEID = "/rest/deviceid";
public static final String URI_DEVICEID = "/rest/svc/deviceid";
private String mHttpsCertPath;
public GetTask(String httpsCertPath) {
mHttpsCertPath = httpsCertPath;
}
/**
* params[0] Syncthing hostname
@ -52,7 +59,7 @@ public class GetTask extends AsyncTask<String, Void, String> {
// Retry at most 10 times before failing
for (int i = 0; i < 10; i++) {
String fullUri = params[0] + params[1];
HttpClient httpclient = new DefaultHttpClient();
HttpClient httpclient = Https.createHttpsClient(mHttpsCertPath);
if (params.length == 5) {
LinkedList<NameValuePair> urlParams = new LinkedList<>();
urlParams.add(new BasicNameValuePair(params[3], params[4]));
@ -77,13 +84,15 @@ public class GetTask extends AsyncTask<String, Void, String> {
br.close();
return result;
}
} catch (IOException e) {
} catch (IOException|IllegalArgumentException e) {
Log.w(TAG, "Failed to call Rest API at " + fullUri, e);
}
try {
// Don't push the API too hard
Thread.sleep(500 * i);
} catch (InterruptedException e) { }
} catch (InterruptedException e) {
Log.w(TAG, e);
}
Log.w(TAG, "Retrying GetTask Rest API call ("+i+")");
}
return null;

View file

@ -4,12 +4,13 @@ package com.nutomic.syncthingandroid.syncthing;
import android.os.AsyncTask;
import android.util.Log;
import com.nutomic.syncthingandroid.util.Https;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.impl.client.DefaultHttpClient;
import java.io.IOException;
@ -27,13 +28,19 @@ public abstract class PollWebGuiAvailableTask extends AsyncTask<String, Void, Vo
*/
private static final long WEB_GUI_POLL_INTERVAL = 100;
private String mHttpsCertPath;
public PollWebGuiAvailableTask(String httpsCertPath) {
mHttpsCertPath = httpsCertPath;
}
/**
* @param url The URL of the web GUI (eg 127.0.0.1:8384).
* @param @url The URL of the web GUI (eg 127.0.0.1:8384).
*/
@Override
protected Void doInBackground(String... url) {
int status = 0;
HttpClient httpclient = new DefaultHttpClient();
HttpClient httpclient = Https.createHttpsClient(mHttpsCertPath);
HttpHead head = new HttpHead(url[0]);
do {
try {
@ -45,7 +52,7 @@ public abstract class PollWebGuiAvailableTask extends AsyncTask<String, Void, Vo
} catch (HttpHostConnectException e) {
// We catch this in every call, as long as the service is not online,
// so we ignore and continue.
} catch (IOException|InterruptedException e) {
} catch (IOException|InterruptedException|IllegalArgumentException e) {
Log.w(TAG, "Failed to poll for web interface", e);
}
} while (status != HttpStatus.SC_OK && status != HttpStatus.SC_UNAUTHORIZED);

View file

@ -3,10 +3,11 @@ package com.nutomic.syncthingandroid.syncthing;
import android.os.AsyncTask;
import android.util.Log;
import com.nutomic.syncthingandroid.util.Https;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HTTP;
@ -19,9 +20,15 @@ public class PostTask extends AsyncTask<String, Void, Boolean> {
private static final String TAG = "PostTask";
public static final String URI_CONFIG = "/rest/config";
public static final String URI_CONFIG = "/rest/system/config";
public static final String URI_SCAN = "/rest/scan";
public static final String URI_SCAN = "/rest/db/scan";
private String mHttpsCertPath;
public PostTask(String httpsCertPath) {
mHttpsCertPath = httpsCertPath;
}
/**
* params[0] Syncthing hostname
@ -32,7 +39,7 @@ public class PostTask extends AsyncTask<String, Void, Boolean> {
@Override
protected Boolean doInBackground(String... params) {
String fullUri = params[0] + params[1];
HttpClient httpclient = new DefaultHttpClient();
HttpClient httpclient = Https.createHttpsClient(mHttpsCertPath);
HttpPost post = new HttpPost(fullUri);
post.addHeader(new BasicHeader(RestApi.HEADER_API_KEY, params[2]));
@ -41,7 +48,7 @@ public class PostTask extends AsyncTask<String, Void, Boolean> {
post.setEntity(new StringEntity(params[3], HTTP.UTF_8));
}
httpclient.execute(post);
} catch (IOException e) {
} catch (IOException|IllegalArgumentException e) {
Log.w(TAG, "Failed to call Rest API at " + fullUri, e);
return false;
}

View file

@ -11,7 +11,6 @@ import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
@ -32,9 +31,6 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
/**
@ -48,12 +44,12 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
/**
* Parameter for {@link #getValue} or {@link #setValue} referring to "options" config item.
*/
public static final String TYPE_OPTIONS = "Options";
public static final String TYPE_OPTIONS = "options";
/**
* Parameter for {@link #getValue} or {@link #setValue} referring to "gui" config item.
*/
public static final String TYPE_GUI = "GUI";
public static final String TYPE_GUI = "gui";
/**
* The name of the HTTP header used for the syncthing API key.
@ -64,14 +60,14 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
* Key of the map element containing connection info for the local device, in the return
* value of {@link #getConnections}
*/
public static final String LOCAL_DEVICE_CONNECTIONS = "total";
public static final String TOTAL_STATS = "total";
public static class Device implements Serializable {
public String Addresses;
public String Name;
public String DeviceID;
public String Compression;
public boolean Introducer;
public String addresses;
public String name;
public String deviceID;
public String compression;
public boolean introducer;
}
public static class SystemInfo {
@ -85,13 +81,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
}
public static class Folder implements Serializable {
public String Path;
public String ID;
public String Invalid;
public List<String> DeviceIds;
public boolean ReadOnly;
public int RescanIntervalS;
public Versioning Versioning;
public String path;
public String id;
public String invalid;
public List<String> deviceIds;
public boolean readOnly;
public int rescanIntervalS;
public Versioning versioning;
}
public static class Versioning implements Serializable {
@ -118,14 +114,14 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
}
public static class Connection {
public String At;
public long InBytesTotal;
public long OutBytesTotal;
public long InBits;
public long OutBits;
public String Address;
public String ClientVersion;
public int Completion;
public String at;
public long inBytesTotal;
public long outBytesTotal;
public long inBits;
public long outBits;
public String address;
public String clientVersion;
public int completion;
}
public static class Model {
@ -151,7 +147,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
private final String mUrl;
private String mApiKey;
private final String mApiKey;
private final String mGuiUser;
private final String mGuiPassword;
private final String mHttpsCertPath;
private JSONObject mConfig;
@ -161,9 +163,9 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
/**
* Stores the result of the last successful request to {@link GetTask#URI_CONNECTIONS},
* or an empty HashMap.
* or an empty Map.
*/
private HashMap<String, Connection> mPreviousConnections = new HashMap<>();
private Map<String, Connection> mPreviousConnections = new HashMap<>();
/**
* Stores the timestamp of the last successful request to {@link GetTask#URI_CONNECTIONS}.
@ -176,10 +178,14 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
*/
private HashMap<String, Model> mCachedModelInfo = new HashMap<>();
public RestApi(Context context, String url, String apiKey, OnApiAvailableListener listener) {
public RestApi(Context context, String url, String apiKey, String guiUser, String guiPassword,
OnApiAvailableListener listener) {
mContext = context;
mUrl = url;
mApiKey = apiKey;
mGuiUser = guiUser;
mGuiPassword = guiPassword;
mHttpsCertPath = mContext.getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE;
mOnApiAvailableListener = listener;
}
@ -197,15 +203,15 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
public void onApiAvailable();
}
private OnApiAvailableListener mOnApiAvailableListener;
private final OnApiAvailableListener mOnApiAvailableListener;
/**
* Gets local device id, syncthing version and config, then calls all OnApiAvailableListeners.
* Gets local device ID, syncthing version and config, then calls all OnApiAvailableListeners.
*/
@Override
public void onWebGuiAvailable() {
mAvailableCount.set(0);
new GetTask() {
new GetTask(mHttpsCertPath) {
@Override
protected void onPostExecute(String s) {
try {
@ -219,7 +225,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
}
}
}.execute(mUrl, GetTask.URI_VERSION, mApiKey);
new GetTask() {
new GetTask(mHttpsCertPath) {
@Override
protected void onPostExecute(String config) {
try {
@ -340,7 +346,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
*/
@TargetApi(11)
public void requireRestart(Activity activity) {
new PostTask().execute(mUrl, PostTask.URI_CONFIG, mApiKey, mConfig.toString());
new PostTask(mHttpsCertPath).execute(mUrl, PostTask.URI_CONFIG, mApiKey, mConfig.toString());
if (mRestartPostponed)
return;
@ -404,17 +410,17 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
return new ArrayList<>();
try {
JSONArray devices = mConfig.getJSONArray("Devices");
JSONArray devices = mConfig.getJSONArray("devices");
List<Device> ret = new ArrayList<>(devices.length());
for (int i = 0; i < devices.length(); i++) {
JSONObject json = devices.getJSONObject(i);
Device n = new Device();
n.Addresses = json.optJSONArray("Addresses").join(" ").replace("\"", "");
n.Name = json.getString("Name");
n.DeviceID = json.getString("DeviceID");
n.Compression = json.getString("Compression");
n.Introducer = json.getBoolean("Introducer");
if (includeLocal || !mLocalDeviceId.equals(n.DeviceID)) {
n.addresses = json.optJSONArray("addresses").join(" ").replace("\"", "");
n.name = json.getString("name");
n.deviceID = json.getString("deviceID");
n.compression = json.getString("compression");
n.introducer = json.getBoolean("introducer");
if (includeLocal || !mLocalDeviceId.equals(n.deviceID)) {
ret.add(n);
}
}
@ -438,7 +444,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
* @param listener Callback invoked when the result is received.
*/
public void getSystemInfo(final OnReceiveSystemInfoListener listener) {
new GetTask() {
new GetTask(mHttpsCertPath) {
@Override
protected void onPostExecute(String s) {
if (s == null)
@ -479,31 +485,31 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
List<Folder> ret;
try {
JSONArray folders = mConfig.getJSONArray("Folders");
JSONArray folders = mConfig.getJSONArray("folders");
ret = new ArrayList<>(folders.length());
for (int i = 0; i < folders.length(); i++) {
JSONObject json = folders.getJSONObject(i);
Folder r = new Folder();
r.Path = json.getString("Path");
r.ID = json.getString("ID");
r.Invalid = json.getString("Invalid");
r.DeviceIds = new ArrayList<>();
JSONArray devices = json.getJSONArray("Devices");
r.path = json.getString("path");
r.id = json.getString("id");
r.invalid = json.getString("invalid");
r.deviceIds = new ArrayList<>();
JSONArray devices = json.getJSONArray("devices");
for (int j = 0; j < devices.length(); j++) {
JSONObject n = devices.getJSONObject(j);
r.DeviceIds.add(n.getString("DeviceID"));
r.deviceIds.add(n.getString("deviceID"));
}
r.ReadOnly = json.getBoolean("ReadOnly");
r.RescanIntervalS = json.getInt("RescanIntervalS");
JSONObject versioning = json.getJSONObject("Versioning");
if (versioning.getString("Type").equals("simple")) {
r.readOnly = json.getBoolean("readOnly");
r.rescanIntervalS = json.getInt("rescanIntervalS");
JSONObject versioning = json.getJSONObject("versioning");
if (versioning.getString("type").equals("simple")) {
SimpleVersioning sv = new SimpleVersioning();
JSONObject params = versioning.getJSONObject("Params");
JSONObject params = versioning.getJSONObject("params");
sv.setParams(params.getInt("keep"));
r.Versioning = sv;
r.versioning = sv;
} else {
r.Versioning = new Versioning();
r.versioning = new Versioning();
}
ret.add(r);
@ -543,7 +549,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
public interface OnReceiveConnectionsListener {
/**
* @param connections Map from Device ID to {@link Connection}.
* @param connections Map from Device id to {@link Connection}.
* <p/>
* NOTE: The parameter connections is cached internally. Do not modify it or
* any of its contents.
@ -554,10 +560,10 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
/**
* Returns connection info for the local device and all connected devices.
* <p/>
* Use the key {@link #LOCAL_DEVICE_CONNECTIONS} to get connection info for the local device.
* Use the key {@link #TOTAL_STATS} to get connection info for the local device.
*/
public void getConnections(final OnReceiveConnectionsListener listener) {
new GetTask() {
new GetTask(mHttpsCertPath) {
@Override
protected void onPostExecute(String s) {
if (s == null)
@ -572,27 +578,35 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
try {
JSONObject json = new JSONObject(s);
String[] names = json.names().join(" ").replace("\"", "").split(" ");
HashMap<String, Connection> connections = new HashMap<String, Connection>();
for (String deviceId : names) {
Map<String, JSONObject> jsonConnections = new HashMap<>();
jsonConnections.put(TOTAL_STATS, json.getJSONObject(TOTAL_STATS));
JSONArray extConnections = json.getJSONObject("connections").names();
if (extConnections != null) {
for (int i = 0; i < extConnections.length(); i++) {
String deviceId = extConnections.get(i).toString();
jsonConnections.put(deviceId, json.getJSONObject("connections").getJSONObject(deviceId));
}
}
Map<String, Connection> connections = new HashMap<>();
for (String deviceId : jsonConnections.keySet()) {
Connection c = new Connection();
JSONObject conn = json.getJSONObject(deviceId);
c.Address = deviceId;
c.At = conn.getString("At");
c.InBytesTotal = conn.getLong("InBytesTotal");
c.OutBytesTotal = conn.getLong("OutBytesTotal");
c.Address = conn.getString("Address");
c.ClientVersion = conn.getString("ClientVersion");
c.Completion = getDeviceCompletion(deviceId);
JSONObject conn = jsonConnections.get(deviceId);
c.address = deviceId;
c.at = conn.getString("at");
c.inBytesTotal = conn.getLong("inBytesTotal");
c.outBytesTotal = conn.getLong("outBytesTotal");
c.address = conn.getString("address");
c.clientVersion = conn.getString("clientVersion");
c.completion = getDeviceCompletion(deviceId);
Connection prev = (mPreviousConnections.containsKey(deviceId))
? mPreviousConnections.get(deviceId)
: new Connection();
mPreviousConnectionTime = now;
c.InBits = Math.max(0, 8 *
(conn.getLong("InBytesTotal") - prev.InBytesTotal) / timeElapsed);
c.OutBits = Math.max(0, 8 *
(conn.getLong("OutBytesTotal") - prev.OutBytesTotal) / timeElapsed);
c.inBits = Math.max(0, 8 *
(conn.getLong("inBytesTotal") - prev.inBytesTotal) / timeElapsed);
c.outBits = Math.max(0, 8 *
(conn.getLong("outBytesTotal") - prev.outBytesTotal) / timeElapsed);
connections.put(deviceId, c);
@ -616,7 +630,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
boolean isShared = false;
outerloop:
for (Folder r : getFolders()) {
for (String n : r.DeviceIds) {
for (String n : r.deviceIds) {
if (n.equals(deviceId)) {
isShared = true;
break outerloop;
@ -647,10 +661,10 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
}
/**
* Returns status information about the folder with the given ID.
* Returns status information about the folder with the given id.
*/
public void getModel(final String folderId, final OnReceiveModelListener listener) {
new GetTask() {
new GetTask(mHttpsCertPath) {
@Override
protected void onPostExecute(String s) {
if (s == null)
@ -708,7 +722,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
*/
public void editDevice(final Device device, final Activity activity,
final OnDeviceIdNormalizedListener listener) {
normalizeDeviceId(device.DeviceID,
normalizeDeviceId(device.deviceID,
new RestApi.OnDeviceIdNormalizedListener() {
@Override
public void onDeviceIdNormalized(String normalizedId, String error) {
@ -716,17 +730,17 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
if (normalizedId == null)
return;
device.DeviceID = normalizedId;
device.deviceID = normalizedId;
// If the device already exists, just update it.
boolean create = true;
for (RestApi.Device n : getDevices(true)) {
if (n.DeviceID.equals(device.DeviceID)) {
if (n.deviceID.equals(device.deviceID)) {
create = false;
}
}
try {
JSONArray devices = mConfig.getJSONArray("Devices");
JSONArray devices = mConfig.getJSONArray("devices");
JSONObject n = null;
if (create) {
n = new JSONObject();
@ -734,17 +748,17 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
} else {
for (int i = 0; i < devices.length(); i++) {
JSONObject json = devices.getJSONObject(i);
if (device.DeviceID.equals(json.getString("DeviceID"))) {
if (device.deviceID.equals(json.getString("deviceID"))) {
n = devices.getJSONObject(i);
break;
}
}
}
n.put("DeviceID", device.DeviceID);
n.put("Name", device.Name);
n.put("Addresses", listToJson(device.Addresses.split(" ")));
n.put("Compression", device.Compression);
n.put("Introducer", device.Introducer);
n.put("deviceID", device.deviceID);
n.put("name", device.name);
n.put("addresses", listToJson(device.addresses.split(" ")));
n.put("compression", device.compression);
n.put("introducer", device.introducer);
requireRestart(activity);
} catch (JSONException e) {
Log.w(TAG, "Failed to read devices", e);
@ -759,13 +773,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
*/
public boolean deleteDevice(Device device, Activity activity) {
try {
JSONArray devices = mConfig.getJSONArray("Devices");
JSONArray devices = mConfig.getJSONArray("devices");
for (int i = 0; i < devices.length(); i++) {
JSONObject json = devices.getJSONObject(i);
if (device.DeviceID.equals(json.getString("DeviceID"))) {
mConfig.remove("Devices");
mConfig.put("Devices", delete(devices, devices.getJSONObject(i)));
if (device.deviceID.equals(json.getString("deviceID"))) {
mConfig.remove("devices");
mConfig.put("devices", delete(devices, devices.getJSONObject(i)));
break;
}
}
@ -782,7 +796,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
*/
public boolean editFolder(Folder folder, boolean create, Activity activity) {
try {
JSONArray folders = mConfig.getJSONArray("Folders");
JSONArray folders = mConfig.getJSONArray("folders");
JSONObject r = null;
if (create) {
r = new JSONObject();
@ -790,35 +804,35 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
} else {
for (int i = 0; i < folders.length(); i++) {
JSONObject json = folders.getJSONObject(i);
if (folder.ID.equals(json.getString("ID"))) {
if (folder.id.equals(json.getString("id"))) {
r = folders.getJSONObject(i);
break;
}
}
}
r.put("Path", folder.Path);
r.put("ID", folder.ID);
r.put("IgnorePerms", true);
r.put("ReadOnly", folder.ReadOnly);
r.put("path", folder.path);
r.put("id", folder.id);
r.put("ignorePerms", true);
r.put("readOnly", folder.readOnly);
JSONArray devices = new JSONArray();
for (String n : folder.DeviceIds) {
for (String n : folder.deviceIds) {
JSONObject element = new JSONObject();
element.put("DeviceID", n);
element.put("deviceID", n);
devices.put(element);
}
r.put("Devices", devices);
r.put("devices", devices);
JSONObject versioning = new JSONObject();
versioning.put("Type", folder.Versioning.getType());
versioning.put("type", folder.versioning.getType());
JSONObject params = new JSONObject();
versioning.put("Params", params);
for (String key : folder.Versioning.getParams().keySet()) {
params.put(key, folder.Versioning.getParams().get(key));
versioning.put("params", params);
for (String key : folder.versioning.getParams().keySet()) {
params.put(key, folder.versioning.getParams().get(key));
}
r.put("RescanIntervalS", folder.RescanIntervalS);
r.put("Versioning", versioning);
r.put("rescanIntervalS", folder.rescanIntervalS);
r.put("versioning", versioning);
requireRestart(activity);
} catch (JSONException e) {
Log.w(TAG, "Failed to edit folder " + folder.ID + " at " + folder.Path, e);
Log.w(TAG, "Failed to edit folder " + folder.id + " at " + folder.path, e);
return false;
}
return true;
@ -829,13 +843,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
*/
public boolean deleteFolder(Folder folder, Activity activity) {
try {
JSONArray folders = mConfig.getJSONArray("Folders");
JSONArray folders = mConfig.getJSONArray("folders");
for (int i = 0; i < folders.length(); i++) {
JSONObject json = folders.getJSONObject(i);
if (folder.ID.equals(json.getString("ID"))) {
mConfig.remove("Folders");
mConfig.put("Folders", delete(folders, folders.getJSONObject(i)));
if (folder.id.equals(json.getString("id"))) {
mConfig.remove("folders");
mConfig.put("folders", delete(folders, folders.getJSONObject(i)));
break;
}
}
@ -877,7 +891,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
* Normalizes a given device ID.
*/
public void normalizeDeviceId(String id, final OnDeviceIdNormalizedListener listener) {
new GetTask() {
new GetTask(mHttpsCertPath) {
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
@ -896,7 +910,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
}
/**
* Shares the given device id via Intent. Must be called from an Activity.
* Shares the given device ID via Intent. Must be called from an Activity.
*/
public static void shareDeviceId(Context context, String id) {
Intent shareIntent = new Intent();
@ -934,7 +948,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
*/
@Override
public void onFolderFileChange(String folderId, String relativePath) {
new PostTask().execute(mUrl, PostTask.URI_SCAN, mApiKey, "folder", folderId, "sub",
new PostTask(mHttpsCertPath).execute(mUrl, PostTask.URI_SCAN, mApiKey, "folder", folderId, "sub",
relativePath);
}
@ -943,11 +957,23 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
*/
public Device getLocalDevice() {
for (Device d : getDevices(true)) {
if (d.DeviceID.equals(mLocalDeviceId)) {
if (d.deviceID.equals(mLocalDeviceId)) {
return d;
}
}
return new Device();
}
public String getApiKey() {
return mApiKey;
}
public String getGuiUser() {
return mGuiUser;
}
public String getGuiPassword() {
return mGuiPassword;
}
}

View file

@ -55,15 +55,20 @@ public class SyncthingService extends Service {
public static final int GUI_UPDATE_INTERVAL = 1000;
/**
* Name of the public key file in the data directory.
* name of the public key file in the data directory.
*/
public static final String PUBLIC_KEY_FILE = "cert.pem";
/**
* Name of the private key file in the data directory.
* name of the private key file in the data directory.
*/
public static final String PRIVATE_KEY_FILE = "key.pem";
/**
* name of the public HTTPS CA file in the data directory.
*/
public static final String HTTPS_CERT_FILE = "https-cert.pem";
/**
* Directory where config is exported to and imported from.
*/
@ -71,7 +76,7 @@ public class SyncthingService extends Service {
new File(Environment.getExternalStorageDirectory(), "backups/syncthing");
/**
* Path to the native, integrated syncthing binary, relative to the data folder
* path to the native, integrated syncthing binary, relative to the data folder
*/
public static final String BINARY_NAME = "lib/libsyncthing.so";
@ -194,7 +199,7 @@ public class SyncthingService extends Service {
mConfig = new ConfigXml(SyncthingService.this);
mCurrentState = State.STARTING;
registerOnWebGuiAvailableListener(mApi);
new PollWebGuiAvailableTaskImpl().execute(mConfig.getWebGuiUrl());
new PollWebGuiAvailableTaskImpl(getFilesDir() + "/" + HTTPS_CERT_FILE).execute(mConfig.getWebGuiUrl());
new Thread(new SyncthingRunnable(
this, getApplicationInfo().dataDir + "/" + BINARY_NAME)).start();
Notification n = new NotificationCompat.Builder(this)
@ -254,7 +259,7 @@ public class SyncthingService extends Service {
mDeviceStateHolder = new DeviceStateHolder(SyncthingService.this);
registerReceiver(mDeviceStateHolder, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
new StartupTask().execute();
new StartupTask(sp.getString("gui_user",""), sp.getString("gui_password","")).execute();
}
/**
@ -263,6 +268,14 @@ public class SyncthingService extends Service {
* {@code Pair<String, String>}.
*/
private class StartupTask extends AsyncTask<Void, Void, Pair<String, String>> {
String mGuiUser;
String mGuiPassword;
public StartupTask(String guiUser, String guiPassword) {
mGuiUser = guiUser;
mGuiPassword = guiPassword;
}
@Override
protected Pair<String, String> doInBackground(Void... voids) {
mConfig = new ConfigXml(SyncthingService.this);
@ -272,6 +285,7 @@ public class SyncthingService extends Service {
@Override
protected void onPostExecute(Pair<String, String> urlAndKey) {
mApi = new RestApi(SyncthingService.this, urlAndKey.first, urlAndKey.second,
mGuiUser, mGuiPassword,
new RestApi.OnApiAvailableListener() {
@Override
public void onApiAvailable() {
@ -367,6 +381,11 @@ public class SyncthingService extends Service {
}
private class PollWebGuiAvailableTaskImpl extends PollWebGuiAvailableTask {
public PollWebGuiAvailableTaskImpl(String httpsCertPath) {
super(httpsCertPath);
}
@Override
protected void onPostExecute(Void aVoid) {
if (mStopScheduled) {

View file

@ -88,7 +88,7 @@ public class ConfigXml {
}
public String getWebGuiUrl() {
return "http://" + getGuiElement().getElementsByTagName("address").item(0).getTextContent();
return "https://" + getGuiElement().getElementsByTagName("address").item(0).getTextContent();
}
public String getApiKey() {
@ -142,6 +142,27 @@ public class ConfigXml {
}
}
// Enforce TLS.
Element gui = (Element) mConfig.getDocumentElement()
.getElementsByTagName("gui").item(0);
boolean tls = Boolean.parseBoolean(gui.getAttribute("tls"));
if (!tls) {
Log.i(TAG, "Enforce TLS");
gui.setAttribute("tls", Boolean.toString(true));
changed = true;
}
// Update deprecated 8080 port to 8384
NodeList addressList = gui.getElementsByTagName("address");
for (int i = 0; i < addressList.getLength(); i++) {
Element g = (Element) addressList.item(i);
if (g.getTextContent().equals("127.0.0.1:8080")) {
Log.i(TAG, "Replacing 127.0.0.1:8080 address with 127.0.0.1:8384");
g.setTextContent("127.0.0.1:8384");
changed = true;
}
}
if (changed) {
saveChanges();
}

View file

@ -0,0 +1,105 @@
package com.nutomic.syncthingandroid.util;
import android.util.Log;
import org.apache.http.conn.ssl.SSLSocketFactory;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.security.InvalidKeyException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
/*
* TrustManager allowing the Syncthing https.pem CA
*
* Based on http://stackoverflow.com/questions/16719959#16759793
*
*/
public class CustomX509TrustManager implements X509TrustManager {
private static final String TAG = "CustomX509TrustManager";
/**
* Taken from: http://janis.peisenieks.lv/en/76/english-making-an-ssl-connection-via-android/
*
*/
public static class CustomSSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");
public CustomSSLSocketFactory(SSLContext context)
throws KeyManagementException, NoSuchAlgorithmException,
KeyStoreException, UnrecoverableKeyException {
super(null);
sslContext = context;
}
@Override
public Socket createSocket(Socket socket, String host, int port,
boolean autoClose) throws IOException {
return sslContext.getSocketFactory().createSocket(socket, host, port,
autoClose);
}
@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
}
private String mHttpsCertPath;
public CustomX509TrustManager(String httpsCertPath) {
mHttpsCertPath = httpsCertPath;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
/**
* Verifies certs against public key of the local syncthing instance
*/
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] certs,
String authType) throws CertificateException {
InputStream inStream = null;
try {
inStream = new FileInputStream(mHttpsCertPath);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate ca = (X509Certificate)
cf.generateCertificate(inStream);
for (X509Certificate cert : certs) {
cert.verify(ca.getPublicKey());
}
} catch (FileNotFoundException |NoSuchAlgorithmException|InvalidKeyException|
NoSuchProviderException |SignatureException e) {
throw new CertificateException("Untrusted Certificate!", e);
} finally {
try {
if (inStream != null)
inStream.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}

View file

@ -42,22 +42,22 @@ public class DevicesAdapter extends ArrayAdapter<RestApi.Device>
TextView download = (TextView) convertView.findViewById(R.id.download);
TextView upload = (TextView) convertView.findViewById(R.id.upload);
String deviceId = getItem(position).DeviceID;
String deviceId = getItem(position).deviceID;
RestApi.Connection conn = mConnections.get(deviceId);
name.setText(getItem(position).Name);
name.setText(getItem(position).name);
Resources r = getContext().getResources();
if (conn != null) {
if (conn.Completion == 100) {
if (conn.completion == 100) {
status.setText(r.getString(R.string.device_up_to_date));
status.setTextColor(r.getColor(R.color.text_green));
}
else {
status.setText(r.getString(R.string.device_syncing, conn.Completion));
status.setText(r.getString(R.string.device_syncing, conn.completion));
status.setTextColor(r.getColor(R.color.text_blue));
}
download.setText(RestApi.readableTransferRate(getContext(), conn.InBits));
upload.setText(RestApi.readableTransferRate(getContext(), conn.OutBits));
download.setText(RestApi.readableTransferRate(getContext(), conn.inBits));
upload.setText(RestApi.readableTransferRate(getContext(), conn.outBits));
}
else {
download.setText("0 " + r.getStringArray(R.array.transfer_rate_units)[0]);

View file

@ -42,7 +42,7 @@ public class FolderObserver extends FileObserver {
@Override
public String getMessage() {
return "Path " + mPath + " does not exist, aborting file observer";
return "path " + mPath + " does not exist, aborting file observer";
}
}
@ -51,19 +51,19 @@ public class FolderObserver extends FileObserver {
*
* @param listener The listener where changes should be sent to.
* @param folder The folder where this folder belongs to.
* @param path Path to the monitored folder, relative to folder root.
* @param path path to the monitored folder, relative to folder root.
*/
private FolderObserver(OnFolderFileChangeListener listener, RestApi.Folder folder, String path) {
super(folder.Path + "/" + path,
super(folder.path + "/" + path,
ATTRIB | CLOSE_WRITE | CREATE | DELETE | DELETE_SELF | MODIFY | MOVED_FROM |
MOVED_TO | MOVE_SELF);
mListener = listener;
mFolder = folder;
mPath = path;
Log.v(TAG, "observer created for " + path + " in " + folder.ID);
Log.v(TAG, "observer created for " + path + " in " + folder.id);
startWatching();
File currentFolder = new File(folder.Path, path);
File currentFolder = new File(folder.path, path);
if (!currentFolder.exists()) {
throw new FolderNotExistingException(currentFolder.getAbsolutePath());
}
@ -107,7 +107,7 @@ public class FolderObserver extends FileObserver {
break;
}
}
mListener.onFolderFileChange(mFolder.ID, fullPath.getPath());
mListener.onFolderFileChange(mFolder.id, fullPath.getPath());
break;
case MOVED_TO:
// fall through
@ -117,7 +117,7 @@ public class FolderObserver extends FileObserver {
}
// fall through
default:
mListener.onFolderFileChange(mFolder.ID, fullPath.getPath());
mListener.onFolderFileChange(mFolder.id, fullPath.getPath());
}
}

View file

@ -42,10 +42,10 @@ public class FoldersAdapter extends ArrayAdapter<RestApi.Folder>
TextView invalid = (TextView) convertView.findViewById(R.id.invalid);
RestApi.Folder folder = getItem(position);
RestApi.Model model = mModels.get(folder.ID);
id.setText(folder.ID);
RestApi.Model model = mModels.get(folder.id);
id.setText(folder.id);
state.setTextColor(getContext().getResources().getColor(R.color.text_green));
directory.setText((folder.Path));
directory.setText((folder.path));
if (model != null) {
int percentage = (model.globalBytes != 0)
? (int) Math.floor(100 * model.inSyncBytes / model.globalBytes)
@ -57,13 +57,13 @@ public class FoldersAdapter extends ArrayAdapter<RestApi.Folder>
.getString(R.string.files, model.inSyncFiles, model.globalFiles));
size.setText(RestApi.readableFileSize(getContext(), model.inSyncBytes) + " / " +
RestApi.readableFileSize(getContext(), model.globalBytes));
if (folder.Invalid.equals("")) {
if (folder.invalid.equals("")) {
invalid.setText(model.invalid);
invalid.setVisibility((model.invalid.equals("")) ? View.GONE : View.VISIBLE);
}
} else {
invalid.setText(folder.Invalid);
invalid.setVisibility((folder.Invalid.equals("")) ? View.GONE : View.VISIBLE);
invalid.setText(folder.invalid);
invalid.setVisibility((folder.invalid.equals("")) ? View.GONE : View.VISIBLE);
}
return convertView;
@ -85,7 +85,7 @@ public class FoldersAdapter extends ArrayAdapter<RestApi.Folder>
for (int i = 0; i < getCount(); i++) {
if (i >= listView.getFirstVisiblePosition() &&
i <= listView.getLastVisiblePosition()) {
api.getModel(getItem(i).ID, this);
api.getModel(getItem(i).id, this);
}
}
}

View file

@ -0,0 +1,53 @@
package com.nutomic.syncthingandroid.util;
import android.util.Log;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
/*
* Wrapper for HTTPS Clients allowing the Syncthing https.pem CA
*
*/
public class Https {
private static final String TAG = "HTTPS";
/**
* Create a HTTPClient that verifies a custom PEM certificate
*
* @param httpsCertPath refers to the filepath of a SSL/TLS PEM certificate.
*/
public static HttpClient createHttpsClient(String httpsCertPath) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[] { new CustomX509TrustManager(httpsCertPath) },
new SecureRandom());
HttpClient client = new DefaultHttpClient();
SSLSocketFactory ssf = new CustomX509TrustManager.CustomSSLSocketFactory(ctx);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ClientConnectionManager ccm = client.getConnectionManager();
SchemeRegistry sr = ccm.getSchemeRegistry();
sr.register(new Scheme("https", ssf, 443));
return new DefaultHttpClient(ccm,
client.getParams());
} catch (NoSuchAlgorithmException|KeyManagementException|KeyStoreException|
UnrecoverableKeyException e) {
Log.w(TAG, e);
}
return null;
}
}

View file

@ -35,49 +35,49 @@
android:title="@string/syncthing_options">
<EditTextPreference
android:key="DeviceName"
android:key="deviceName"
android:title="@string/device_name"
android:persistent="false" />
<EditTextPreference
android:key="ListenAddress"
android:key="listenAddress"
android:title="@string/listen_address"
android:persistent="false" />
<EditTextPreference
android:key="MaxRecvKbps"
android:key="maxRecvKbps"
android:title="@string/max_recv_kbps"
android:numeric="integer"
android:persistent="false" />
<EditTextPreference
android:key="MaxSendKbps"
android:key="maxSendKbps"
android:title="@string/max_send_kbps"
android:numeric="integer"
android:persistent="false" />
<CheckBoxPreference
android:key="GlobalAnnEnabled"
android:key="globalAnnounceEnabled"
android:title="@string/global_announce_enabled"
android:persistent="false" />
<CheckBoxPreference
android:key="LocalAnnEnabled"
android:key="localAnnounceEnabled"
android:title="@string/local_announce_enabled"
android:persistent="false" />
<CheckBoxPreference
android:key="UPnPEnabled"
android:key="upnpEnabled"
android:title="@string/upnp_enabled"
android:persistent="false" />
<EditTextPreference
android:key="GlobalAnnServers"
android:key="globalAnnounceServers"
android:title="@string/global_announce_server"
android:persistent="false" />
<CheckBoxPreference
android:key="URAccepted"
android:key="urAccepted"
android:title="@string/usage_reporting"
android:persistent="false" />
@ -88,7 +88,7 @@
android:title="@string/syncthing_gui">
<EditTextPreference
android:key="Address"
android:key="address"
android:title="@string/gui_address"
android:persistent="false" />
@ -100,12 +100,6 @@
android:key="gui_password"
android:title="@string/gui_password" />
<CheckBoxPreference
android:key="UseTLS"
android:title="@string/gui_https_enabled"
android:enabled="false"
android:persistent="false" />
</PreferenceScreen>
<Preference