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