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.car.settings.applications;
18 
19 import android.app.Application;
20 import android.app.usage.UsageStats;
21 import android.app.usage.UsageStatsManager;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.PackageManager;
25 import android.os.UserHandle;
26 import android.util.ArrayMap;
27 import android.util.SparseArray;
28 
29 import androidx.annotation.NonNull;
30 import androidx.annotation.VisibleForTesting;
31 
32 import com.android.car.settings.R;
33 import com.android.car.settings.common.Logger;
34 import com.android.settingslib.applications.AppUtils;
35 import com.android.settingslib.applications.ApplicationsState;
36 import com.android.settingslib.utils.ThreadUtils;
37 
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Calendar;
41 import java.util.Collections;
42 import java.util.Comparator;
43 import java.util.List;
44 import java.util.Map;
45 
46 /**
47  * Class for fetching and returning recently used apps. Largely derived from
48  * {@link com.android.settings.applications.RecentAppStatsMixin}.
49  */
50 public class RecentAppsItemManager implements Comparator<UsageStats> {
51 
52     private static final Logger LOG = new Logger(RecentAppsItemManager.class);
53 
54     @VisibleForTesting
55     final List<UsageStats> mRecentApps;
56     private final int mUserId;
57     private final int mMaximumApps;
58     private final Context mContext;
59     private final PackageManager mPm;
60     private final UsageStatsManager mUsageStatsManager;
61     private final ApplicationsState mApplicationsState;
62     private final SparseArray<RecentAppStatsListener> mAppStatsListeners;
63     private final int mDaysThreshold;
64     private final List<String> mIgnoredPackages;
65     private Calendar mCalendar;
66 
RecentAppsItemManager(Context context, int maximumApps)67     public RecentAppsItemManager(Context context, int maximumApps) {
68         this(context, maximumApps, ApplicationsState.getInstance(
69                 (Application) context.getApplicationContext()));
70     }
71 
72     @VisibleForTesting
RecentAppsItemManager(Context context, int maximumApps, ApplicationsState applicationsState)73     RecentAppsItemManager(Context context, int maximumApps, ApplicationsState applicationsState) {
74         mContext = context;
75         mMaximumApps = maximumApps;
76         mUserId = UserHandle.myUserId();
77         mPm = mContext.getPackageManager();
78         mUsageStatsManager = mContext.getSystemService(UsageStatsManager.class);
79         mApplicationsState = applicationsState;
80         mRecentApps = new ArrayList<>();
81         mAppStatsListeners = new SparseArray<>();
82         mDaysThreshold = mContext.getResources()
83                 .getInteger(R.integer.recent_apps_days_threshold);
84         mIgnoredPackages = Arrays.asList(mContext.getResources()
85                 .getStringArray(R.array.recent_apps_ignored_packages));
86     }
87 
88     /**
89      * Starts fetching recently used apps
90      */
startLoading()91     public void startLoading() {
92         ThreadUtils.postOnBackgroundThread(() -> {
93             loadDisplayableRecentApps(mMaximumApps);
94             for (int i = 0; i < mAppStatsListeners.size(); i++) {
95                 int finalIndex = i;
96                 ThreadUtils.postOnMainThread(() -> mAppStatsListeners.valueAt(finalIndex)
97                         .onRecentAppStatsLoaded(mRecentApps));
98             }
99         });
100     }
101 
102     @Override
compare(UsageStats a, UsageStats b)103     public final int compare(UsageStats a, UsageStats b) {
104         // return by descending order
105         return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed());
106     }
107 
108     /**
109      * Registers a listener that will be notified once the data is loaded.
110      */
addListener(@onNull RecentAppStatsListener listener)111     public void addListener(@NonNull RecentAppStatsListener listener) {
112         mAppStatsListeners.append(mAppStatsListeners.size(), listener);
113     }
114 
115     @VisibleForTesting
loadDisplayableRecentApps(int number)116     void loadDisplayableRecentApps(int number) {
117         mRecentApps.clear();
118         mCalendar = Calendar.getInstance();
119         mCalendar.add(Calendar.DAY_OF_YEAR, -mDaysThreshold);
120         List<UsageStats> mStats = mUsageStatsManager.queryUsageStats(
121                         UsageStatsManager.INTERVAL_BEST, mCalendar.getTimeInMillis(),
122                         System.currentTimeMillis());
123 
124         Map<String, UsageStats> map = new ArrayMap<>();
125         for (UsageStats pkgStats : mStats) {
126             if (!shouldIncludePkgInRecents(pkgStats)) {
127                 continue;
128             }
129             String pkgName = pkgStats.getPackageName();
130             UsageStats existingStats = map.get(pkgName);
131             if (existingStats == null) {
132                 map.put(pkgName, pkgStats);
133             } else {
134                 existingStats.add(pkgStats);
135             }
136         }
137         List<UsageStats> packageStats = new ArrayList<>();
138         packageStats.addAll(map.values());
139         Collections.sort(packageStats, /* comparator= */ this);
140         int count = 0;
141         for (UsageStats stat : packageStats) {
142             ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(
143                     stat.getPackageName(), mUserId);
144             if (appEntry == null) {
145                 continue;
146             }
147             mRecentApps.add(stat);
148             count++;
149             if (count >= number) {
150                 break;
151             }
152         }
153     }
154 
155     /**
156      * Whether or not the app should be included in recent list.
157      */
shouldIncludePkgInRecents(UsageStats stat)158     private boolean shouldIncludePkgInRecents(UsageStats stat) {
159         String pkgName = stat.getPackageName();
160         if (stat.getLastTimeUsed() < mCalendar.getTimeInMillis()) {
161             LOG.d("Invalid timestamp (usage time is more than 24 hours ago), skipping "
162                     + pkgName);
163             return false;
164         }
165 
166         if (mIgnoredPackages.contains(pkgName)) {
167             LOG.d("System package, skipping " + pkgName);
168             return false;
169         }
170 
171         if (AppUtils.isHiddenSystemModule(mContext, pkgName)) {
172             return false;
173         }
174 
175         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             ApplicationsState.AppEntry appEntry =
181                     mApplicationsState.getEntry(pkgName, mUserId);
182             if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) {
183                 LOG.d("Not a user visible or instant app, skipping " + pkgName);
184                 return false;
185             }
186         }
187         return true;
188     }
189 
190     /**
191      * Callback that is called once the recently used apps have been fetched.
192      */
193     public interface RecentAppStatsListener {
194         /**
195          * Called when the recently used apps are successfully loaded
196          */
onRecentAppStatsLoaded(List<UsageStats> recentAppStats)197         void onRecentAppStatsLoaded(List<UsageStats> recentAppStats);
198     }
199 }
200