Android9.x应用待机群组特性导致后台应用无法联网问题分析

Android9.x应用待机群组特性导致后台应用无法联网问题分析

    • 9.x增加的电源管理新特性
      • 1 应用分组
      • 2 查看和修改分组
      • 3 省电管理白名单
    • 源码分析

9.x增加的电源管理新特性

我们在工作中发现一个问题,我们的一个应用,启动时只启动一个后台服务,而不会启动界面。并且在后台服务中网络请求超时。但是如果启动一个activity之后,就可以正常联网。

Android P(9.x)为了更加严格的限制后台应用的行为,新增了一个特性,叫做应用待机群组,可以参考官方文档:

https://developer.android.com/about/versions/pie/power

从这个文档中,我们可以得到如下关键信息:

1 应用分组

系统会根据用户对应用的使用情况,将应用分到不同的组中,系统对不同的组内的应用限制程度是不同的。一共有五个组,文档上是这样定义的:

活跃
如果用户当前正在使用应用,应用将被归到“活跃”群组中,例如:

应用已启动一个 Activity
应用正在运行前台服务
应用的同步适配器与某个前台应用使用的 content provider 关联
用户在应用中点击了某个通知
如果应用处于“活跃”群组,系统不会对应用的作业、报警或 FCM 消息施加任何限制。

工作集
如果应用经常运行,但当前未处于活跃状态,它将被归到“工作集”群组中。 例如,用户在大部分时间都启动的某个社交媒体应用可能就属于“工作集”群组。 如果应用被间接使用,它们也会被升级到“工作集”群组中 。

如果应用处于“工作集”群组,系统会对它运行作业和触发报警的能力施加轻度限制。 如需了解详细信息,请参阅电源管理限制。

常用
如果应用会定期使用,但不是每天都必须使用,它将被归到“常用”群组中。 例如,用户在健身房运行的某个锻炼跟踪应用可能就属于“常用”群组。

如果应用处于“常用”群组,系统将对它运行作业和触发报警的能力施加较强的限制,也会对高优先级 FCM 消息的数量设定限制。 如需了解详细信息,请参阅电源管理限制。

极少使用
如果应用不经常使用,那么它属于“极少使用”群组。 例如,用户仅在入住酒店期间运行的酒店应用就可能属于“极少使用”群组。

如果应用处于“极少使用”群组,系统将对它运行作业、触发警报和接收高优先级 FCM 消息的能力施加严格限制。系统还会限制应用连接到网络的能力。 如需了解详细信息,请参阅电源管理限制。

从未使用
安装但是从未运行过的应用会被归到“从未使用”群组中。 系统会对这些应用施加极强的限制。

2 查看和修改分组

有两种方式可以查看一个应用的分组

1 通过调用UsageStatsManager.getAppStandbyBucket()
2 通过adb命令查看(下面是官方文档的介绍)

您可以使用 ADB 为您的应用手动指定应用待机群组。 要更改应用的群组,请使用以下命令:

$ adb shell am set-standby-bucket packagename active|working_set|frequent|rare
您还可以使用该命令一次设置多个软件包:

$ adb shell am set-standby-bucket package1 bucket1 package2 bucket2…
要检查应用处于哪一个群组,请运行以下命令:

$ adb shell am get-standby-bucket [packagename]
如果您不传递 packagename 参数,命令将列出所有应用的群组。 应用还可以调用新函数 UsageStatsManager.getAppStandbyBucket(),在运行时查找所属的群组。

3 省电管理白名单

从文档上我们还可以知道,有个省电管理白名单,省电管理白名单中的应用不受应用待机群组特性的限制。

因为我们的应用刚装上时,处于从未使用这个分组,官方文档中也说了,会对从未使用这个分组中的应用施加极强的限制。

知道了上述信息后,需要解决这个问题,我们需要把自己加到白名单中。关于如何设置白名单,参考如下文档:

https://developer.android.com/training/monitoring-device-state/doze-standby#support_for_other_use_cases

关键信息如下:

用户可以在 Settings > Battery > Battery Optimization 中手动配置该白名单。或者,系统会为应用提供请求用户将应用加入白名单的方式。
应用可以触发 ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS Intent,让用户直接进入 Battery Optimization,他们可以在其中添加应用。
具有 REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 权限的应用可以触发系统对话框,让用户无需转到“设置”即可直接将应用添加到白名单。应用将通过触发 ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS Intent 来触发该对话框。
用户可以根据需要手动从白名单中移除应用。
在请求用户将应用添加到白名单之前,请确保应用符合加入白名单的可接受用例。

