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

Fix Syncthing getting killed by ActivityManager when started using Intent (fixes #1247)

This commit is contained in:
Catfriend1 2019-01-09 22:49:58 +01:00 committed by Audrius Butkevicius
parent f1e578e4fc
commit 346cc35237
12 changed files with 169 additions and 136 deletions

View file

@ -257,7 +257,12 @@ public class MainActivity extends StateDialogActivity
// SyncthingService needs to be started from this activity as the user
// can directly launch this activity from the recent activity switcher.
startService(new Intent(this, SyncthingService.class));
Intent serviceIntent = new Intent(this, SyncthingService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent);
} else {
startService(serviceIntent);
}
onNewIntent(getIntent());
}

View file

@ -87,7 +87,7 @@ public class SettingsActivity extends SyncthingActivity {
public static class SettingsFragment extends PreferenceFragment
implements SyncthingActivity.OnServiceConnectedListener,
SyncthingService.OnServiceStateChangeListener, Preference.OnPreferenceChangeListener,
Preference.OnPreferenceClickListener, SharedPreferences.OnSharedPreferenceChangeListener {
Preference.OnPreferenceClickListener {
private static final String TAG = "SettingsFragment";
private static final String KEY_EXPORT_CONFIG = "export_config";
@ -100,7 +100,7 @@ public class SettingsActivity extends SyncthingActivity {
@Inject SharedPreferences mPreferences;
private Preference mCategoryRunConditions;
private CheckBoxPreference mAlwaysRunInBackground;
private CheckBoxPreference mStartServiceOnBoot;
private ListPreference mPowerSource;
private CheckBoxPreference mRunOnMobileData;
private CheckBoxPreference mRunOnWifi;
@ -152,7 +152,6 @@ public class SettingsActivity extends SyncthingActivity {
super.onCreate(savedInstanceState);
((SyncthingApp) getActivity().getApplication()).component().inject(this);
((SyncthingActivity) getActivity()).registerOnServiceConnectedListener(this);
mPreferences.registerOnSharedPreferenceChangeListener(this);
}
/**
@ -166,8 +165,8 @@ public class SettingsActivity extends SyncthingActivity {
addPreferencesFromResource(R.xml.app_settings);
PreferenceScreen screen = getPreferenceScreen();
mAlwaysRunInBackground =
(CheckBoxPreference) findPreference(Constants.PREF_ALWAYS_RUN_IN_BACKGROUND);
mStartServiceOnBoot =
(CheckBoxPreference) findPreference(Constants.PREF_START_SERVICE_ON_BOOT);
mPowerSource =
(ListPreference) findPreference(Constants.PREF_POWER_SOURCE);
mRunOnMobileData =
@ -196,10 +195,6 @@ public class SettingsActivity extends SyncthingActivity {
});
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
categoryBehaviour.removePreference(findPreference(Constants.PREF_NOTIFICATION_TYPE));
}
mDeviceName = (EditTextPreference) findPreference("deviceName");
mListenAddresses = (EditTextPreference) findPreference("listenAddresses");
mMaxRecvKbps = (EditTextPreference) findPreference("maxRecvKbps");
@ -346,7 +341,6 @@ public class SettingsActivity extends SyncthingActivity {
@Override
public void onDestroy() {
mPreferences.unregisterOnSharedPreferenceChangeListener(this);
if (mSyncthingService != null) {
mSyncthingService.unregisterOnServiceStateChangeListener(this);
}
@ -455,15 +449,16 @@ public class SettingsActivity extends SyncthingActivity {
@Override
public void onStop() {
if (mPendingConfig) {
if (mSyncthingService != null && mApi != null &&
mSyncthingService.getCurrentState() != SyncthingService.State.DISABLED) {
mApi.saveConfigAndRestart();
mPendingConfig = false;
if (mSyncthingService != null) {
mNotificationHandler.updatePersistentNotification(mSyncthingService);
if (mPendingConfig) {
if (mApi != null &&
mSyncthingService.getCurrentState() != SyncthingService.State.DISABLED) {
mApi.saveConfigAndRestart();
mPendingConfig = false;
}
}
}
if (mPendingRunConditions) {
if (mSyncthingService != null) {
if (mPendingRunConditions) {
mSyncthingService.evaluateRunConditions();
}
}
@ -620,21 +615,6 @@ public class SettingsActivity extends SyncthingActivity {
}
}
/**
* Update notification after that preference changes. We can't use onPreferenceChange() as
* the preference value isn't persisted there, and the NotificationHandler accesses the
* preference directly.
*
* This function is called when the activity is opened, so we need to make sure the service
* is connected.
*/
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(Constants.PREF_NOTIFICATION_TYPE) && mSyncthingService != null) {
mNotificationHandler.updatePersistentNotification(mSyncthingService);
}
}
/**
* Enables or disables {@link #mUseRoot} preference depending whether root is available.
*/

View file

@ -145,7 +145,12 @@ public class WebGuiActivity extends StateDialogActivity
// SyncthingService needs to be started from this activity as the user
// can directly launch this activity from the recent activity switcher.
startService(new Intent(this, SyncthingService.class));
Intent serviceIntent = new Intent(this, SyncthingService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent);
} else {
startService(serviceIntent);
}
}
@Override

