After Android O, many background startup activities are restricted. For example, in Android O, services cannot be launched in the background. In Android10, even activities are restricted in the background. In the background startService limitation brief analysis of Android O, after the layer analysis of Android O, the background limits the scenario of starting Service. Generally speaking, the APP backs to the background (such as pressing the Home button) and becomes the background APP one minute later. Although the process is alive, However, you can no longer start a Service using startService, but there are no restrictions on sending notifications. You can start a Service using notifications. At this point, the Service is not used as a background startup. Why? Intuitively, notifications are user-aware interactions and should not count as background launches. StartService limits in Android10

This article is based on Android10-Release

The notification launches the Service with the PendingIntent

You can simulate a scenario where you send a notification, then kill the APP, and then start the Service with PendingIntent in the notification bar to see if a situation occurs where the Service is prohibited from starting in the background.

void notify() { NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID); builder.setContentIntent(PendingIntent.getService(this, (int) System.currentTimeMillis(), new Intent(this, BackGroundService.class), PendingIntent.FLAG_UPDATE_CURRENT)) .setContentText("content")...) NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, "Channel human readable title", NotificationManager.IMPORTANCE_DEFAULT); if (nm ! = null) { nm.createNotificationChannel(channel); } } nm.notify(1, builder.build()); }Copy the code

The actual result: the Service starts normally after the notification is clicked. So let’s do it step by step.

PendingIntent is an Intent that starts a Service with a PendingIntent Intent. The first step in starting a Service via notification is to get a PendingIntent to start a particular Service via pendingIntent.getService:

public static PendingIntent getService(Context context, int requestCode, @NonNull Intent intent, @Flags int flags) { return buildServicePendingIntent(context, requestCode, intent, flags, ActivityManager.INTENT_SENDER_SERVICE); } private static PendingIntent buildServicePendingIntent(Context context, int requestCode, Intent intent, int flags, int serviceKind) { String packageName = context.getPackageName(); String resolvedType = intent ! = null ? intent.resolveTypeIfNeeded( context.getContentResolver()) : null; try { intent.prepareToLeaveProcess(context); IIntentSender target = ActivityManager.getService().getIntentSender( serviceKind, packageName, null, null, requestCode, new Intent[] { intent }, resolvedType ! = null ? new String[] { resolvedType } : null, flags, null, context.getUserId()); return target ! = null ? new PendingIntent(target) : null; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); }}Copy the code

The PendingIntentRecord extends iIntentSender.Stub entity for the APP. The IIntentSender extends iIntentSender.stub entity. PendingIntentRecord can be regarded as the record of PendingIntent on AMS, and finally forms the corresponding two-way communication channel between the two. PendingIntent (” PendingIntent “, “sendAndReturnResult”, “endAndreTurnResult”, “endAndreTurnResult”, “endAndreTurnResult”)

public int sendAndReturnResult(Context context, int code, @Nullable Intent intent, @Nullable OnFinished onFinished, @Nullable Handler handler, @Nullable String requiredPermission, @Nullable Bundle options) throws CanceledException { try { String resolvedType = intent ! = null ? intent.resolveTypeIfNeeded(context.getContentResolver()) : null; return ActivityManager.getService().sendIntentSender( mTarget, mWhitelistToken, code, intent, resolvedType, onFinished ! = null ? new FinishedDispatcher(this, onFinished, handler) : null, requiredPermission, options); } catch (RemoteException e) { throw new CanceledException(e); }}Copy the code

Binder finally went to AMS end, found the corresponding PendingIntentRecord, entered its sendInner function, and used ActivityManager.INTENT_SENDER_SERVICE in the previous buildIntent. Enter the corresponding branch:

public int sendInner(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, IBinder resultTo, String resultWho, int requestCode, int flagsMask, int flagsValues, Bundle options) { if (whitelistDuration ! = null) { duration = whitelistDuration.get(whitelistToken); } <! --> int res = START_SUCCESS; try { <! - duration not null will perform tempWhitelistForPendingIntent added to white list -- - > if (duration! = null) { int procState = controller.mAmInternal.getUidProcessState(callingUid); <! --u0_a16 2102 1742 4104448 174924 0 0 S com.android.systemui ActivityManager.isProcStateBackground(procState)) { ... <! Duration Specifies the duration of the whitelist. This is when send notification Settings - > controller. MAmInternal. TempWhitelistForPendingIntent (callingPid callingUid, uid, duration, tag.toString()); } else { } } ... case ActivityManager.INTENT_SENDER_SERVICE: case ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE: try { controller.mAmInternal.startServiceInPackage(uid, finalIntent, resolvedType, key.type == ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE, key.packageName, userId, mAllowBgActivityStartsForServiceSender.contains(whitelistToken) || allowTrampoline); } catch (RuntimeException e) { ...Copy the code

Actually end up in the controller. MAmInternal. StartServiceInPackage, finally to AMS startServiceInPackage, the next process in Android startService limit analysis O the background analysis, Including detection of background restrictions, but there is one point that was not analyzed in the previous section,

 int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
       ...        
       
       // Is this app on the battery whitelist?
        if (isOnDeviceIdleWhitelistLocked(uid, /*allowExceptIdleToo=*/ false)) {
            return ActivityManager.APP_START_MODE_NORMAL;
        }

        // None of the service-policy criteria apply, so we apply the common criteria
        return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);
    }

 */