可以在设置中加入白名单,也可以在应用内通过Intent拉起对话框,让用户将应用加入白名单。但是这两种方式都依赖用户的行为。因为我们是系统应用,并且具有platform签名,所以我们要在系统中找到设置白名单的接口,并且测试调用该接口是否生效。

源码分析

下面我们就根据源码来分析如何设置白名单。官方文档中提到UsageStatsManager这个类,我们就从这个类开始。

源码位于frameworks/base/core/java/android/app/usage/UsageStatsManager.java

/**
     * The app is whitelisted for some reason and the bucket cannot be changed.
     * {@hide}
     */
    @SystemApi
    public static final int STANDBY_BUCKET_EXEMPTED = 5;

    /**
     * The app was used very recently, currently in use or likely to be used very soon. Standby
     * bucket values that are ≤ {@link #STANDBY_BUCKET_ACTIVE} will not be throttled by the
     * system while they are in this bucket. Buckets > {@link #STANDBY_BUCKET_ACTIVE} will most
     * likely be restricted in some way. For instance, jobs and alarms may be deferred.
     * @see #getAppStandbyBucket()
     */
    public static final int STANDBY_BUCKET_ACTIVE = 10;

    /**
     * The app was used recently and/or likely to be used in the next few hours. Restrictions will
     * apply to these apps, such as deferral of jobs and alarms.
     * @see #getAppStandbyBucket()
     */
    public static final int STANDBY_BUCKET_WORKING_SET = 20;

    /**
     * The app was used in the last few days and/or likely to be used in the next few days.
     * Restrictions will apply to these apps, such as deferral of jobs and alarms. The delays may be
     * greater than for apps in higher buckets (lower bucket value). Bucket values >
     * {@link #STANDBY_BUCKET_FREQUENT} may additionally have network access limited.
     * @see #getAppStandbyBucket()
     */
    public static final int STANDBY_BUCKET_FREQUENT = 30;

    /**
     * The app has not be used for several days and/or is unlikely to be used for several days.
     * Apps in this bucket will have the most restrictions, including network restrictions, except
     * during certain short periods (at a minimum, once a day) when they are allowed to execute
     * jobs, access the network, etc.
     * @see #getAppStandbyBucket()
     */
    public static final int STANDBY_BUCKET_RARE = 40;

    /**
     * The app has never been used.
     * {@hide}
     */
    @SystemApi
    public static final int STANDBY_BUCKET_NEVER = 50;

这5个常量,就是五个分组的定义。可以看到STANDBY_BUCKET_EXEMPTED就是白名单。

下面我们在frameworks中搜索STANDBY_BUCKET_EXEMPTED,看一下如何使用该常量。搜索结果如下:

base/core/java/android/app/usage/UsageStatsManager.java
108:    public static final int STANDBY_BUCKET_EXEMPTED = 5;
199:            STANDBY_BUCKET_EXEMPTED,

base/config/hiddenapi-public-dex.txt
4712:Landroid/app/usage/UsageStatsManager;->STANDBY_BUCKET_EXEMPTED:I

base/api/system-current.txt
765:    field public static final int STANDBY_BUCKET_EXEMPTED = 5; // 0x5

base/services/core/java/com/android/server/AppStateTracker.java
704:            if (bucket == UsageStatsManager.STANDBY_BUCKET_EXEMPTED) {

base/services/usage/java/com/android/server/usage/AppStandbyController.java
40:import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED;
630:                        STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
633:                    STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT, false);

base/services/tests/servicestests/src/com/android/server/AppStateTrackerTest.java
632:                UsageStatsManager.STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
644:                UsageStatsManager.STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
664:                UsageStatsManager.STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
666:                UsageStatsManager.STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);

