Lots of new unit tests, refactoring.

New tests:
RestApiTest
NodesAdapterTest
ReposAdapterTest

Refactored:
extracted PollWebGuiAvailableTask from SyncthingService
some changes in return values/calling behaviour for easier/better testing
This commit is contained in:
Felix Ableitner 2014-08-24 16:37:14 +02:00
parent 7b3d1b4052
commit b5f38c5c19
12 changed files with 561 additions and 149 deletions

View File

@ -0,0 +1,53 @@
package com.nutomic.syncthingandroid.test.syncthing;
import android.test.AndroidTestCase;
import com.nutomic.syncthingandroid.syncthing.PollWebGuiAvailableTask;
import com.nutomic.syncthingandroid.syncthing.PostTask;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import com.nutomic.syncthingandroid.syncthing.SyncthingRunnable;
import com.nutomic.syncthingandroid.syncthing.SyncthingService;
import com.nutomic.syncthingandroid.test.TestContext;
import com.nutomic.syncthingandroid.util.ConfigXml;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class PollWebGuiAvailableTaskTest extends AndroidTestCase {
private SyncthingRunnable mSyncthing;
private ConfigXml mConfig;
@Override
protected void setUp() throws Exception {
super.setUp();
mConfig = new ConfigXml(new TestContext(getContext()));
mConfig.updateIfNeeded();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
ConfigXml.getConfigFile(new TestContext(getContext())).delete();
}
public void testPolling() throws InterruptedException {
mSyncthing = new SyncthingRunnable(new TestContext(null),
getContext().getApplicationInfo().dataDir + "/" + SyncthingService.BINARY_NAME);
final CountDownLatch latch = new CountDownLatch(1);
new PollWebGuiAvailableTask() {
@Override
protected void onPostExecute(Void aVoid) {
latch.countDown();
}
}.execute(mConfig.getWebGuiUrl());
latch.await(1, TimeUnit.SECONDS);
new PostTask().execute(mConfig.getWebGuiUrl(), PostTask.URI_SHUTDOWN, mConfig.getApiKey());
}
}

View File

