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