View file

@ -1,5 +1,6 @@
package com.nutomic.syncthingandroid.fragments;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
@ -53,6 +54,7 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
private Timer mTimer;
private MainActivity mActivity;
private SharedPreferences sharedPreferences = null;
public void onDrawerOpened() {
mTimer = new Timer();
@ -94,6 +96,9 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mActivity = (MainActivity) getActivity();
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mActivity);
mCpuUsage = view.findViewById(R.id.cpu_usage);
mRamUsage = view.findViewById(R.id.ram_usage);
mDownload = view.findViewById(R.id.download);
@ -246,8 +251,23 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
mActivity.closeDrawer();
break;
case R.id.drawerActionExit:
mActivity.stopService(new Intent(mActivity, SyncthingService.class));
mActivity.finish();
if (sharedPreferences != null && sharedPreferences.getBoolean(Constants.PREF_START_SERVICE_ON_BOOT, false)) {
/**
* App is running as a service. Show an explanation why exiting syncthing is an
* extraordinary request, then ask the user to confirm.
*/
AlertDialog mExitConfirmationDialog = new AlertDialog.Builder(mActivity)
.setTitle(R.string.dialog_exit_while_running_as_service_title)
.setMessage(R.string.dialog_exit_while_running_as_service_message)
.setPositiveButton(R.string.yes, (d, i) -> {
doExit();
})
.setNegativeButton(R.string.no, (d, i) -> {})
.show();
} else {
// App is not running as a service.
doExit();
}
mActivity.closeDrawer();
break;
case R.id.drawerActionShowQrCode:
@ -258,6 +278,15 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
private boolean alwaysRunInBackground() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
return sp.getBoolean(Constants.PREF_ALWAYS_RUN_IN_BACKGROUND, false);
return sp.getBoolean(Constants.PREF_START_SERVICE_ON_BOOT, false);
}
private void doExit() {
if (mActivity == null || mActivity.isFinishing()) {
return;
}
Log.i(TAG, "Exiting app on user request");
mActivity.stopService(new Intent(mActivity, SyncthingService.class));
mActivity.finish();
}
}

View file

