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