1 /* 2 * Copyright (C) 2018 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.settings.notification; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.mockito.ArgumentMatchers.any; 22 import static org.mockito.ArgumentMatchers.anyInt; 23 import static org.mockito.ArgumentMatchers.anyLong; 24 import static org.mockito.ArgumentMatchers.anyString; 25 import static org.mockito.ArgumentMatchers.argThat; 26 import static org.mockito.ArgumentMatchers.eq; 27 import static org.mockito.Mockito.doNothing; 28 import static org.mockito.Mockito.doReturn; 29 import static org.mockito.Mockito.mock; 30 import static org.mockito.Mockito.never; 31 import static org.mockito.Mockito.spy; 32 import static org.mockito.Mockito.times; 33 import static org.mockito.Mockito.verify; 34 import static org.mockito.Mockito.when; 35 36 import android.app.usage.IUsageStatsManager; 37 import android.app.usage.UsageEvents; 38 import android.app.usage.UsageEvents.Event; 39 import android.content.Context; 40 import android.content.Intent; 41 import android.content.pm.ApplicationInfo; 42 import android.content.pm.PackageManager; 43 import android.content.pm.ResolveInfo; 44 import android.os.Parcel; 45 import android.os.UserHandle; 46 import android.os.UserManager; 47 import android.service.notification.NotifyingApp; 48 import android.text.TextUtils; 49 50 import com.android.settings.R; 51 import com.android.settingslib.applications.AppUtils; 52 import com.android.settingslib.applications.ApplicationsState; 53 import com.android.settingslib.applications.instantapps.InstantAppDataProvider; 54 55 import org.junit.Before; 56 import org.junit.Test; 57 import org.junit.runner.RunWith; 58 import org.mockito.ArgumentCaptor; 59 import org.mockito.ArgumentMatcher; 60 import org.mockito.Mock; 61 import org.mockito.MockitoAnnotations; 62 import org.robolectric.RobolectricTestRunner; 63 import org.robolectric.RuntimeEnvironment; 64 import org.robolectric.annotation.Config; 65 import org.robolectric.util.ReflectionHelpers; 66 67 import java.util.ArrayList; 68 import java.util.List; 69 70 import androidx.fragment.app.Fragment; 71 import androidx.fragment.app.FragmentActivity; 72 import androidx.preference.Preference; 73 import androidx.preference.PreferenceCategory; 74 import androidx.preference.PreferenceScreen; 75 76 @RunWith(RobolectricTestRunner.class) 77 public class RecentNotifyingAppsPreferenceControllerTest { 78 79 @Mock 80 private PreferenceScreen mScreen; 81 @Mock 82 private PreferenceCategory mCategory; 83 @Mock 84 private Preference mSeeAllPref; 85 @Mock 86 private PreferenceCategory mDivider; 87 @Mock 88 private UserManager mUserManager; 89 @Mock 90 private ApplicationsState mAppState; 91 @Mock 92 private PackageManager mPackageManager; 93 @Mock 94 private ApplicationsState.AppEntry mAppEntry; 95 @Mock 96 private ApplicationInfo mApplicationInfo; 97 @Mock 98 private NotificationBackend mBackend; 99 @Mock 100 private Fragment mHost; 101 @Mock 102 private FragmentActivity mActivity; 103 @Mock 104 private IUsageStatsManager mIUsageStatsManager; 105 106 private Context mContext; 107 private RecentNotifyingAppsPreferenceController mController; 108 109 @Before setUp()110 public void setUp() { 111 MockitoAnnotations.initMocks(this); 112 mContext = spy(RuntimeEnvironment.application); 113 doReturn(mUserManager).when(mContext).getSystemService(Context.USER_SERVICE); 114 doReturn(mPackageManager).when(mContext).getPackageManager(); 115 when(mUserManager.getProfileIdsWithDisabled(0)).thenReturn(new int[] {0}); 116 117 mController = new RecentNotifyingAppsPreferenceController( 118 mContext, mBackend, mIUsageStatsManager, mUserManager, mAppState, mHost); 119 when(mScreen.findPreference(anyString())).thenReturn(mCategory); 120 121 when(mScreen.findPreference(RecentNotifyingAppsPreferenceController.KEY_SEE_ALL)) 122 .thenReturn(mSeeAllPref); 123 when(mScreen.findPreference(RecentNotifyingAppsPreferenceController.KEY_DIVIDER)) 124 .thenReturn(mDivider); 125 when(mCategory.getContext()).thenReturn(mContext); 126 when(mHost.getActivity()).thenReturn(mActivity); 127 } 128 129 @Test isAlwaysAvailable()130 public void isAlwaysAvailable() { 131 assertThat(mController.isAvailable()).isTrue(); 132 } 133 134 @Test doNotIndexCategory()135 public void doNotIndexCategory() { 136 final List<String> nonIndexable = new ArrayList<>(); 137 138 mController.updateNonIndexableKeys(nonIndexable); 139 140 assertThat(nonIndexable).containsAllOf(mController.getPreferenceKey(), 141 RecentNotifyingAppsPreferenceController.KEY_DIVIDER); 142 } 143 144 @Test onDisplayAndUpdateState_shouldRefreshUi()145 public void onDisplayAndUpdateState_shouldRefreshUi() { 146 mController = spy(new RecentNotifyingAppsPreferenceController( 147 mContext, null, mIUsageStatsManager, mUserManager, (ApplicationsState) null, null)); 148 149 doNothing().when(mController).refreshUi(mContext); 150 151 mController.displayPreference(mScreen); 152 mController.updateState(mCategory); 153 154 verify(mController, times(2)).refreshUi(mContext); 155 } 156 157 @Test 158 @Config(qualifiers = "mcc999") display_shouldNotShowRecents_showAppInfoPreference()159 public void display_shouldNotShowRecents_showAppInfoPreference() { 160 mController.displayPreference(mScreen); 161 162 verify(mCategory, never()).addPreference(any(Preference.class)); 163 verify(mCategory).setTitle(null); 164 verify(mSeeAllPref).setTitle(R.string.notifications_title); 165 verify(mSeeAllPref).setIcon(null); 166 verify(mDivider).setVisible(false); 167 } 168 169 @Test display_showRecents()170 public void display_showRecents() throws Exception { 171 List<Event> events = new ArrayList<>(); 172 Event app = new Event(); 173 app.mEventType = Event.NOTIFICATION_INTERRUPTION; 174 app.mPackage = "a"; 175 app.mTimeStamp = System.currentTimeMillis(); 176 events.add(app); 177 Event app1 = new Event(); 178 app1.mEventType = Event.NOTIFICATION_INTERRUPTION; 179 app1.mPackage = "com.android.settings"; 180 app1.mTimeStamp = System.currentTimeMillis(); 181 events.add(app1); 182 Event app2 = new Event(); 183 app2.mEventType = Event.NOTIFICATION_INTERRUPTION; 184 app2.mPackage = "pkg.class2"; 185 app2.mTimeStamp = System.currentTimeMillis() - 1000; 186 events.add(app2); 187 188 // app1, app2 are valid apps. app3 is invalid. 189 when(mAppState.getEntry(app.getPackageName(), UserHandle.myUserId())) 190 .thenReturn(mAppEntry); 191 when(mAppState.getEntry(app1.getPackageName(), UserHandle.myUserId())) 192 .thenReturn(mAppEntry); 193 when(mAppState.getEntry(app2.getPackageName(), UserHandle.myUserId())) 194 .thenReturn(null); 195 when(mPackageManager.resolveActivity(any(Intent.class), anyInt())).thenReturn( 196 new ResolveInfo()); 197 198 UsageEvents usageEvents = getUsageEvents( 199 new String[] {app.getPackageName(), app1.getPackageName(), app2.getPackageName()}, 200 events); 201 when(mIUsageStatsManager.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString())) 202 .thenReturn(usageEvents); 203 204 mAppEntry.info = mApplicationInfo; 205 206 mController.displayPreference(mScreen); 207 208 verify(mCategory).setTitle(R.string.recent_notifications); 209 // Only add app1 & app2. app3 skipped because it's invalid app. 210 verify(mCategory, times(2)).addPreference(any(Preference.class)); 211 212 verify(mSeeAllPref).setSummary(null); 213 verify(mSeeAllPref).setIcon(R.drawable.ic_chevron_right_24dp); 214 verify(mDivider).setVisible(true); 215 } 216 217 @Test display_showRecentsWithInstantApp()218 public void display_showRecentsWithInstantApp() throws Exception { 219 List<Event> events = new ArrayList<>(); 220 Event app = new Event(); 221 app.mEventType = Event.NOTIFICATION_INTERRUPTION; 222 app.mPackage = "com.foo.bar"; 223 app.mTimeStamp = System.currentTimeMillis(); 224 events.add(app); 225 Event app1 = new Event(); 226 app1.mEventType = Event.NOTIFICATION_INTERRUPTION; 227 app1.mPackage = "com.foo.barinstant"; 228 app1.mTimeStamp = System.currentTimeMillis() + 200; 229 events.add(app1); 230 UsageEvents usageEvents = getUsageEvents( 231 new String[] {"com.foo.bar", "com.foo.barinstant"}, events); 232 when(mIUsageStatsManager.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString())) 233 .thenReturn(usageEvents); 234 235 ApplicationsState.AppEntry app1Entry = mock(ApplicationsState.AppEntry.class); 236 ApplicationsState.AppEntry app2Entry = mock(ApplicationsState.AppEntry.class); 237 app1Entry.info = mApplicationInfo; 238 app2Entry.info = mApplicationInfo; 239 240 when(mAppState.getEntry( 241 app.getPackageName(), UserHandle.myUserId())).thenReturn(app1Entry); 242 when(mAppState.getEntry( 243 app1.getPackageName(), UserHandle.myUserId())).thenReturn(app2Entry); 244 245 // Only the regular app app1 should have its intent resolve. 246 when(mPackageManager.resolveActivity(argThat(intentMatcher(app.getPackageName())), 247 anyInt())).thenReturn(new ResolveInfo()); 248 249 // Make sure app2 is considered an instant app. 250 ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider", 251 (InstantAppDataProvider) (ApplicationInfo info) -> { 252 if (info == app2Entry.info) { 253 return true; 254 } else { 255 return false; 256 } 257 }); 258 259 mController.displayPreference(mScreen); 260 261 ArgumentCaptor<Preference> prefCaptor = ArgumentCaptor.forClass(Preference.class); 262 verify(mCategory, times(2)).addPreference(prefCaptor.capture()); 263 List<Preference> prefs = prefCaptor.getAllValues(); 264 assertThat(prefs.get(1).getKey()).isEqualTo( 265 RecentNotifyingAppsPreferenceController.getKey(UserHandle.myUserId(), 266 app.getPackageName())); 267 assertThat(prefs.get(0).getKey()).isEqualTo( 268 RecentNotifyingAppsPreferenceController.getKey(UserHandle.myUserId(), 269 app1.getPackageName())); 270 } 271 272 @Test display_showRecents_formatSummary()273 public void display_showRecents_formatSummary() throws Exception { 274 List<Event> events = new ArrayList<>(); 275 Event app = new Event(); 276 app.mEventType = Event.NOTIFICATION_INTERRUPTION; 277 app.mPackage = "pkg.class"; 278 app.mTimeStamp = System.currentTimeMillis(); 279 events.add(app); 280 UsageEvents usageEvents = getUsageEvents(new String[] {"pkg.class"}, events); 281 when(mIUsageStatsManager.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString())) 282 .thenReturn(usageEvents); 283 284 when(mAppState.getEntry(app.getPackageName(), UserHandle.myUserId())) 285 .thenReturn(mAppEntry); 286 when(mPackageManager.resolveActivity(any(Intent.class), anyInt())).thenReturn( 287 new ResolveInfo()); 288 289 mAppEntry.info = mApplicationInfo; 290 291 mController.displayPreference(mScreen); 292 293 verify(mCategory).addPreference(argThat(summaryMatches("Just now"))); 294 } 295 296 @Test reloadData()297 public void reloadData() throws Exception { 298 when(mUserManager.getProfileIdsWithDisabled(0)).thenReturn(new int[] {0, 10}); 299 300 mController = new RecentNotifyingAppsPreferenceController( 301 mContext, mBackend, mIUsageStatsManager, mUserManager, mAppState, mHost); 302 303 List<Event> events = new ArrayList<>(); 304 Event app = new Event(); 305 app.mEventType = Event.NOTIFICATION_INTERRUPTION; 306 app.mPackage = "b"; 307 app.mTimeStamp = 1; 308 events.add(app); 309 Event app1 = new Event(); 310 app1.mEventType = Event.MAX_EVENT_TYPE; 311 app1.mPackage = "com.foo.bar"; 312 app1.mTimeStamp = 10; 313 events.add(app1); 314 UsageEvents usageEvents = getUsageEvents( 315 new String[] {"b", "com.foo.bar"}, events); 316 when(mIUsageStatsManager.queryEventsForUser(anyLong(), anyLong(), eq(0), anyString())) 317 .thenReturn(usageEvents); 318 319 List<Event> events10 = new ArrayList<>(); 320 Event app10 = new Event(); 321 app10.mEventType = Event.NOTIFICATION_INTERRUPTION; 322 app10.mPackage = "a"; 323 app10.mTimeStamp = 2; 324 events10.add(app10); 325 Event app10a = new Event(); 326 app10a.mEventType = Event.NOTIFICATION_INTERRUPTION; 327 app10a.mPackage = "a"; 328 app10a.mTimeStamp = 20; 329 events10.add(app10a); 330 UsageEvents usageEvents10 = getUsageEvents( 331 new String[] {"a"}, events10); 332 when(mIUsageStatsManager.queryEventsForUser(anyLong(), anyLong(), eq(10), anyString())) 333 .thenReturn(usageEvents10); 334 335 mController.reloadData(); 336 337 assertThat(mController.mApps.size()).isEqualTo(2); 338 boolean foundPkg0 = false; 339 boolean foundPkg10 = false; 340 for (NotifyingApp notifyingApp : mController.mApps) { 341 if (notifyingApp.getLastNotified() == 20 342 && notifyingApp.getPackage().equals("a") 343 && notifyingApp.getUserId() == 10) { 344 foundPkg10 = true; 345 } 346 if (notifyingApp.getLastNotified() == 1 347 && notifyingApp.getPackage().equals("b") 348 && notifyingApp.getUserId() == 0) { 349 foundPkg0 = true; 350 } 351 } 352 assertThat(foundPkg0).isTrue(); 353 assertThat(foundPkg10).isTrue(); 354 } 355 summaryMatches(String expected)356 private static ArgumentMatcher<Preference> summaryMatches(String expected) { 357 return preference -> TextUtils.equals(expected, preference.getSummary()); 358 } 359 360 // Used for matching an intent with a specific package name. intentMatcher(String packageName)361 private static ArgumentMatcher<Intent> intentMatcher(String packageName) { 362 return intent -> packageName.equals(intent.getPackage()); 363 } 364 getUsageEvents(String[] pkgs, List<Event> events)365 private UsageEvents getUsageEvents(String[] pkgs, List<Event> events) { 366 UsageEvents usageEvents = new UsageEvents(events, pkgs); 367 Parcel parcel = Parcel.obtain(); 368 parcel.setDataPosition(0); 369 usageEvents.writeToParcel(parcel, 0); 370 parcel.setDataPosition(0); 371 return UsageEvents.CREATOR.createFromParcel(parcel); 372 } 373 } 374