@ -0,0 +1,189 @@
package com.nutomic.syncthingandroid.test.syncthing;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import com.nutomic.syncthingandroid.syncthing.PollWebGuiAvailableTask;
import com.nutomic.syncthingandroid.syncthing.PostTask;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import com.nutomic.syncthingandroid.syncthing.SyncthingRunnable;
import com.nutomic.syncthingandroid.syncthing.SyncthingService;
import com.nutomic.syncthingandroid.test.TestContext;
import com.nutomic.syncthingandroid.util.ConfigXml;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
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 TestContext(null),
getContext().getApplicationInfo().dataDir + "/" + SyncthingService.BINARY_NAME);
mConfig = new ConfigXml(new TestContext(getContext()));
mConfig.createCameraRepo();
mConfig.updateIfNeeded();
final CountDownLatch latch = new CountDownLatch(2);
new PollWebGuiAvailableTask() {
@Override
protected void onPostExecute(Void aVoid) {
mApi.onWebGuiAvailable();
latch.countDown();
}
}.execute(mConfig.getWebGuiUrl());
mApi = new RestApi(getContext(), mConfig.getWebGuiUrl(), mConfig.getApiKey(),
new RestApi.OnApiAvailableListener() {
@Override
public void onApiAvailable() {
latch.countDown();
}
});
latch.await(1, TimeUnit.SECONDS);
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
final CountDownLatch latch = new CountDownLatch(1);
new PostTask() {
@Override
protected void onPostExecute(Boolean aBoolean) {
assertTrue(aBoolean);
latch.countDown();
}
}.execute(mConfig.getWebGuiUrl(), PostTask.URI_SHUTDOWN, mConfig.getApiKey());
latch.await(1, TimeUnit.SECONDS);
ConfigXml.getConfigFile(new TestContext(getContext())).delete();
}
@SmallTest
public void testGetVersion() {
assertNotNull(mApi.getVersion());
assertFalse(mApi.getVersion().equals(""));
}
@SmallTest
public void testGetNodes() {
assertNotNull(mApi.getNodes());
}
@MediumTest
public void testGetSystemInfo() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
mApi.getSystemInfo(new RestApi.OnReceiveSystemInfoListener() {
@Override
public void onReceiveSystemInfo(RestApi.SystemInfo info) {
assertNotNull(info);
latch.countDown();
}
});
latch.await(1, TimeUnit.SECONDS);
}
@SmallTest
public void testGetRepos() {
assertNotNull(mApi.getRepos());
}
@SmallTest
public void testReadableFileSize() {
assertEquals("1 MB", RestApi.readableFileSize(getContext(), 1048576));
assertEquals("1 GB", RestApi.readableFileSize(getContext(), 1073741824));
}
@SmallTest
public void testGetReadableTransferRate() {
assertEquals("1 Mb/s", RestApi.readableTransferRate(getContext(), 1048576));
assertEquals("1 Gb/s", RestApi.readableTransferRate(getContext(), 1073741824));
}
@MediumTest
public void testGetConnections() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
mApi.getConnections(new RestApi.OnReceiveConnectionsListener() {
@Override
public void onReceiveConnections(Map<String, RestApi.Connection> connections) {
assertNotNull(connections);
latch.countDown();
}
});
latch.await(1, TimeUnit.SECONDS);
}
@MediumTest
public void testGetModel() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
mApi.getModel("camera", new RestApi.OnReceiveModelListener() {
@Override
public void onReceiveModel(String repoId, RestApi.Model model) {
assertNotNull(model);
latch.countDown();
}
});
latch.await(1, TimeUnit.SECONDS);
}
@LargeTest
public void testModifyNode() throws InterruptedException {
final RestApi.Node node = new RestApi.Node();
node.NodeID = "P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2";
node.Addresses = "dynamic";
node.Name = "my node";
final CountDownLatch latch = new CountDownLatch(1);
mApi.editNode(node, new RestApi.OnNodeIdNormalizedListener() {
@Override
public void onNodeIdNormalized(String normalizedId, String error) {
assertEquals(node.NodeID, normalizedId);
assertEquals(null, error);
latch.countDown();
}
});
latch.await(10, TimeUnit.SECONDS);
assertTrue(mApi.deleteNode(node, getContext()));
}
@SmallTest
public void testModifyRepo() {
RestApi.Repo repo = new RestApi.Repo();
repo.Directory = "/my/dir";
repo.ID = "my-repo";
repo.Nodes = new ArrayList<>();
repo.ReadOnly = false;
repo.Versioning = new RestApi.Versioning();
assertTrue(mApi.editRepo(repo, true, getContext()));
assertTrue(mApi.deleteRepo(repo, getContext()));
}
@MediumTest
public void testNormalizeNodeId() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
mApi.normalizeNodeId("p56ioi7m--zjnu2iq-gdr-eydm-2mgtmgl3bxnpq6w5btbbz4tjxzwicq",
new RestApi.OnNodeIdNormalizedListener() {
@Override
public void onNodeIdNormalized(String normalizedId, String error) {
assertEquals("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2",
normalizedId);
latch.countDown();
}
});
latch.await(1, TimeUnit.SECONDS);
}
}

View File

@ -0,0 +1,67 @@
package com.nutomic.syncthingandroid.test.util;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import android.view.View;
import android.widget.TextView;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import com.nutomic.syncthingandroid.test.TestContext;
import com.nutomic.syncthingandroid.util.NodesAdapter;
import com.nutomic.syncthingandroid.util.ReposAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
public class NodesAdapterTest extends AndroidTestCase {
private NodesAdapter mAdapter;
private RestApi.Node mNode = new RestApi.Node();
private RestApi.Connection mConnection = new RestApi.Connection();
@Override
protected void setUp() throws Exception {
super.setUp();
mAdapter = new NodesAdapter(getContext());
mNode.Addresses = "127.0.0.1:12345";
mNode.Name = "the node";
mNode.NodeID = "123-456-789";
mConnection.Completion = 100;
mConnection.InBits = 1048576;
mConnection.OutBits = 1073741824;
}
@MediumTest
public void testGetViewNoConnections() {
mAdapter.add(Arrays.asList(mNode));
View v = mAdapter.getView(0, null, null);
assertEquals(mNode.Name, ((TextView) v.findViewById(R.id.name)).getText());
assertEquals(getContext().getString(R.string.node_disconnected),
((TextView) v.findViewById(R.id.status)).getText().toString());
assertFalse(((TextView) v.findViewById(R.id.status)).getText().equals(""));
assertFalse(((TextView) v.findViewById(R.id.download)).getText().equals(""));
assertFalse(((TextView) v.findViewById(R.id.upload)).getText().equals(""));
}
@MediumTest
public void testGetViewConnections() {
mAdapter.add(Arrays.asList(mNode));
mAdapter.onReceiveConnections(
new HashMap<String, RestApi.Connection>() {{ put(mNode.NodeID, mConnection); }});
View v = mAdapter.getView(0, null, null);
assertEquals(getContext().getString(R.string.node_up_to_date),
((TextView) v.findViewById(R.id.status)).getText().toString());
assertEquals("1 Mb/s", ((TextView) v.findViewById(R.id.download)).getText().toString());
assertEquals("1 Gb/s", ((TextView) v.findViewById(R.id.upload)).getText().toString());
}
}

