"running in the background" in the Android O notification bar

Keywords: Android Front-end Attribute

A new feature of Android O is that the system displays the application currently running in the background in the notification bar, which actually shows the application that started the front-end service, and the Activity of the current application is not in the front-end. Specifically, let's see how the source code is implemented.

1 APP calls startService or startForegroundService to start a service.

startService and startForegroundService have two main differences in Android O:
One is that background applications cannot start a service through startService, and both front-end and back-end applications can start a service through startForegroundService.
Additionally, Android stipulates that after calling startForegroundService to start a service, the startForegroundMethod needs to be invoked within five seconds after the service is started.
Otherwise, the service is terminated and an ANR exception is thrown. Specifications for Front-end and Back-end Applications See official website

After the service is started, the startForeground method needs to be called to set the service as the front-end service. One thing to note is that the first parameter id cannot be equal to 0.

    public final void startForeground(int id, Notification notification) {
        try {
            mActivityManager.setServiceForeground(
                    new ComponentName(this, mClassName), mToken, id,
                    notification, 0);
        } catch (RemoteException ex) {
        }
    }

Then the AMS setService Foreground method is called, which calls the setService ForegroundLocked method of Active Services.
Active Services is a class used to assist AMS management application service.

    public void setServiceForegroundLocked(ComponentName className, IBinder token,
            int id, Notification notification, int flags) {
        final int userId = UserHandle.getCallingUserId();
        final long origId = Binder.clearCallingIdentity();
        try {
            ServiceRecord r = findServiceLocked(className, token, userId);
            if (r != null) {
                setServiceForegroundInnerLocked(r, id, notification, flags);
            }
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }

3 calls the setService ForegroundInnerLocked method

   private void setServiceForegroundInnerLocked(ServiceRecord r, int id,
            Notification notification, int flags) {
        if (id != 0) {
            if (notification == null) {
                throw new IllegalArgumentException("null notification");
            }
            // Instant apps need permission to create foreground services.
            // ...A lot of code is omitted
            notification.flags |= Notification.FLAG_FOREGROUND_SERVICE;
            r.foregroundNoti = notification;
            if (!r.isForeground) {
                final ServiceMap smap = getServiceMapLocked(r.userId);
                if (smap != null) {
                    ActiveForegroundApp active = smap.mActiveForegroundApps.get(r.packageName);
                    if (active == null) {
                        active = new ActiveForegroundApp();
                        active.mPackageName = r.packageName;
                        active.mUid = r.appInfo.uid;
                        active.mShownWhileScreenOn = mScreenOn;
                        if (r.app != null) {
                            active.mAppOnTop = active.mShownWhileTop =
                                    r.app.uidRecord.curProcState <= ActivityManager.PROCESS_STATE_TOP;
                        }
                        active.mStartTime = active.mStartVisibleTime
                                = SystemClock.elapsedRealtime();
                        smap.mActiveForegroundApps.put(r.packageName, active);
                        requestUpdateActiveForegroundAppsLocked(smap, 0);
                    }
                    active.mNumActive++;
                }
                r.isForeground = true;
            }
            r.postNotification();
            if (r.app != null) {
                updateServiceForegroundLocked(r.app, true);
            }
            getServiceMapLocked(r.userId).ensureNotStartingBackgroundLocked(r);
            mAm.notifyPackageUse(r.serviceInfo.packageName,
                                 PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE);
        } 
    }

The main thing of this method is to create an ActiveForegroundApp instance and add it to smap.mActiveForegroundApps.
Call request Update Active ForegroundAppsLocked to set isForeground = true for Service Record.
Thus, all the front-end services correspond to an instance in the smap.mActiveForegroundApps list.
The requestUpdateActiveForegroundAppsLocked method calls the updateForegroundApps method again, as shown in the code below.
Here's a key code

active.mAppOnTop = active.mShownWhileTop =
                                    r.app.uidRecord.curProcState <= ActivityManager.PROCESS_STATE_TOP;

This code will be mentioned again.

4 updateForegroundApps method. The "running in the background" above the notification bar is updated in this way.

    void updateForegroundApps(ServiceMap smap) {
        // This is called from the handler without the lock held.
        ArrayList<ActiveForegroundApp> active = null;
        synchronized (mAm) {
            final long now = SystemClock.elapsedRealtime();
            long nextUpdateTime = Long.MAX_VALUE;
            if (smap != null) {
                for (int i = smap.mActiveForegroundApps.size()-1; i >= 0; i--) {
                    ActiveForegroundApp aa = smap.mActiveForegroundApps.valueAt(i);
                    // ...A lot of code is omitted
                    if (!aa.mAppOnTop) {
                        if (active == null) {
                            active = new ArrayList<>();
                        }
                        active.add(aa);
                    }
                }
            }
        }

        final NotificationManager nm = (NotificationManager) mAm.mContext.getSystemService(
                Context.NOTIFICATION_SERVICE);
        final Context context = mAm.mContext;

        if (active != null) {
            // ...A lot of code is omitted
            //Here is the place to update the notification. The specific code is too long and omitted.
        } else {
            //If the activity is empty, cancel the notification.
            nm.cancelAsUser(null, SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES,
                    new UserHandle(smap.mUserId));
        }
    }
  • Traverse the smap.mActiveForegroundApps list to determine the elements in the list. If the mAppOnTop member attribute is false, add it to the active list.

  • Update notification according to the active list.

As you can see, only in the smap.mActiveForegroundApps list, and mAppOnTop is the front-end service for false, will it be displayed in the "running in the background" in the notification bar.

The above is a process for an application to start a front-end service on the code displayed in the "running in the background" in the notification bar. In addition, we have some more relevant logic.

Status of mAppOnTop

From the above analysis, it can be seen that the value of mAppOnTop determines whether a front-end service will be displayed in the "running in the background" of the notification bar. The state of mAppOnTop will be updated in another method besides assigning values at creation time.
After each update, if the value changes, the request Update Active ForegroundAppsLocked is called, which is analyzed above.

    void foregroundServiceProcStateChangedLocked(UidRecord uidRec) {
        ServiceMap smap = mServiceMap.get(UserHandle.getUserId(uidRec.uid));
        if (smap != null) {
            boolean changed = false;
            for (int j = smap.mActiveForegroundApps.size()-1; j >= 0; j--) {
                ActiveForegroundApp active = smap.mActiveForegroundApps.valueAt(j);
                if (active.mUid == uidRec.uid) {
                    if (uidRec.curProcState <= ActivityManager.PROCESS_STATE_TOP) {
                        if (!active.mAppOnTop) {
                            active.mAppOnTop = true;
                            changed = true;
                        }
                        active.mShownWhileTop = true;
                    } else if (active.mAppOnTop) {
                        active.mAppOnTop = false;
                        changed = true;
                    }
                }
            }
            if (changed) {
                requestUpdateActiveForegroundAppsLocked(smap, 0);
            }
        }
    }

This method passes in a uidrecord parameter, specifying the specific uid and related state. The judgment logic is consistent with the previous setService ForegroundInnerLocked method.
The official explanation for ActivityManager.PROCESS_STATE_TOP is

Process is hosting the current top activities. Note that this covers all activities that are visible to the user.

This means that an activity of the current process is on the top of the stack, covering all other activities that users can actually see.
In other words, if the user can't see the activity of the application directly and the application starts a front-end service, it will be displayed in "running in the background".
The foregroundService ProcStateChangedLocked method has only one call, and the AMS updateOomAdjLocked method has too many calls to analyze one by one.

Updates to the "running in the background" notification.

In addition to the above two scenarios, which trigger the update of the notification when the application calls startForeground and mAppOnTop status changes in the service, there are other scenarios that trigger the update.
From the analysis of the code above, we know that the requestUpdateActiveForegroundAppsLocked method must be invoked where the update is triggered.
This method is called in the following methods in the ActiveSerces class.

SetService ForegroundInner Locked (which we analyzed earlier)
DecActive ForegroundAppLocked
Update Screen State Locked
ForegroundService ProcStateChangedLocked
forceStopPackageLocked

Let's look at the decActiveForegroundAppLocked method

decActiveForegroundAppLocked

    private void decActiveForegroundAppLocked(ServiceMap smap, ServiceRecord r) {
        ActiveForegroundApp active = smap.mActiveForegroundApps.get(r.packageName);
        if (active != null) {
            active.mNumActive--;
            if (active.mNumActive <= 0) {
                active.mEndTime = SystemClock.elapsedRealtime();
                if (foregroundAppShownEnoughLocked(active, active.mEndTime)) {
                    // Have been active for long enough that we will remove it immediately.
                    smap.mActiveForegroundApps.remove(r.packageName);
                    smap.mActiveForegroundAppsChanged = true;
                    requestUpdateActiveForegroundAppsLocked(smap, 0);
                } else if (active.mHideTime < Long.MAX_VALUE){
                    requestUpdateActiveForegroundAppsLocked(smap, active.mHideTime);
                }
            }
        }
    }

The main method is to remove the front-end service. According to foregroundAppShown Enough Locked, whether to remove it immediately or after a period of time is judged.
This method is called mainly in two places. One is called in setService ForegroundInnerLocked. When the application calls startForeground, the first parameter is set to 0, it goes to this path.
Another bringDownService Locked is called when the number of bindings to the service decreases.

Posted by sweetstuff2003 on Fri, 04 Jan 2019 09:45:09 -0800