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

Added functionality to manually stop sync (fixes #131).

This functionality can not be used together with wifi/charging settings.

By default, this is set so that syncthing is only active when requested,
and can be stopped by the user.
This commit is contained in:
Felix Ableitner 2014-10-01 13:01:47 +03:00
parent e76f354ddd
commit f383393ce4
16 changed files with 224 additions and 35 deletions

View file

@ -1,6 +1,7 @@
package com.nutomic.syncthingandroid.test.syncthing;
import android.content.Intent;
import android.preference.PreferenceManager;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
@ -18,11 +19,21 @@ public class BatteryReceiverTest extends AndroidTestCase {
protected void setUp() throws Exception {
super.setUp();
mReceiver = new BatteryReceiver();
mContext = new MockContext(null);
mContext = new MockContext(getContext());
}
@Override
protected void tearDown() throws Exception {
PreferenceManager.getDefaultSharedPreferences(mContext).edit().clear().commit();
super.tearDown();
}
@MediumTest
public void testOnReceiveCharging() {
PreferenceManager.getDefaultSharedPreferences(mContext)
.edit()
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, true)
.commit();
Intent intent = new Intent(Intent.ACTION_POWER_CONNECTED);
mReceiver.onReceive(mContext, intent);
@ -35,6 +46,10 @@ public class BatteryReceiverTest extends AndroidTestCase {
@MediumTest
public void testOnReceiveNotCharging() {
PreferenceManager.getDefaultSharedPreferences(mContext)
.edit()
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, true)
.commit();
Intent intent = new Intent(Intent.ACTION_POWER_DISCONNECTED);
mReceiver.onReceive(mContext, intent);
@ -46,4 +61,16 @@ public class BatteryReceiverTest extends AndroidTestCase {
mContext.clearReceivedIntents();
}
@MediumTest
public void testOnlyRunInForeground() {
PreferenceManager.getDefaultSharedPreferences(getContext())
.edit()
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, false)
.commit();
mReceiver.onReceive(mContext, new Intent(Intent.ACTION_POWER_CONNECTED));
assertEquals(0, mContext.getReceivedIntents().size());
mReceiver.onReceive(mContext, new Intent(Intent.ACTION_POWER_DISCONNECTED));
assertEquals(0, mContext.getReceivedIntents().size());
}
}

View file

@ -1,6 +1,7 @@
package com.nutomic.syncthingandroid.test.syncthing;
import android.content.Intent;
import android.preference.PreferenceManager;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
@ -21,11 +22,21 @@ public class BootReceiverTest extends AndroidTestCase {
protected void setUp() throws Exception {
super.setUp();
mReceiver = new BootReceiver();
mContext = new MockContext(null);
mContext = new MockContext(getContext());
}
@Override
protected void tearDown() throws Exception {
PreferenceManager.getDefaultSharedPreferences(mContext).edit().clear().commit();
super.tearDown();
}
@MediumTest
public void testOnReceiveCharging() {
PreferenceManager.getDefaultSharedPreferences(mContext)
.edit()
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, true)
.commit();
mReceiver.onReceive(mContext, null);
assertEquals(1, mContext.getReceivedIntents().size());
@ -33,4 +44,14 @@ public class BootReceiverTest extends AndroidTestCase {
assertEquals(SyncthingService.class.getName(), receivedIntent.getComponent().getClassName());
mContext.clearReceivedIntents();
}
public void testOnlyRunInForeground() {
PreferenceManager.getDefaultSharedPreferences(getContext())
.edit()
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, false)
.commit();
mReceiver.onReceive(mContext, null);
assertEquals(0, mContext.getReceivedIntents().size());
}
}

View file

@ -1,6 +1,7 @@
package com.nutomic.syncthingandroid.test.syncthing;
import android.content.Intent;
import android.preference.PreferenceManager;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
@ -28,8 +29,18 @@ public class NetworkReceiverTest extends AndroidTestCase {
mContext = new MockContext(getContext());
}
@Override
protected void tearDown() throws Exception {
PreferenceManager.getDefaultSharedPreferences(mContext).edit().clear().commit();
super.tearDown();
}
@MediumTest
public void testOnReceive() {
PreferenceManager.getDefaultSharedPreferences(mContext)
.edit()
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, true)
.commit();
mReceiver.onReceive(mContext, null);
assertEquals(1, mContext.getReceivedIntents().size());
@ -40,4 +51,14 @@ public class NetworkReceiverTest extends AndroidTestCase {
mContext.clearReceivedIntents();
}
@MediumTest
public void testOnlyRunInForeground() {
PreferenceManager.getDefaultSharedPreferences(getContext())
.edit()
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, false)
.commit();
mReceiver.onReceive(mContext, null);
assertEquals(0, mContext.getReceivedIntents().size());
}
}

