1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2024-12-23 03:11:30 +00:00

Added proper restart handling (new config data is automatically loaded after restart).

This commit is contained in:
Felix Ableitner 2014-06-22 22:36:35 +02:00
parent b67c625318
commit dff9156d77
10 changed files with 233 additions and 188 deletions

View file

@ -34,9 +34,8 @@ import com.nutomic.syncthingandroid.syncthing.SyncthingService;
/**
* {@link android.support.v4.app.ListFragment} that shows a configurable loading text.
*/
public abstract class LoadingListFragment extends Fragment implements RestApi.OnApiAvailableListener, AdapterView.OnItemClickListener {
private boolean mInitialized = false;
public abstract class LoadingListFragment extends Fragment implements
SyncthingService.OnApiAvailableListener, AdapterView.OnItemClickListener {
private ListFragment mListFragment;
@ -109,11 +108,10 @@ public abstract class LoadingListFragment extends Fragment implements RestApi.On
@Override
public void onApiAvailable() {
MainActivity activity = (MainActivity) getActivity();
if (!mInitialized && getActivity() != null &&
if (getActivity() != null &&
activity.getApi() != null && mListFragment != null) {
onInitAdapter(activity);
getListView().setOnItemClickListener(this);
mInitialized = true;
}
}

View file

@ -26,7 +26,7 @@ import java.util.TimerTask;
*/
public class LocalNodeInfoFragment extends Fragment
implements RestApi.OnReceiveSystemInfoListener, RestApi.OnReceiveConnectionsListener,
RestApi.OnApiAvailableListener {
SyncthingService.OnApiAvailableListener {
private TextView mNodeId;

View file

@ -54,9 +54,9 @@ public class MainActivity extends ActionBarActivity
*/
@Override
public void onWebGuiAvailable() {
mSyncthingService.getApi().registerOnApiAvailableListener(mRepositoriesFragment);
mSyncthingService.getApi().registerOnApiAvailableListener(mNodesFragment);
mSyncthingService.getApi().registerOnApiAvailableListener(mLocalNodeInfoFragment);
mSyncthingService.registerOnApiAvailableListener(mRepositoriesFragment);
mSyncthingService.registerOnApiAvailableListener(mNodesFragment);
mSyncthingService.registerOnApiAvailableListener(mLocalNodeInfoFragment);
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
mDrawerLayout.setDrawerListener(mDrawerToggle);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@ -220,14 +220,14 @@ public class MainActivity extends ActionBarActivity
}
switch (item.getItemId()) {
case R.id.add_node:
Intent intent = new Intent(this, NodeSettingsActivity.class);
intent.setAction(NodeSettingsActivity.ACTION_CREATE);
case R.id.add_repository:
Intent intent = new Intent(this, RepoSettingsActivity.class);
intent.setAction(RepoSettingsActivity.ACTION_CREATE);
startActivity(intent);
return true;
case R.id.add_repository:
intent = new Intent(this, RepoSettingsActivity.class);
intent.setAction(RepoSettingsActivity.ACTION_CREATE);
case R.id.add_node:
intent = new Intent(this, NodeSettingsActivity.class);
intent.setAction(NodeSettingsActivity.ACTION_CREATE);
startActivity(intent);
return true;
case R.id.web_gui:

View file

@ -28,7 +28,7 @@ import java.util.Map;
*/
public class NodeSettingsActivity extends PreferenceActivity implements
Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener,
RestApi.OnReceiveConnectionsListener, RestApi.OnApiAvailableListener {
RestApi.OnReceiveConnectionsListener, SyncthingService.OnApiAvailableListener {
public static final String ACTION_CREATE = "create";
@ -43,8 +43,7 @@ public class NodeSettingsActivity extends PreferenceActivity implements
public void onServiceConnected(ComponentName className, IBinder service) {
SyncthingServiceBinder binder = (SyncthingServiceBinder) service;
mSyncthingService = binder.getService();
mSyncthingService.getApi()
.registerOnApiAvailableListener(NodeSettingsActivity.this);
mSyncthingService.registerOnApiAvailableListener(NodeSettingsActivity.this);
}
public void onServiceDisconnected(ComponentName className) {

View file

@ -17,7 +17,7 @@ import java.util.TimerTask;
* Displays a list of all existing nodes.
*/
public class NodesFragment extends LoadingListFragment implements
RestApi.OnApiAvailableListener, ListView.OnItemClickListener {
SyncthingService.OnApiAvailableListener, ListView.OnItemClickListener {
private NodeAdapter mAdapter;

View file

@ -31,7 +31,7 @@ import java.util.List;
*/
public class RepoSettingsActivity extends PreferenceActivity
implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener,
RestApi.OnApiAvailableListener {
SyncthingService.OnApiAvailableListener {
public static final String ACTION_CREATE = "create";
@ -48,8 +48,7 @@ public class RepoSettingsActivity extends PreferenceActivity
public void onServiceConnected(ComponentName className, IBinder service) {
SyncthingServiceBinder binder = (SyncthingServiceBinder) service;
mSyncthingService = binder.getService();
mSyncthingService.getApi()
.registerOnApiAvailableListener(RepoSettingsActivity.this);
mSyncthingService.registerOnApiAvailableListener(RepoSettingsActivity.this);
}
public void onServiceDisconnected(ComponentName className) {

View file

@ -16,7 +16,7 @@ import java.util.TimerTask;
* Displays a list of all existing repositories.
*/
public class ReposFragment extends LoadingListFragment implements
RestApi.OnApiAvailableListener, AdapterView.OnItemClickListener {
SyncthingService.OnApiAvailableListener, AdapterView.OnItemClickListener {
private ReposAdapter mAdapter;

View file

@ -14,6 +14,7 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
@ -115,16 +116,9 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
public String invalid;
}
public interface OnApiAvailableListener {
public void onApiAvailable();
}
private final LinkedList<OnApiAvailableListener> mOnApiAvailableListeners =
new LinkedList<OnApiAvailableListener>();
private static final int NOTIFICATION_RESTART = 2;
private final Context mContext;
private final SyncthingService mSyncthingService;
private String mVersion;
@ -149,12 +143,12 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
*/
private long mPreviousConnectionTime = 0;
public RestApi(Context context, String url, String apiKey) {
mContext = context;
public RestApi(SyncthingService syncthingService, String url, String apiKey) {
mSyncthingService = syncthingService;
mUrl = url;
mApiKey = apiKey;
mNotificationManager = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
mSyncthingService.getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
@ -164,6 +158,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
return mUrl;
}
/**
* Returns the API key needed to access the Rest API.
*/
public String getApiKey() {
return mApiKey;
}
/**
* Number of previous calls to {@link #tryIsAvailable()}.
*/
@ -208,18 +209,14 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
});
}
/**
* Increments mAvailableCount by one, and, if it reached TOTAL_STARTUP_CALLS, notifies
* all registered {@link OnApiAvailableListener} listeners.
* Increments mAvailableCount by one, and, if it reached TOTAL_STARTUP_CALLS,
* calls {@link SyncthingService#onApiAvailable()}.
*/
private void tryIsAvailable() {
int value = mAvailableCount.incrementAndGet();
if (value == TOTAL_STARTUP_CALLS) {
for (OnApiAvailableListener listener : mOnApiAvailableListeners) {
listener.onApiAvailable();
}
mOnApiAvailableListeners.clear();
mSyncthingService.onApiAvailable();
}
}
@ -238,13 +235,6 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
new PostTask().execute(mUrl, PostTask.URI_SHUTDOWN, mApiKey);
}
/**
* Restarts the syncthing binary.
*/
public void restart() {
new PostTask().execute(mUrl, PostTask.URI_RESTART, mApiKey);
}
/**
* Gets a value from config,
*
@ -308,13 +298,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
private void configUpdated() {
new PostTask().execute(mUrl, PostTask.URI_CONFIG, mApiKey, mConfig.toString());
Intent i = new Intent(mContext, SyncthingService.class)
Intent i = new Intent(mSyncthingService, SyncthingService.class)
.setAction(SyncthingService.ACTION_RESTART);
PendingIntent pi = PendingIntent.getService(mContext, 0, i, 0);
PendingIntent pi = PendingIntent.getService(mSyncthingService, 0, i, 0);
Notification n = new NotificationCompat.Builder(mContext)
.setContentTitle(mContext.getString(R.string.restart_notif_title))
.setContentText(mContext.getString(R.string.restart_notif_text))
Notification n = new NotificationCompat.Builder(mSyncthingService)
.setContentTitle(mSyncthingService.getString(R.string.restart_notif_title))
.setContentText(mSyncthingService.getString(R.string.restart_notif_text))
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(pi)
.build();
@ -433,21 +423,6 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
return ret;
}
/**
* Register a listener for the web gui becoming available..
*
* If the web gui is already available, listener will be called immediately.
* Listeners are unregistered automatically after being called.
*/
public void registerOnApiAvailableListener(OnApiAvailableListener listener) {
if (mConfig != null) {
listener.onApiAvailable();
}
else {
mOnApiAvailableListeners.addLast(listener);
}
}
/**
* Converts a number of bytes to a human readable file size (eg 3.5 GB).
*/
@ -715,4 +690,8 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
return newArray;
}
public boolean isApiAvailable() {
return mAvailableCount.get() == TOTAL_STARTUP_CALLS;
}
}

View file

@ -6,7 +6,6 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.support.v4.app.NotificationCompat;
@ -15,6 +14,7 @@ import android.util.Pair;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.gui.MainActivity;
import com.nutomic.syncthingandroid.util.ConfigXml;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
@ -22,10 +22,6 @@ 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 org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.BufferedReader;
import java.io.DataOutputStream;
@ -35,20 +31,10 @@ import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.util.LinkedList;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/**
* Holds the native syncthing instance and provides an API to access it.
*/
@ -114,10 +100,27 @@ public class SyncthingService extends Service {
private boolean mIsWebGuiAvailable = false;
public interface OnApiAvailableListener {
public void onApiAvailable();
}
private final LinkedList<WeakReference<OnApiAvailableListener>> mOnApiAvailableListeners =
new LinkedList<WeakReference<OnApiAvailableListener>>();
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && ACTION_RESTART.equals(intent.getAction())) {
mApi.restart();
mIsWebGuiAvailable = false;
new PostTask() {
@Override
protected void onPostExecute(Void aVoid) {
ConfigXml config = new ConfigXml(getConfigFile());
mApi = new RestApi(SyncthingService.this,
config.getWebGuiUrl(), config.getApiKey());
registerOnWebGuiAvailableListener(mApi);
new PollWebGuiAvailableTask().execute();
}
}.execute(mApi.getUrl(), PostTask.URI_RESTART, mApi.getApiKey());
}
return START_STICKY;
}
@ -325,30 +328,9 @@ public class SyncthingService extends Service {
copyDefaultConfig();
}
moveConfigFiles();
updateConfig();
String syncthingUrl = null;
String apiKey = null;
try {
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document d = db.parse(getConfigFile());
Element gui = (Element) d.getDocumentElement()
.getElementsByTagName("gui").item(0);
syncthingUrl = gui.getElementsByTagName("address").item(0).getTextContent();
apiKey = gui.getElementsByTagName("apikey").item(0).getTextContent();
}
catch (SAXException e) {
throw new RuntimeException("Failed to read gui url, aborting", e);
}
catch (ParserConfigurationException e) {
throw new RuntimeException("Failed to read gui url, aborting", e);
}
catch (IOException e) {
throw new RuntimeException("Failed to read gui url, aborting", e);
}
finally {
return new Pair<String, String>("http://" + syncthingUrl, apiKey);
}
ConfigXml config = new ConfigXml(getConfigFile());
config.update();
return new Pair<String, String>(config.getWebGuiUrl(), config.getApiKey());
}
@Override
@ -403,85 +385,6 @@ public class SyncthingService extends Service {
return new File(getFilesDir(), CONFIG_FILE);
}
/**
* Updates the config file.
*
* Coming from 0.2.0 and earlier, globalAnnounceServer value "announce.syncthing.net:22025" is
* replaced with "194.126.249.5:22025" (as domain resolve is broken).
*
* Coming from 0.3.0 and earlier, the ignorePerms flag is set to true on every repository.
*/
private void updateConfig() {
try {
Log.i(TAG, "Checking for needed config updates");
boolean changed = false;
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = db.parse(getConfigFile());
Element options = (Element) doc.getDocumentElement()
.getElementsByTagName("options").item(0);
Element gui = (Element) doc.getDocumentElement()
.getElementsByTagName("gui").item(0);
// Create an API key if it does not exist.
if (gui.getElementsByTagName("apikey").getLength() == 0) {
Log.i(TAG, "Initializing API key with random string");
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < 20; i++) {
sb.append(chars[random.nextInt(chars.length)]);
}
Element apiKey = doc.createElement("apikey");
apiKey.setTextContent(sb.toString());
gui.appendChild(apiKey);
changed = true;
}
// Hardcode default globalAnnounceServer ip.
Element globalAnnounceServer = (Element)
options.getElementsByTagName("globalAnnounceServer").item(0);
if (globalAnnounceServer.getTextContent().equals("announce.syncthing.net:22025")) {
Log.i(TAG, "Replacing globalAnnounceServer host with ip");
globalAnnounceServer.setTextContent("194.126.249.5:22025");
changed = true;
}
// Set ignorePerms attribute.
NodeList repos = doc.getDocumentElement().getElementsByTagName("repository");
for (int i = 0; i < repos.getLength(); i++) {
Element r = (Element) repos.item(i);
if (!r.hasAttribute("ignorePerms") ||
!Boolean.parseBoolean(r.getAttribute("ignorePerms"))) {
Log.i(TAG, "Set 'ignorePerms' on repository " + r.getAttribute("id"));
r.setAttribute("ignorePerms", Boolean.toString(true));
changed = true;
}
}
// Write the changes back to file.
if (changed) {
Log.i(TAG, "Writing updated config back to file");
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
DOMSource domSource = new DOMSource(doc);
StreamResult streamResult = new StreamResult(getConfigFile());
transformer.transform(domSource, streamResult);
}
}
catch (ParserConfigurationException e) {
Log.w(TAG, "Failed to parse config", e);
}
catch (IOException e) {
Log.w(TAG, "Failed to parse config", e);
}
catch (SAXException e) {
Log.w(TAG, "Failed to parse config", e);
}
catch (TransformerException e) {
Log.w(TAG, "Failed to save updated config", e);
}
}
/**
* Returns true if this service has not been started before (ie config.xml does not exist).
*
@ -525,4 +428,37 @@ public class SyncthingService extends Service {
return mApi;
}
/**
* Register a listener for the syncthing API becoming available..
*
* If the API is already available, listener will be called immediately.
*
* Listeners are kept around (as weak reference) and called again after any configuration
* changes to allow a data refresh.
*/
public void registerOnApiAvailableListener(OnApiAvailableListener listener) {
if (mApi.isApiAvailable()) {
listener.onApiAvailable();
}
else {
mOnApiAvailableListeners.addLast(new WeakReference<OnApiAvailableListener>(listener));
}
}
/**
* Called by {@link RestApi} once it is fully initialized.
*
* Must not be called from anywhere else.
*/
public void onApiAvailable() {
for (WeakReference<OnApiAvailableListener> listener : mOnApiAvailableListeners) {
if (listener.get() != null) {
listener.get().onApiAvailable();
}
else {
mOnApiAvailableListeners.remove(listener);
}
}
}
}

View file

@ -0,0 +1,134 @@
package com.nutomic.syncthingandroid.util;
import android.util.Log;
import android.util.Pair;
import com.nutomic.syncthingandroid.syncthing.SyncthingService;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.IOException;
import java.util.Random;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/**
* Provides direct access to the config.xml file in the file system.
*
* This class should only be used if the syncthing API is not available (usually during startup).
*/
public class ConfigXml {
private static final String TAG = "ConfigXml";
private File mConfigFile;
private Document mConfig;
public ConfigXml(File configFile) {
mConfigFile = configFile;
try {
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
mConfig = db.parse(configFile);
} catch (SAXException e) {
throw new RuntimeException("Failed to parse config file", e);
} catch (ParserConfigurationException e) {
throw new RuntimeException("Failed to parse config file", e);
} catch (IOException e) {
throw new RuntimeException("Failed to open config file", e);
}
}
public String getWebGuiUrl() {
return "http://" + getGuiElement().getElementsByTagName("address").item(0).getTextContent();
}
public String getApiKey() {
return getGuiElement().getElementsByTagName("apikey").item(0).getTextContent();
}
/**
* Updates the config file.
*
* Coming from 0.2.0 and earlier, globalAnnounceServer value "announce.syncthing.net:22025" is
* replaced with "194.126.249.5:22025" (as domain resolve is broken).
*
* Coming from 0.3.0 and earlier, the ignorePerms flag is set to true on every repository.
*/
public void update() {
try {
Log.i(TAG, "Checking for needed config updates");
boolean changed = false;
Element options = (Element) mConfig.getDocumentElement()
.getElementsByTagName("options").item(0);
Element gui = (Element) mConfig.getDocumentElement()
.getElementsByTagName("gui").item(0);
// Create an API key if it does not exist.
if (gui.getElementsByTagName("apikey").getLength() == 0) {
Log.i(TAG, "Initializing API key with random string");
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < 20; i++) {
sb.append(chars[random.nextInt(chars.length)]);
}
Element apiKey = mConfig.createElement("apikey");
apiKey.setTextContent(sb.toString());
gui.appendChild(apiKey);
changed = true;
}
// Hardcode default globalAnnounceServer ip.
Element globalAnnounceServer = (Element)
options.getElementsByTagName("globalAnnounceServer").item(0);
if (globalAnnounceServer.getTextContent().equals("announce.syncthing.net:22025")) {
Log.i(TAG, "Replacing globalAnnounceServer host with ip");
globalAnnounceServer.setTextContent("194.126.249.5:22025");
changed = true;
}
// Set ignorePerms attribute.
NodeList repos = mConfig.getDocumentElement().getElementsByTagName("repository");
for (int i = 0; i < repos.getLength(); i++) {
Element r = (Element) repos.item(i);
if (!r.hasAttribute("ignorePerms") ||
!Boolean.parseBoolean(r.getAttribute("ignorePerms"))) {
Log.i(TAG, "Set 'ignorePerms' on repository " + r.getAttribute("id"));
r.setAttribute("ignorePerms", Boolean.toString(true));
changed = true;
}
}
// Write the changes back to file.
if (changed) {
Log.i(TAG, "Writing updated config back to file");
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
DOMSource domSource = new DOMSource(mConfig);
StreamResult streamResult = new StreamResult(mConfigFile);
transformer.transform(domSource, streamResult);
}
}
catch (TransformerException e) {
Log.w(TAG, "Failed to save updated config", e);
}
}
private Element getGuiElement() {
return (Element) mConfig.getDocumentElement()
.getElementsByTagName("gui").item(0);
}
}