mirror of
https://github.com/syncthing/syncthing-android.git
synced 2024-12-27 13:21:30 +00:00
commit
7c54b762af
28 changed files with 631 additions and 305 deletions
|
@ -1 +1 @@
|
|||
Subproject commit 3cc4cb0a0b71908ae2d6392f14457e7ca6712278
|
||||
Subproject commit b35958d024175609a9e07934cdb1bedd3243939c
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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:"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
53
src/main/java/com/nutomic/syncthingandroid/util/Https.java
Normal file
53
src/main/java/com/nutomic/syncthingandroid/util/Https.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue