1 /* 2 * Copyright (C) 2020 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.server.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.anyString; 24 import static org.mockito.Mockito.mock; 25 import static org.mockito.Mockito.times; 26 import static org.mockito.Mockito.verify; 27 import static org.mockito.Mockito.when; 28 29 import android.app.Notification; 30 import android.app.Person; 31 import android.content.pm.LauncherApps; 32 import android.content.pm.LauncherApps.ShortcutQuery; 33 import android.content.pm.ShortcutInfo; 34 import android.content.pm.ShortcutQueryWrapper; 35 import android.content.pm.ShortcutServiceInternal; 36 import android.os.UserHandle; 37 import android.os.UserManager; 38 import android.service.notification.StatusBarNotification; 39 import android.test.suitebuilder.annotation.SmallTest; 40 import android.testing.TestableLooper; 41 42 import androidx.test.runner.AndroidJUnit4; 43 44 import com.android.server.UiServiceTestCase; 45 46 import org.junit.Before; 47 import org.junit.Ignore; 48 import org.junit.Test; 49 import org.junit.runner.RunWith; 50 import org.mockito.ArgumentCaptor; 51 import org.mockito.Captor; 52 import org.mockito.Mock; 53 import org.mockito.Mockito; 54 import org.mockito.MockitoAnnotations; 55 56 import java.util.ArrayList; 57 import java.util.Collections; 58 import java.util.List; 59 60 @SmallTest 61 @RunWith(AndroidJUnit4.class) 62 @TestableLooper.RunWithLooper 63 public class ShortcutHelperTest extends UiServiceTestCase { 64 65 private static final String SHORTCUT_ID = "shortcut"; 66 private static final String PKG = "pkg"; 67 private static final String KEY = "key"; 68 private static final Person PERSON = mock(Person.class); 69 70 @Mock 71 LauncherApps mLauncherApps; 72 @Mock 73 ShortcutHelper.ShortcutListener mShortcutListener; 74 @Mock 75 UserManager mUserManager; 76 @Mock 77 ShortcutServiceInternal mShortcutServiceInternal; 78 @Mock 79 NotificationRecord mNr; 80 @Mock 81 Notification mNotif; 82 @Mock 83 StatusBarNotification mSbn; 84 @Mock 85 Notification.BubbleMetadata mBubbleMetadata; 86 @Mock 87 ShortcutInfo mShortcutInfo; 88 89 @Captor private ArgumentCaptor<ShortcutQuery> mShortcutQueryCaptor; 90 91 ShortcutHelper mShortcutHelper; 92 93 @Before setUp()94 public void setUp() { 95 MockitoAnnotations.initMocks(this); 96 97 mShortcutHelper = new ShortcutHelper( 98 mLauncherApps, mShortcutListener, mShortcutServiceInternal, mUserManager); 99 when(mSbn.getPackageName()).thenReturn(PKG); 100 when(mShortcutInfo.getId()).thenReturn(SHORTCUT_ID); 101 when(mNotif.getBubbleMetadata()).thenReturn(mBubbleMetadata); 102 when(mBubbleMetadata.getShortcutId()).thenReturn(SHORTCUT_ID); 103 when(mUserManager.isUserUnlocked(any(UserHandle.class))).thenReturn(true); 104 105 setUpMockNotificationRecord(mNr, KEY); 106 } 107 setUpMockNotificationRecord(NotificationRecord mockRecord, String key)108 private void setUpMockNotificationRecord(NotificationRecord mockRecord, String key) { 109 when(mockRecord.getKey()).thenReturn(key); 110 when(mockRecord.getSbn()).thenReturn(mSbn); 111 when(mockRecord.getNotification()).thenReturn(mNotif); 112 when(mockRecord.getShortcutInfo()).thenReturn(mShortcutInfo); 113 } 114 addShortcutBubbleAndVerifyListener()115 private LauncherApps.Callback addShortcutBubbleAndVerifyListener() { 116 mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr, 117 false /* removed */, 118 null /* handler */); 119 120 ArgumentCaptor<LauncherApps.Callback> launcherAppsCallback = 121 ArgumentCaptor.forClass(LauncherApps.Callback.class); 122 123 verify(mLauncherApps, times(1)).registerCallback( 124 launcherAppsCallback.capture(), any()); 125 return launcherAppsCallback.getValue(); 126 } 127 128 @Test testBubbleAdded_listenedAdded()129 public void testBubbleAdded_listenedAdded() { 130 addShortcutBubbleAndVerifyListener(); 131 } 132 133 @Test testBubbleRemoved_listenerRemoved()134 public void testBubbleRemoved_listenerRemoved() { 135 // First set it up to listen 136 addShortcutBubbleAndVerifyListener(); 137 138 // Then remove the notif 139 mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr, 140 true /* removed */, 141 null /* handler */); 142 143 verify(mLauncherApps, times(1)).unregisterCallback(any()); 144 } 145 146 @Test testBubbleNoLongerHasBubbleMetadata_listenerRemoved()147 public void testBubbleNoLongerHasBubbleMetadata_listenerRemoved() { 148 // First set it up to listen 149 addShortcutBubbleAndVerifyListener(); 150 151 // Then make it not a bubble 152 when(mNotif.getBubbleMetadata()).thenReturn(null); 153 mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr, 154 false /* removed */, 155 null /* handler */); 156 157 verify(mLauncherApps, times(1)).unregisterCallback(any()); 158 } 159 160 @Test testBubbleNoLongerHasShortcutId_listenerRemoved()161 public void testBubbleNoLongerHasShortcutId_listenerRemoved() { 162 // First set it up to listen 163 addShortcutBubbleAndVerifyListener(); 164 165 // Clear out shortcutId 166 when(mBubbleMetadata.getShortcutId()).thenReturn(null); 167 mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr, 168 false /* removed */, 169 null /* handler */); 170 171 verify(mLauncherApps, times(1)).unregisterCallback(any()); 172 } 173 174 @Test testNotifNoLongerHasShortcut_listenerRemoved()175 public void testNotifNoLongerHasShortcut_listenerRemoved() { 176 // First set it up to listen 177 addShortcutBubbleAndVerifyListener(); 178 179 NotificationRecord validMock1 = Mockito.mock(NotificationRecord.class); 180 setUpMockNotificationRecord(validMock1, "KEY1"); 181 182 NotificationRecord validMock2 = Mockito.mock(NotificationRecord.class); 183 setUpMockNotificationRecord(validMock2, "KEY2"); 184 185 NotificationRecord validMock3 = Mockito.mock(NotificationRecord.class); 186 setUpMockNotificationRecord(validMock3, "KEY3"); 187 188 mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock1, 189 false /* removed */, 190 null /* handler */); 191 192 mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock2, 193 false /* removed */, 194 null /* handler */); 195 196 mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock3, 197 false /* removed */, 198 null /* handler */); 199 200 // Clear out shortcutId of the bubble in the middle, to double check that we don't hit a 201 // concurrent modification exception (removing the last bubble would sidestep that check). 202 when(validMock2.getShortcutInfo()).thenReturn(null); 203 mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock2, 204 false /* removed */, 205 null /* handler */); 206 207 verify(mLauncherApps, times(1)).unregisterCallback(any()); 208 } 209 210 @Test testOnShortcutsChanged_listenerRemoved()211 public void testOnShortcutsChanged_listenerRemoved() { 212 // First set it up to listen 213 LauncherApps.Callback callback = addShortcutBubbleAndVerifyListener(); 214 215 // App shortcuts are removed: 216 callback.onShortcutsChanged(PKG, Collections.emptyList(), mock(UserHandle.class)); 217 218 verify(mLauncherApps, times(1)).unregisterCallback(any()); 219 } 220 221 @Test testListenerNotifiedOnShortcutRemoved()222 public void testListenerNotifiedOnShortcutRemoved() { 223 LauncherApps.Callback callback = addShortcutBubbleAndVerifyListener(); 224 225 List<ShortcutInfo> shortcutInfos = new ArrayList<>(); 226 when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcutInfos); 227 228 callback.onShortcutsChanged(PKG, shortcutInfos, mock(UserHandle.class)); 229 verify(mShortcutListener).onShortcutRemoved(mNr.getKey()); 230 } 231 232 @Test testGetValidShortcutInfo_noMatchingShortcut()233 public void testGetValidShortcutInfo_noMatchingShortcut() { 234 when(mLauncherApps.getShortcuts(any(), any())).thenReturn(null); 235 when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), 236 anyString(), anyInt(), any())).thenReturn(true); 237 238 assertThat(mShortcutHelper.getValidShortcutInfo("a", "p", UserHandle.SYSTEM)).isNull(); 239 } 240 241 @Test testGetValidShortcutInfo_nullShortcut()242 public void testGetValidShortcutInfo_nullShortcut() { 243 ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); 244 shortcuts.add(null); 245 when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcuts); 246 when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), 247 anyString(), anyInt(), any())).thenReturn(true); 248 249 assertThat(mShortcutHelper.getValidShortcutInfo("a", "p", UserHandle.SYSTEM)).isNull(); 250 } 251 252 @Test testGetValidShortcutInfo_notLongLived()253 public void testGetValidShortcutInfo_notLongLived() { 254 ShortcutInfo si = mock(ShortcutInfo.class); 255 when(si.getPackage()).thenReturn(PKG); 256 when(si.getId()).thenReturn(SHORTCUT_ID); 257 when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM); 258 when(si.isLongLived()).thenReturn(false); 259 when(si.isEnabled()).thenReturn(true); 260 ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); 261 shortcuts.add(si); 262 when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcuts); 263 when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), 264 anyString(), anyInt(), any())).thenReturn(true); 265 266 assertThat(mShortcutHelper.getValidShortcutInfo("a", "p", UserHandle.SYSTEM)).isNull(); 267 } 268 269 @Ignore("b/155016294") 270 @Test testGetValidShortcutInfo_notSharingShortcut()271 public void testGetValidShortcutInfo_notSharingShortcut() { 272 ShortcutInfo si = mock(ShortcutInfo.class); 273 when(si.getPackage()).thenReturn(PKG); 274 when(si.getId()).thenReturn(SHORTCUT_ID); 275 when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM); 276 when(si.isLongLived()).thenReturn(true); 277 when(si.isEnabled()).thenReturn(true); 278 ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); 279 shortcuts.add(si); 280 when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcuts); 281 when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), 282 anyString(), anyInt(), any())).thenReturn(false); 283 284 assertThat(mShortcutHelper.getValidShortcutInfo("a", "p", UserHandle.SYSTEM)).isNull(); 285 } 286 287 @Test testGetValidShortcutInfo_notEnabled()288 public void testGetValidShortcutInfo_notEnabled() { 289 ShortcutInfo si = mock(ShortcutInfo.class); 290 when(si.getPackage()).thenReturn(PKG); 291 when(si.getId()).thenReturn(SHORTCUT_ID); 292 when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM); 293 when(si.isLongLived()).thenReturn(true); 294 when(si.isEnabled()).thenReturn(false); 295 ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); 296 shortcuts.add(si); 297 when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcuts); 298 when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), 299 anyString(), anyInt(), any())).thenReturn(true); 300 301 assertThat(mShortcutHelper.getValidShortcutInfo("a", "p", UserHandle.SYSTEM)).isNull(); 302 } 303 304 @Test testGetValidShortcutInfo_isValid()305 public void testGetValidShortcutInfo_isValid() { 306 ShortcutInfo si = mock(ShortcutInfo.class); 307 when(si.getPackage()).thenReturn(PKG); 308 when(si.getId()).thenReturn(SHORTCUT_ID); 309 when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM); 310 when(si.isLongLived()).thenReturn(true); 311 when(si.isEnabled()).thenReturn(true); 312 when(si.getPersons()).thenReturn(new Person[]{PERSON}); 313 ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); 314 shortcuts.add(si); 315 when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcuts); 316 // TODO: b/155016294 317 //when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), 318 // anyString(), anyInt(), any())).thenReturn(true); 319 320 assertThat(mShortcutHelper.getValidShortcutInfo("a", "p", UserHandle.SYSTEM)) 321 .isSameInstanceAs(si); 322 } 323 324 325 @Test testGetValidShortcutInfo_isValidButUserLocked()326 public void testGetValidShortcutInfo_isValidButUserLocked() { 327 ShortcutInfo si = mock(ShortcutInfo.class); 328 when(si.getPackage()).thenReturn(PKG); 329 when(si.getId()).thenReturn(SHORTCUT_ID); 330 when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM); 331 when(si.isLongLived()).thenReturn(true); 332 when(si.isEnabled()).thenReturn(true); 333 when(si.getPersons()).thenReturn(new Person[]{PERSON}); 334 ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); 335 shortcuts.add(si); 336 when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcuts); 337 when(mUserManager.isUserUnlocked(any(UserHandle.class))).thenReturn(false); 338 339 assertThat(mShortcutHelper.getValidShortcutInfo("a", "p", UserHandle.SYSTEM)) 340 .isNull(); 341 } 342 343 @Test testGetValidShortcutInfo_hasGetPersonsDataFlag()344 public void testGetValidShortcutInfo_hasGetPersonsDataFlag() { 345 346 ShortcutInfo info = mShortcutHelper.getValidShortcutInfo( 347 "a", "p", UserHandle.SYSTEM); 348 verify(mLauncherApps).getShortcuts(mShortcutQueryCaptor.capture(), any()); 349 ShortcutQueryWrapper shortcutQuery = 350 new ShortcutQueryWrapper(mShortcutQueryCaptor.getValue()); 351 assertThat(hasFlag(shortcutQuery.getQueryFlags(), ShortcutQuery.FLAG_GET_PERSONS_DATA)) 352 .isTrue(); 353 } 354 355 /** 356 * Returns {@code true} iff {@link ShortcutQuery}'s {@code queryFlags} has {@code flag} set. 357 */ hasFlag(int queryFlags, int flag)358 private static boolean hasFlag(int queryFlags, int flag) { 359 return (queryFlags & flag) != 0; 360 } 361 } 362