1 /* 2 * Copyright (C) 2019 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 com.android.settings.Utils.SETTINGS_PACKAGE_NAME; 20 21 import android.app.Application; 22 import android.app.usage.UsageStats; 23 import android.app.usage.UsageStatsManager; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.PackageManager; 27 import android.os.PowerManager; 28 import android.os.UserHandle; 29 import android.util.ArrayMap; 30 import android.util.ArraySet; 31 import android.util.Log; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.VisibleForTesting; 35 36 import com.android.settingslib.applications.AppUtils; 37 import com.android.settingslib.applications.ApplicationsState; 38 import com.android.settingslib.core.lifecycle.LifecycleObserver; 39 import com.android.settingslib.core.lifecycle.events.OnStart; 40 import com.android.settingslib.utils.ThreadUtils; 41 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 import java.util.Calendar; 45 import java.util.Collections; 46 import java.util.Comparator; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Set; 50 51 52 public class RecentAppStatsMixin implements Comparator<UsageStats>, LifecycleObserver, OnStart { 53 54 private static final String TAG = "RecentAppStatsMixin"; 55 private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>(); 56 57 @VisibleForTesting 58 final List<UsageStats> mRecentApps; 59 private final int mUserId; 60 private final int mMaximumApps; 61 private final Context mContext; 62 private final PackageManager mPm; 63 private final PowerManager mPowerManager;; 64 private final UsageStatsManager mUsageStatsManager; 65 private final ApplicationsState mApplicationsState; 66 private final List<RecentAppStatsListener> mAppStatsListeners; 67 private Calendar mCalendar; 68 69 static { 70 SKIP_SYSTEM_PACKAGES.addAll(Arrays.asList( 71 "android", 72 "com.android.phone", 73 SETTINGS_PACKAGE_NAME, 74 "com.android.systemui", 75 "com.android.providers.calendar", 76 "com.android.providers.media" 77 )); 78 } 79 RecentAppStatsMixin(Context context, int maximumApps)80 public RecentAppStatsMixin(Context context, int maximumApps) { 81 mContext = context; 82 mMaximumApps = maximumApps; 83 mUserId = UserHandle.myUserId(); 84 mPm = mContext.getPackageManager(); 85 mPowerManager = mContext.getSystemService(PowerManager.class); 86 mUsageStatsManager = mContext.getSystemService(UsageStatsManager.class); 87 mApplicationsState = ApplicationsState.getInstance( 88 (Application) mContext.getApplicationContext()); 89 mRecentApps = new ArrayList<>(); 90 mAppStatsListeners = new ArrayList<>(); 91 } 92 93 @Override onStart()94 public void onStart() { 95 ThreadUtils.postOnBackgroundThread(() -> { 96 loadDisplayableRecentApps(mMaximumApps); 97 for (RecentAppStatsListener listener : mAppStatsListeners) { 98 ThreadUtils.postOnMainThread(() -> listener.onReloadDataCompleted(mRecentApps)); 99 } 100 }); 101 } 102 103 @Override compare(UsageStats a, UsageStats b)104 public final int compare(UsageStats a, UsageStats b) { 105 // return by descending order 106 return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed()); 107 } 108 addListener(@onNull RecentAppStatsListener listener)109 public void addListener(@NonNull RecentAppStatsListener listener) { 110 mAppStatsListeners.add(listener); 111 } 112 113 @VisibleForTesting loadDisplayableRecentApps(int number)114 void loadDisplayableRecentApps(int number) { 115 mRecentApps.clear(); 116 mCalendar = Calendar.getInstance(); 117 mCalendar.add(Calendar.DAY_OF_YEAR, -1); 118 final List<UsageStats> mStats = mPowerManager.isPowerSaveMode() 119 ? new ArrayList<>() 120 : mUsageStatsManager.queryUsageStats( 121 UsageStatsManager.INTERVAL_BEST, mCalendar.getTimeInMillis(), 122 System.currentTimeMillis()); 123 124 final Map<String, UsageStats> map = new ArrayMap<>(); 125 final int statCount = mStats.size(); 126 for (int i = 0; i < statCount; i++) { 127 final UsageStats pkgStats = mStats.get(i); 128 if (!shouldIncludePkgInRecents(pkgStats)) { 129 continue; 130 } 131 final String pkgName = pkgStats.getPackageName(); 132 final UsageStats existingStats = map.get(pkgName); 133 if (existingStats == null) { 134 map.put(pkgName, pkgStats); 135 } else { 136 existingStats.add(pkgStats); 137 } 138 } 139 final List<UsageStats> packageStats = new ArrayList<>(); 140 packageStats.addAll(map.values()); 141 Collections.sort(packageStats, this /* comparator */); 142 int count = 0; 143 for (UsageStats stat : packageStats) { 144 final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( 145 stat.getPackageName(), mUserId); 146 if (appEntry == null) { 147 continue; 148 } 149 mRecentApps.add(stat); 150 count++; 151 if (count >= number) { 152 break; 153 } 154 } 155 } 156 157 /** 158 * Whether or not the app should be included in recent list. 159 */ shouldIncludePkgInRecents(UsageStats stat)160 private boolean shouldIncludePkgInRecents(UsageStats stat) { 161 final String pkgName = stat.getPackageName(); 162 if (stat.getLastTimeUsed() < mCalendar.getTimeInMillis()) { 163 Log.d(TAG, "Invalid timestamp (usage time is more than 24 hours ago), skipping " 164 + pkgName); 165 return false; 166 } 167 168 if (SKIP_SYSTEM_PACKAGES.contains(pkgName)) { 169 Log.d(TAG, "System package, skipping " + pkgName); 170 return false; 171 } 172 if (AppUtils.isHiddenSystemModule(mContext, pkgName)) { 173 return false; 174 } 175 final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER) 176 .setPackage(pkgName); 177 178 if (mPm.resolveActivity(launchIntent, 0) == null) { 179 // Not visible on launcher -> likely not a user visible app, skip if non-instant. 180 final ApplicationsState.AppEntry appEntry = 181 mApplicationsState.getEntry(pkgName, mUserId); 182 if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { 183 Log.d(TAG, "Not a user visible or instant app, skipping " + pkgName); 184 return false; 185 } 186 } 187 return true; 188 } 189 190 public interface RecentAppStatsListener { 191 onReloadDataCompleted(List<UsageStats> recentApps)192 void onReloadDataCompleted(List<UsageStats> recentApps); 193 } 194 } 195