1 /* 2 * Copyright (C) 2021 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.notifications; 18 19 import android.app.usage.IUsageStatsManager; 20 import android.app.usage.UsageEvents; 21 import android.car.drivingstate.CarUxRestrictions; 22 import android.content.Context; 23 import android.os.RemoteException; 24 import android.os.ServiceManager; 25 import android.service.notification.NotifyingApp; 26 import android.text.format.DateUtils; 27 28 import androidx.annotation.VisibleForTesting; 29 import androidx.collection.ArrayMap; 30 import androidx.preference.PreferenceCategory; 31 32 import com.android.car.settings.R; 33 import com.android.car.settings.applications.ApplicationDetailsFragment; 34 import com.android.car.settings.applications.ApplicationListItemManager; 35 import com.android.car.settings.common.FragmentController; 36 import com.android.car.settings.common.Logger; 37 import com.android.car.ui.preference.CarUiTwoActionSwitchPreference; 38 import com.android.settingslib.applications.ApplicationsState; 39 import com.android.settingslib.utils.ThreadUtils; 40 41 import java.util.ArrayList; 42 import java.util.Calendar; 43 import java.util.Collections; 44 import java.util.List; 45 46 /** 47 * This controller displays a list of recently used apps. Only non-system apps are displayed. 48 * This class is largely taken from 49 * {@link com.android.settings.notification.RecentNotifyingAppsPreferenceController} 50 */ 51 public class RecentNotificationsAppsPreferenceController extends 52 BaseNotificationsPreferenceController<PreferenceCategory> implements 53 ApplicationListItemManager.AppListItemListener { 54 55 private static final Logger LOG = new Logger(RecentNotificationsAppsPreferenceController.class); 56 57 private static final String KEY_PLACEHOLDER = "app"; 58 59 @VisibleForTesting 60 IUsageStatsManager mUsageStatsManager; 61 62 private final Integer mUserId; 63 private final int mRecentAppsMaxCount; 64 private final int mDaysThreshold; 65 private List<NotifyingApp> mApps; 66 private ApplicationsState mApplicationsState; 67 private NotificationsFragment.NotificationSwitchListener mNotificationSwitchListener; 68 RecentNotificationsAppsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)69 public RecentNotificationsAppsPreferenceController(Context context, String preferenceKey, 70 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 71 super(context, preferenceKey, fragmentController, uxRestrictions); 72 mUsageStatsManager = IUsageStatsManager.Stub.asInterface( 73 ServiceManager.getService(Context.USAGE_STATS_SERVICE)); 74 mUserId = context.getUserId(); 75 mRecentAppsMaxCount = context.getResources() 76 .getInteger(R.integer.recent_notifications_apps_list_count); 77 mDaysThreshold = context.getResources() 78 .getInteger(R.integer.recent_notifications_days_threshold); 79 } 80 setApplicationsState(ApplicationsState applicationsState)81 public void setApplicationsState(ApplicationsState applicationsState) { 82 mApplicationsState = applicationsState; 83 } 84 setNotificationSwitchListener( NotificationsFragment.NotificationSwitchListener listener)85 public void setNotificationSwitchListener( 86 NotificationsFragment.NotificationSwitchListener listener) { 87 mNotificationSwitchListener = listener; 88 } 89 90 @Override getPreferenceType()91 protected Class<PreferenceCategory> getPreferenceType() { 92 return PreferenceCategory.class; 93 } 94 95 @Override onDataLoaded(ArrayList<ApplicationsState.AppEntry> apps)96 public void onDataLoaded(ArrayList<ApplicationsState.AppEntry> apps) { 97 // App entries updated, refresh since filtered apps may have changed 98 refresh(); 99 } 100 101 @Override updateState(PreferenceCategory preference)102 public void updateState(PreferenceCategory preference) { 103 super.updateState(preference); 104 refresh(); 105 } 106 refresh()107 private void refresh() { 108 ThreadUtils.postOnBackgroundThread(() -> { 109 reloadData(); 110 List<NotifyingApp> recentApps = getDisplayableRecentAppList(); 111 ThreadUtils.postOnMainThread(() -> { 112 if (recentApps != null && !recentApps.isEmpty()) { 113 getPreference().setVisible(true); 114 displayRecentApps(recentApps); 115 } else { 116 getPreference().setVisible(false); 117 getPreference().removeAll(); 118 } 119 }); 120 }); 121 } 122 reloadData()123 private void reloadData() { 124 Calendar calendar = Calendar.getInstance(); 125 calendar.add(Calendar.DAY_OF_YEAR, -mDaysThreshold); 126 UsageEvents events = null; 127 try { 128 events = mUsageStatsManager.queryEventsForUser(calendar.getTimeInMillis(), 129 System.currentTimeMillis(), mUserId, getContext().getPackageName()); 130 } catch (RemoteException e) { 131 LOG.e("Failed querying user events", e); 132 } 133 134 if (events != null) { 135 ArrayMap<String, NotifyingApp> aggregatedStats = new ArrayMap<>(); 136 137 UsageEvents.Event event = new UsageEvents.Event(); 138 while (events.hasNextEvent()) { 139 events.getNextEvent(event); 140 if (event.getEventType() == UsageEvents.Event.NOTIFICATION_INTERRUPTION) { 141 NotifyingApp app = 142 aggregatedStats.get(getKey(mUserId, event.getPackageName())); 143 if (app == null) { 144 app = new NotifyingApp(); 145 aggregatedStats.put(getKey(mUserId, event.getPackageName()), app); 146 app.setPackage(event.getPackageName()); 147 app.setUserId(mUserId); 148 } 149 if (event.getTimeStamp() > app.getLastNotified()) { 150 app.setLastNotified(event.getTimeStamp()); 151 } 152 } 153 } 154 mApps = new ArrayList<>(); 155 mApps.addAll(aggregatedStats.values()); 156 } 157 } 158 getDisplayableRecentAppList()159 private List<NotifyingApp> getDisplayableRecentAppList() { 160 Collections.sort(mApps); 161 List<NotifyingApp> displayableApps = new ArrayList<>(mRecentAppsMaxCount); 162 int count = 0; 163 for (NotifyingApp app : mApps) { 164 try { 165 ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( 166 app.getPackage(), app.getUserId()); 167 if (appEntry == null || isSystemApp(appEntry)) { 168 continue; 169 } 170 displayableApps.add(app); 171 count++; 172 if (count >= mRecentAppsMaxCount) { 173 break; 174 } 175 } catch (Exception e) { 176 LOG.e("Failed to find app " + app.getPackage() + "/" + app.getUserId(), e); 177 } 178 } 179 return displayableApps; 180 } 181 displayRecentApps(List<NotifyingApp> recentApps)182 private void displayRecentApps(List<NotifyingApp> recentApps) { 183 int keyIndex = 1; 184 int recentAppsCount = recentApps.size(); 185 for (int i = 0; i < recentAppsCount; i++, keyIndex++) { 186 NotifyingApp app = recentApps.get(i); 187 // Bind recent apps to existing prefs if possible, or create a new pref. 188 String pkgName = app.getPackage(); 189 ApplicationsState.AppEntry appEntry = 190 mApplicationsState.getEntry(app.getPackage(), app.getUserId()); 191 if (appEntry == null || appEntry.label == null) { 192 continue; 193 } 194 195 CarUiTwoActionSwitchPreference pref = getPreference() 196 .findPreference(KEY_PLACEHOLDER + keyIndex); 197 if (pref == null) { 198 pref = new CarUiTwoActionSwitchPreference(getContext()); 199 pref.setKey(KEY_PLACEHOLDER + keyIndex); 200 getPreference().addPreference(pref); 201 } 202 pref.setTitle(appEntry.label); 203 pref.setIcon(appEntry.icon); 204 pref.setSummary(DateUtils.getRelativeTimeSpanString(app.getLastNotified(), 205 System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS)); 206 pref.setOnPreferenceClickListener(p -> { 207 getFragmentController().launchFragment( 208 ApplicationDetailsFragment.getInstance(pkgName)); 209 return true; 210 }); 211 212 pref.setOnSecondaryActionClickListener((newValue) -> { 213 toggleNotificationsSetting(pkgName, appEntry.info.uid, newValue); 214 if (mNotificationSwitchListener != null) { 215 mNotificationSwitchListener.onSwitchChanged(); 216 } 217 }); 218 pref.setSecondaryActionChecked(areNotificationsEnabled(pkgName, appEntry.info.uid)); 219 } 220 // If there are less than SHOW_RECENT_APP_COUNT recent apps, remove placeholders 221 for (int i = keyIndex; i <= mRecentAppsMaxCount; i++) { 222 getPreference().removePreferenceRecursively(KEY_PLACEHOLDER + i); 223 } 224 } 225 getKey(int userId, String pkg)226 private String getKey(int userId, String pkg) { 227 return userId + "|" + pkg; 228 } 229 230 /** Returns true if the app for the given package name is a system app for this device */ isSystemApp(ApplicationsState.AppEntry appEntry)231 private boolean isSystemApp(ApplicationsState.AppEntry appEntry) { 232 return !ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER_AND_INSTANT.filterApp(appEntry); 233 } 234 } 235