1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2024-12-01 16:51:16 +00:00

Fix service.startForeground on Android 8+ (fixes #18) (fixes #80) (#79)

* Add push APK to device after build script

* Fix typo in SyncthingService comment

* Refactor SettingsActivity - updatePersistentNotification

Call updatePersistentNotification after the settings screen was left and not when the user toggles the alwaysRunInBackgroundAsAService checkbox.

* Automatically start app if push-to-device is enabled

* Remove PREF_NOTIFICATION_TYPE

* Only push APK to device if build succeeded

* Fix notifications and foreground service start/stop

* Use startForegroundService on Android 8+

* Fix startActivity called from non-Activity context (fixes #80)

* Fix comment
This commit is contained in:
Catfriend1 2018-10-08 05:00:59 +02:00 committed by GitHub
parent cd3bca5141
commit c224bcb04a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 229 additions and 133 deletions

3
.gitignore vendored
View file

@ -44,3 +44,6 @@ syncthing/mingit.zip
# External build artifacts
ext/
# push-to-device after build marker file
\#enable_push_to_device

View file

@ -58,6 +58,11 @@ android {
signingConfig = signingConfigs.release.storeFile ? signingConfigs.release : null
}
debug {
gradle.buildFinished {
buildResult -> if (!buildResult.failure) {
pushToDevice.execute()
}
}
}
}
@ -82,3 +87,8 @@ task deleteUnsupportedPlayTranslations(type: Delete) {
delete 'src/main/play/nb'
delete 'src/main/play/en/'
}
task pushToDevice(type: Exec) {
executable = 'python'
args = ['-u', './push-to-device.py']
}

91
app/push-to-device.py Normal file
View file

@ -0,0 +1,91 @@
from __future__ import print_function
import os
import os.path
import sys
import subprocess
import platform
#
# Script Compatibility:
# - Python 2.7.15
# - Python 3.7.0
#
SUPPORTED_PYTHON_PLATFORMS = ['Windows', 'Linux', 'Darwin']
def fail(message, *args, **kwargs):
print((message % args).format(**kwargs))
sys.exit(1)
def which(program):
import os
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
if (sys.platform == 'win32'):
program += ".exe"
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
#
# Push APK to device.
#
if platform.system() not in SUPPORTED_PYTHON_PLATFORMS:
fail('Unsupported python platform %s. Supported platforms: %s', platform.system(),
', '.join(SUPPORTED_PYTHON_PLATFORMS))
print ('')
# Build FullFN of "app-debug.apk".
current_dir = os.path.dirname(os.path.realpath(__file__))
enable_push_to_device = os.path.realpath(os.path.join(current_dir, "..", "#enable_push_to_device"))
# Check if push to device is enabled.
if not enable_push_to_device or not os.path.isfile(enable_push_to_device):
print('[INFO] push-to-device after build is DISABLED. To enable it, create the file \'' + enable_push_to_device + '\'')
sys.exit(0)
debug_apk = os.path.realpath(os.path.join(current_dir, 'build', 'outputs', 'apk', 'debug', 'app-debug.apk'))
if not debug_apk or not os.path.isfile(debug_apk):
fail('[ERROR] app-debug.apk not found.');
print('[INFO] debug_apk=' + debug_apk)
# Check if adb is available.
adb_bin = which("adb");
if not adb_bin:
print('[WARNING] adb is not available on the PATH.')
# install_adb();
# Retry: Check if adb is available.
# adb_bin = which("adb");
# if not adb_bin:
# fail('[ERROR] adb is not available on the PATH.')
print('[INFO] adb_bin=\'' + adb_bin + '\'')
print('[INFO] Connecting to attached usb device ...')
try:
subprocess.check_call([
adb_bin,
'devices'
])
except:
sys.exit(0)
print('[INFO] Installing APK to attached usb device ...')
try:
subprocess.check_call(adb_bin + ' install -r --user 0 ' + debug_apk)
except:
sys.exit(0)
print('[INFO] Starting app ...')
try:
subprocess.check_call(adb_bin + ' shell monkey -p com.github.catfriend1.syncthingandroid.debug 1')
except:
sys.exit(0)

View file

@ -196,7 +196,12 @@ public class MainActivity extends SyncthingActivity
// 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;
@ -150,7 +150,6 @@ public class SettingsActivity extends SyncthingActivity {
super.onCreate(savedInstanceState);
((SyncthingApp) getActivity().getApplication()).component().inject(this);
((SyncthingActivity) getActivity()).registerOnServiceConnectedListener(this);
mPreferences.registerOnSharedPreferenceChangeListener(this);
}
/**
@ -164,8 +163,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 =
@ -194,10 +193,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");
@ -348,7 +343,6 @@ public class SettingsActivity extends SyncthingActivity {
@Override
public void onDestroy() {
mPreferences.unregisterOnSharedPreferenceChangeListener(this);
if (mSyncthingService != null) {
mSyncthingService.unregisterOnServiceStateChangeListener(this);
}
@ -457,15 +451,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();
}
}
@ -631,21 +626,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

@ -148,7 +148,12 @@ public class WebGuiActivity extends SyncthingActivity
// 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

@ -174,7 +174,7 @@ public class DrawerFragment extends Fragment implements SyncthingService.OnServi
mActivity.closeDrawer();
break;
case R.id.drawerActionExit:
if (sharedPreferences != null && sharedPreferences.getBoolean(Constants.PREF_ALWAYS_RUN_IN_BACKGROUND, false)) {
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.

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";
@ -24,7 +24,6 @@ public class Constants {
// Preferences - Behaviour
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,100 @@ 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(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, String extraInfo) {
@ -278,9 +296,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

@ -219,6 +219,10 @@ public class SyncthingService extends Service {
mStoragePermissionGranted = (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED);
if (mNotificationHandler != null) {
mNotificationHandler.setAppShutdownInProgress(false);
}
}
/**
@ -505,6 +509,9 @@ public class SyncthingService extends Service {
*/
mRunConditionMonitor.shutdown();
}
if (mNotificationHandler != null) {
mNotificationHandler.setAppShutdownInProgress(true);
}
if (mStoragePermissionGranted) {
synchronized (mStateLock) {
if (mCurrentState == State.STARTING) {
@ -559,10 +566,6 @@ public class SyncthingService extends Service {
mApi = null;
}
if (mNotificationHandler != null) {
mNotificationHandler.cancelPersistentNotification(this);
}
if (mSyncthingRunnable != null) {
mSyncthingRunnable.killSyncthing();
if (mSyncthingRunnableThread != null) {
@ -621,7 +624,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);
@ -766,6 +769,7 @@ public class SyncthingService extends Service {
switch (prefKey) {
// Preferences that are no longer used and left-overs from previous versions of the app.
case "first_start":
case "notification_type":
case "notify_crashes":
Log.v(TAG, "importConfig: Ignoring deprecated pref \"" + prefKey + "\".");
break;

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

@ -79,14 +79,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"