mirror of
https://github.com/syncthing/syncthing-android.git
synced 2024-11-26 14:21:16 +00:00
Added a IntentService to receive Broadcast-Intents to remotely control / configure the app.
MainActivity: Moved binding-functions to onPause() and onResume() so that the SyncThingService is only bound to the activity if the activity is active. New class AppConfigReceiver: Support start and stop of the SyncThingService - restarting a running service again should not be an issue - stop service only if "always run in background"-mode is disabled. Otherwise show a notification indicating this. Instrumentation-tests: - Added tests for AppConfigReceiver - Extended MockContext to also consume stopService commands. - testGetReadableTransferRate: Apparently the return-values have changed a bit. Adjusted the asserts to the current return-values. SycthingService: Added code for thread-safety in case the service still starting when it should be stopped. Then PollWebGuiAvailableTaskImpl is active and waits for the Synthing-API to become active. So that and onDestroy have to be synchronized. Added a stopSelf() in PollWebGuiAvailableTaskImpl.onPostExecute() in case mStopScheduled was active. Commented my change in the javadoc at onDestroy. Put a reference to that comment to .onPostExecution()
This commit is contained in:
parent
63aa9d2d09
commit
b9919adccd
7 changed files with 250 additions and 17 deletions
|
@ -16,6 +16,7 @@ import java.util.List;
|
||||||
public class MockContext extends ContextWrapper {
|
public class MockContext extends ContextWrapper {
|
||||||
|
|
||||||
private ArrayList<Intent> mReceivedIntents = new ArrayList<>();
|
private ArrayList<Intent> mReceivedIntents = new ArrayList<>();
|
||||||
|
private ArrayList<Intent> mStopServiceIntents = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use the actual context for calls that aren't easily mocked. May be null if those
|
* Use the actual context for calls that aren't easily mocked. May be null if those
|
||||||
|
@ -36,10 +37,20 @@ public class MockContext extends ContextWrapper {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean stopService(Intent intent) {
|
||||||
|
mStopServiceIntents.add(intent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public List<Intent> getReceivedIntents() {
|
public List<Intent> getReceivedIntents() {
|
||||||
return mReceivedIntents;
|
return mReceivedIntents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Intent> getStopServiceIntents() {
|
||||||
|
return mStopServiceIntents;
|
||||||
|
}
|
||||||
|
|
||||||
public void clearReceivedIntents() {
|
public void clearReceivedIntents() {
|
||||||
mReceivedIntents.clear();
|
mReceivedIntents.clear();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
package com.nutomic.syncthingandroid.test.syncthing;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.test.AndroidTestCase;
|
||||||
|
|
||||||
|
import com.nutomic.syncthingandroid.syncthing.AppConfigReceiver;
|
||||||
|
import com.nutomic.syncthingandroid.syncthing.SyncthingService;
|
||||||
|
import com.nutomic.syncthingandroid.test.MockContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the correct behaviour of the AppConfigReceiver
|
||||||
|
*
|
||||||
|
* Created by sqrt-1674 on 27.03.16.
|
||||||
|
*/
|
||||||
|
public class AppConfigReceiverTest extends AndroidTestCase {
|
||||||
|
private AppConfigReceiver mReceiver;
|
||||||
|
private MockContext mContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUp() throws Exception {
|
||||||
|
super.setUp();
|
||||||
|
mReceiver = new AppConfigReceiver();
|
||||||
|
mContext = new MockContext(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void tearDown() throws Exception {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(mContext).edit().clear().commit();
|
||||||
|
super.tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test starting the Syncthing-Service if "always run in background" is enabled
|
||||||
|
* In this case starting the service is allowed
|
||||||
|
*/
|
||||||
|
public void testStartSyncthingServiceBackground() {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(mContext)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, true)
|
||||||
|
.commit();
|
||||||
|
|
||||||
|
Intent intent = new Intent(new Intent(mContext, AppConfigReceiver.class));
|
||||||
|
intent.setAction(AppConfigReceiver.ACTION_START);
|
||||||
|
|
||||||
|
mReceiver.onReceive(mContext, intent);
|
||||||
|
assertEquals("Start SyncthingService Background", 1, mContext.getReceivedIntents().size());
|
||||||
|
assertEquals("Start SyncthingService Background", SyncthingService.class.getName(),
|
||||||
|
mContext.getReceivedIntents().get(0).getComponent().getClassName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test stopping the service if "alway run in background" is enabled.
|
||||||
|
* Stopping the service in this mode is not allowed, so no stopService-intent may be issued.
|
||||||
|
*/
|
||||||
|
public void testStopSyncthingServiceBackground() {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(mContext)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, true)
|
||||||
|
.commit();
|
||||||
|
|
||||||
|
Intent intent = new Intent(new Intent(mContext, AppConfigReceiver.class));
|
||||||
|
intent.setAction(AppConfigReceiver.ACTION_STOP);
|
||||||
|
|
||||||
|
mReceiver.onReceive(mContext, intent);
|
||||||
|
assertEquals("Stop SyncthingService Background", 0, mContext.getStopServiceIntents().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test starting the Syncthing-Service if "always run in background" is disabled
|
||||||
|
* In this case starting the service is allowed
|
||||||
|
*/
|
||||||
|
public void testStartSyncthingServiceForeground() {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(mContext)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, false)
|
||||||
|
.commit();
|
||||||
|
|
||||||
|
Intent intent = new Intent(new Intent(mContext, AppConfigReceiver.class));
|
||||||
|
intent.setAction(AppConfigReceiver.ACTION_START);
|
||||||
|
|
||||||
|
mReceiver.onReceive(mContext, intent);
|
||||||
|
assertEquals("Start SyncthingService Foreround", 1, mContext.getReceivedIntents().size());
|
||||||
|
assertEquals("Start SyncthingService Foreround", SyncthingService.class.getName(),
|
||||||
|
mContext.getReceivedIntents().get(0).getComponent().getClassName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test stopping the Syncthing-Service if "always run in background" is disabled
|
||||||
|
* In this case stopping the service is allowed
|
||||||
|
*/
|
||||||
|
public void testStopSyncthingServiceForeground() {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(mContext)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, false)
|
||||||
|
.commit();
|
||||||
|
|
||||||
|
Intent intent = new Intent(new Intent(mContext, AppConfigReceiver.class));
|
||||||
|
intent.setAction(AppConfigReceiver.ACTION_STOP);
|
||||||
|
|
||||||
|
mReceiver.onReceive(mContext, intent);
|
||||||
|
assertEquals("Stop SyncthingService Foreround", 1, mContext.getStopServiceIntents().size());
|
||||||
|
Intent receivedIntent = mContext.getStopServiceIntents().get(0);
|
||||||
|
assertEquals("Stop SyncthingService Foreround", SyncthingService.class.getName(),
|
||||||
|
receivedIntent.getComponent().getClassName());
|
||||||
|
}
|
||||||
|
}
|
|
@ -91,6 +91,12 @@
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<receiver android:name=".syncthing.AppConfigReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.nutomic.syncthingandroid.action.START" />
|
||||||
|
<action android:name="com.nutomic.syncthingandroid.action.STOP" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -33,14 +33,20 @@ public abstract class SyncthingActivity extends ToolbarBindingActivity implement
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
startService(new Intent(this, SyncthingService.class));
|
startService(new Intent(this, SyncthingService.class));
|
||||||
bindService(new Intent(this, SyncthingService.class),
|
|
||||||
this, Context.BIND_AUTO_CREATE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onPause() {
|
||||||
super.onDestroy();
|
|
||||||
unbindService(this);
|
unbindService(this);
|
||||||
|
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
bindService(new Intent(this, SyncthingService.class), this, Context.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
package com.nutomic.syncthingandroid.syncthing;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.v4.app.NotificationCompat;
|
||||||
|
|
||||||
|
import com.nutomic.syncthingandroid.R;
|
||||||
|
import com.nutomic.syncthingandroid.activities.MainActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast-receiver to control and configure SyncThing remotely
|
||||||
|
*
|
||||||
|
* Created by sqrt-1764 on 25.03.16.
|
||||||
|
*/
|
||||||
|
public class AppConfigReceiver extends BroadcastReceiver {
|
||||||
|
private static final int ID_NOTIFICATION_BACKGROUND_ACTIVE = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the Syncthing-Service
|
||||||
|
*/
|
||||||
|
public static final String ACTION_START = "com.nutomic.syncthingandroid.action.START";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the Syncthing-Service
|
||||||
|
* If alwaysRunInBackground is enabled the service must not be stopped. Instead a
|
||||||
|
* notification is presented to the user.
|
||||||
|
*/
|
||||||
|
public static final String ACTION_STOP = "com.nutomic.syncthingandroid.action.STOP";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
switch (intent.getAction()) {
|
||||||
|
case ACTION_START:
|
||||||
|
context.startService(new Intent(context, SyncthingService.class));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ACTION_STOP:
|
||||||
|
if (SyncthingService.alwaysRunInBackground(context)) {
|
||||||
|
final String msg = context.getString(R.string.appconfig_receiver_background_enabled);
|
||||||
|
|
||||||
|
Context appContext = context.getApplicationContext();
|
||||||
|
|
||||||
|
NotificationCompat.Builder nb = new NotificationCompat.Builder(context)
|
||||||
|
.setContentText(msg)
|
||||||
|
.setTicker(msg)
|
||||||
|
.setStyle(new NotificationCompat.BigTextStyle().bigText(msg))
|
||||||
|
.setContentTitle(context.getText(context.getApplicationInfo().labelRes))
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_notify)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(PendingIntent.getActivity(appContext,
|
||||||
|
0,
|
||||||
|
new Intent(appContext, MainActivity.class),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT));
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
nb.setCategory(Notification.CATEGORY_ERROR); // Only supported in API 21 or better
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationManager nm =
|
||||||
|
(NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
|
nm.notify(ID_NOTIFICATION_BACKGROUND_ACTIVE, nb.build());
|
||||||
|
|
||||||
|
} else {
|
||||||
|
context.stopService(new Intent(context, SyncthingService.class));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,7 +59,6 @@ public class SyncthingService extends Service implements
|
||||||
public static final String ACTION_RESET =
|
public static final String ACTION_RESET =
|
||||||
"com.nutomic.syncthingandroid.service.SyncthingService.RESET";
|
"com.nutomic.syncthingandroid.service.SyncthingService.RESET";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interval in ms at which the GUI is updated (eg {@link com.nutomic.syncthingandroid.fragments.DrawerFragment}).
|
* Interval in ms at which the GUI is updated (eg {@link com.nutomic.syncthingandroid.fragments.DrawerFragment}).
|
||||||
*/
|
*/
|
||||||
|
@ -144,6 +143,12 @@ public class SyncthingService extends Service implements
|
||||||
|
|
||||||
private State mCurrentState = State.INIT;
|
private State mCurrentState = State.INIT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object that can be locked upon when accessing mCurrentState
|
||||||
|
* Currently used to male onDestroy() and PollWebGuiAvailableTaskImpl.onPostExcecute() tread-safe
|
||||||
|
*/
|
||||||
|
private final Object stateLock = new Object();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if a stop was requested while syncthing is starting, in that case, perform stop in
|
* True if a stop was requested while syncthing is starting, in that case, perform stop in
|
||||||
* {@link PollWebGuiAvailableTaskImpl}.
|
* {@link PollWebGuiAvailableTaskImpl}.
|
||||||
|
@ -227,10 +232,8 @@ public class SyncthingService extends Service implements
|
||||||
if (mConfig != null) {
|
if (mConfig != null) {
|
||||||
mCurrentState = State.STARTING;
|
mCurrentState = State.STARTING;
|
||||||
|
|
||||||
if (mApi != null) {
|
if (mApi != null)
|
||||||
registerOnWebGuiAvailableListener(mApi);
|
registerOnWebGuiAvailableListener(mApi);
|
||||||
mApi.setWebGuiUrl(mConfig.getWebGuiUrl());
|
|
||||||
}
|
|
||||||
if (mEventProcessor != null)
|
if (mEventProcessor != null)
|
||||||
registerOnWebGuiAvailableListener(mEventProcessor);
|
registerOnWebGuiAvailableListener(mEventProcessor);
|
||||||
new PollWebGuiAvailableTaskImpl(getFilesDir() + "/" + HTTPS_CERT_FILE)
|
new PollWebGuiAvailableTaskImpl(getFilesDir() + "/" + HTTPS_CERT_FILE)
|
||||||
|
@ -426,12 +429,25 @@ public class SyncthingService extends Service implements
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the native binary.
|
* Stops the native binary.
|
||||||
|
*
|
||||||
|
* The native binary crashes if stopped before it is fully active. In that case signal the
|
||||||
|
* stop request to PollWebGuiAvailableTaskImpl that is active in that situation and terminate
|
||||||
|
* the service there.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
|
||||||
Log.i(TAG, "Shutting down service");
|
synchronized (stateLock) {
|
||||||
shutdown();
|
if (mCurrentState == State.INIT || mCurrentState == State.STARTING) {
|
||||||
|
Log.i(TAG, "Delay shutting down service until initialisation of Syncthing finished");
|
||||||
|
mStopScheduled = true;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Shutting down service immediately");
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
|
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
sp.unregisterOnSharedPreferenceChangeListener(this);
|
sp.unregisterOnSharedPreferenceChangeListener(this);
|
||||||
}
|
}
|
||||||
|
@ -512,14 +528,24 @@ public class SyncthingService extends Service implements
|
||||||
super(httpsCertPath);
|
super(httpsCertPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the web-gui of the native syncthing binary to come online.
|
||||||
|
*
|
||||||
|
* In case the binary is to be stopped, also be aware that another thread could request
|
||||||
|
* to stop the binary in the time while waiting for the GUI to become active. See the comment
|
||||||
|
* for SyncthingService.onDestroy for details.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Void aVoid) {
|
protected void onPostExecute(Void aVoid) {
|
||||||
if (mStopScheduled) {
|
synchronized (stateLock) {
|
||||||
mCurrentState = State.DISABLED;
|
if (mStopScheduled) {
|
||||||
onApiChange();
|
mCurrentState = State.DISABLED;
|
||||||
shutdown();
|
onApiChange();
|
||||||
mStopScheduled = false;
|
shutdown();
|
||||||
return;
|
mStopScheduled = false;
|
||||||
|
stopSelf();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Web GUI has come online at " + mConfig.getWebGuiUrl());
|
Log.i(TAG, "Web GUI has come online at " + mConfig.getWebGuiUrl());
|
||||||
mCurrentState = State.STARTING;
|
mCurrentState = State.STARTING;
|
||||||
|
|
|
@ -468,4 +468,5 @@ Please report any problems you encounter via Github.</string>
|
||||||
<!-- Format string for folder size, eg "500 MiB / 1 GiB" -->
|
<!-- Format string for folder size, eg "500 MiB / 1 GiB" -->
|
||||||
<string name="folder_size_format">%1$s / %2$s</string>
|
<string name="folder_size_format">%1$s / %2$s</string>
|
||||||
|
|
||||||
|
<string name="appconfig_receiver_background_enabled">Stopping Syncthing is not supported when running in background is enabled.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue