/* * Copyright (C) 2017 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.settings.applications; import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; import android.app.Application; import android.app.settings.SettingsEnums; 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.PowerManager; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IconDrawableFactory; import android.util.Log; import android.view.View; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.applications.appinfo.AppInfoDashboardFragment; import com.android.settings.applications.manageapplications.ManageApplications; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.utils.StringUtil; import com.android.settingslib.widget.AppEntitiesHeaderController; import com.android.settingslib.widget.AppEntityInfo; import com.android.settingslib.widget.LayoutPreference; 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; import java.util.Set; /** * This controller displays up to three recently used apps. * If there is no recently used app, we only show up an "App Info" preference. */ public class RecentAppsPreferenceController extends BasePreferenceController implements Comparator { @VisibleForTesting static final String KEY_DIVIDER = "recent_apps_divider"; private static final String TAG = "RecentAppsCtrl"; private static final Set SKIP_SYSTEM_PACKAGES = new ArraySet<>(); @VisibleForTesting AppEntitiesHeaderController mAppEntitiesController; @VisibleForTesting LayoutPreference mRecentAppsPreference; @VisibleForTesting Preference mDivider; private final PackageManager mPm; private final UsageStatsManager mUsageStatsManager; private final ApplicationsState mApplicationsState; private final int mUserId; private final IconDrawableFactory mIconDrawableFactory; private final PowerManager mPowerManager; private Fragment mHost; private Calendar mCal; private List mStats; private List mRecentApps; private boolean mHasRecentApps; static { SKIP_SYSTEM_PACKAGES.addAll(Arrays.asList( "android", "com.android.phone", SETTINGS_PACKAGE_NAME, "com.android.systemui", "com.android.providers.calendar", "com.android.providers.media" )); } public RecentAppsPreferenceController(Context context, String key) { super(context, key); mApplicationsState = ApplicationsState.getInstance( (Application) mContext.getApplicationContext()); mUserId = UserHandle.myUserId(); mPm = mContext.getPackageManager(); mIconDrawableFactory = IconDrawableFactory.newInstance(mContext); mPowerManager = mContext.getSystemService(PowerManager.class); mUsageStatsManager = mContext.getSystemService(UsageStatsManager.class); mRecentApps = new ArrayList<>(); reloadData(); } public void setFragment(Fragment fragment) { mHost = fragment; } @Override public int getAvailabilityStatus() { return mRecentApps.isEmpty() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); mDivider = screen.findPreference(KEY_DIVIDER); mRecentAppsPreference = (LayoutPreference) screen.findPreference(getPreferenceKey()); final View view = mRecentAppsPreference.findViewById(R.id.app_entities_header); mAppEntitiesController = AppEntitiesHeaderController.newInstance(mContext, view) .setHeaderTitleRes(R.string.recent_app_category_title) .setHeaderDetailsClickListener((View v) -> { new SubSettingLauncher(mContext) .setDestination(ManageApplications.class.getName()) .setArguments(null /* arguments */) .setTitleRes(R.string.application_info_label) .setSourceMetricsCategory(SettingsEnums.SETTINGS_APP_NOTIF_CATEGORY) .launch(); }); refreshUi(); } @Override public void updateState(Preference preference) { super.updateState(preference); refreshUi(); // Show total number of installed apps as See all's summary. new InstalledAppCounter(mContext, InstalledAppCounter.IGNORE_INSTALL_REASON, mContext.getPackageManager()) { @Override protected void onCountComplete(int num) { mAppEntitiesController.setHeaderDetails( mContext.getString(R.string.see_all_apps_title, num)); mAppEntitiesController.apply(); } }.execute(); } @Override public final int compare(UsageStats a, UsageStats b) { // return by descending order return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed()); } List getRecentApps() { return mRecentApps; } @VisibleForTesting void refreshUi() { if (mRecentApps != null && !mRecentApps.isEmpty()) { displayRecentApps(); } else { mDivider.setVisible(false); } } @VisibleForTesting void reloadData() { mCal = Calendar.getInstance(); mCal.add(Calendar.DAY_OF_YEAR, -1); mStats = mPowerManager.isPowerSaveMode() ? new ArrayList<>() : mUsageStatsManager.queryUsageStats( UsageStatsManager.INTERVAL_BEST, mCal.getTimeInMillis(), System.currentTimeMillis()); updateDisplayableRecentAppList(); } private void displayRecentApps() { int showAppsCount = 0; for (UsageStats stat : mRecentApps) { final AppEntityInfo appEntityInfoInfo = createAppEntity(stat); if (appEntityInfoInfo != null) { mAppEntitiesController.setAppEntity(showAppsCount++, appEntityInfoInfo); } if (showAppsCount == AppEntitiesHeaderController.MAXIMUM_APPS) { break; } } mAppEntitiesController.apply(); mDivider.setVisible(true); } private AppEntityInfo createAppEntity(UsageStats stat) { final String pkgName = stat.getPackageName(); final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(pkgName, mUserId); if (appEntry == null) { return null; } return new AppEntityInfo.Builder() .setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info)) .setTitle(appEntry.label) .setSummary(StringUtil.formatRelativeTime(mContext, System.currentTimeMillis() - stat.getLastTimeUsed(), false)) .setOnClickListener(v -> AppInfoBase.startAppInfoFragment(AppInfoDashboardFragment.class, R.string.application_info_label, pkgName, appEntry.info.uid, mHost, 1001 /*RequestCode*/, SettingsEnums.SETTINGS_APP_NOTIF_CATEGORY)) .build(); } private void updateDisplayableRecentAppList() { mRecentApps.clear(); final Map map = new ArrayMap<>(); final int statCount = mStats.size(); for (int i = 0; i < statCount; i++) { final UsageStats pkgStats = mStats.get(i); if (!shouldIncludePkgInRecents(pkgStats)) { continue; } final String pkgName = pkgStats.getPackageName(); final UsageStats existingStats = map.get(pkgName); if (existingStats == null) { map.put(pkgName, pkgStats); } else { existingStats.add(pkgStats); } } final List packageStats = new ArrayList<>(); packageStats.addAll(map.values()); Collections.sort(packageStats, this /* comparator */); int count = 0; for (UsageStats stat : packageStats) { final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( stat.getPackageName(), mUserId); if (appEntry == null) { continue; } mRecentApps.add(stat); count++; if (count >= AppEntitiesHeaderController.MAXIMUM_APPS) { break; } } } /** * Whether or not the app should be included in recent list. */ private boolean shouldIncludePkgInRecents(UsageStats stat) { final String pkgName = stat.getPackageName(); if (stat.getLastTimeUsed() < mCal.getTimeInMillis()) { Log.d(TAG, "Invalid timestamp (usage time is more than 24 hours ago), skipping " + pkgName); return false; } if (SKIP_SYSTEM_PACKAGES.contains(pkgName)) { Log.d(TAG, "System package, skipping " + pkgName); return false; } if (AppUtils.isHiddenSystemModule(mContext, pkgName)) { return false; } final 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. final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(pkgName, mUserId); if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { Log.d(TAG, "Not a user visible or instant app, skipping " + pkgName); return false; } } return true; } }