boolean isOnDeviceIdleWhitelistLocked(int uid, boolean allowExceptIdleToo) {
    final int appId = UserHandle.getAppId(uid);

    final int[] whitelist = allowExceptIdleToo
            ? mDeviceIdleExceptIdleWhitelist
            : mDeviceIdleWhitelist;

    return Arrays.binarySearch(whitelist, appId) >= 0
            || Arrays.binarySearch(mDeviceIdleTempWhitelist, appId) >= 0
            || mPendingTempWhitelist.indexOfKey(uid) >= 0;
}
Copy the code

** That is the mPendingTempWhitelist **. This is the key to notifying the Service to start without restriction.

MPendingTempWhitelist This is the time between the time the notification is clicked and the time it is actually started. If the notification is not started, the start is invalid. The valid lifetime is set when the notification is sent, and this time is only when the notification is sent, there is no other entry:

  /Users/XXX/server/notification/NotificationManagerService.java:
Copy the code
void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid, final int callingPid, final String tag, final int id, final Notification notification, int incomingUserId) { ... // Whitelist pending intents. if (notification.allPendingIntents ! = null) { final int intentCount = notification.allPendingIntents.size(); if (intentCount > 0) { final ActivityManagerInternal am = LocalServices .getService(ActivityManagerInternal.class); final long duration = LocalServices.getService( DeviceIdleController.LocalService.class).getNotificationWhitelistDuration(); for (int i = 0; i < intentCount; i++) { PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i); if (pendingIntent ! = null) { <! -- Part of the whitelist update mechanism, Only through the test can be added to the mPendingTempWhitelist white list -- - > am. SetPendingIntentWhitelistDuration (pendingIntent. GetTarget (), WHITELIST_TOKEN, duration); }}}}Copy the code

SetPendingIntentWhitelistDuration will update PendingIntentRecord whitelistDuration list, this list identifies the

public void setPendingIntentWhitelistDuration(IIntentSender target, IBinder whitelistToken, long duration) { synchronized (ActivityManagerService.this) { ((PendingIntentRecord) target).setWhitelistDurationLocked(whitelistToken, duration); } } void setWhitelistDurationLocked(IBinder whitelistToken, long duration) { if (duration > 0) { if (whitelistDuration == null) { whitelistDuration = new ArrayMap<>(); } <! --> whitelistDuration --> whitelistDuration. Put (whitelistToken, duration); }... }Copy the code

After the lifetime is set, the launch Service Intent is placed in mPendingTempWhitelist by clicking on it to avoid background detection. PendingIntent “send” is an Intent with the same effect as a normal Intent. It is also limited by the background launch.

Android10 background start Activity restriction (Android10 -release source code branch)

After Android10, the background definition of an Activity is stricter than that of a Service. The background definition of an Activity is stricter than that of a Service. The background definition of an Activity is stricter than that of a Service. The background of an Activity is more restrictive and intuitively understandable: no visible window can be counted as background, and the interval between them may be a few seconds at most, for example, we can see this effect after 10 seconds.

void delayStartActivity() {
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            Intent intent = new Intent(LabApplication.getContext(), MainActivity.class);
            startActivity(intent);
        }
    }, 1000 * 10);

}
Copy the code

When the time is up, startActivity will report the following exception on Android Q phones:

Background activity start [callingPackage: com.snail.labaffinity; callingUid: 10102; 
		
	* 			 isCallingUidForeground: false; 
	* 			 isCallingUidPersistentSystemProcess: false; 
	* 			 realCallingUid: 10102; 
	* 			 sRealCallingUidForeground: false; 
	* 			 isRealCallingUidPersistentSystemProcess: false; 
	* 			 originatingPendingIntent: null; 
	* 			 isBgStartWhitelisted: false; 

 intent: Intent { cmp=com.snail.labaffinity/.activity.MainActivity }; callerApp: ProcessRecord{f17cc20 4896:com.snail.labaffinity/u0a102}]
