/* * Copyright (C) 2018 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.notification; import android.app.Application; import android.app.settings.SettingsEnums; import android.app.usage.IUsageStatsManager; import android.app.usage.UsageEvents; import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.service.notification.NotifyingApp; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IconDrawableFactory; import android.util.Slog; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.applications.AppInfoBase; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.SubSettingLauncher; import com.android.settings.notification.app.AppNotificationSettings; import com.android.settings.widget.PrimarySwitchPreference; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.utils.StringUtil; import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.TwoTargetPreference; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; /** * This controller displays a list of recently used apps and a "See all" button. If there is * no recently used app, "See all" will be displayed as "Notifications". */ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin { private static final String TAG = "RecentNotisCtrl"; private static final String KEY_PREF_CATEGORY = "recent_notifications_category"; @VisibleForTesting static final String KEY_SEE_ALL = "all_notifications"; static final String KEY_PLACEHOLDER = "app"; private static final int SHOW_RECENT_APP_COUNT = 3; private static final int DAYS = 3; private final Fragment mHost; private final PackageManager mPm; private final NotificationBackend mNotificationBackend; private IUsageStatsManager mUsageStatsManager; private final IconDrawableFactory mIconDrawableFactory; private Calendar mCal; List mApps; private final ApplicationsState mApplicationsState; private PreferenceCategory mCategory; private Preference mSeeAllPref; protected List mUserIds; public RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, IUsageStatsManager usageStatsManager, UserManager userManager, Application app, Fragment host) { this(context, backend, usageStatsManager, userManager, app == null ? null : ApplicationsState.getInstance(app), host); } @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, IUsageStatsManager usageStatsManager, UserManager userManager, ApplicationsState appState, Fragment host) { super(context); mIconDrawableFactory = IconDrawableFactory.newInstance(context); mPm = context.getPackageManager(); mHost = host; mApplicationsState = appState; mNotificationBackend = backend; mUsageStatsManager = usageStatsManager; mUserIds = new ArrayList<>(); mUserIds.add(mContext.getUserId()); int workUserId = Utils.getManagedProfileId(userManager, mContext.getUserId()); if (workUserId != UserHandle.USER_NULL) { mUserIds.add(workUserId); } } @Override public boolean isAvailable() { return mApplicationsState != null; } @Override public String getPreferenceKey() { return KEY_PREF_CATEGORY; } @Override public void updateNonIndexableKeys(List keys) { PreferenceControllerMixin.super.updateNonIndexableKeys(keys); // Don't index category name into search. It's not actionable. keys.add(KEY_PREF_CATEGORY); } @Override public void displayPreference(PreferenceScreen screen) { mCategory = screen.findPreference(getPreferenceKey()); mSeeAllPref = screen.findPreference(KEY_SEE_ALL); super.displayPreference(screen); refreshUi(mCategory.getContext()); } @Override public void updateState(Preference preference) { super.updateState(preference); refreshUi(mCategory.getContext()); mSeeAllPref.setTitle(mContext.getString(R.string.recent_notifications_see_all_title)); } @VisibleForTesting void refreshUi(Context prefContext) { ((PrimarySwitchPreference) mCategory.findPreference(KEY_PLACEHOLDER + 1)).setChecked(true); ((PrimarySwitchPreference) mCategory.findPreference(KEY_PLACEHOLDER + 2)).setChecked(true); ((PrimarySwitchPreference) mCategory.findPreference(KEY_PLACEHOLDER + 3)).setChecked(true); ThreadUtils.postOnBackgroundThread(() -> { reloadData(); final List recentApps = getDisplayableRecentAppList(); ThreadUtils.postOnMainThread(() -> { if (recentApps != null && !recentApps.isEmpty()) { displayRecentApps(prefContext, recentApps); } else { displayOnlyAllAppsLink(); } }); }); } @VisibleForTesting void reloadData() { mApps = new ArrayList<>(); mCal = Calendar.getInstance(); mCal.add(Calendar.DAY_OF_YEAR, -DAYS); for (int userId : mUserIds) { UsageEvents events = null; try { events = mUsageStatsManager.queryEventsForUser(mCal.getTimeInMillis(), System.currentTimeMillis(), userId, mContext.getPackageName()); } catch (RemoteException e) { e.printStackTrace(); } if (events != null) { ArrayMap aggregatedStats = new ArrayMap<>(); UsageEvents.Event event = new UsageEvents.Event(); while (events.hasNextEvent()) { events.getNextEvent(event); if (event.getEventType() == UsageEvents.Event.NOTIFICATION_INTERRUPTION) { NotifyingApp app = aggregatedStats.get(getKey(userId, event.getPackageName())); if (app == null) { app = new NotifyingApp(); aggregatedStats.put(getKey(userId, event.getPackageName()), app); app.setPackage(event.getPackageName()); app.setUserId(userId); } if (event.getTimeStamp() > app.getLastNotified()) { app.setLastNotified(event.getTimeStamp()); } } } mApps.addAll(aggregatedStats.values()); } } } private static String getKey(int userId, String pkg) { return userId + "|" + pkg; } private void displayOnlyAllAppsLink() { mCategory.setTitle(null); mSeeAllPref.setTitle(R.string.notifications_title); mSeeAllPref.setIcon(null); int prefCount = mCategory.getPreferenceCount(); for (int i = prefCount - 1; i >= 0; i--) { final Preference pref = mCategory.getPreference(i); if (!TextUtils.equals(pref.getKey(), KEY_SEE_ALL)) { mCategory.removePreference(pref); } } } private void displayRecentApps(Context prefContext, List recentApps) { mCategory.setTitle(R.string.recent_notifications); mSeeAllPref.setSummary(null); mSeeAllPref.setIcon(R.drawable.ic_chevron_right_24dp); int keyIndex = 1; final int recentAppsCount = recentApps.size(); for (int i = 0; i < recentAppsCount; i++, keyIndex++) { final NotifyingApp app = recentApps.get(i); // Bind recent apps to existing prefs if possible, or create a new pref. final String pkgName = app.getPackage(); final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(app.getPackage(), app.getUserId()); if (appEntry == null) { continue; } PrimarySwitchPreference pref = mCategory.findPreference(KEY_PLACEHOLDER + keyIndex); pref.setTitle(appEntry.label); pref.setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info)); pref.setIconSize(TwoTargetPreference.ICON_SIZE_SMALL); pref.setSummary(StringUtil.formatRelativeTime(mContext, System.currentTimeMillis() - app.getLastNotified(), true)); Bundle args = new Bundle(); args.putString(AppInfoBase.ARG_PACKAGE_NAME, pkgName); args.putInt(AppInfoBase.ARG_PACKAGE_UID, appEntry.info.uid); pref.setOnPreferenceClickListener(preference -> { new SubSettingLauncher(mHost.getActivity()) .setDestination(AppNotificationSettings.class.getName()) .setTitleRes(R.string.notifications_title) .setArguments(args) .setUserHandle(new UserHandle(UserHandle.getUserId(appEntry.info.uid))) .setSourceMetricsCategory( SettingsEnums.MANAGE_APPLICATIONS_NOTIFICATIONS) .launch(); return true; }); pref.setSwitchEnabled(mNotificationBackend.isBlockable(mContext, appEntry.info)); pref.setOnPreferenceChangeListener((preference, newValue) -> { mNotificationBackend.setNotificationsEnabledForPackage( pkgName, appEntry.info.uid, (Boolean) newValue); return true; }); pref.setChecked( !mNotificationBackend.getNotificationsBanned(pkgName, appEntry.info.uid)); } // If there are less than SHOW_RECENT_APP_COUNT recent apps, remove placeholders for (int i = keyIndex; i <= SHOW_RECENT_APP_COUNT; i++) { mCategory.removePreferenceRecursively(KEY_PLACEHOLDER + i); } } private List getDisplayableRecentAppList() { Collections.sort(mApps); List displayableApps = new ArrayList<>(SHOW_RECENT_APP_COUNT); int count = 0; for (NotifyingApp app : mApps) { try { final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( app.getPackage(), app.getUserId()); if (appEntry == null) { continue; } displayableApps.add(app); count++; if (count >= SHOW_RECENT_APP_COUNT) { break; } } catch (Exception e) { Slog.e(TAG, "Failed to find app " + app.getPackage() + "/" + app.getUserId(), e); } } return displayableApps; } }