View File

@ -0,0 +1,65 @@
package com.nutomic.syncthingandroid.test.util;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import android.view.View;
import android.widget.TextView;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import com.nutomic.syncthingandroid.test.TestContext;
import com.nutomic.syncthingandroid.util.ReposAdapter;
import java.util.ArrayList;
import java.util.Arrays;
public class ReposAdapterTest extends AndroidTestCase {
private ReposAdapter mAdapter;
private RestApi.Repo mRepo = new RestApi.Repo();
private RestApi.Model mModel = new RestApi.Model();
@Override
protected void setUp() throws Exception {
super.setUp();
mAdapter = new ReposAdapter(getContext());
mRepo.Directory = "/my/dir/";
mRepo.ID = "id 123";
mRepo.Invalid = "all good";
mRepo.Nodes = new ArrayList<>();
mRepo.ReadOnly = false;
mRepo.Versioning = new RestApi.Versioning();
mModel.localFiles = 50;
mModel.globalFiles = 500;
mModel.localBytes = 1048576;
mModel.globalBytes = 1073741824;
}
@MediumTest
public void testGetViewNoModel() {
mAdapter.add(Arrays.asList(mRepo));
View v = mAdapter.getView(0, null, null);
assertEquals(mRepo.ID, ((TextView) v.findViewById(R.id.id)).getText());
assertEquals(mRepo.Directory, ((TextView) v.findViewById(R.id.directory)).getText());
assertEquals(mRepo.Invalid, ((TextView) v.findViewById(R.id.invalid)).getText());
}
@MediumTest
public void testGetViewModel() {
mAdapter.add(Arrays.asList(mRepo));
mAdapter.onReceiveModel(mRepo.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();
assertTrue(items.contains(Long.toString(mModel.localFiles)));
assertTrue(items.contains(Long.toString(mModel.localFiles)));
String size = ((TextView) v.findViewById(R.id.size)).getText().toString();
assertTrue(size.contains("1 MB"));
assertTrue(size.contains("1 GB"));
}
}

View File

@ -35,7 +35,7 @@ public class WebGuiActivity extends SyncthingActivity implements SyncthingServic
/**
* Initialize WebView.
* <p/>
*
* Ignore lint javascript warning as js is loaded only from our known, local service.
*/
@Override
@ -66,7 +66,7 @@ public class WebGuiActivity extends SyncthingActivity implements SyncthingServic
*/
@Override
public void onWebGuiAvailable() {
mWebView.loadUrl(getApi().getUrl());
mWebView.loadUrl(getService().getWebGuiUrl());
}
}

View File