@ -25,7 +25,7 @@ public class AppConfigReceiver extends BroadcastReceiver {
/**
* Stop the Syncthing-Service
* If alwaysRunInBackground is enabled the service must not be stopped. Instead a
* If startServiceOnBoot is enabled the service must not be stopped. Instead a
* notification is presented to the user.
*/
private static final String ACTION_STOP = "com.nutomic.syncthingandroid.action.STOP";
@ -40,7 +40,7 @@ public class AppConfigReceiver extends BroadcastReceiver {
BootReceiver.startServiceCompat(context);
break;
case ACTION_STOP:
if (alwaysRunInBackground(context)) {
if (startServiceOnBoot(context)) {
mNotificationHandler.showStopSyncthingWarningNotification();
} else {
context.stopService(new Intent(context, SyncthingService.class));
@ -49,8 +49,8 @@ public class AppConfigReceiver extends BroadcastReceiver {
}
}
private static boolean alwaysRunInBackground(Context context) {
private static boolean startServiceOnBoot(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
return sp.getBoolean(Constants.PREF_ALWAYS_RUN_IN_BACKGROUND, false);
return sp.getBoolean(Constants.PREF_START_SERVICE_ON_BOOT, false);
}
}

View file

@ -18,14 +18,14 @@ public class BootReceiver extends BroadcastReceiver {
!intent.getAction().equals(Intent.ACTION_MY_PACKAGE_REPLACED))
return;
if (!alwaysRunInBackground(context))
if (!startServiceOnBoot(context))
return;
startServiceCompat(context);
}
/**
* Workaround for starting service from background on Android 8.
* Workaround for starting service from background on Android 8+.
*
* https://stackoverflow.com/a/44505719/1837158
*/
@ -39,8 +39,8 @@ public class BootReceiver extends BroadcastReceiver {
}
}
private static boolean alwaysRunInBackground(Context context) {
private static boolean startServiceOnBoot(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
return sp.getBoolean(Constants.PREF_ALWAYS_RUN_IN_BACKGROUND, false);
return sp.getBoolean(Constants.PREF_START_SERVICE_ON_BOOT, false);
}
}

View file

@ -11,7 +11,7 @@ public class Constants {
public static final String FILENAME_SYNCTHING_BINARY = "libsyncthing.so";
// Preferences - Run conditions
public static final String PREF_ALWAYS_RUN_IN_BACKGROUND = "always_run_in_background";
public static final String PREF_START_SERVICE_ON_BOOT = "always_run_in_background";
public static final String PREF_RUN_ON_MOBILE_DATA = "run_on_mobile_data";
public static final String PREF_RUN_ON_WIFI = "run_on_wifi";
public static final String PREF_RUN_ON_METERED_WIFI = "run_on_metered_wifi";
@ -25,7 +25,6 @@ public class Constants {
public static final String PREF_FIRST_START = "first_start";
public static final String PREF_START_INTO_WEB_GUI = "start_into_web_gui";
public static final String PREF_USE_ROOT = "use_root";
public static final String PREF_NOTIFICATION_TYPE = "notification_type";
public static final String PREF_ENVIRONMENT_VARIABLES = "environment_variables";
public static final String PREF_DEBUG_FACILITIES_ENABLED = "debug_facilities_enabled";
public static final String PREF_USE_WAKE_LOCK = "wakelock_while_binary_running";

View file

@ -8,7 +8,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.StringRes;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
@ -43,6 +42,9 @@ public class NotificationHandler {
private final NotificationChannel mPersistentChannelWaiting;
private final NotificationChannel mInfoChannel;
private Boolean lastStartForegroundService = false;
private Boolean appShutdownInProgress = false;
public NotificationHandler(Context context) {
((SyncthingApp) context.getApplicationContext()).component().inject(this);
mContext = context;
@ -88,84 +90,102 @@ public class NotificationHandler {
}
/**
* Shows or hides the persistent notification based on running state and
* {@link Constants#PREF_NOTIFICATION_TYPE}.
* Shows, updates or hides the notification.
*/
public void updatePersistentNotification(SyncthingService service) {
String type = mPreferences.getString(Constants.PREF_NOTIFICATION_TYPE, "low_priority");
// Always use startForeground() if app is set to always run. This makes sure the app
// is not killed, and we don't miss wifi/charging events.
// On Android 8, this behaviour is mandatory to receive broadcasts.
// https://stackoverflow.com/a/44505719/1837158
boolean foreground = mPreferences.getBoolean(Constants.PREF_ALWAYS_RUN_IN_BACKGROUND, false);
// Foreground priority requires a notification so this ensures that we either have a
// "default" or "low_priority" notification, but not "none".
if ("none".equals(type) && foreground) {
type = "low_priority";
}
boolean startServiceOnBoot = mPreferences.getBoolean(Constants.PREF_START_SERVICE_ON_BOOT, false);
State currentServiceState = service.getCurrentState();
boolean syncthingRunning = currentServiceState == SyncthingService.State.ACTIVE ||
currentServiceState == SyncthingService.State.STARTING;
if (foreground || (syncthingRunning && !type.equals("none"))) {
int title = R.string.syncthing_terminated;
switch (currentServiceState) {
case ERROR:
case INIT:
break;
case DISABLED:
title = R.string.syncthing_disabled;
break;
case STARTING:
case ACTIVE:
title = R.string.syncthing_active;
break;
default:
break;
boolean startForegroundService = false;
if (!appShutdownInProgress) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
/**
* Android 7 and lower:
* The app may run in background and monitor run conditions even if it is not
* running as a foreground service. For that reason, we can use a normal
* notification if syncthing is DISABLED.
*/
startForegroundService = startServiceOnBoot || syncthingRunning;
} else {
/**
* Android 8+:
* Always use startForeground.
* This makes sure the app is not killed, and we don't miss run condition events.
* On Android 8+, this behaviour is mandatory to receive broadcasts.
* https://stackoverflow.com/a/44505719/1837158
* Foreground priority requires a notification so this ensures that we either have a
* "default" or "low_priority" notification, but not "none".
*/
startForegroundService = true;
}
}
/**
* We no longer need to launch FirstStartActivity instead of MainActivity as
* {@link SyncthingService#onStartCommand} will check for denied permissions.
*/
Intent intent = new Intent(mContext, MainActivity.class);
// Check if we have to stopForeground.
if (startForegroundService != lastStartForegroundService) {
if (!startForegroundService) {
Log.v(TAG, "Stopping foreground service");
service.stopForeground(false);
}
}
// Reason for two separate IDs: if one of the notification channels is hidden then
// the startForeground() below won't update the notification but use the old one
int idToShow = syncthingRunning ? ID_PERSISTENT : ID_PERSISTENT_WAITING;
int idToCancel = syncthingRunning ? ID_PERSISTENT_WAITING : ID_PERSISTENT;
NotificationChannel channel = syncthingRunning ? mPersistentChannel : mPersistentChannelWaiting;
NotificationCompat.Builder builder = getNotificationBuilder(channel)
.setContentTitle(mContext.getString(title))
.setSmallIcon(R.drawable.ic_stat_notify)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0));
if (type.equals("low_priority"))
builder.setPriority(NotificationCompat.PRIORITY_MIN);
// Prepare notification builder.
int title = R.string.syncthing_terminated;
switch (currentServiceState) {
case ERROR:
case INIT:
break;
case DISABLED:
title = R.string.syncthing_disabled;
break;
case STARTING:
title = R.string.syncthing_starting;
break;
case ACTIVE:
title = R.string.syncthing_active;
break;
default:
break;
}
if (foreground) {
/**
* Reason for two separate IDs: if one of the notification channels is hidden then
* the startForeground() below won't update the notification but use the old one.
*/
int idToShow = syncthingRunning ? ID_PERSISTENT : ID_PERSISTENT_WAITING;
int idToCancel = syncthingRunning ? ID_PERSISTENT_WAITING : ID_PERSISTENT;
Intent intent = new Intent(mContext, MainActivity.class);
NotificationChannel channel = syncthingRunning ? mPersistentChannel : mPersistentChannelWaiting;
NotificationCompat.Builder builder = getNotificationBuilder(channel)
.setContentTitle(mContext.getString(title))
.setSmallIcon(R.drawable.ic_stat_notify)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
if (!appShutdownInProgress) {
if (startForegroundService) {
Log.v(TAG, "Starting foreground service or updating notification");
service.startForeground(idToShow, builder.build());
} else {
service.stopForeground(false); // ensure no longer running with foreground priority
Log.v(TAG, "Updating notification");
mNotificationManager.notify(idToShow, builder.build());
}
mNotificationManager.cancel(idToCancel);
} else {
// ensure no longer running with foreground priority
cancelPersistentNotification(service);
mNotificationManager.cancel(idToShow);
}
mNotificationManager.cancel(idToCancel);
// Remember last notification visibility.
lastStartForegroundService = startForegroundService;
}
public void cancelPersistentNotification(SyncthingService service) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && alwaysRunInBackground())
return;
service.stopForeground(false);
mNotificationManager.cancel(ID_PERSISTENT);
mNotificationManager.cancel(ID_PERSISTENT_WAITING);
/**
* Called by {@link SyncthingService#onStart} {@link SyncthingService#onDestroy}
* to indicate app startup and shutdown.
*/
public void setAppShutdownInProgress(Boolean newValue) {
appShutdownInProgress = newValue;
}
public void showCrashedNotification(@StringRes int title, boolean force) {
@ -280,9 +300,4 @@ public class NotificationHandler {
}
mNotificationManager.notify(ID_STOP_BACKGROUND_WARNING, nb.build());
}
private boolean alwaysRunInBackground() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
return sp.getBoolean(Constants.PREF_ALWAYS_RUN_IN_BACKGROUND, false);
}
}

View file

@ -179,8 +179,12 @@ public class SyncthingService extends Service {
* We need to recheck if we still have the storage permission.
*/
mStoragePermissionGranted = (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED);
Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED);
if (mNotificationHandler != null) {
mNotificationHandler.setAppShutdownInProgress(false);
}
}
/**
@ -449,6 +453,9 @@ public class SyncthingService extends Service {
*/
mRunConditionMonitor.shutdown();
}
if (mNotificationHandler != null) {
mNotificationHandler.setAppShutdownInProgress(true);
}
if (mStoragePermissionGranted) {
synchronized (mStateLock) {
if (mCurrentState == State.STARTING) {
@ -494,10 +501,6 @@ public class SyncthingService extends Service {
mApi = null;
}
if (mNotificationHandler != null) {
mNotificationHandler.cancelPersistentNotification(this);
}
if (mSyncthingRunnable != null) {
mSyncthingRunnable.killSyncthing();
if (mSyncthingRunnableThread != null) {
@ -556,7 +559,7 @@ public class SyncthingService extends Service {
}
/**
* Called to notifiy listeners of an API change.
* Called to notify listeners of an API change.
*/
private void onServiceStateChange(State newState) {
Log.v(TAG, "onServiceStateChange: from " + mCurrentState + " to " + newState);

View file

@ -15,10 +15,4 @@
<item>battery_power</item>
</string-array>
<string-array name="notification_type_entry_values">
<item>normal</item>
<item>low_priority</item>
<item>none</item>
</string-array>
</resources>

View file

@ -43,6 +43,10 @@ Please report any problems you encounter via Github.</string>
<!-- Title of the exit app when running as a service confirmation dialog -->
<string name="dialog_exit_while_running_as_service_title">Confirm to quit app</string>
<string name="dialog_exit_while_running_as_service_message">For your consideration: You configured the app to start automatically on boot. Therefore it monitors run conditions and syncs at any time in the background when conditions match. You should only quit manually if you run into severe problems. Otherwise, disable \'Start automatically on boot \' in the settings. Would you like to quit now until the device rebooted?</string>
<!-- Title of the "add folder" menu action -->
<string name="add_folder">Add Folder</string>
@ -135,6 +139,13 @@ Please report any problems you encounter via Github.</string>
<!-- Title for current upload rate -->
<string name="upload_title">Upload</string>
<!-- StatusFragment -->
<string name="status_fragment_title">Status</string>
<string name="syncthing_starting">Syncthing is starting.</string>
<string name="syncthing_running">Syncthing is running.</string>
<string name="syncthing_not_running">Syncthing is not running.</string>
<string name="syncthing_has_crashed">Syncthing has crashed.</string>
<!-- DrawerFragment -->

View file

@ -73,14 +73,6 @@
android:summary="@string/advanced_folder_picker_summary"
android:defaultValue="false" />
<ListPreference
android:key="notification_type"
android:title="@string/notification_type_title"
android:entryValues="@array/notification_type_entry_values"
android:entries="@array/notification_type_entries"
android:summary="@string/notification_type_summary"
android:defaultValue="low_priority" />
<ListPreference
android:key="pref_current_language"
android:title="@string/preference_language_title"