Copy the code

The unreleased version also has the following Toast

Restrict background applications from starting activities.

The core logic is in this section of ActivityStarter

boolean shouldAbortBackgroundActivityStart(int callingUid, int callingPid, final String callingPackage, int realCallingUid, int realCallingPid, WindowProcessController callerApp, PendingIntentRecord originatingPendingIntent, boolean allowBackgroundActivityStart, Intent intent) { <! // Abort for the most important UIDs final int callingAppId = userhandle.getAppId (callingUid); // Abort for the most important UIDs final int callingAppId = userhandle.getAppId; if (callingUid == Process.ROOT_UID || callingAppId == Process.SYSTEM_UID || callingAppId == Process.NFC_UID) { return false; } <! // Abort if the callingUid has a visible window or is a persistent system process final int callingUidProcState = mService.getUidState(callingUid); <! - if there is a visible window - > final Boolean callingUidHasAnyVisibleWindow = mService.mWindowManager.mRoot.isAnyNonToastWindowVisibleForUid(callingUid); <! - whether CallingUid front desk show - > final Boolean isCallingUidForeground = callingUidHasAnyVisibleWindow | | callingUidProcState = = ActivityManager.PROCESS_STATE_TOP || callingUidProcState == ActivityManager.PROCESS_STATE_BOUND_TOP; <! - whether PersistentSystemProcess - > final Boolean isCallingUidPersistentSystemProcess = callingUidProcState < = ActivityManager.PROCESS_STATE_PERSISTENT_UI; if (callingUidHasAnyVisibleWindow || isCallingUidPersistentSystemProcess) { return false; } // take realCallingUid into consideration final int realCallingUidProcState = (callingUid == realCallingUid) ? callingUidProcState : mService.getUidState(realCallingUid); final boolean realCallingUidHasAnyVisibleWindow = (callingUid == realCallingUid) ? callingUidHasAnyVisibleWindow : mService.mWindowManager.mRoot.isAnyNonToastWindowVisibleForUid(realCallingUid); final boolean isRealCallingUidForeground = (callingUid == realCallingUid) ? isCallingUidForeground : realCallingUidHasAnyVisibleWindow || realCallingUidProcState == ActivityManager.PROCESS_STATE_TOP; final int realCallingAppId = UserHandle.getAppId(realCallingUid); final boolean isRealCallingUidPersistentSystemProcess = (callingUid == realCallingUid) ? isCallingUidPersistentSystemProcess : (realCallingAppId == Process.SYSTEM_UID) || realCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI; . <! // Abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission if (mService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND, callingPid, callingUid) == PERMISSION_GRANTED) { return false; } // don't abort if the caller has the same uid as the recents component if (mSupervisor.mRecentTasks.isCallerRecents(callingUid)) { return false; }... Some systems judge <! // Don't abort if the callerApp or other processes of that uid are whitelisted in any way if (callerApp! = null) { // first check the original calling process if (callerApp.areBackgroundActivityStartsAllowed()) { return false; } // only if that one wasn't whitelisted, check the other ones final ArraySet<WindowProcessController> uidProcesses = mService.mProcessMap.getProcesses(callerAppUid); if (uidProcesses ! = null) { for (int i = uidProcesses.size() - 1; i >= 0; i--) { final WindowProcessController proc = uidProcesses.valueAt(i); if (proc ! = callerApp && proc.areBackgroundActivityStartsAllowed()) { return false; }}}} <! // Abort if the callingUid has SYSTEM_ALERT_WINDOW permission if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) { Slog.w(TAG, "Background activity start for " + callingPackage + " allowed because SYSTEM_ALERT_WINDOW permission is granted."); return false; } <! // any that has fallen through would be aborted slog. w(TAG, "Background activity start [callingPackage: " + callingPackage + "; callingUid: " + callingUid + "; isCallingUidForeground: " + isCallingUidForeground + "; isCallingUidPersistentSystemProcess: " + isCallingUidPersistentSystemProcess + "; realCallingUid: " + realCallingUid + "; isRealCallingUidForeground: " + isRealCallingUidForeground + "; isRealCallingUidPersistentSystemProcess: " + isRealCallingUidPersistentSystemProcess + "; originatingPendingIntent: " + originatingPendingIntent + "; isBgStartWhitelisted: " + allowBackgroundActivityStart + "; intent: " + intent + "; callerApp: " + callerApp + "]"); // log aborted activity start to TRON if (mService.isActivityStartsLoggingEnabled()) { mSupervisor.getActivityMetricsLogger().logAbortedBgActivityStart(intent, callerApp, callingUid, callingPackage, callingUidProcState, callingUidHasAnyVisibleWindow, realCallingUid, realCallingUidProcState, realCallingUidHasAnyVisibleWindow, (originatingPendingIntent ! = null)); } return true; }Copy the code

According to Google, an application running on Android Q can only start an Activity if it meets one or more of the following conditions: Common ones are as follows

  • Having a visible window, such as an Activity running in the foreground. (Foreground services do not limit applications to running in the foreground.)

  • The application has an Activity in the back stack of foreground tasks. (Must be on the same Task return stack as the foreground Activity, if both Task stacks do not work.)

  • The application has the SYSTEM_ALERT_WINDOW permission granted by the user.

  • PendingIntent Temporary whitelist mechanism that does not block applications that are pulled through notifications.

    Start the Activity with a notification using pendingIntent. With notifications, a broadcast is sent in a PendingIntent. Upon receiving the broadcast, the Activity is launched. With notifications, start a Service in a PendingIntent (you can always start a Service) and start an Activity in a Service.Copy the code
  • One service of the application is bound to another visible application (the process priority is actually the same). Note that the application bound to the service must remain visible to the application in the background to successfully start the Activity.

Here’s an interesting point: if the application has an Activity in the foreground Task’s return stack, it does not necessarily mean that the APP’s Activity is displayed, but rather that the currently displayed Task has its own Activity in the stack

boolean areBackgroundActivityStartsAllowed() { <! - white list - > / / allow the if the whitelisting flag was explicitly set the if (mAllowBackgroundActivityStarts) {return true; }... <! --> // Allow if the caller has an activity in any foreground task if (hasActivityInVisibleTask()) { return true; } <! // allow if the caller is bound by a UID that's foreground if (isBoundByForegroundUid()) { return true; } return false; }Copy the code

HasActivityInVisibleTask Determines whether the foreground TASK stack has a CallAPP Activity

private boolean hasActivityInVisibleTask() { for (int i = mActivities.size() - 1; i >= 0; --i) { TaskRecord task = mActivities.get(i).getTaskRecord(); if (task == null) { continue; } ActivityRecord topActivity = task.getTopActivity(); if (topActivity == null) { continue; } // If an activity has just been started it will not yet be visible, but // is expected to be soon. We treat this as if it were already visible. // This ensures a subsequent activity can be  started even before this one // becomes visible. <! CallAPP is visible or about to be visible as long as TOPActivity is displayed in the Task. TOPActivity isn't necessarily CallAPP -- > if (TOPActivity. Visible | | TOPActivity. IsState (the INITIALIZING)) {return true; } } return false; }Copy the code

As long as the TOPActivity in the Task is displayed, the CallAPP is visible or about to be visible. TOPActivity does not necessarily belong to CallAPP. For example, when the APP opens wechat sharing, if the APP is directly viewed in the background, However, wechat shared activities do not have a separate Activity Task, so CallAPP is still regarded as the foreground, that is, it can also start the Activity. In the judgment of front and background, it is more like sinking into the Task dimension, rather than the Activity dimension. Unlike a Service, an Activity relies heavily on the state of the CallAPP, which is more concerned with the state of the APP being launched.

Android10 has a system bug that restricts the start of activities in the background

If you start the Activity twice in a row, the background startup limit will be broken

private boolean hasActivityInVisibleTask() { for (int i = mActivities.size() - 1; i >= 0; --i) { TaskRecord task = mActivities.get(i).getTaskRecord(); if (task == null) { continue; } ActivityRecord topActivity = task.getTopActivity(); if (topActivity == null) { continue; } <! // If an activity has just been started it will not yet be visible, but // is expected to be soon. We treat this as if it were already visible. // This ensures a subsequent activity can be  started even before this one // becomes visible. if (topActivity.visible || topActivity.isState(INITIALIZING)) { return  true; } } return false; }Copy the code

If the application is in the background, the first time the Activity is started is treated as background, but the ActiivityRecord is still created, State is set to INITIALIZING, and it is located at the top of the stack where the Task is being started.

  ActivityRecord(ActivityTaskManagerService _service, WindowProcessController _caller,
            int _launchedFromPid, int _launchedFromUid, String _launchedFromPackage, Intent _intent,
           ...
        setState(INITIALIZING, "ActivityRecord ctor");
Copy the code

So if you start startActivity again in the background, the current process is assumed to be in the foreground and the application is pulled up, which is a weird bug. Because the following conditions are met.

 topActivity.isState(INITIALIZING)
Copy the code

At this point, the Activity can be started in the background. In fact, the background of Android10 does not prevent the Activity from starting completely, but only delays it. When the APP is visible again, it can still call up the Activity that was not started before.

PendingIntent Starts an Activity without restriction

The notified process is the system process

u0_a16        2102  1742 4104448 174924 0                   0 S com.android.systemui
Copy the code

System processes are not restricted, that’s how bad it is.

Notifies the Service to start, and then allows the Activity to start without background restrictions within the Service.

For apps launched with a PendingIntent notification, a short period of time does not count as a background start Activity

If sendInner is triggered by a foreground application, then the Activity is allowed to start for a short period of time. If sendInner is triggered by a foreground application, then the Activity is allowed to start for a short period of time.

Update a logo mHasStartedWhitelistingBgActivityStarts, first is whether to allow the Service background to start the Activity of identity, this is set to true, this moment process may not start,

// is this service currently whitelisted to start activities from background by providing
// allowBackgroundActivityStarts=true to startServiceLocked()?
private boolean mHasStartedWhitelistingBgActivityStarts;
Copy the code

After the next process starts, the Service startup process will continue when attaching

Here because mHasStartedWhitelistingBgActivityStarts is set to true,

Will go setAllowBackgroundActivityStarts will mAllowBackgroundActivityStarts set to true

public void setAllowBackgroundActivityStarts(boolean allowBackgroundActivityStarts) {
    mAllowBackgroundActivityStarts = allowBackgroundActivityStarts;
}
Copy the code

This returns true when the Activity is started to determine whether background startup is allowed

boolean areBackgroundActivityStartsAllowed() {
    // allow if the whitelisting flag was explicitly set
    if (mAllowBackgroundActivityStarts) {
        return true;
    }
Copy the code

This builds a scenario that allows the Activity to start in the background within 10 seconds, which is guaranteed to be fine.

// For how long after a whitelisted service's start its process can start a background activity
public long SERVICE_BG_ACTIVITY_START_TIMEOUT = DEFAULT_SERVICE_BG_ACTIVITY_START_TIMEOUT;
Copy the code

A 10s-clearing listening callback was added to the startup

   ams.mHandler.postDelayed(mStartedWhitelistingBgActivityStartsCleanUp,
            ams.mConstants.SERVICE_BG_ACTIVITY_START_TIMEOUT);
Copy the code

10 seconds later, I will check again whether it needs to be cleaned up, but not necessarily.

/** * Called when the service is started with allowBackgroundActivityStarts set. We whitelist * it for background activity starts, setting up a callback to remove the whitelisting after a * timeout. Note that the whitelisting persists for the process even if the service is * subsequently stopped. */ void whitelistBgActivityStartsOnServiceStart() { setHasStartedWhitelistingBgActivityStarts(true); if (app ! = null) { mAppForStartedWhitelistingBgActivityStarts = app; } // This callback is stateless, so we create it once when we first need it. if (mStartedWhitelistingBgActivityStartsCleanUp == null) { mStartedWhitelistingBgActivityStartsCleanUp = () -> { synchronized (ams) { <! If the Service process is alive, clean up the start part directly. But bind part need to be confirmed - > if (app = = mAppForStartedWhitelistingBgActivityStarts) {/ / The process we whitelisted is still running  the service. We remove // the started whitelisting, but it may still be whitelisted via bound // connections. setHasStartedWhitelistingBgActivityStarts(false); } else if (mAppForStartedWhitelistingBgActivityStarts ! = null) { <! -- If the process dies and dies before 10 seconds, kill it all. // The process we whitelisted is not running The service. It therefore // can't be so we can unconditionally remove the whitelist. mAppForStartedWhitelistingBgActivityStarts .removeAllowBackgroundActivityStartsToken(ServiceRecord.this); } mAppForStartedWhitelistingBgActivityStarts = null; }}; } // if there's a request pending from the past, drop it before scheduling a new one ams.mHandler.removeCallbacks(mStartedWhitelistingBgActivityStartsCleanUp); ams.mHandler.postDelayed(mStartedWhitelistingBgActivityStartsCleanUp, ams.mConstants.SERVICE_BG_ACTIVITY_START_TIMEOUT);  }Copy the code

conclusion

  • The reason that starting a Service by notification is not restricted by the background is that there is an updatable PendingTempWhitelist
  • Background Activity launching relies heavily on the state of the CallAPP, whereas services care more about the state of the APP being started
  • In the background, you can start the Activity with startActivity several times in a row
  • Android10 background restrictions on starting an Activity don’t stop it completely, they just delay it. When the APP is visible again, you can still call up an Activity that hasn’t been started before.

Author: reading the little snail

AAndroid Notification, PendingIntent and background start Service, Activity analysis

For reference only, welcome correction