base/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
30:import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED;
280:        assertEquals(STANDBY_BUCKET_EXEMPTED,
283:        assertNotEquals(STANDBY_BUCKET_EXEMPTED,

可以看到AppStandbyController.java这个类引用了STANDBY_BUCKET_EXEMPTED。我们打开base/services/usage/java/com/android/server/usage/AppStandbyController.java的630行:

/** Check if we need to update the standby state of a specific app. */                          
    private void checkAndUpdateStandbyState(String packageName, @UserIdInt int userId,              
            int uid, long elapsedRealtime) {                                                        
        if (uid <= 0) {                                                                             
            try {                                                                                   
                uid = mPackageManager.getPackageUidAsUser(packageName, userId);                     
            } catch (PackageManager.NameNotFoundException e) {                                      
                // Not a valid package for this user, nothing to do                                 
                // TODO: Remove any history of removed packages                                     
                return;                                                                             
            }                                                                                       
        }                                                                                           
        final boolean isSpecial = isAppSpecial(packageName,                                         
                UserHandle.getAppId(uid),                                                           
                userId);                                                                            
        if (DEBUG) {                                                                                
            Slog.d(TAG, "   Checking idle state for " + packageName + " special=" +                 
                    isSpecial);                                                                     
        }                                                                                           
        if (isSpecial) {                                                                            
            synchronized (mAppIdleLock) {                                                           
                mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,           
                        STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);                              
            }                                                                                       
            maybeInformListeners(packageName, userId, elapsedRealtime,                              
                    STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT, false);                           
        } else {   

所在的方法是checkAndUpdateStandbyState,该方法在系统需要改变某个app的分组时调用。可以看到,如果符合isSpecial的条件,就会把应用设为STANDBY_BUCKET_EXEMPTED分组,也就是加入白名单。

我们接着看什么样的应用是special的:

/** Returns true if this app should be whitelisted for some reason, to never go into standby */
    boolean isAppSpecial(String packageName, int appId, int userId) {
        if (packageName == null) return false;
        // If not enabled at all, of course nobody is ever idle.
        if (!mAppIdleEnabled) {
            return true;
        }
        if (appId < Process.FIRST_APPLICATION_UID) {
            // System uids never go idle.
            return true;
        }
        if (packageName.equals("android")) {
            // Nor does the framework (which should be redundant with the above, but for MR1 we will
            // retain this for safety).
            return true;
        }
        if (mSystemServicesReady) {
            try {
                // We allow all whitelisted apps, including those that don't want to be whitelisted
                // for idle mode, because app idle (aka app standby) is really not as big an issue
                // for controlling who participates vs. doze mode.
                if (mInjector.isPowerSaveWhitelistExceptIdleApp(packageName)) {
                    return true;
                }
            } catch (RemoteException re) {
                throw re.rethrowFromSystemServer();
            }

            if (isActiveDeviceAdmin(packageName, userId)) {
                return true;
            }

            if (isActiveNetworkScorer(packageName)) {
                return true;
            }

            if (mAppWidgetManager != null
                    && mInjector.isBoundWidgetPackage(mAppWidgetManager, packageName, userId)) {
                return true;
            }

            if (isDeviceProvisioningPackage(packageName)) {
                return true;
            }
        }

        // Check this last, as it can be the most expensive check
        if (isCarrierApp(packageName)) {
            return true;
        }

        return false;
    }

一些特殊的包是special的,比如包名为android的包,uid小于Process.FIRST_APPLICATION_UID的包,设备管理器,开机向导等。这些对于解决我们的问题没有帮助。除此之外mInjector.isPowerSaveWhitelistExceptIdleApp(packageName)的包也是special的,这个就是省电管理的白名单。

下面我们看一下如何加到这个名单中。

AppStandbyController.java的内部类Injector的实现如下:

    static class Injector {

        private final Context mContext;
        private final Looper mLooper;
        private IDeviceIdleController mDeviceIdleController;
        private IBatteryStats mBatteryStats;
        private PackageManagerInternal mPackageManagerInternal;
        private DisplayManager mDisplayManager;
        private PowerManager mPowerManager;
        int mBootPhase;

        Injector(Context context, Looper looper) {
            mContext = context;
            mLooper = looper;
        }

        Context getContext() {
            return mContext;
        }

        Looper getLooper() {
            return mLooper;
        }

        void onBootPhase(int phase) {
            if (phase == PHASE_SYSTEM_SERVICES_READY) {
                mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
                        ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
                mBatteryStats = IBatteryStats.Stub.asInterface(
                        ServiceManager.getService(BatteryStats.SERVICE_NAME));
                mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
                mDisplayManager = (DisplayManager) mContext.getSystemService(
                        Context.DISPLAY_SERVICE);
                mPowerManager = mContext.getSystemService(PowerManager.class);
            }
            mBootPhase = phase;
        }

        int getBootPhase() {
            return mBootPhase;
        }

        /**
         * Returns the elapsed realtime since the device started. Override this
         * to control the clock.
         * @return elapsed realtime
         */
        long elapsedRealtime() {
            return SystemClock.elapsedRealtime();
        }

        long currentTimeMillis() {
            return System.currentTimeMillis();
        }

        boolean isAppIdleEnabled() {
            final boolean buildFlag = mContext.getResources().getBoolean(
                    com.android.internal.R.bool.config_enableAutoPowerModes);
            final boolean runtimeFlag = Global.getInt(mContext.getContentResolver(),
                    Global.APP_STANDBY_ENABLED, 1) == 1
                    && Global.getInt(mContext.getContentResolver(),
                    Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED, 1) == 1;
            return buildFlag && runtimeFlag;
        }

        boolean isCharging() {
            return mContext.getSystemService(BatteryManager.class).isCharging();
        }

        boolean isPowerSaveWhitelistExceptIdleApp(String packageName) throws RemoteException {
            return mDeviceIdleController.isPowerSaveWhitelistExceptIdleApp(packageName);
        }

获取省电管理的白名单调用的是mDeviceIdleController的方法:

mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
                        ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
        boolean isPowerSaveWhitelistExceptIdleApp(String packageName) throws RemoteException {
            return mDeviceIdleController.isPowerSaveWhitelistExceptIdleApp(packageName);
        }

所以我们可以猜测,添加白名单的操作也会实现在mDeviceIdleController中。下面我们找DeviceIdleController的实现。从IDeviceIdleController.Stub.asInterface可以看出来DeviceIdleController应该是一个系统服务。

zhangjg@zjg:~/deve/android/frameworks$ find . -name "DeviceIdleController*"
./base/services/core/java/com/android/server/DeviceIdleController.java

通过find搜索,我们找到一个DeviceIdleController.java,该文件在frameworks/base/services/下,所以我们可以确定这个就是DeviceIdleController服务端的实现。在该文件中,我们确实找到了DeviceIdleController服务的实现:

  private final class BinderService extends IDeviceIdleController.Stub {
        @Override public void addPowerSaveWhitelistApp(String name) {
            if (DEBUG) {
                Slog.i(TAG, "addPowerSaveWhitelistApp(name = " + name + ")");
            }
            getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER,
                    null);
            long ident = Binder.clearCallingIdentity();
            try {
                addPowerSaveWhitelistAppInternal(name);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

可以确定添加白名单的方法为addPowerSaveWhitelistApp,调用该方法需要android.Manifest.permission.DEVICE_POWER权限。我们看一下是否能够申请到这个权限。

该权限定义在frameworks/base/core/res/AndroidManifest.xml中:

                                                                                                
   <permission android:name="android.permission.DEVICE_POWER"                                       
        android:protectionLevel="signature" /> 

该权限的protectionLevel为signature,而我们的应用是系统platform签名,所以可以申请到该权限。

下面我们搜一下如何调用addPowerSaveWhitelistApp这个方法,搜索结果如下:

zhangjg@zjg:~/deve/android/frameworks$ ag "addPowerSaveWhitelistApp"
base/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/PowerWhitelistBackendTest.java
71:        doNothing().when(mDeviceIdleService).addPowerSaveWhitelistApp(anyString());
92:        verify(mDeviceIdleService, atLeastOnce()).addPowerSaveWhitelistApp(PACKAGE_TWO);

base/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
135:            mDeviceIdleService.addPowerSaveWhitelistApp(pkg);

base/core/java/android/os/IDeviceIdleController.aidl
24:    void addPowerSaveWhitelistApp(String name);

base/services/core/java/com/android/server/DeviceIdleController.java
1160:        @Override public void addPowerSaveWhitelistApp(String name) {
1162:                Slog.i(TAG, "addPowerSaveWhitelistApp(name = " + name + ")");
1168:                addPowerSaveWhitelistAppInternal(name);
1551:    public boolean addPowerSaveWhitelistAppInternal(String name) {
3065:                            if (addPowerSaveWhitelistAppInternal(pkg)) {

base/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
174:            mDeviceIdleController.addPowerSaveWhitelistApp(TEST_APP_PACKAGE);

base/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
454:                mIdleController.addPowerSaveWhitelistApp(packageInfo.packageName);

base/config/boot-image-profile.txt
27580:HSPLandroid/os/IDeviceIdleController;->addPowerSaveWhitelistApp(Ljava/lang/String;)V

base/config/hiddenapi-private-dex.txt
62300:Landroid/os/IDeviceIdleController$Stub$Proxy;->addPowerSaveWhitelistApp(Ljava/lang/String;)V
62328:Landroid/os/IDeviceIdleController$Stub;->TRANSACTION_addPowerSaveWhitelistApp:I
62351:Landroid/os/IDeviceIdleController;->addPowerSaveWhitelistApp(Ljava/lang/String;)V

base/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java中有这个调用方式。下面我们打开这个文件的135行:

    public void addApp(String pkg) {
        try {
            mDeviceIdleService.addPowerSaveWhitelistApp(pkg);
            mWhitelistedApps.add(pkg);
        } catch (RemoteException e) {
            Log.w(TAG, "Unable to reach IDeviceIdleController", e);
        }
    }

mDeviceIdleService的初始化如下:

    public PowerWhitelistBackend(Context context) {
        this(context, IDeviceIdleController.Stub.asInterface(
                ServiceManager.getService(DEVICE_IDLE_SERVICE)));
    }

    @VisibleForTesting
    PowerWhitelistBackend(Context context, IDeviceIdleController deviceIdleService) {
        mAppContext = context.getApplicationContext();
        mDeviceIdleService = deviceIdleService;
        refreshList();
    }

所以通过在我们的app中调用如下代码,可以加到白名单中:

IDeviceIdleController deviceIdleService = IDeviceIdleController.Stub.asInterface(
                ServiceManager.getService(DEVICE_IDLE_SERVICE));
deviceIdleService.addPowerSaveWhitelistApp(pkg);

到此为止问题可以解决了。





其实在分析frameworks/base/core/java/android/app/usage/UsageStatsManager.java文件的时候,我们只看到了5个分组的定义。但是在该文件中,还定义了两个方法用来获取分组和设置分组:

   /**
     * {@hide}
     * Returns the current standby bucket of the specified app. The caller must hold the permission
     * android.permission.PACKAGE_USAGE_STATS.
     * @param packageName the package for which to fetch the current standby bucket.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
    public @StandbyBuckets int getAppStandbyBucket(String packageName) {
        try {
            return mService.getAppStandbyBucket(packageName, mContext.getOpPackageName(),
                    mContext.getUserId());
        } catch (RemoteException e) {
        }
        return STANDBY_BUCKET_ACTIVE;
    }

    /**
     * {@hide}
     * Changes an app's standby bucket to the provided value. The caller can only set the standby
     * bucket for a different app than itself.
     * @param packageName the package name of the app to set the bucket for. A SecurityException
     *                    will be thrown if the package name is that of the caller.
     * @param bucket the standby bucket to set it to, which should be one of STANDBY_BUCKET_*.
     *               Setting a standby bucket outside of the range of STANDBY_BUCKET_ACTIVE to
     *               STANDBY_BUCKET_NEVER will result in a SecurityException.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.CHANGE_APP_IDLE_STATE)
    public void setAppStandbyBucket(String packageName, @StandbyBuckets int bucket) {
        try {
            mService.setAppStandbyBucket(packageName, bucket, mContext.getUserId());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

可以看到直接调用setAppStandbyBucket,是可以改变应用分组的。只要我们把分组设为STANDBY_BUCKET_EXEMPTED,我们的应用就不会受后台限制。第一种方式是通过添加到省电管理白名单中,间接将应用的分组设为STANDBY_BUCKET_EXEMPTED, 相对于上面的第一种方式,这种方式更直接。

调用setAppStandbyBucket方法,需要CHANGE_APP_IDLE_STATE权限。该权限同样定义在frameworks/base/core/res/AndroidManifest.xml中:

    
    <permission android:name="android.permission.CHANGE_APP_IDLE_STATE"
        android:protectionLevel="signature|privileged" />

该权限的protectionLevel为signature|privileged,因为我们的应用是系统platform签名,所以可以申请到该权限,是可以调用该方法的。

下一篇文章分析应用待机分组是如何限制应用的联网行为的。

你可能感兴趣的:(Android)