View file

@ -8,6 +8,10 @@ import com.nutomic.syncthingandroid.test.MockContext;
import java.io.File;
/**
* NOTE: This test will cause a "syncthing binary crashed" notification, because
* {@code -home " + mContext.getFilesDir()} is run as a "command" and fails.
*/
public class SyncthingRunnableTest extends AndroidTestCase {
@SmallTest

View file

@ -7,6 +7,7 @@ import android.test.ServiceTestCase;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Pair;
import com.nutomic.syncthingandroid.syncthing.DeviceStateHolder;
import com.nutomic.syncthingandroid.syncthing.SyncthingService;
@ -16,19 +17,27 @@ import com.nutomic.syncthingandroid.test.Util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* FIXME: There are some problems with shutting down the service after tests. It may be that the
* service remains running after short tests. As a workaround, kill the app in Android.
* NOTE: It seems that @link #tearDown()} is not executed if a test fails, so the test data folder
* is not deleted (which may cause following tests to fail).
* These tests assume that syncthing keys have already been generated. If not, tests may fail
* because startup takes too long.
*
* FIXME: These tests are rather fragile and may fail even if they shouldn't. Repeating them
* should fix this.
* NOTE: If a test fails with "expected:<ACTIVE> but was:<INIT>", you may have to increase
* {@link #STARTUP_TIME_SECONDS}.
*/
public class SyncthingServiceTest extends ServiceTestCase<SyncthingService> {
private static final int STARTUP_TIME_SECONDS = 90;
private Context mContext;
private CountDownLatch mLatch;
public SyncthingServiceTest() {
super(SyncthingService.class);
}
@ -37,19 +46,18 @@ public class SyncthingServiceTest extends ServiceTestCase<SyncthingService> {
protected void setUp() throws Exception {
super.setUp();
mContext = new MockContext(getContext());
setContext(mContext);
}
@Override
protected void tearDown() throws Exception {
Util.deleteRecursive(getContext().getFilesDir());
Util.deleteRecursive(mContext.getFilesDir());
PreferenceManager.getDefaultSharedPreferences(getContext()).edit().clear().commit();
super.tearDown();
}
@LargeTest
public void testStartService() throws InterruptedException {
startService(new Intent(mContext, SyncthingService.class));
startService(new Intent(getContext(), SyncthingService.class));
final CountDownLatch latch = new CountDownLatch(2);
getService().registerOnWebGuiAvailableListener(new SyncthingService.OnWebGuiAvailableListener() {
@Override
@ -70,12 +78,14 @@ public class SyncthingServiceTest extends ServiceTestCase<SyncthingService> {
@SmallTest
public void testFirstStart() {
setContext(mContext);
startService(new Intent(mContext, SyncthingService.class));
assertTrue(getService().isFirstStart());
}
@MediumTest
public void testNotFirstStart() throws IOException {
setContext(mContext);
startService(new Intent(mContext, SyncthingService.class));
new File(mContext.getFilesDir(), SyncthingService.PUBLIC_KEY_FILE).createNewFile();
assertFalse(getService().isFirstStart());
@ -84,7 +94,7 @@ public class SyncthingServiceTest extends ServiceTestCase<SyncthingService> {
@SmallTest
public void testBindService() throws InterruptedException {
SyncthingServiceBinder binder = (SyncthingServiceBinder)
bindService(new Intent(mContext, SyncthingService.class));
bindService(new Intent(getContext(), SyncthingService.class));
SyncthingService service = binder.getService();
final CountDownLatch latch = new CountDownLatch(2);
getService().registerOnWebGuiAvailableListener(new SyncthingService.OnWebGuiAvailableListener() {
@ -109,6 +119,8 @@ public class SyncthingServiceTest extends ServiceTestCase<SyncthingService> {
@Override
public void onApiChange(SyncthingService.State currentState) {
mLatch.countDown();
mLastState = currentState;
}
@ -122,9 +134,10 @@ public class SyncthingServiceTest extends ServiceTestCase<SyncthingService> {
@MediumTest
public void testStatesAllRequired() throws InterruptedException {
setupStatesTest(true, true);
setupStatesTest(true, true, true);
assertState(true, true, SyncthingService.State.ACTIVE);
assertState(true, false, SyncthingService.State.DISABLED);
assertState(false, true, SyncthingService.State.DISABLED);
assertState(false, false, SyncthingService.State.DISABLED);
@ -132,27 +145,29 @@ public class SyncthingServiceTest extends ServiceTestCase<SyncthingService> {
@MediumTest
public void testStatesWifiRequired() throws InterruptedException {
setupStatesTest(true, false);
setupStatesTest(true, true, false);
assertState(true, true, SyncthingService.State.ACTIVE);
assertState(true, false, SyncthingService.State.DISABLED);
assertState(false, true, SyncthingService.State.ACTIVE);
assertState(true, false, SyncthingService.State.DISABLED);
assertState(false, false, SyncthingService.State.DISABLED);
}
@MediumTest
public void testStatesChargingRequired() throws InterruptedException {
setupStatesTest(false, true);
setupStatesTest(true, false, true);
assertState(true, true, SyncthingService.State.ACTIVE);
assertState(true, false, SyncthingService.State.ACTIVE);
assertState(false, true, SyncthingService.State.DISABLED);
assertState(false, false, SyncthingService.State.DISABLED);
}
@MediumTest
public void testStatesNoneRequired() throws InterruptedException {
setupStatesTest(false, false);
setupStatesTest(true, false, false);
assertState(true, true, SyncthingService.State.ACTIVE);
assertState(true, false, SyncthingService.State.ACTIVE);
@ -162,25 +177,55 @@ public class SyncthingServiceTest extends ServiceTestCase<SyncthingService> {
public void assertState(boolean charging, boolean wifi, SyncthingService.State expected)
throws InterruptedException {
Intent i = new Intent(mContext, SyncthingService.class);
Intent i = new Intent(getContext(), SyncthingService.class);
i.putExtra(DeviceStateHolder.EXTRA_IS_CHARGING, charging);
i.putExtra(DeviceStateHolder.EXTRA_HAS_WIFI, wifi);
mLatch = new CountDownLatch(1);
startService(i);
// Wait for service to react to preference change.
Thread.sleep(7500);
mLatch.await(1, TimeUnit.SECONDS);
assertEquals(expected, mListener.getLastState());
}
public void setupStatesTest(boolean syncOnlyWifi, boolean syncOnlyCharging)
throws InterruptedException {
PreferenceManager.getDefaultSharedPreferences(getContext()).edit()
public void setupStatesTest(boolean alwaysRunInBackground,
boolean syncOnlyWifi, boolean syncOnlyCharging) throws InterruptedException {
PreferenceManager.getDefaultSharedPreferences(getContext())
.edit()
.putBoolean(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND, alwaysRunInBackground)
.putBoolean(SyncthingService.PREF_SYNC_ONLY_WIFI, syncOnlyWifi)
.putBoolean(SyncthingService.PREF_SYNC_ONLY_CHARGING, syncOnlyCharging)
.commit();
// Wait for service to react to preference change.
Thread.sleep(1000);
startService(new Intent(getContext(), SyncthingService.class));
// 3 calls plus 1 call immediately when registering.
mLatch = new CountDownLatch(4);
getService().registerOnApiChangeListener(mListener);
assertEquals(SyncthingService.State.INIT, mListener.getLastState());
if (mListener.getLastState() != SyncthingService.State.ACTIVE) {
// Wait for service to start.
mLatch.await(STARTUP_TIME_SECONDS, TimeUnit.SECONDS);
assertEquals(SyncthingService.State.ACTIVE, mListener.getLastState());
}
}
/**
* For all possible settings and charging/wifi states, service should be active.
*/
@LargeTest
public void testOnlyForeground() throws InterruptedException {
ArrayList<Pair<Boolean, Boolean>> values = new ArrayList<>();
values.add(new Pair(true, true));
values.add(new Pair(true, false));
values.add(new Pair(false, true));
values.add(new Pair(false, false));
for (Pair<Boolean, Boolean> v : values) {
setupStatesTest(false, v.first, v.second);
assertState(true, true, SyncthingService.State.ACTIVE);
assertState(true, false, SyncthingService.State.ACTIVE);
assertState(false, true, SyncthingService.State.ACTIVE);
assertState(false, false, SyncthingService.State.ACTIVE);
}
}
}

View file

@ -247,6 +247,7 @@ public class MainActivity extends SyncthingActivity
public boolean onPrepareOptionsMenu(Menu menu) {
boolean drawerOpen = mDrawerLayout.isDrawerOpen(findViewById(R.id.drawer));
menu.findItem(R.id.share_node_id).setVisible(drawerOpen);
menu.findItem(R.id.exit).setVisible(!SyncthingService.alwaysRunInBackground(this));
return true;
}
@ -277,6 +278,9 @@ public class MainActivity extends SyncthingActivity
startActivity(new Intent(this, SettingsActivity.class)
.setAction(SettingsActivity.ACTION_APP_SETTINGS_FRAGMENT));
return true;
case R.id.exit:
stopService(new Intent(this, SyncthingService.class));
finish();
default:
return super.onOptionsItemSelected(item);
}

View file

@ -29,6 +29,8 @@ public class SettingsFragment extends PreferenceFragment
private static final String SYNCTHING_VERSION_KEY = "syncthing_version";
private CheckBoxPreference mAlwaysRunInBackground;
private CheckBoxPreference mSyncOnlyCharging;
private CheckBoxPreference mSyncOnlyWifi;
@ -96,18 +98,24 @@ public class SettingsFragment extends PreferenceFragment
addPreferencesFromResource(R.xml.app_settings);
PreferenceScreen screen = getPreferenceScreen();
mAlwaysRunInBackground = (CheckBoxPreference)
findPreference(SyncthingService.PREF_ALWAYS_RUN_IN_BACKGROUND);
mSyncOnlyCharging = (CheckBoxPreference)
findPreference(SyncthingService.PREF_SYNC_ONLY_CHARGING);
mSyncOnlyCharging.setOnPreferenceChangeListener(this);
mSyncOnlyWifi = (CheckBoxPreference) findPreference(SyncthingService.PREF_SYNC_ONLY_WIFI);
mVersion = screen.findPreference(SYNCTHING_VERSION_KEY);
mOptionsScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_OPTIONS_KEY);
mGuiScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_GUI_KEY);
mAlwaysRunInBackground.setOnPreferenceChangeListener(this);
mSyncOnlyCharging.setOnPreferenceChangeListener(this);
mSyncOnlyWifi.setOnPreferenceChangeListener(this);
// Force summary update and wifi/charging preferences enable/disable.
onPreferenceChange(mAlwaysRunInBackground, mAlwaysRunInBackground.isChecked());
Preference sttrace = findPreference("sttrace");
sttrace.setOnPreferenceChangeListener(this);
sttrace.setSummary(PreferenceManager
.getDefaultSharedPreferences(getActivity()).getString("sttrace", ""));
mVersion = screen.findPreference(SYNCTHING_VERSION_KEY);
mOptionsScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_OPTIONS_KEY);
mGuiScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_GUI_KEY);
}
@Override
@ -146,6 +154,13 @@ public class SettingsFragment extends PreferenceFragment
if (preference.equals(mSyncOnlyCharging) || preference.equals(mSyncOnlyWifi)) {
mSyncthingService.updateState();
} else if (preference.equals(mAlwaysRunInBackground)) {
preference.setSummary(((Boolean) o)
? R.string.always_run_in_background_enabled
: R.string.always_run_in_background_disabled);
mSyncOnlyCharging.setEnabled((Boolean) o);
mSyncOnlyWifi.setEnabled((Boolean) o);
} else if (mOptionsScreen.findPreference(preference.getKey()) != null) {
mSyncthingService.getApi().setValue(RestApi.TYPE_OPTIONS, preference.getKey(), o,
preference.getKey().equals("ListenAddress"), getActivity());

View file

@ -3,6 +3,8 @@ package com.nutomic.syncthingandroid.syncthing;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;
/**
@ -14,6 +16,9 @@ public class BatteryReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!SyncthingService.alwaysRunInBackground(context))
return;
boolean isCharging = Intent.ACTION_POWER_CONNECTED.equals(intent.getAction());
Log.v(TAG, "Received charger " + (isCharging ? "connected" : "disconnected") + " event");
Intent i = new Intent(context, SyncthingService.class);

View file

@ -3,11 +3,16 @@ package com.nutomic.syncthingandroid.syncthing;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!SyncthingService.alwaysRunInBackground(context))
return;
context.startService(new Intent(context, SyncthingService.class));
}

View file

@ -16,6 +16,9 @@ public class NetworkReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!SyncthingService.alwaysRunInBackground(context))
return;
ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo wifiInfo = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);

View file

@ -849,7 +849,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
normalized = json.optString("id", null);
error = json.optString("error", null);
} catch (JSONException e) {
Log.d(TAG, "Failed to parse normalized node ID JSON", e);
Log.w(TAG, "Failed to parse normalized node ID JSON", e);
}
listener.onNodeIdNormalized(normalized, error);
}

View file

@ -3,6 +3,7 @@ package com.nutomic.syncthingandroid.syncthing;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Service;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
@ -57,6 +58,8 @@ public class SyncthingService extends Service {
*/
public static final String BINARY_NAME = "lib/libsyncthing.so";
public static final String PREF_ALWAYS_RUN_IN_BACKGROUND = "always_run_in_background";
public static final String PREF_SYNC_ONLY_WIFI = "sync_only_wifi";
public static final String PREF_SYNC_ONLY_CHARGING = "sync_only_charging";
@ -141,13 +144,23 @@ public class SyncthingService extends Service {
* called.
*/
public void updateState() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean prefStopMobileData = prefs.getBoolean(PREF_SYNC_ONLY_WIFI, false);
boolean prefStopNotCharging = prefs.getBoolean(PREF_SYNC_ONLY_CHARGING, false);
boolean shouldRun;
if (!alwaysRunInBackground(this)) {
// Always run, ignoring wifi/charging state.
shouldRun = true;
}
else {
// Check wifi/charging state against preferences and start if ok.
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean prefStopMobileData = prefs.getBoolean(PREF_SYNC_ONLY_WIFI, false);
boolean prefStopNotCharging = prefs.getBoolean(PREF_SYNC_ONLY_CHARGING, false);
shouldRun = (mDeviceStateHolder.isCharging() || !prefStopNotCharging) &&
(mDeviceStateHolder.isWifiConnected() || !prefStopMobileData);
}
// Start syncthing.
if ((mDeviceStateHolder.isCharging() || !prefStopNotCharging) &&
(mDeviceStateHolder.isWifiConnected() || !prefStopMobileData)) {
if (shouldRun) {
if (mCurrentState == State.ACTIVE || mCurrentState == State.STARTING) {
mStopScheduled = false;
return;
@ -396,4 +409,12 @@ public class SyncthingService extends Service {
return mConfig.getWebGuiUrl();
}
/**
* Returns the value of "always_run_in_background" preference.
*/
public static boolean alwaysRunInBackground(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
return sp.getBoolean(PREF_ALWAYS_RUN_IN_BACKGROUND, false);
}
}

View file

@ -56,7 +56,7 @@ public class RepoObserver extends FileObserver {
}
});
mChilds = new ArrayList<>(directories.length);
mChilds = new ArrayList<>();
for (File f : directories) {
mChilds.add(new RepoObserver(mListener, mRepo, path + "/" + f.getName()));
}

View file

@ -28,4 +28,8 @@
android:id="@+id/settings"
android:title="@string/settings_title" />
<item
android:id="@+id/exit"
android:title="@string/exit" />
</menu>

View file

@ -199,6 +199,15 @@ Please report any problems you encounter via Github.</string>
<!-- Activity title -->
<string name="settings_title">Settings</string>
<!-- Preference title -->
<string name="always_run_in_background">Always run in background</string>
<!-- Preference summary in case it is enabled -->
<string name="always_run_in_background_enabled">Syncthing always runs in the background, according to preferences below.</string>
<!-- Preference summary in case it is disabled -->
<string name="always_run_in_background_disabled">Syncthing only runs when explicitly started, and can be stopped by menu button.</string>
<string name="sync_only_charging">Sync only when charging</string>
<string name="sync_only_wifi">Sync only on wifi</string>
@ -252,7 +261,7 @@ If this error persists, try restarting your device.</string>
<!-- Button text on the "syncthing disabled" dialog -->
<string name="syncthing_disabled_change_settings">Change Settings</string>
<!-- Button text on the "syncthing disabled" dialog -->
<!-- Button text on the "syncthing disabled" dialog, used as menu item to stop syncthing service if "always_run_in_background" is true -->
<string name="exit">Exit</string>
<!-- RestApi -->

View file

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference
android:key="always_run_in_background"
android:title="@string/always_run_in_background"
android:defaultValue="false" />
<CheckBoxPreference
android:key="sync_only_charging"
android:title="@string/sync_only_charging"