@ -0,0 +1,57 @@
package com.nutomic.syncthingandroid.syncthing;
import android.os.AsyncTask;
import android.util.Log;
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;
/**
* Polls SYNCTHING_URL until it returns HTTP status OK, then calls all listeners
* in mOnWebGuiAvailableListeners and clears it.
*/
public abstract class PollWebGuiAvailableTask extends AsyncTask<String, Void, Void> {
private static final String TAG = "PollWebGuiAvailableTask";
/**
* Interval in ms, at which connections to the web gui are performed on first start
* to find out if it's online.
*/
private static final long WEB_GUI_POLL_INTERVAL = 100;
/**
* @param url The URL of the web GUI (eg 127.0.0.1:8080).
*/
@Override
protected Void doInBackground(String... url) {
int status = 0;
HttpClient httpclient = new DefaultHttpClient();
HttpHead head = new HttpHead(url[0]);
do {
try {
Thread.sleep(WEB_GUI_POLL_INTERVAL);
HttpResponse response = httpclient.execute(head);
// NOTE: status is not really needed, as HttpHostConnectException is thrown
// earlier.
status = response.getStatusLine().getStatusCode();
} catch (HttpHostConnectException e) {
// We catch this in every call, as long as the service is not online,
// so we ignore and continue.
} catch (IOException e) {
Log.w(TAG, "Failed to poll for web interface", e);
} catch (InterruptedException e) {
Log.w(TAG, "Failed to poll for web interface", e);
}
} while (status != HttpStatus.SC_OK);
return null;
}
}

View File

