Android O 通知栏的"running in the background"

Android O新增的一个特性,系统会在通知栏显示当前在后台运行的应用,其实际是显示启动了前台服务的应用,并且当前应用的Activity不在前台。具体我们看下源码是怎么实现的。

1 APP调用startServicestartForegroundService启动一个service.

startServicestartForegroundService在Android O上主要有两个区别:
一个是后台应用无法通过startService启动一个服务,而无论前台应用还是后台应用,都可以通过startForegroundService启动一个服务。
此外Android规定,在调用startForegroundService启动一个服务后,需要在服务被启动后5秒内调用startForeground方法,
否则会结束掉该service并且抛出一个ANR异常。关于前台应用和后台应用的规范见官网

2 Service被启动后,需要调用startForeground方法,将service置为前台服务。其中有一个地方要注意,第一个参数id不能等于0。

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

接着调用了AMS的setServiceForeground方法,该方法会调用ActiveServicessetServiceForegroundLocked方法。
ActiveServices是用来辅助AMS管理应用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 调用了setServiceForegroundInnerLocked方法

   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);
        } 
    }

该方法主要做的事情,创建一个ActiveForegroundApp实例,并把实例加入到smap.mActiveForegroundApps
调用requestUpdateActiveForegroundAppsLocked,设置ServiceRecord的isForeground = true.
由此可见,所有的前台服务都会在smap.mActiveForegroundApps列表中对应一个实例。
requestUpdateActiveForegroundAppsLocked方法又调用了updateForegroundApps方法,见下面代码。
这里有个关键代码是

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

下面会再次提到这段代码。

4 updateForegroundApps方法。通知栏上面的“running in the background”就是在这个方法里面去更新的。

    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
            //这里是更新通知的地方,具体代码太长,省略掉了。
        } else {
            //如果active为空,取消掉通知。
            nm.cancelAsUser(null, SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES,
                    new UserHandle(smap.mUserId));
        }
    }
  • 遍历smap.mActiveForegroundApps列表,判断列表中的元素,如果其mAppOnTop成员属性为false,则加入active列表中。

  • 根据active列表,更新notification。

可见,只有在smap.mActiveForegroundApps列表中,并且mAppOnTop为false的前台服务才会显示在通知栏中的“running in the background”中。

以上是一个应用启动一个前台服务到被显示在通知栏中的“running in the background”中的代码上的流程。此外我们再了解些相关的逻辑。

mAppOnTop的状态

从上面的分析看出,mAppOnTop的值决定了一个前台服务是否会被显示在通知栏的“running in the background”中。mAppOnTop的状态除了会在创建的时候赋值,还会在另一个方法中被更新。
每次更新后,如果值有变化,就会调用requestUpdateActiveForegroundAppsLocked,该方法上面分析过了。

    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);
            }
        }
    }

该方法传入一个uidrecord参数,指定具体的uid及相关状态。判断逻辑跟前面setServiceForegroundInnerLocked方法的逻辑一致。
其中ActivityManager.PROCESS_STATE_TOP的官方解释是

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

意思就是,当前进程的一个activity在栈顶,覆盖了所有其它activity,用户可以真正看到的。
换句话,如果用户不能直接看到该应用的activity,并且该应用启动了一个前台服务,那么就会被显示在“running in the background”中。
foregroundServiceProcStateChangedLocked方法只有一处调用,AMS的updateOomAdjLocked,该方法的调用地方太多,无法一一分析。

"running in the background"通知的更新。

除了以上两种情况(应用在service中调用startForegroundmAppOnTop的状态变更)会触发该通知的更新外,还有一些其它情况会触发更新。
从上面代码的分析中,我们知道,触发更新的地方必须要调用requestUpdateActiveForegroundAppsLocked方法。
该方法会在ActiveSercices类中的如下几个方法中调用。

setServiceForegroundInnerLocked (这个我们前面分析过)
decActiveForegroundAppLocked (减小前台应用)
updateScreenStateLocked (屏幕状态变化)
foregroundServiceProcStateChangedLocked (进程状态变化)
forceStopPackageLocked (强制停止应用)

我们看下其中的decActiveForegroundAppLocked方法

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);
                }
            }
        }
    }

该方法主要是移除前台service,根据foregroundAppShownEnoughLocked判断,是否马上移除还是过一段时间移除。
该方法主要在两个地方调用。一个是在setServiceForegroundInnerLocked中调用,当应用调用startForeground,第一个参数设为0时,会走到这个路径。
另一个bringDownServiceLocked,也就是当绑定到该service的数量减小时,会调用该方法。

前台服务是否一定会在通知栏显示应用自己的通知

如果是一定的话,我想系统也没必要再额外显示一条“running in the background”的通知,列出所有后台运行的应用了。

所以答案是不一定,虽然在调用startForeground方法时,必须要传一个notification作为参数,但依然会有两种情况会导致不会在通知栏显示应用发的通知。

  • 用户主动屏蔽应用通知,可以通过长按通知,点击“ALL CATEGORIES”进入通知管理,关闭通知。关闭后前台服务依然生效。
  • NotificationManager在显示应用通知的时候,因为某些原因显示失败,失败原因可能是应用创建了不规范的通知,比如Android O新增了NotificationChannel,应用在创建通知的时候,必须指定一个NotificationChannel,但是如果应用创建通知的时候,指定的NotificationChannel是无效的,或者直接传null作为参数值,那么在NotificationManager就没办法显示该通知。这种情况下,前台服务还是会生效,但是却不会在通知栏显示应用的通知,不过NotificationManager发现不规范的通知时,一般会弹出一个toast提醒用户。
原文地址:https://www.cnblogs.com/xiaji5572/p/7729987.html