From abb04be44de9e85f66f2ec0d3f18909f772a61b3 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Thu, 2 May 2019 16:44:52 -0400 Subject: [PATCH] Populate recent notifying apps from usage events Which is the data source that the screen this widget links to uses Test: robotests Fixes: 131641848 Change-Id: I02ce178823e72b3b3606ad4f7110587b6dc0afe3 --- .../ConfigureNotificationSettings.java | 7 +- .../notification/NotificationBackend.java | 9 - ...centNotifyingAppsPreferenceController.java | 81 ++++++- ...NotifyingAppsPreferenceControllerTest.java | 202 +++++++++++++----- 4 files changed, 225 insertions(+), 74 deletions(-) diff --git a/src/com/android/settings/notification/ConfigureNotificationSettings.java b/src/com/android/settings/notification/ConfigureNotificationSettings.java index d9d2b9be182..5f9cf5fc25a 100644 --- a/src/com/android/settings/notification/ConfigureNotificationSettings.java +++ b/src/com/android/settings/notification/ConfigureNotificationSettings.java @@ -21,10 +21,13 @@ import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY; import android.app.Activity; import android.app.Application; import android.app.settings.SettingsEnums; +import android.app.usage.IUsageStatsManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.os.ServiceManager; import android.os.UserHandle; +import android.os.UserManager; import android.provider.SearchIndexableResource; import android.text.TextUtils; @@ -112,7 +115,9 @@ public class ConfigureNotificationSettings extends DashboardFragment implements lifecycle.addObserver(lockScreenNotificationController); } controllers.add(new RecentNotifyingAppsPreferenceController( - context, new NotificationBackend(), app, host)); + context, new NotificationBackend(), IUsageStatsManager.Stub.asInterface( + ServiceManager.getService(Context.USAGE_STATS_SERVICE)), + context.getSystemService(UserManager.class), app, host)); controllers.add(lockScreenNotificationController); controllers.add(new NotificationRingtonePreferenceController(context) { @Override diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java index 747c541e4b6..7d7ca8231f9 100644 --- a/src/com/android/settings/notification/NotificationBackend.java +++ b/src/com/android/settings/notification/NotificationBackend.java @@ -312,15 +312,6 @@ public class NotificationBackend { } } - public List getRecentApps() { - try { - return sINM.getRecentNotifyingAppsForUser(UserHandle.myUserId()).getList(); - } catch (Exception e) { - Log.w(TAG, "Error calling NoMan", e); - return new ArrayList<>(); - } - } - public int getBlockedAppCount() { try { return sINM.getBlockedAppCount(UserHandle.myUserId()); diff --git a/src/com/android/settings/notification/RecentNotifyingAppsPreferenceController.java b/src/com/android/settings/notification/RecentNotifyingAppsPreferenceController.java index 8122df7bdc0..1fe7e7d1d8a 100644 --- a/src/com/android/settings/notification/RecentNotifyingAppsPreferenceController.java +++ b/src/com/android/settings/notification/RecentNotifyingAppsPreferenceController.java @@ -18,11 +18,15 @@ 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.Intent; 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; @@ -30,13 +34,8 @@ import android.util.ArraySet; import android.util.IconDrawableFactory; import android.util.Log; -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; @@ -47,11 +46,18 @@ import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.utils.StringUtil; import java.util.ArrayList; +import java.util.Calendar; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + /** * 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". @@ -66,28 +72,35 @@ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceC @VisibleForTesting static final String KEY_SEE_ALL = "all_notifications"; private static final int SHOW_RECENT_APP_COUNT = 5; + private static final int DAYS = 3; private static final Set SKIP_SYSTEM_PACKAGES = new ArraySet<>(); private final Fragment mHost; private final PackageManager mPm; private final NotificationBackend mNotificationBackend; + private IUsageStatsManager mUsageStatsManager; private final int mUserId; private final IconDrawableFactory mIconDrawableFactory; - private List mApps; + private Calendar mCal; + List mApps; private final ApplicationsState mApplicationsState; private PreferenceCategory mCategory; private Preference mSeeAllPref; private Preference mDivider; + protected List mUserIds; public RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, + IUsageStatsManager usageStatsManager, UserManager userManager, Application app, Fragment host) { - this(context, backend, app == null ? null : ApplicationsState.getInstance(app), host); + this(context, backend, usageStatsManager, userManager, + app == null ? null : ApplicationsState.getInstance(app), host); } @VisibleForTesting(otherwise = VisibleForTesting.NONE) RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, + IUsageStatsManager usageStatsManager, UserManager userManager, ApplicationsState appState, Fragment host) { super(context); mIconDrawableFactory = IconDrawableFactory.newInstance(context); @@ -96,6 +109,13 @@ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceC 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 @@ -145,7 +165,48 @@ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceC @VisibleForTesting void reloadData() { - mApps = mNotificationBackend.getRecentApps(); + 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() { @@ -185,7 +246,7 @@ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceC // 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(), mUserId); + mApplicationsState.getEntry(app.getPackage(), app.getUserId()); if (appEntry == null) { continue; } diff --git a/tests/robotests/src/com/android/settings/notification/RecentNotifyingAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/RecentNotifyingAppsPreferenceControllerTest.java index 4221a2117cf..d47d1257816 100644 --- a/tests/robotests/src/com/android/settings/notification/RecentNotifyingAppsPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/RecentNotifyingAppsPreferenceControllerTest.java @@ -20,8 +20,10 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -31,22 +33,20 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.usage.IUsageStatsManager; +import android.app.usage.UsageEvents; +import android.app.usage.UsageEvents.Event; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.os.Parcel; import android.os.UserHandle; import android.os.UserManager; import android.service.notification.NotifyingApp; import android.text.TextUtils; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceScreen; - import com.android.settings.R; import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; @@ -67,6 +67,12 @@ import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; import java.util.List; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + @RunWith(RobolectricTestRunner.class) public class RecentNotifyingAppsPreferenceControllerTest { @@ -94,6 +100,8 @@ public class RecentNotifyingAppsPreferenceControllerTest { private Fragment mHost; @Mock private FragmentActivity mActivity; + @Mock + private IUsageStatsManager mIUsageStatsManager; private Context mContext; private RecentNotifyingAppsPreferenceController mController; @@ -104,9 +112,10 @@ public class RecentNotifyingAppsPreferenceControllerTest { mContext = spy(RuntimeEnvironment.application); doReturn(mUserManager).when(mContext).getSystemService(Context.USER_SERVICE); doReturn(mPackageManager).when(mContext).getPackageManager(); + when(mUserManager.getProfileIdsWithDisabled(0)).thenReturn(new int[] {0}); mController = new RecentNotifyingAppsPreferenceController( - mContext, mBackend, mAppState, mHost); + mContext, mBackend, mIUsageStatsManager, mUserManager, mAppState, mHost); when(mScreen.findPreference(anyString())).thenReturn(mCategory); when(mScreen.findPreference(RecentNotifyingAppsPreferenceController.KEY_SEE_ALL)) @@ -135,7 +144,7 @@ public class RecentNotifyingAppsPreferenceControllerTest { @Test public void onDisplayAndUpdateState_shouldRefreshUi() { mController = spy(new RecentNotifyingAppsPreferenceController( - mContext, null, (ApplicationsState) null, null)); + mContext, null, mIUsageStatsManager, mUserManager, (ApplicationsState) null, null)); doNothing().when(mController).refreshUi(mContext); @@ -158,32 +167,41 @@ public class RecentNotifyingAppsPreferenceControllerTest { } @Test - public void display_showRecents() { - final List apps = new ArrayList<>(); - final NotifyingApp app1 = new NotifyingApp() - .setPackage("pkg.class") - .setLastNotified(System.currentTimeMillis()); - final NotifyingApp app2 = new NotifyingApp() - .setLastNotified(System.currentTimeMillis()) - .setPackage("com.android.settings"); - final NotifyingApp app3 = new NotifyingApp() - .setLastNotified(System.currentTimeMillis() - 1000) - .setPackage("pkg.class2"); + public void display_showRecents() throws Exception { - apps.add(app1); - apps.add(app2); - apps.add(app3); + List events = new ArrayList<>(); + Event app = new Event(); + app.mEventType = Event.NOTIFICATION_INTERRUPTION; + app.mPackage = "a"; + app.mTimeStamp = System.currentTimeMillis(); + events.add(app); + Event app1 = new Event(); + app1.mEventType = Event.NOTIFICATION_INTERRUPTION; + app1.mPackage = "com.android.settings"; + app1.mTimeStamp = System.currentTimeMillis(); + events.add(app1); + Event app2 = new Event(); + app2.mEventType = Event.NOTIFICATION_INTERRUPTION; + app2.mPackage = "pkg.class2"; + app2.mTimeStamp = System.currentTimeMillis() - 1000; + events.add(app2); // app1, app2 are valid apps. app3 is invalid. - when(mAppState.getEntry(app1.getPackage(), UserHandle.myUserId())) + when(mAppState.getEntry(app.getPackageName(), UserHandle.myUserId())) .thenReturn(mAppEntry); - when(mAppState.getEntry(app2.getPackage(), UserHandle.myUserId())) + when(mAppState.getEntry(app1.getPackageName(), UserHandle.myUserId())) .thenReturn(mAppEntry); - when(mAppState.getEntry(app3.getPackage(), UserHandle.myUserId())) + when(mAppState.getEntry(app2.getPackageName(), UserHandle.myUserId())) .thenReturn(null); when(mPackageManager.resolveActivity(any(Intent.class), anyInt())).thenReturn( new ResolveInfo()); - when(mBackend.getRecentApps()).thenReturn(apps); + + UsageEvents usageEvents = getUsageEvents( + new String[] {app.getPackageName(), app1.getPackageName(), app2.getPackageName()}, + events); + when(mIUsageStatsManager.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString())) + .thenReturn(usageEvents); + mAppEntry.info = mApplicationInfo; mController.displayPreference(mScreen); @@ -198,34 +216,37 @@ public class RecentNotifyingAppsPreferenceControllerTest { } @Test - public void display_showRecentsWithInstantApp() { - // Regular app. - final List apps = new ArrayList<>(); - final NotifyingApp app1 = new NotifyingApp(). - setLastNotified(System.currentTimeMillis()) - .setPackage("com.foo.bar"); - apps.add(app1); - - // Instant app. - final NotifyingApp app2 = new NotifyingApp() - .setLastNotified(System.currentTimeMillis() + 200) - .setPackage("com.foo.barinstant"); - apps.add(app2); + public void display_showRecentsWithInstantApp() throws Exception { + List events = new ArrayList<>(); + Event app = new Event(); + app.mEventType = Event.NOTIFICATION_INTERRUPTION; + app.mPackage = "com.foo.bar"; + app.mTimeStamp = System.currentTimeMillis(); + events.add(app); + Event app1 = new Event(); + app1.mEventType = Event.NOTIFICATION_INTERRUPTION; + app1.mPackage = "com.foo.barinstant"; + app1.mTimeStamp = System.currentTimeMillis() + 200; + events.add(app1); + UsageEvents usageEvents = getUsageEvents( + new String[] {"com.foo.bar", "com.foo.barinstant"}, events); + when(mIUsageStatsManager.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString())) + .thenReturn(usageEvents); ApplicationsState.AppEntry app1Entry = mock(ApplicationsState.AppEntry.class); ApplicationsState.AppEntry app2Entry = mock(ApplicationsState.AppEntry.class); app1Entry.info = mApplicationInfo; app2Entry.info = mApplicationInfo; - when(mAppState.getEntry(app1.getPackage(), UserHandle.myUserId())).thenReturn(app1Entry); - when(mAppState.getEntry(app2.getPackage(), UserHandle.myUserId())).thenReturn(app2Entry); + when(mAppState.getEntry( + app.getPackageName(), UserHandle.myUserId())).thenReturn(app1Entry); + when(mAppState.getEntry( + app1.getPackageName(), UserHandle.myUserId())).thenReturn(app2Entry); // Only the regular app app1 should have its intent resolve. - when(mPackageManager.resolveActivity(argThat(intentMatcher(app1.getPackage())), + when(mPackageManager.resolveActivity(argThat(intentMatcher(app.getPackageName())), anyInt())).thenReturn(new ResolveInfo()); - when(mBackend.getRecentApps()).thenReturn(apps); - // Make sure app2 is considered an instant app. ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider", (InstantAppDataProvider) (ApplicationInfo info) -> { @@ -241,23 +262,27 @@ public class RecentNotifyingAppsPreferenceControllerTest { ArgumentCaptor prefCaptor = ArgumentCaptor.forClass(Preference.class); verify(mCategory, times(2)).addPreference(prefCaptor.capture()); List prefs = prefCaptor.getAllValues(); - assertThat(prefs.get(1).getKey()).isEqualTo(app1.getPackage()); - assertThat(prefs.get(0).getKey()).isEqualTo(app2.getPackage()); + assertThat(prefs.get(1).getKey()).isEqualTo(app.getPackageName()); + assertThat(prefs.get(0).getKey()).isEqualTo(app1.getPackageName()); } @Test - public void display_showRecents_formatSummary() { - final List apps = new ArrayList<>(); - final NotifyingApp app1 = new NotifyingApp() - .setLastNotified(System.currentTimeMillis()) - .setPackage("pkg.class"); - apps.add(app1); + public void display_showRecents_formatSummary() throws Exception { + List events = new ArrayList<>(); + Event app = new Event(); + app.mEventType = Event.NOTIFICATION_INTERRUPTION; + app.mPackage = "pkg.class"; + app.mTimeStamp = System.currentTimeMillis(); + events.add(app); + UsageEvents usageEvents = getUsageEvents(new String[] {"pkg.class"}, events); + when(mIUsageStatsManager.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString())) + .thenReturn(usageEvents); - when(mAppState.getEntry(app1.getPackage(), UserHandle.myUserId())) + when(mAppState.getEntry(app.getPackageName(), UserHandle.myUserId())) .thenReturn(mAppEntry); when(mPackageManager.resolveActivity(any(Intent.class), anyInt())).thenReturn( new ResolveInfo()); - when(mBackend.getRecentApps()).thenReturn(apps); + mAppEntry.info = mApplicationInfo; mController.displayPreference(mScreen); @@ -265,6 +290,66 @@ public class RecentNotifyingAppsPreferenceControllerTest { verify(mCategory).addPreference(argThat(summaryMatches("Just now"))); } + @Test + public void reloadData() throws Exception { + when(mUserManager.getProfileIdsWithDisabled(0)).thenReturn(new int[] {0, 10}); + + mController = new RecentNotifyingAppsPreferenceController( + mContext, mBackend, mIUsageStatsManager, mUserManager, mAppState, mHost); + + List events = new ArrayList<>(); + Event app = new Event(); + app.mEventType = Event.NOTIFICATION_INTERRUPTION; + app.mPackage = "b"; + app.mTimeStamp = 1; + events.add(app); + Event app1 = new Event(); + app1.mEventType = Event.MAX_EVENT_TYPE; + app1.mPackage = "com.foo.bar"; + app1.mTimeStamp = 10; + events.add(app1); + UsageEvents usageEvents = getUsageEvents( + new String[] {"b", "com.foo.bar"}, events); + when(mIUsageStatsManager.queryEventsForUser(anyLong(), anyLong(), eq(0), anyString())) + .thenReturn(usageEvents); + + List events10 = new ArrayList<>(); + Event app10 = new Event(); + app10.mEventType = Event.NOTIFICATION_INTERRUPTION; + app10.mPackage = "a"; + app10.mTimeStamp = 2; + events10.add(app10); + Event app10a = new Event(); + app10a.mEventType = Event.NOTIFICATION_INTERRUPTION; + app10a.mPackage = "a"; + app10a.mTimeStamp = 20; + events10.add(app10a); + UsageEvents usageEvents10 = getUsageEvents( + new String[] {"a"}, events10); + when(mIUsageStatsManager.queryEventsForUser(anyLong(), anyLong(), eq(10), anyString())) + .thenReturn(usageEvents10); + + mController.reloadData(); + + assertThat(mController.mApps.size()).isEqualTo(2); + boolean foundPkg0 = false; + boolean foundPkg10 = false; + for (NotifyingApp notifyingApp : mController.mApps) { + if (notifyingApp.getLastNotified() == 20 + && notifyingApp.getPackage().equals("a") + && notifyingApp.getUserId() == 10) { + foundPkg10 = true; + } + if (notifyingApp.getLastNotified() == 1 + && notifyingApp.getPackage().equals("b") + && notifyingApp.getUserId() == 0) { + foundPkg0 = true; + } + } + assertThat(foundPkg0).isTrue(); + assertThat(foundPkg10).isTrue(); + } + private static ArgumentMatcher summaryMatches(String expected) { return preference -> TextUtils.equals(expected, preference.getSummary()); } @@ -273,4 +358,13 @@ public class RecentNotifyingAppsPreferenceControllerTest { private static ArgumentMatcher intentMatcher(String packageName) { return intent -> packageName.equals(intent.getPackage()); } + + private UsageEvents getUsageEvents(String[] pkgs, List events) { + UsageEvents usageEvents = new UsageEvents(events, pkgs); + Parcel parcel = Parcel.obtain(); + parcel.setDataPosition(0); + usageEvents.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return UsageEvents.CREATOR.createFromParcel(parcel); + } }