@ -14,7 +14,7 @@ import java.io.IOException;
/**
* Performs a POST request with no parameters to the URL in uri[0] with the path in uri[1].
*/
public class PostTask extends AsyncTask<String, Void, Void> {
public class PostTask extends AsyncTask<String, Void, Boolean> {
private static final String TAG = "PostTask";
@ -31,7 +31,7 @@ public class PostTask extends AsyncTask<String, Void, Void> {
* params[3] The request content (optional)
*/
@Override
protected Void doInBackground(String... params) {
protected Boolean doInBackground(String... params) {
String fullUri = params[0] + params[1];
HttpClient httpclient = new DefaultHttpClient();
HttpPost post = new HttpPost(fullUri);
@ -44,8 +44,9 @@ public class PostTask extends AsyncTask<String, Void, Void> {
httpclient.execute(post);
} catch (IOException e) {
Log.w(TAG, "Failed to call Rest API at " + fullUri, e);
return false;
}
return null;
return true;
}
}

View File

@ -134,7 +134,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
private static final int NOTIFICATION_RESTART = 2;
private final SyncthingService mSyncthingService;
private final Context mContext;
private String mVersion;
@ -165,28 +165,15 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
* Stores the latest result of {@link #getModel(String, OnReceiveModelListener)} for each repo,
* for calculating node percentage in {@link #getConnections(OnReceiveConnectionsListener)}.
*/
private HashMap<String, Model> mCachedModelInfo = new HashMap<String, Model>();
private HashMap<String, Model> mCachedModelInfo = new HashMap<>();
public RestApi(SyncthingService syncthingService, String url, String apiKey) {
mSyncthingService = syncthingService;
public RestApi(Context context, String url, String apiKey, OnApiAvailableListener listener) {
mContext = context;
mUrl = url;
mApiKey = apiKey;
mNotificationManager = (NotificationManager)
mSyncthingService.getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* Returns the full URL of the web gui.
*/
public String getUrl() {
return mUrl;
}
/**
* Returns the API key needed to access the Rest API.
*/
public String getApiKey() {
return mApiKey;
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
mOnApiAvailableListener = listener;
}
/**
@ -199,6 +186,12 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
*/
private static final int TOTAL_STARTUP_CALLS = 3;
public interface OnApiAvailableListener {
public void onApiAvailable();
}
private OnApiAvailableListener mOnApiAvailableListener;
/**
* Gets local node id, syncthing version and config, then calls all OnApiAvailableListeners.
*/
@ -243,7 +236,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
throw new AssertionError("Too many startup calls");
}
if (value == TOTAL_STARTUP_CALLS) {
mSyncthingService.onApiChange();
mOnApiAvailableListener.onApiAvailable();
}
}
@ -327,7 +320,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
if (mRestartPostponed)
return;
final Intent intent = new Intent(mSyncthingService, SyncthingService.class)
final Intent intent = new Intent(mContext, SyncthingService.class)
.setAction(SyncthingService.ACTION_RESTART);
AlertDialog.Builder builder = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
@ -337,7 +330,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
mSyncthingService.startService(intent);
mContext.startService(intent);
}
})
.setNegativeButton(R.string.restart_later, new DialogInterface.OnClickListener() {
@ -361,13 +354,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
* Creates a notification prompting the user to restart the app.
*/
private void createRestartNotification() {
Intent intent = new Intent(mSyncthingService, SyncthingService.class)
Intent intent = new Intent(mContext, SyncthingService.class)
.setAction(SyncthingService.ACTION_RESTART);
PendingIntent pi = PendingIntent.getService(mSyncthingService, 0, intent, 0);
PendingIntent pi = PendingIntent.getService(mContext, 0, intent, 0);
Notification n = new NotificationCompat.Builder(mSyncthingService)
.setContentTitle(mSyncthingService.getString(R.string.restart_title))
.setContentText(mSyncthingService.getString(R.string.restart_notification_text))
Notification n = new NotificationCompat.Builder(mContext)
.setContentTitle(mContext.getString(R.string.restart_title))
.setContentText(mContext.getString(R.string.restart_notification_text))
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(pi)
.build();
@ -381,13 +374,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
*/
public List<Node> getNodes() {
if (mConfig == null)
return new ArrayList<Node>();
return null;
try {
return getNodes(mConfig.getJSONArray("Nodes"));
} catch (JSONException e) {
Log.w(TAG, "Failed to read nodes", e);
return new ArrayList<Node>();
return null;
}
}
@ -432,7 +425,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
*/
private List<Node> getNodes(JSONArray nodes) throws JSONException {
List<Node> ret;
ret = new ArrayList<Node>(nodes.length());
ret = new ArrayList<>(nodes.length());
for (int i = 0; i < nodes.length(); i++) {
JSONObject json = nodes.getJSONObject(i);
Node n = new Node();
@ -453,12 +446,12 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
*/
public List<Repo> getRepos() {
if (mConfig == null)
return new ArrayList<Repo>();
return new ArrayList<>();
List<Repo> ret = null;
List<Repo> ret;
try {
JSONArray repos = mConfig.getJSONArray("Repositories");
ret = new ArrayList<Repo>(repos.length());
ret = new ArrayList<>(repos.length());
for (int i = 0; i < repos.length(); i++) {
JSONObject json = repos.getJSONObject(i);
Repo r = new Repo();
@ -482,6 +475,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
}
} catch (JSONException e) {
Log.w(TAG, "Failed to read nodes", e);
return null;
}
return ret;
}
@ -661,7 +655,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
*/
public void editNode(final Node node,
final OnNodeIdNormalizedListener listener) {
mSyncthingService.getApi().normalizeNodeId(node.NodeID,
normalizeNodeId(node.NodeID,
new RestApi.OnNodeIdNormalizedListener() {
@Override
public void onNodeIdNormalized(String normalizedId, String error) {
@ -696,7 +690,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
n.put("NodeID", node.NodeID);
n.put("Name", node.Name);
n.put("Addresses", listToJson(node.Addresses.split(" ")));
configUpdated(mSyncthingService);
configUpdated(mContext);
} catch (JSONException e) {
Log.w(TAG, "Failed to read nodes", e);
}
@ -708,7 +702,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
/**
* Deletes the given node from syncthing.
*/
public void deleteNode(Node node, Activity activity) {
public boolean deleteNode(Node node, Context context) {
try {
JSONArray nodes = mConfig.getJSONArray("Nodes");
@ -720,16 +714,18 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
break;
}
}
configUpdated(activity);
configUpdated(context);
} catch (JSONException e) {
Log.w(TAG, "Failed to edit repo", e);
return false;
}
return true;
}
/**
* Updates or creates the given node.
*/
public void editRepo(Repo repo, boolean create, Context context) {
public boolean editRepo(Repo repo, boolean create, Context context) {
try {
JSONArray repos = mConfig.getJSONArray("Repositories");
JSONObject r = null;
@ -769,13 +765,15 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
configUpdated(context);
} catch (JSONException e) {
Log.w(TAG, "Failed to edit repo " + repo.ID + " at " + repo.Directory, e);
return false;
}
return true;
}
/**
* Deletes the given repository from syncthing.
*/
public void deleteRepo(Repo repo, Activity activity) {
public boolean deleteRepo(Repo repo, Context context) {
try {
JSONArray repos = mConfig.getJSONArray("Repositories");
@ -787,10 +785,12 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
break;
}
}
configUpdated(activity);
configUpdated(context);
} catch (JSONException e) {
Log.w(TAG, "Failed to edit repo", e);
return false;
}
return true;
}
/**
@ -844,13 +844,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
/**
* Shares the given node id via Intent. Must be called from an Activity.
*/
public static void shareNodeId(Activity activity, String id) {
public static void shareNodeId(Context context, String id) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(android.content.Intent.EXTRA_TEXT, id);
activity.startActivity(Intent.createChooser(
shareIntent, activity.getString(R.string.send_node_id_to)));
context.startActivity(Intent.createChooser(
shareIntent, context.getString(R.string.send_node_id_to)));
}
/**
@ -863,15 +863,15 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
int sdk = android.os.Build.VERSION.SDK_INT;
if (sdk < android.os.Build.VERSION_CODES.HONEYCOMB) {
android.text.ClipboardManager clipboard = (android.text.ClipboardManager)
mSyncthingService.getSystemService(Context.CLIPBOARD_SERVICE);
mContext.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setText(id);
} else {
ClipboardManager clipboard = (ClipboardManager)
mSyncthingService.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(mSyncthingService.getString(R.string.node_id), id);
mContext.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(mContext.getString(R.string.node_id), id);
clipboard.setPrimaryClip(clip);
}
Toast.makeText(mSyncthingService, R.string.node_id_copied_to_clipboard, Toast.LENGTH_SHORT)
Toast.makeText(mContext, R.string.node_id_copied_to_clipboard, Toast.LENGTH_SHORT)
.show();
}

View File

@ -54,12 +54,6 @@ public class SyncthingService extends Service {
*/
public static final int GUI_UPDATE_INTERVAL = 1000;
/**
* Interval in ms, at which connections to the web gui are performed on first start
* to find out if it's online.
*/
private static final long WEB_GUI_POLL_INTERVAL = 100;
/**
* Name of the public key file in the data directory.
*/
@ -73,7 +67,9 @@ public class SyncthingService extends Service {
/**
* Path to the native, integrated syncthing binary, relative to the data folder
*/
private static final String BINARY_NAME = "lib/libsyncthing.so";
public static final String BINARY_NAME = "lib/libsyncthing.so";
private ConfigXml mConfig;
private RestApi mApi;
@ -113,7 +109,7 @@ public class SyncthingService extends Service {
/**
* True if a stop was requested while syncthing is starting, in that case, perform stop in
* {@link PollWebGuiAvailableTask}.
* {@link PollWebGuiAvailableTaskImpl}.
*/
private boolean mStopScheduled = false;
@ -121,7 +117,7 @@ public class SyncthingService extends Service {
/**
* Handles intents, either {@link #ACTION_RESTART}, or intents having
* {@link DeviceStateHolder.EXTRA_HAS_WIFI} or {@link DeviceStateHolder.EXTRA_IS_CHARGING}
* {@link DeviceStateHolder#EXTRA_HAS_WIFI} or {@link DeviceStateHolder#EXTRA_IS_CHARGING}
* (which are handled by {@link DeviceStateHolder}.
*/
@Override
@ -131,15 +127,10 @@ public class SyncthingService extends Service {
} else if (ACTION_RESTART.equals(intent.getAction()) && mCurrentState == State.ACTIVE) {
new PostTask() {
@Override
protected void onPostExecute(Void aVoid) {
ConfigXml config = new ConfigXml(SyncthingService.this);
mApi = new RestApi(SyncthingService.this,
config.getWebGuiUrl(), config.getApiKey());
mCurrentState = State.STARTING;
registerOnWebGuiAvailableListener(mApi);
new PollWebGuiAvailableTask().execute();
protected void onPostExecute(Boolean aBoolean) {
new StartupTask().execute();
}
}.execute(mApi.getUrl(), PostTask.URI_RESTART, mApi.getApiKey());
}.execute(mConfig.getWebGuiUrl(), PostTask.URI_RESTART, mConfig.getApiKey());
} else if (mCurrentState != State.INIT) {
mDeviceStateHolder.update(intent);
updateState();
@ -169,7 +160,7 @@ public class SyncthingService extends Service {
mCurrentState = State.STARTING;
registerOnWebGuiAvailableListener(mApi);
new PollWebGuiAvailableTask().execute();
new PollWebGuiAvailableTaskImpl().execute(mConfig.getWebGuiUrl());
new Thread(new SyncthingRunnable(
this, getApplicationInfo().dataDir + "/" + BINARY_NAME)).start();
}
@ -190,53 +181,6 @@ public class SyncthingService extends Service {
onApiChange();
}
/**
* Polls SYNCTHING_URL until it returns HTTP status OK, then calls all listeners
* in mOnWebGuiAvailableListeners and clears it.
*/
private class PollWebGuiAvailableTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
int status = 0;
HttpClient httpclient = new DefaultHttpClient();
HttpHead head = new HttpHead(mApi.getUrl());
do {
try {
Thread.sleep(WEB_GUI_POLL_INTERVAL);
HttpResponse response = httpclient.execute(head);
// NOTE: status is not really needed, as HttpHostConnectException is thrown
// earlier.
status = response.getStatusLine().getStatusCode();
} catch (HttpHostConnectException e) {
// We catch this in every call, as long as the service is not online,
// so we ignore and continue.
} catch (IOException e) {
Log.w(TAG, "Failed to poll for web interface", e);
} catch (InterruptedException e) {
Log.w(TAG, "Failed to poll for web interface", e);
}
} while (status != HttpStatus.SC_OK);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
if (mStopScheduled) {
mCurrentState = State.DISABLED;
onApiChange();
mApi.shutdown();
mStopScheduled = false;
return;
}
Log.i(TAG, "Web GUI has come online at " + mApi.getUrl());
mCurrentState = State.ACTIVE;
for (OnWebGuiAvailableListener listener : mOnWebGuiAvailableListeners) {
listener.onWebGuiAvailable();
}
mOnWebGuiAvailableListeners.clear();
}
}
/**
* Move config file, keys, and index files to "official" folder
* <p/>
@ -299,22 +243,29 @@ public class SyncthingService extends Service {
@Override
protected Pair<String, String> doInBackground(Void... voids) {
moveConfigFiles();
ConfigXml config = new ConfigXml(SyncthingService.this);
config.updateIfNeeded();
mConfig = new ConfigXml(SyncthingService.this);
mConfig.updateIfNeeded();
if (isFirstStart()) {
Log.i(TAG, "App started for the first time. " +
"Copying default config, keys will be generated automatically");
config.createCameraRepo();
mConfig.createCameraRepo();
}
return new Pair<>(config.getWebGuiUrl(), config.getApiKey());
return new Pair<>(mConfig.getWebGuiUrl(), mConfig.getApiKey());
}
@Override
protected void onPostExecute(Pair<String, String> urlAndKey) {
mApi = new RestApi(SyncthingService.this, urlAndKey.first, urlAndKey.second);
Log.i(TAG, "Web GUI will be available at " + mApi.getUrl());
mApi = new RestApi(SyncthingService.this, urlAndKey.first, urlAndKey.second,
new RestApi.OnApiAvailableListener() {
@Override
public void onApiAvailable() {
onApiChange();
}
});
registerOnWebGuiAvailableListener(mApi);
Log.i(TAG, "Web GUI will be available at " + mConfig.getWebGuiUrl());
// HACK: Make sure there is no syncthing binary left running from an improper
// shutdown (eg Play Store updateIfNeeded).
@ -379,12 +330,31 @@ public class SyncthingService extends Service {
mOnApiChangeListeners.add(new WeakReference<OnApiChangeListener>(listener));
}
private class PollWebGuiAvailableTaskImpl extends PollWebGuiAvailableTask {
@Override
protected void onPostExecute(Void aVoid) {
if (mStopScheduled) {
mCurrentState = State.DISABLED;
onApiChange();
mApi.shutdown();
mStopScheduled = false;
return;
}
Log.i(TAG, "Web GUI has come online at " + mConfig.getWebGuiUrl());
mCurrentState = State.ACTIVE;
for (OnWebGuiAvailableListener listener : mOnWebGuiAvailableListeners) {
listener.onWebGuiAvailable();
}
mOnWebGuiAvailableListeners.clear();
}
}
/**
* Called to notifiy listeners of an API change.
* <p/>
*
* Must only be called from SyncthingService or {@link RestApi}.
*/
public void onApiChange() {
private void onApiChange() {
for (Iterator<WeakReference<OnApiChangeListener>> i = mOnApiChangeListeners.iterator();
i.hasNext(); ) {
WeakReference<OnApiChangeListener> listener = i.next();
@ -427,4 +397,8 @@ public class SyncthingService extends Service {
.setCancelable(false);
}
public String getWebGuiUrl() {
return mConfig.getWebGuiUrl();
}
}

View File

@ -42,26 +42,28 @@ public class NodesAdapter extends ArrayAdapter<RestApi.Node>
TextView download = (TextView) convertView.findViewById(R.id.download);
TextView upload = (TextView) convertView.findViewById(R.id.upload);
name.setText(getItem(position).Name);
final String nodeId = getItem(position).NodeID;
String nodeId = getItem(position).NodeID;
RestApi.Connection conn = mConnections.get(nodeId);
Resources res = getContext().getResources();
name.setText(getItem(position).Name);
Resources r = getContext().getResources();
if (conn != null) {
if (conn.Completion == 100) {
status.setText(res.getString(R.string.node_up_to_date));
status.setTextColor(res.getColor(R.color.text_green));
} else {
status.setText(res.getString(R.string.node_syncing, conn.Completion));
status.setTextColor(res.getColor(R.color.text_blue));
status.setText(r.getString(R.string.node_up_to_date));
status.setTextColor(r.getColor(R.color.text_green));
}
else {
status.setText(r.getString(R.string.node_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));
} else {
download.setText("0 " + res.getStringArray(R.array.transfer_rate_units)[0]);
upload.setText("0 " + res.getStringArray(R.array.transfer_rate_units)[0]);
status.setText(res.getString(R.string.node_disconnected));
status.setTextColor(res.getColor(R.color.text_red));
}
else {
download.setText("0 " + r.getStringArray(R.array.transfer_rate_units)[0]);
upload.setText("0 " + r.getStringArray(R.array.transfer_rate_units)[0]);
status.setText(r.getString(R.string.node_disconnected));
status.setTextColor(r.getColor(R.color.text_red));
}
return convertView;

View File

@ -35,17 +35,17 @@ public class ReposAdapter extends ArrayAdapter<RestApi.Repo>
}
TextView id = (TextView) convertView.findViewById(R.id.id);
TextView state = (TextView) convertView.findViewById(R.id.state);
TextView folder = (TextView) convertView.findViewById(R.id.folder);
TextView directory = (TextView) convertView.findViewById(R.id.directory);
TextView items = (TextView) convertView.findViewById(R.id.items);
TextView size = (TextView) convertView.findViewById(R.id.size);
TextView invalid = (TextView) convertView.findViewById(R.id.invalid);
id.setText(getItem(position).ID);
RestApi.Repo repo = getItem(position);
RestApi.Model model = mModels.get(repo.ID);
id.setText(repo.ID);
state.setTextColor(getContext().getResources().getColor(R.color.text_green));
folder.setText((getItem(position).Directory));
RestApi.Model model = mModels.get(getItem(position).ID);
directory.setText((repo.Directory));
if (model != null) {
state.setText(getContext().getString(R.string.repo_progress_format, model.state,
(model.globalBytes <= 0)
@ -56,10 +56,13 @@ public class ReposAdapter extends ArrayAdapter<RestApi.Repo>
.getString(R.string.files, model.localFiles, model.globalFiles));
size.setText(RestApi.readableFileSize(getContext(), model.localBytes) + " / " +
RestApi.readableFileSize(getContext(), model.globalBytes));
if (repo.Invalid.equals("")) {
invalid.setText(model.invalid);
invalid.setVisibility((model.invalid.equals("")) ? View.GONE : View.VISIBLE);
}
} else {
invalid.setVisibility(View.GONE);
invalid.setText(repo.Invalid);
invalid.setVisibility((repo.Invalid.equals("")) ? View.GONE : View.VISIBLE);
}
return convertView;
@ -91,4 +94,5 @@ public class ReposAdapter extends ArrayAdapter<RestApi.Repo>
mModels.put(repoId, model);
notifyDataSetChanged();
}
}

View File

@ -24,7 +24,7 @@
android:layout_height="wrap_content" />
<TextView
android:id="@+id/folder"
android:id="@+id/directory"
android:layout_below="@id/state"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
@ -33,7 +33,7 @@
<TextView
android:id="@+id/items"
android:layout_below="@id/folder"
android:layout_below="@id/directory"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />