/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.settings.applications; import android.app.Application; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.UserHandle; import android.util.ArrayMap; import android.util.SparseArray; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.car.settings.R; import com.android.car.settings.common.Logger; import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.utils.ThreadUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; /** * Class for fetching and returning recently used apps. Largely derived from * {@link com.android.settings.applications.RecentAppStatsMixin}. */ public class RecentAppsItemManager implements Comparator { private static final Logger LOG = new Logger(RecentAppsItemManager.class); @VisibleForTesting final List mRecentApps; private final int mUserId; private final int mMaximumApps; private final Context mContext; private final PackageManager mPm; private final UsageStatsManager mUsageStatsManager; private final ApplicationsState mApplicationsState; private final SparseArray mAppStatsListeners; private final int mDaysThreshold; private final List mIgnoredPackages; private Calendar mCalendar; public RecentAppsItemManager(Context context, int maximumApps) { this(context, maximumApps, ApplicationsState.getInstance( (Application) context.getApplicationContext())); } @VisibleForTesting RecentAppsItemManager(Context context, int maximumApps, ApplicationsState applicationsState) { mContext = context; mMaximumApps = maximumApps; mUserId = UserHandle.myUserId(); mPm = mContext.getPackageManager(); mUsageStatsManager = mContext.getSystemService(UsageStatsManager.class); mApplicationsState = applicationsState; mRecentApps = new ArrayList<>(); mAppStatsListeners = new SparseArray<>(); mDaysThreshold = mContext.getResources() .getInteger(R.integer.recent_apps_days_threshold); mIgnoredPackages = Arrays.asList(mContext.getResources() .getStringArray(R.array.recent_apps_ignored_packages)); } /** * Starts fetching recently used apps */ public void startLoading() { ThreadUtils.postOnBackgroundThread(() -> { loadDisplayableRecentApps(mMaximumApps); for (int i = 0; i < mAppStatsListeners.size(); i++) { int finalIndex = i; ThreadUtils.postOnMainThread(() -> mAppStatsListeners.valueAt(finalIndex) .onRecentAppStatsLoaded(mRecentApps)); } }); } @Override public final int compare(UsageStats a, UsageStats b) { // return by descending order return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed()); } /** * Registers a listener that will be notified once the data is loaded. */ public void addListener(@NonNull RecentAppStatsListener listener) { mAppStatsListeners.append(mAppStatsListeners.size(), listener); } @VisibleForTesting void loadDisplayableRecentApps(int number) { mRecentApps.clear(); mCalendar = Calendar.getInstance(); mCalendar.add(Calendar.DAY_OF_YEAR, -mDaysThreshold); List mStats = mUsageStatsManager.queryUsageStats( UsageStatsManager.INTERVAL_BEST, mCalendar.getTimeInMillis(), System.currentTimeMillis()); Map map = new ArrayMap<>(); for (UsageStats pkgStats : mStats) { if (!shouldIncludePkgInRecents(pkgStats)) { continue; } String pkgName = pkgStats.getPackageName(); UsageStats existingStats = map.get(pkgName); if (existingStats == null) { map.put(pkgName, pkgStats); } else { existingStats.add(pkgStats); } } List packageStats = new ArrayList<>(); packageStats.addAll(map.values()); Collections.sort(packageStats, /* comparator= */ this); int count = 0; for (UsageStats stat : packageStats) { ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( stat.getPackageName(), mUserId); if (appEntry == null) { continue; } mRecentApps.add(stat); count++; if (count >= number) { break; } } } /** * Whether or not the app should be included in recent list. */ private boolean shouldIncludePkgInRecents(UsageStats stat) { String pkgName = stat.getPackageName(); if (stat.getLastTimeUsed() < mCalendar.getTimeInMillis()) { LOG.d("Invalid timestamp (usage time is more than 24 hours ago), skipping " + pkgName); return false; } if (mIgnoredPackages.contains(pkgName)) { LOG.d("System package, skipping " + pkgName); return false; } if (AppUtils.isHiddenSystemModule(mContext, pkgName)) { return false; } Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER) .setPackage(pkgName); if (mPm.resolveActivity(launchIntent, 0) == null) { // Not visible on launcher -> likely not a user visible app, skip if non-instant. ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(pkgName, mUserId); if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { LOG.d("Not a user visible or instant app, skipping " + pkgName); return false; } } return true; } /** * Callback that is called once the recently used apps have been fetched. */ public interface RecentAppStatsListener { /** * Called when the recently used apps are successfully loaded */ void onRecentAppStatsLoaded(List recentAppStats); } }