• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.applications;
18 
19 import static android.app.usage.UsageStatsManager.INTERVAL_MONTHLY;
20 import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;
21 
22 import static com.android.settings.Utils.PROPERTY_APP_HIBERNATION_ENABLED;
23 
24 import android.app.usage.UsageStats;
25 import android.app.usage.UsageStatsManager;
26 import android.apphibernation.AppHibernationManager;
27 import android.content.Context;
28 import android.content.pm.PackageInfo;
29 import android.content.pm.PackageManager;
30 import android.provider.DeviceConfig;
31 import android.util.ArrayMap;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.WorkerThread;
35 import androidx.lifecycle.Lifecycle;
36 import androidx.lifecycle.LifecycleObserver;
37 import androidx.lifecycle.OnLifecycleEvent;
38 import androidx.preference.Preference;
39 import androidx.preference.PreferenceScreen;
40 
41 import com.android.settings.R;
42 import com.android.settings.core.BasePreferenceController;
43 
44 import com.google.common.annotations.VisibleForTesting;
45 
46 import java.util.List;
47 import java.util.Map;
48 import java.util.concurrent.Executor;
49 import java.util.concurrent.Executors;
50 import java.util.concurrent.TimeUnit;
51 
52 /**
53  * A preference controller handling the logic for updating summary of hibernated apps.
54  */
55 public final class HibernatedAppsPreferenceController extends BasePreferenceController
56         implements LifecycleObserver {
57     private static final String TAG = "HibernatedAppsPrefController";
58     private static final String PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS =
59             "auto_revoke_unused_threshold_millis2";
60     private static final long DEFAULT_UNUSED_THRESHOLD_MS = TimeUnit.DAYS.toMillis(90);
61     private PreferenceScreen mScreen;
62     private int mUnusedCount = 0;
63     private boolean mLoadingUnusedApps;
64     private boolean mLoadedUnusedCount;
65     private final Executor mBackgroundExecutor;
66     private final Executor mMainExecutor;
67 
HibernatedAppsPreferenceController(Context context, String preferenceKey)68     public HibernatedAppsPreferenceController(Context context, String preferenceKey) {
69         this(context, preferenceKey, Executors.newSingleThreadExecutor(),
70                 context.getMainExecutor());
71     }
72 
73     @VisibleForTesting
HibernatedAppsPreferenceController(Context context, String preferenceKey, Executor bgExecutor, Executor mainExecutor)74     HibernatedAppsPreferenceController(Context context, String preferenceKey,
75             Executor bgExecutor, Executor mainExecutor) {
76         super(context, preferenceKey);
77         mBackgroundExecutor = bgExecutor;
78         mMainExecutor = mainExecutor;
79     }
80 
81     @Override
getAvailabilityStatus()82     public int getAvailabilityStatus() {
83         return isHibernationEnabled() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
84     }
85 
86     @Override
getSummary()87     public CharSequence getSummary() {
88         return mLoadedUnusedCount
89                 ? mContext.getResources().getQuantityString(
90                         R.plurals.unused_apps_summary, mUnusedCount, mUnusedCount)
91                 : mContext.getResources().getString(R.string.summary_placeholder);
92     }
93 
94     @Override
displayPreference(PreferenceScreen screen)95     public void displayPreference(PreferenceScreen screen) {
96         super.displayPreference(screen);
97         mScreen = screen;
98     }
99 
100     /**
101      * On lifecycle resume event.
102      */
103     @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
onResume()104     public void onResume() {
105         updatePreference();
106     }
107 
updatePreference()108     private void updatePreference() {
109         if (mScreen == null) {
110             return;
111         }
112         if (!mLoadingUnusedApps) {
113             loadUnusedCount(unusedCount -> {
114                 mUnusedCount = unusedCount;
115                 mLoadingUnusedApps = false;
116                 mLoadedUnusedCount = true;
117                 mMainExecutor.execute(() -> {
118                     Preference pref = mScreen.findPreference(mPreferenceKey);
119                     refreshSummary(pref);
120                 });
121             });
122             mLoadingUnusedApps = true;
123         }
124     }
125 
126     /**
127      * Asynchronously load the count of unused apps.
128      *
129      * @param callback callback to call when the number of unused apps is calculated
130      */
loadUnusedCount(@onNull UnusedCountLoadedCallback callback)131     private void loadUnusedCount(@NonNull UnusedCountLoadedCallback callback) {
132         mBackgroundExecutor.execute(() -> {
133             final int unusedCount = getUnusedCount();
134             callback.onUnusedCountLoaded(unusedCount);
135         });
136     }
137 
138     @WorkerThread
getUnusedCount()139     private int getUnusedCount() {
140         // TODO(b/187465752): Find a way to export this logic from PermissionController module
141         final PackageManager pm = mContext.getPackageManager();
142         final AppHibernationManager ahm = mContext.getSystemService(AppHibernationManager.class);
143         final List<String> hibernatedPackages = ahm.getHibernatingPackagesForUser();
144         int numHibernated = hibernatedPackages.size();
145 
146         // Also need to count packages that are auto revoked but not hibernated.
147         int numAutoRevoked = 0;
148         final UsageStatsManager usm = mContext.getSystemService(UsageStatsManager.class);
149         final long now = System.currentTimeMillis();
150         final long unusedThreshold = DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS,
151                 PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS, DEFAULT_UNUSED_THRESHOLD_MS);
152         final List<UsageStats> usageStatsList = usm.queryUsageStats(INTERVAL_MONTHLY,
153                 now - unusedThreshold, now);
154         final Map<String, UsageStats> recentlyUsedPackages = new ArrayMap<>();
155         for (UsageStats us : usageStatsList) {
156             recentlyUsedPackages.put(us.mPackageName, us);
157         }
158         final List<PackageInfo> packages = pm.getInstalledPackages(
159                 PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_PERMISSIONS);
160         for (PackageInfo pi : packages) {
161             final String packageName = pi.packageName;
162             final UsageStats usageStats = recentlyUsedPackages.get(packageName);
163             // Only count packages that have not been used recently as auto-revoked permissions may
164             // stay revoked even after use if the user has not regranted them.
165             final boolean usedRecently = (usageStats != null
166                     && (now - usageStats.getLastTimeAnyComponentUsed() < unusedThreshold
167                         || now - usageStats.getLastTimeVisible() < unusedThreshold));
168             if (!hibernatedPackages.contains(packageName)
169                     && pi.requestedPermissions != null
170                     && !usedRecently) {
171                 for (String perm : pi.requestedPermissions) {
172                     if ((pm.getPermissionFlags(perm, packageName, mContext.getUser())
173                             & PackageManager.FLAG_PERMISSION_AUTO_REVOKED) != 0) {
174                         numAutoRevoked++;
175                         break;
176                     }
177                 }
178             }
179         }
180         return numHibernated + numAutoRevoked;
181     }
182 
isHibernationEnabled()183     private static boolean isHibernationEnabled() {
184         return DeviceConfig.getBoolean(
185                 NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, true);
186     }
187 
188     /**
189      * Callback for when we've determined the number of unused apps.
190      */
191     private interface UnusedCountLoadedCallback {
onUnusedCountLoaded(int unusedCount)192         void onUnusedCountLoaded(int unusedCount);
193     }
194 }
195