启动和移除
启动
通常在Service的onStartCommand()方法中回调startForeground()方法,避免10s超时发生ANR。
Context context = getApplicationContext();
Intent intent = new Intent(...);
context.startForegroundService(intent);
startForeground()方法中需要传入唯一标识通知的正整数id,跟通知本身
Intent notificationIntent = new Intent(this, ExampleActivity.class);
PendingIntent pendingIntent =
PendingIntent.getActivity(this, 0, notificationIntent, 0);
Notification notification =
new Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE)
.setContentTitle(getText(R.string.notification_title))
.setContentText(getText(R.string.notification_message))
.setSmallIcon(R.drawable.icon)
.setContentIntent(pendingIntent)
.setTicker(getText(R.string.ticker_text))
.build();
startForeground(ONGOING_NOTIFICATION_ID, notification);
移除
调用stopForeground()将Service从前台状态移除,Service本身仍然继续运行;int值参数表明是否移除与该前台服务有关的通知。 如果在前台服务运行时停止该服务,则与之相关的通知会被移除。
通知
android 12+通知延迟10s显示
https://developer.android.com/guide/components/foreground-services#notification-immediate android 12+系统会等待10s才显示跟前台Service有关的通知(为短期运行的FGS提供优化体验),以下情况可以豁免(即立刻显示通知):
- The service is associated with a notification that includes action buttons.
- The service has a foregroundServiceType of mediaPlayback, mediaProjection, or phoneCall.
- The service provides a use case related to phone calls, navigation, or media playback, as defined in the notification’s category attribute.
- Service在配置通知时调用setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
static final int FGS_IMMEDIATE_DISPLAY_MASK =
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
| ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
| ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
| ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION;
public boolean shouldShowForegroundImmediately() {
if (mFgsDeferBehavior == FOREGROUND_SERVICE_IMMEDIATE) {
return true;
}
if (mFgsDeferBehavior == FOREGROUND_SERVICE_DEFERRED) {
return false;
}
if (isMediaNotification() || hasMediaSession()
|| CATEGORY_CALL.equals(category)
|| CATEGORY_NAVIGATION.equals(category)
|| (actions != null && actions.length > 0)) {
return true;
}
return false;
}
通知入队时确定是否延迟显示,延迟显示再更新下Service的通知
android 13+通知运行时权限
https://developer.android.com/about/versions/13/changes/notification-permission Android 13 中引入了新的运行时权限,用于从应用发送非豁免通知:POST_NOTIFICATIONS。此更改有助于用户专注于最重要的通知。
权限
android 9+前台Service普通权限
https://developer.android.com/guide/components/foreground-services#request-foreground-service-permissions 未在功能请单声明权限的话,启动时会抛出安全异常:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application ...>
...
</application>
</manifest>
android 13+通知运行时权限
https://developer.android.com/about/versions/13/changes/notification-permission
<manifest ...>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application ...>
...
</application>
</manifest>
限制
android 11+ while-in-use权限
https://developer.android.com/guide/components/foreground-services#bg-access-restrictions 为了保护用户隐私,当前台Service在后台运行时,有以下限制:
boolean mAllowWhileInUsePermissionInFgs;
不满足豁免条件下,系统会有如下log打印:
Foreground service started from background can not have location/camera/microphone access: service SERVICE_NAME
豁免:
- The service is started by a system component.
- The service is started by interacting with app widgets.
- The service is started by interacting with a notification.
- The service is started as a PendingIntent that is sent from a different, visible app.
- The service is started by an app that is a device policy controller that is running in device owner mode.
- The service is started by an app which provides the VoiceInteractionService.
- The service is started by an app that has the START_ACTIVITIES_FROM_BACKGROUND privileged permission.
https://www.yuque.com/amytan-l2rhk/qstog7/ryb5tq/
private @ReasonCode int shouldAllowFgsWhileInUsePermissionLocked(String callingPackage,
int callingPid, int callingUid, @Nullable ServiceRecord targetService,
boolean allowBackgroundActivityStarts) {
int ret = REASON_DENIED;
final int uidState = mAm.getUidStateLocked(callingUid);
if (ret == REASON_DENIED) {
if (uidState <= PROCESS_STATE_TOP) {
ret = getReasonCodeFromProcState(uidState);
}
}
if (ret == REASON_DENIED) {
final boolean isCallingUidVisible = mAm.mAtmInternal.isUidForeground(callingUid);
if (isCallingUidVisible) {
ret = REASON_UID_VISIBLE;
}
}
if (ret == REASON_DENIED) {
if (allowBackgroundActivityStarts) {
ret = REASON_START_ACTIVITY_FLAG;
}
}
if (ret == REASON_DENIED) {
boolean isCallerSystem = false;
final int callingAppId = UserHandle.getAppId(callingUid);
switch (callingAppId) {
case ROOT_UID:
case SYSTEM_UID:
case NFC_UID:
case SHELL_UID:
isCallerSystem = true;
break;
default:
isCallerSystem = false;
break;
}
if (isCallerSystem) {
ret = REASON_SYSTEM_UID;
}
}
if (ret == REASON_DENIED) {
final Integer allowedType = mAm.mProcessList.searchEachLruProcessesLOSP(false, pr -> {
if (pr.uid == callingUid) {
if (pr.getWindowProcessController().areBackgroundFgsStartsAllowed()) {
return REASON_ACTIVITY_STARTER;
}
}
return null;
});
if (allowedType != null) {
ret = allowedType;
}
}
if (ret == REASON_DENIED) {
if (mAm.mInternal.isTempAllowlistedForFgsWhileInUse(callingUid)) {
return REASON_TEMP_ALLOWED_WHILE_IN_USE;
}
}
if (ret == REASON_DENIED) {
if (targetService != null && targetService.app != null) {
ActiveInstrumentation instr = targetService.app.getActiveInstrumentation();
if (instr != null && instr.mHasBackgroundActivityStartsPermission) {
ret = REASON_INSTR_BACKGROUND_ACTIVITY_PERMISSION;
}
}
}
if (ret == REASON_DENIED) {
if (mAm.checkPermission(START_ACTIVITIES_FROM_BACKGROUND, callingPid, callingUid)
== PERMISSION_GRANTED) {
ret = REASON_BACKGROUND_ACTIVITY_PERMISSION;
}
}
if (ret == REASON_DENIED) {
final boolean isAllowedPackage =
mAllowListWhileInUsePermissionInFgs.contains(callingPackage);
if (isAllowedPackage) {
ret = REASON_ALLOWLISTED_PACKAGE;
}
}
if (ret == REASON_DENIED) {
final boolean isDeviceOwner = mAm.mInternal.isDeviceOwner(callingUid);
if (isDeviceOwner) {
ret = REASON_DEVICE_OWNER;
}
}
return ret;
}
来自不同的处于可见app发送的PendingingIntent启动的Service,有效期为10s 4.The service is started as a PendingIntent that is sent from a different, visible app
android 12+后台启动限制
https://developer.android.com/guide/components/foreground-services#background-start-restrictions android 12+ app不能在后台启动前台Service,否则系统会抛出ForegroundServiceStartNotAllowedException. 但是如果一个app启动另一个app的FGS,这个限制只有在两个app的target sdk同时为android 12+时才生效。 检查应用是否有后台启动:
adb shell device_config put activity_manager \
default_fgs_starts_restriction_notification_enabled true
如果App有后台启动前台Service的行为,建议App更换使用WorkManager来代替FGS。
if (!ignoreForeground
&& !appIsTopLocked(r.appInfo.uid)
&& appRestrictedAnyInBackground(r.appInfo.uid, r.packageName)) {
Slog.w(TAG,
"Service.startForeground() not allowed due to bg restriction: service "
+ r.shortInstanceName);
updateServiceForegroundLocked(psr, false);
ignoreForeground = true;
}
豁免:
- Your app transitions from a user-visible state, such as an activity.
- Your app can start an activity from the background, except for the case where the app has an activity in the back stack of an existing task.
- Your app receives a high-priority message using Firebase Cloud Messaging.
Note: When your app is in the frequent bucket or a more restrictive bucket, your high-priority FCM messages might be downgraded to normal priority. If the message’s priority is downgraded, your app can’t start a foreground service. To check the priority of an FCM message that your app receives, call getPriority().
- The user performs an action on a UI element related to your app. For example, they might interact with a bubble, notification, widget, or activity.
- Your app invokes an exact alarm to complete an action that the user requests.
- Your app is the device’s current input method.
- Your app receives an event that’s related to geofencing or activity recognition transition.
- After the device reboots and receives the ACTION_BOOT_COMPLETED, ACTION_LOCKED_BOOT_COMPLETED, or ACTION_MY_PACKAGE_REPLACED intent action in a broadcast receiver.
- Your app receives the ACTION_TIMEZONE_CHANGED, ACTION_TIME_CHANGED, or ACTION_LOCALE_CHANGED intent action in a broadcast receiver.
- Your app receives a Bluetooth broadcast that requires the BLUETOOTH_CONNECT or BLUETOOTH_SCAN permissions.
- Apps with certain system roles or permission, such as device owners and profile owners.
- Your app uses the Companion Device Manager and declares the REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND permission or the REQUEST_COMPANION_RUN_IN_BACKGROUND permission. Whenever possible, use REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND.
- The user turns off battery optimizations for your app. You can help users find this option by sending them to your app’s App info page in system settings. To do so, invoke an intent that contains the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS intent action.
private @ReasonCode int shouldAllowFgsStartForegroundLocked(@ReasonCode int allowWhileInUse,
int callingPid, int callingUid, String callingPackage,
@Nullable ServiceRecord targetService) {
int ret = allowWhileInUse;
if (ret == REASON_DENIED) {
final int uidState = mAm.getUidStateLocked(callingUid);
if (uidState <= PROCESS_STATE_TOP) {
ret = getReasonCodeFromProcState(uidState);
}
}
if (ret == REASON_DENIED) {
final Integer allowedType = mAm.mProcessList.searchEachLruProcessesLOSP(false, app -> {
if (app.uid == callingUid) {
final ProcessStateRecord state = app.mState;
if (state.isAllowedStartFgsState()) {
return getReasonCodeFromProcState(state.getAllowStartFgsState());
} else {
final ActiveInstrumentation instr = app.getActiveInstrumentation();
if (instr != null
&& instr.mHasBackgroundForegroundServiceStartsPermission) {
return REASON_INSTR_BACKGROUND_FGS_PERMISSION;
}
final long lastInvisibleTime = app.mState.getLastInvisibleTime();
if (lastInvisibleTime > 0 && lastInvisibleTime < Long.MAX_VALUE) {
final long sinceLastInvisible = SystemClock.elapsedRealtime()
- lastInvisibleTime;
if (sinceLastInvisible < mAm.mConstants.mFgToBgFgsGraceDuration) {
return REASON_ACTIVITY_VISIBILITY_GRACE_PERIOD;
}
}
}
}
return null;
});
if (allowedType != null) {
ret = allowedType;
}
}
if (ret == REASON_DENIED) {
if (mAm.checkPermission(START_FOREGROUND_SERVICES_FROM_BACKGROUND, callingPid,
callingUid) == PERMISSION_GRANTED) {
ret = REASON_BACKGROUND_FGS_PERMISSION;
}
}
if (ret == REASON_DENIED) {
if (mAm.mAtmInternal.hasSystemAlertWindowPermission(callingUid, callingPid,
callingPackage)) {
ret = REASON_SYSTEM_ALERT_WINDOW_PERMISSION;
}
}
if (ret == REASON_DENIED) {
final boolean isCompanionApp = mAm.mInternal.isAssociatedCompanionApp(
UserHandle.getUserId(callingUid), callingUid);
if (isCompanionApp) {
if (isPermissionGranted(
REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND,
callingPid, callingUid)
|| isPermissionGranted(REQUEST_COMPANION_RUN_IN_BACKGROUND,
callingPid, callingUid)) {
ret = REASON_COMPANION_DEVICE_MANAGER;
}
}
}
if (ret == REASON_DENIED) {
ActivityManagerService.FgsTempAllowListItem item =
mAm.isAllowlistedForFgsStartLOSP(callingUid);
if (item != null) {
if (item == ActivityManagerService.FAKE_TEMP_ALLOW_LIST_ITEM) {
ret = REASON_SYSTEM_ALLOW_LISTED;
} else {
ret = item.mReasonCode;
}
}
}
if (ret == REASON_DENIED) {
if (UserManager.isDeviceInDemoMode(mAm.mContext)) {
ret = REASON_DEVICE_DEMO_MODE;
}
}
if (ret == REASON_DENIED) {
final boolean isProfileOwner = mAm.mInternal.isProfileOwner(callingUid);
if (isProfileOwner) {
ret = REASON_PROFILE_OWNER;
}
}
if (ret == REASON_DENIED) {
final AppOpsManager appOpsManager = mAm.getAppOpsManager();
if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN, callingUid,
callingPackage) == AppOpsManager.MODE_ALLOWED) {
ret = REASON_OP_ACTIVATE_VPN;
} else if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN,
callingUid, callingPackage) == AppOpsManager.MODE_ALLOWED) {
ret = REASON_OP_ACTIVATE_PLATFORM_VPN;
}
}
if (ret == REASON_DENIED) {
final String inputMethod =
Settings.Secure.getStringForUser(mAm.mContext.getContentResolver(),
Settings.Secure.DEFAULT_INPUT_METHOD,
UserHandle.getUserId(callingUid));
if (inputMethod != null) {
final ComponentName cn = ComponentName.unflattenFromString(inputMethod);
if (cn != null && cn.getPackageName().equals(callingPackage)) {
ret = REASON_CURRENT_INPUT_METHOD;
}
}
}
if (ret == REASON_DENIED) {
if (mAm.mConstants.mFgsAllowOptOut
&& targetService != null
&& targetService.appInfo.hasRequestForegroundServiceExemption()) {
ret = REASON_OPT_OUT_REQUESTED;
}
}
return ret;
}
声明前台Service类型
https://developer.android.com/guide/components/foreground-services#types
声明Service类型
android 10+新增location type android 11+新增camera 和 microphone
<manifest>
...
<service ...
android:foregroundServiceType="location|camera|microphone" />
</manifest>
Notification notification = ...;
Service.startForeground(notification,
FOREGROUND_SERVICE_TYPE_LOCATION | FOREGROUND_SERVICE_TYPE_CAMERA);
声明WorkerManager类型
https://developer.android.com/topic/libraries/architecture/workmanager/advanced/long-running#foreground-service-type 如果app有长时间运行的worker请求location, camera, or microphone,按照上面链接指定worker的类型。
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="location|microphone"
tools:node="merge" />
@NonNull
private ForegroundInfo createForegroundInfo(@NonNull String progress) {
Notification notification = ...;
return new ForegroundInfo(NOTIFICATION_ID, notification,
FOREGROUND_SERVICE_TYPE_LOCATION | FOREGROUND_SERVICE_TYPE_MICROPHONE);
}
|