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