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
17package com.android.settings.applications;
18
19import static android.text.format.DateUtils.DAY_IN_MILLIS;
20
21import static com.android.settings.applications.AppStateNotificationBridge
22        .FILTER_APP_NOTIFICATION_FREQUENCY;
23import static com.android.settings.applications.AppStateNotificationBridge
24        .FILTER_APP_NOTIFICATION_RECENCY;
25import static com.android.settings.applications.AppStateNotificationBridge
26        .FREQUENCY_NOTIFICATION_COMPARATOR;
27import static com.android.settings.applications.AppStateNotificationBridge
28        .RECENT_NOTIFICATION_COMPARATOR;
29
30import static com.google.common.truth.Truth.assertThat;
31
32import static junit.framework.Assert.assertFalse;
33import static junit.framework.Assert.assertTrue;
34
35import static org.mockito.ArgumentMatchers.any;
36import static org.mockito.ArgumentMatchers.anyInt;
37import static org.mockito.ArgumentMatchers.anyLong;
38import static org.mockito.ArgumentMatchers.anyString;
39import static org.mockito.ArgumentMatchers.eq;
40import static org.mockito.Mockito.mock;
41import static org.mockito.Mockito.verify;
42import static org.mockito.Mockito.when;
43
44import android.app.usage.IUsageStatsManager;
45import android.app.usage.UsageEvents;
46import android.app.usage.UsageEvents.Event;
47import android.content.Context;
48import android.content.pm.ApplicationInfo;
49import android.os.Looper;
50import android.os.Parcel;
51import android.os.RemoteException;
52import android.os.UserHandle;
53import android.os.UserManager;
54import android.view.ViewGroup;
55import android.widget.Switch;
56
57import com.android.settings.R;
58import com.android.settings.applications.AppStateNotificationBridge.NotificationsSentState;
59import com.android.settings.notification.NotificationBackend;
60import com.android.settings.testutils.SettingsRobolectricTestRunner;
61import com.android.settingslib.applications.ApplicationsState;
62import com.android.settingslib.applications.ApplicationsState.AppEntry;
63
64import org.junit.Before;
65import org.junit.Test;
66import org.junit.runner.RunWith;
67import org.mockito.Mock;
68import org.mockito.MockitoAnnotations;
69import org.robolectric.RuntimeEnvironment;
70
71import java.util.ArrayList;
72import java.util.List;
73import java.util.Map;
74
75@RunWith(SettingsRobolectricTestRunner.class)
76public class AppStateNotificationBridgeTest {
77
78    private static String PKG1 = "pkg1";
79    private static String PKG2 = "pkg2";
80
81    @Mock
82    private ApplicationsState.Session mSession;
83    @Mock
84    private ApplicationsState mState;
85    @Mock
86    private IUsageStatsManager mUsageStats;
87    @Mock
88    private UserManager mUserManager;
89    @Mock
90    private NotificationBackend mBackend;
91    private Context mContext;
92    private AppStateNotificationBridge mBridge;
93
94    @Before
95    public void setUp() {
96        MockitoAnnotations.initMocks(this);
97        when(mState.newSession(any())).thenReturn(mSession);
98        when(mState.getBackgroundLooper()).thenReturn(mock(Looper.class));
99        when(mBackend.getNotificationsBanned(anyString(), anyInt())).thenReturn(true);
100        when(mBackend.isSystemApp(any(), any())).thenReturn(true);
101        // most tests assume no work profile
102        when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[]{});
103        mContext = RuntimeEnvironment.application.getApplicationContext();
104
105        mBridge = new AppStateNotificationBridge(mContext, mState,
106                mock(AppStateBaseBridge.Callback.class), mUsageStats, mUserManager, mBackend);
107    }
108
109    private AppEntry getMockAppEntry(String pkg) {
110        AppEntry entry = mock(AppEntry.class);
111        entry.info = mock(ApplicationInfo.class);
112        entry.info.packageName = pkg;
113        return entry;
114    }
115
116    private UsageEvents getUsageEvents(List<Event> events) {
117        UsageEvents usageEvents = new UsageEvents(events, new String[] {PKG1, PKG2});
118        Parcel parcel = Parcel.obtain();
119        parcel.setDataPosition(0);
120        usageEvents.writeToParcel(parcel, 0);
121        parcel.setDataPosition(0);
122        return UsageEvents.CREATOR.createFromParcel(parcel);
123    }
124
125    @Test
126    public void testGetAggregatedUsageEvents_noEvents() throws Exception {
127        when(mUsageStats.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString()))
128                .thenReturn(mock(UsageEvents.class));
129
130        assertThat(mBridge.getAggregatedUsageEvents()).isEmpty();
131    }
132
133    @Test
134    public void testGetAggregatedUsageEvents_onlyNotificationEvents() throws Exception {
135        List<Event> events = new ArrayList<>();
136        Event good = new Event();
137        good.mEventType = Event.NOTIFICATION_INTERRUPTION;
138        good.mPackage = PKG1;
139        good.mTimeStamp = 1;
140        events.add(good);
141        Event bad = new Event();
142        bad.mEventType = Event.CHOOSER_ACTION;
143        bad.mPackage = PKG1;
144        bad.mTimeStamp = 2;
145        events.add(bad);
146
147        UsageEvents usageEvents = getUsageEvents(events);
148        when(mUsageStats.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString()))
149                .thenReturn(usageEvents);
150
151        Map<String, NotificationsSentState> map = mBridge.getAggregatedUsageEvents();
152        assertThat(map.get(mBridge.getKey(0, PKG1)).sentCount).isEqualTo(1);
153    }
154
155    @Test
156    public void testGetAggregatedUsageEvents_multipleEventsAgg() throws Exception {
157        List<Event> events = new ArrayList<>();
158        Event good = new Event();
159        good.mEventType = Event.NOTIFICATION_INTERRUPTION;
160        good.mPackage = PKG1;
161        good.mTimeStamp = 6;
162        events.add(good);
163        Event good1 = new Event();
164        good1.mEventType = Event.NOTIFICATION_INTERRUPTION;
165        good1.mPackage = PKG1;
166        good1.mTimeStamp = 1;
167        events.add(good1);
168
169        UsageEvents usageEvents = getUsageEvents(events);
170        when(mUsageStats.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString()))
171                .thenReturn(usageEvents);
172
173        Map<String, NotificationsSentState> map  = mBridge.getAggregatedUsageEvents();
174        assertThat(map.get(mBridge.getKey(0, PKG1)).sentCount).isEqualTo(2);
175        assertThat(map.get(mBridge.getKey(0, PKG1)).lastSent).isEqualTo(6);
176    }
177
178    @Test
179    public void testGetAggregatedUsageEvents_multiplePkgs() throws Exception {
180        List<Event> events = new ArrayList<>();
181        Event good = new Event();
182        good.mEventType = Event.NOTIFICATION_INTERRUPTION;
183        good.mPackage = PKG1;
184        good.mTimeStamp = 6;
185        events.add(good);
186        Event good1 = new Event();
187        good1.mEventType = Event.NOTIFICATION_INTERRUPTION;
188        good1.mPackage = PKG2;
189        good1.mTimeStamp = 1;
190        events.add(good1);
191
192        UsageEvents usageEvents = getUsageEvents(events);
193        when(mUsageStats.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString()))
194                .thenReturn(usageEvents);
195
196        Map<String, NotificationsSentState> map
197                = mBridge.getAggregatedUsageEvents();
198        assertThat(map.get(mBridge.getKey(0, PKG1)).sentCount).isEqualTo(1);
199        assertThat(map.get(mBridge.getKey(0, PKG2)).sentCount).isEqualTo(1);
200        assertThat(map.get(mBridge.getKey(0, PKG1)).lastSent).isEqualTo(6);
201        assertThat(map.get(mBridge.getKey(0, PKG2)).lastSent).isEqualTo(1);
202    }
203
204    @Test
205    public void testLoadAllExtraInfo_noEvents() throws RemoteException {
206        when(mUsageStats.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString()))
207                .thenReturn(mock(UsageEvents.class));
208        ArrayList<AppEntry> apps = new ArrayList<>();
209        apps.add(getMockAppEntry(PKG1));
210        when(mSession.getAllApps()).thenReturn(apps);
211
212        mBridge.loadAllExtraInfo();
213        assertThat(apps.get(0).extraInfo).isNull();
214    }
215
216    @Test
217    public void testLoadAllExtraInfo_multipleEventsAgg() throws RemoteException {
218        List<Event> events = new ArrayList<>();
219        for (int i = 0; i < 7; i++) {
220            Event good = new Event();
221            good.mEventType = Event.NOTIFICATION_INTERRUPTION;
222            good.mPackage = PKG1;
223            good.mTimeStamp = i;
224            events.add(good);
225        }
226
227        UsageEvents usageEvents = getUsageEvents(events);
228        when(mUsageStats.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString()))
229                .thenReturn(usageEvents);
230
231        ArrayList<AppEntry> apps = new ArrayList<>();
232        apps.add(getMockAppEntry(PKG1));
233        when(mSession.getAllApps()).thenReturn(apps);
234
235        mBridge.loadAllExtraInfo();
236        assertThat(((NotificationsSentState) apps.get(0).extraInfo).sentCount).isEqualTo(7);
237        assertThat(((NotificationsSentState) apps.get(0).extraInfo).lastSent).isEqualTo(6);
238        assertThat(((NotificationsSentState) apps.get(0).extraInfo).avgSentDaily).isEqualTo(1);
239        assertThat(((NotificationsSentState) apps.get(0).extraInfo).avgSentWeekly).isEqualTo(0);
240        assertThat(((NotificationsSentState) apps.get(0).extraInfo).blocked).isTrue();
241        assertThat(((NotificationsSentState) apps.get(0).extraInfo).systemApp).isTrue();
242        assertThat(((NotificationsSentState) apps.get(0).extraInfo).blockable).isTrue();
243    }
244
245    @Test
246    public void testLoadAllExtraInfo_multiplePkgs() throws RemoteException {
247        List<Event> events = new ArrayList<>();
248        for (int i = 0; i < 8; i++) {
249            Event good = new Event();
250            good.mEventType = Event.NOTIFICATION_INTERRUPTION;
251            good.mPackage = PKG1;
252            good.mTimeStamp = i;
253            events.add(good);
254        }
255        Event good1 = new Event();
256        good1.mEventType = Event.NOTIFICATION_INTERRUPTION;
257        good1.mPackage = PKG2;
258        good1.mTimeStamp = 1;
259        events.add(good1);
260
261        UsageEvents usageEvents = getUsageEvents(events);
262        when(mUsageStats.queryEventsForUser(anyLong(), anyLong(), anyInt(), anyString()))
263                .thenReturn(usageEvents);
264
265        ArrayList<AppEntry> apps = new ArrayList<>();
266        apps.add(getMockAppEntry(PKG1));
267        apps.add(getMockAppEntry(PKG2));
268        when(mSession.getAllApps()).thenReturn(apps);
269
270        mBridge.loadAllExtraInfo();
271        assertThat(((NotificationsSentState) apps.get(0).extraInfo).sentCount).isEqualTo(8);
272        assertThat(((NotificationsSentState) apps.get(0).extraInfo).lastSent).isEqualTo(7);
273        assertThat(((NotificationsSentState) apps.get(0).extraInfo).avgSentWeekly).isEqualTo(0);
274        assertThat(((NotificationsSentState) apps.get(0).extraInfo).avgSentDaily).isEqualTo(1);
275
276        assertThat(((NotificationsSentState) apps.get(1).extraInfo).sentCount).isEqualTo(1);
277        assertThat(((NotificationsSentState) apps.get(1).extraInfo).lastSent).isEqualTo(1);
278        assertThat(((NotificationsSentState) apps.get(1).extraInfo).avgSentWeekly).isEqualTo(1);
279        assertThat(((NotificationsSentState) apps.get(1).extraInfo).avgSentDaily).isEqualTo(0);
280    }
281
282    @Test
283    public void testLoadAllExtraInfo_multipleUsers() throws RemoteException {
284        // has work profile
285        when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[]{1});
286        mBridge = new AppStateNotificationBridge(mContext, mState,
287                mock(AppStateBaseBridge.Callback.class), mUsageStats, mUserManager, mBackend);
288
289        List<Event> eventsProfileOwner = new ArrayList<>();
290        for (int i = 0; i < 8; i++) {
291            Event good = new Event();
292            good.mEventType = Event.NOTIFICATION_INTERRUPTION;
293            good.mPackage = PKG1;
294            good.mTimeStamp = i;
295            eventsProfileOwner.add(good);
296        }
297
298        List<Event> eventsProfile = new ArrayList<>();
299        for (int i = 0; i < 4; i++) {
300            Event good = new Event();
301            good.mEventType = Event.NOTIFICATION_INTERRUPTION;
302            good.mPackage = PKG1;
303            good.mTimeStamp = i;
304            eventsProfile.add(good);
305        }
306
307        UsageEvents usageEventsOwner = getUsageEvents(eventsProfileOwner);
308        when(mUsageStats.queryEventsForUser(anyLong(), anyLong(), eq(0), anyString()))
309                .thenReturn(usageEventsOwner);
310
311        UsageEvents usageEventsProfile = getUsageEvents(eventsProfile);
312        when(mUsageStats.queryEventsForUser(anyLong(), anyLong(), eq(1), anyString()))
313                .thenReturn(usageEventsProfile);
314
315        ArrayList<AppEntry> apps = new ArrayList<>();
316        AppEntry owner = getMockAppEntry(PKG1);
317        owner.info.uid = 1;
318        apps.add(owner);
319
320        AppEntry profile = getMockAppEntry(PKG1);
321        profile.info.uid = UserHandle.PER_USER_RANGE + 1;
322        apps.add(profile);
323        when(mSession.getAllApps()).thenReturn(apps);
324
325        mBridge.loadAllExtraInfo();
326
327        assertThat(((NotificationsSentState) apps.get(0).extraInfo).sentCount).isEqualTo(8);
328        assertThat(((NotificationsSentState) apps.get(0).extraInfo).lastSent).isEqualTo(7);
329        assertThat(((NotificationsSentState) apps.get(0).extraInfo).avgSentWeekly).isEqualTo(0);
330        assertThat(((NotificationsSentState) apps.get(0).extraInfo).avgSentDaily).isEqualTo(1);
331
332        assertThat(((NotificationsSentState) apps.get(1).extraInfo).sentCount).isEqualTo(4);
333        assertThat(((NotificationsSentState) apps.get(1).extraInfo).lastSent).isEqualTo(3);
334        assertThat(((NotificationsSentState) apps.get(1).extraInfo).avgSentWeekly).isEqualTo(4);
335        assertThat(((NotificationsSentState) apps.get(1).extraInfo).avgSentDaily).isEqualTo(1);
336    }
337
338    @Test
339    public void testUpdateExtraInfo_noEvents() throws RemoteException {
340        when(mUsageStats.queryEventsForPackageForUser(
341                anyLong(), anyLong(), anyInt(), anyString(), anyString()))
342                .thenReturn(mock(UsageEvents.class));
343        AppEntry entry = getMockAppEntry(PKG1);
344
345        mBridge.updateExtraInfo(entry, "", 0);
346        assertThat(entry.extraInfo).isNull();
347    }
348
349    @Test
350    public void testUpdateExtraInfo_multipleEventsAgg() throws RemoteException {
351        List<Event> events = new ArrayList<>();
352        for (int i = 0; i < 13; i++) {
353            Event good = new Event();
354            good.mEventType = Event.NOTIFICATION_INTERRUPTION;
355            good.mPackage = PKG1;
356            good.mTimeStamp = i;
357            events.add(good);
358        }
359
360        UsageEvents usageEvents = getUsageEvents(events);
361        when(mUsageStats.queryEventsForPackageForUser(
362                anyLong(), anyLong(), anyInt(), anyString(), anyString())).thenReturn(usageEvents);
363
364        AppEntry entry = getMockAppEntry(PKG1);
365        mBridge.updateExtraInfo(entry, "", 0);
366
367        assertThat(((NotificationsSentState) entry.extraInfo).sentCount).isEqualTo(13);
368        assertThat(((NotificationsSentState) entry.extraInfo).lastSent).isEqualTo(12);
369        assertThat(((NotificationsSentState) entry.extraInfo).avgSentDaily).isEqualTo(2);
370        assertThat(((NotificationsSentState) entry.extraInfo).avgSentWeekly).isEqualTo(0);
371        assertThat(((NotificationsSentState) entry.extraInfo).blocked).isTrue();
372        assertThat(((NotificationsSentState) entry.extraInfo).systemApp).isTrue();
373        assertThat(((NotificationsSentState) entry.extraInfo).blockable).isTrue();
374    }
375
376    @Test
377    public void testSummary_recency() {
378        NotificationsSentState neverSent = new NotificationsSentState();
379        NotificationsSentState sent = new NotificationsSentState();
380        sent.lastSent = System.currentTimeMillis() - (2 * DAY_IN_MILLIS);
381
382        assertThat(AppStateNotificationBridge.getSummary(mContext, neverSent, true)).isEqualTo(
383                mContext.getString(R.string.notifications_sent_never));
384        assertThat(AppStateNotificationBridge.getSummary(mContext, sent, true).toString())
385                .contains("2");
386    }
387
388    @Test
389    public void testSummary_frequency() {
390        NotificationsSentState sentRarely = new NotificationsSentState();
391        sentRarely.avgSentWeekly = 1;
392        NotificationsSentState sentOften = new NotificationsSentState();
393        sentOften.avgSentDaily = 8;
394
395        assertThat(AppStateNotificationBridge.getSummary(mContext, sentRarely, false).toString())
396                .contains("1");
397        assertThat(AppStateNotificationBridge.getSummary(mContext, sentOften, false).toString())
398                .contains("8");
399    }
400
401    @Test
402    public void testFilterRecency() {
403        NotificationsSentState allowState = new NotificationsSentState();
404        allowState.lastSent = 1;
405        AppEntry allow = mock(AppEntry.class);
406        allow.extraInfo = allowState;
407
408        assertTrue(FILTER_APP_NOTIFICATION_RECENCY.filterApp(allow));
409
410        NotificationsSentState denyState = new NotificationsSentState();
411        denyState.lastSent = 0;
412        AppEntry deny = mock(AppEntry.class);
413        deny.extraInfo = denyState;
414
415        assertFalse(FILTER_APP_NOTIFICATION_RECENCY.filterApp(deny));
416    }
417
418    @Test
419    public void testFilterFrequency() {
420        NotificationsSentState allowState = new NotificationsSentState();
421        allowState.sentCount = 1;
422        AppEntry allow = mock(AppEntry.class);
423        allow.extraInfo = allowState;
424
425        assertTrue(FILTER_APP_NOTIFICATION_FREQUENCY.filterApp(allow));
426
427        NotificationsSentState denyState = new NotificationsSentState();
428        denyState.sentCount = 0;
429        AppEntry deny = mock(AppEntry.class);
430        deny.extraInfo = denyState;
431
432        assertFalse(FILTER_APP_NOTIFICATION_FREQUENCY.filterApp(deny));
433    }
434
435    @Test
436    public void testComparators_nullsNoCrash() {
437        List<AppEntry> entries = new ArrayList<>();
438        AppEntry a = mock(AppEntry.class);
439        a.label = "1";
440        AppEntry b = mock(AppEntry.class);
441        b.label = "2";
442        entries.add(a);
443        entries.add(b);
444
445        entries.sort(RECENT_NOTIFICATION_COMPARATOR);
446        entries.sort(FREQUENCY_NOTIFICATION_COMPARATOR);
447    }
448
449    @Test
450    public void testRecencyComparator() {
451        List<AppEntry> entries = new ArrayList<>();
452
453        NotificationsSentState earlier = new NotificationsSentState();
454        earlier.lastSent = 1;
455        AppEntry earlyEntry = mock(AppEntry.class);
456        earlyEntry.extraInfo = earlier;
457        entries.add(earlyEntry);
458
459        NotificationsSentState later = new NotificationsSentState();
460        later.lastSent = 8;
461        AppEntry lateEntry = mock(AppEntry.class);
462        lateEntry.extraInfo = later;
463        entries.add(lateEntry);
464
465        entries.sort(RECENT_NOTIFICATION_COMPARATOR);
466
467        assertThat(entries).containsExactly(lateEntry, earlyEntry);
468    }
469
470    @Test
471    public void testFrequencyComparator() {
472        List<AppEntry> entries = new ArrayList<>();
473
474        NotificationsSentState notFrequentWeekly = new NotificationsSentState();
475        notFrequentWeekly.sentCount = 2;
476        AppEntry notFrequentWeeklyEntry = mock(AppEntry.class);
477        notFrequentWeeklyEntry.extraInfo = notFrequentWeekly;
478        entries.add(notFrequentWeeklyEntry);
479
480        NotificationsSentState notFrequentDaily = new NotificationsSentState();
481        notFrequentDaily.sentCount = 7;
482        AppEntry notFrequentDailyEntry = mock(AppEntry.class);
483        notFrequentDailyEntry.extraInfo = notFrequentDaily;
484        entries.add(notFrequentDailyEntry);
485
486        NotificationsSentState veryFrequentWeekly = new NotificationsSentState();
487        veryFrequentWeekly.sentCount = 6;
488        AppEntry veryFrequentWeeklyEntry = mock(AppEntry.class);
489        veryFrequentWeeklyEntry.extraInfo = veryFrequentWeekly;
490        entries.add(veryFrequentWeeklyEntry);
491
492        NotificationsSentState veryFrequentDaily = new NotificationsSentState();
493        veryFrequentDaily.sentCount = 19;
494        AppEntry veryFrequentDailyEntry = mock(AppEntry.class);
495        veryFrequentDailyEntry.extraInfo = veryFrequentDaily;
496        entries.add(veryFrequentDailyEntry);
497
498        entries.sort(FREQUENCY_NOTIFICATION_COMPARATOR);
499
500        assertThat(entries).containsExactly(veryFrequentDailyEntry, notFrequentDailyEntry,
501                veryFrequentWeeklyEntry, notFrequentWeeklyEntry);
502    }
503
504    @Test
505    public void testSwitchOnClickListener() {
506        ViewGroup parent = mock(ViewGroup.class);
507        Switch toggle = mock(Switch.class);
508        when(toggle.isChecked()).thenReturn(true);
509        when(toggle.isEnabled()).thenReturn(true);
510        when(parent.findViewById(anyInt())).thenReturn(toggle);
511
512        AppEntry entry = mock(AppEntry.class);
513        entry.info = new ApplicationInfo();
514        entry.info.packageName = "pkg";
515        entry.info.uid = 1356;
516        entry.extraInfo = new NotificationsSentState();
517
518        ViewGroup.OnClickListener listener = mBridge.getSwitchOnClickListener(entry);
519        listener.onClick(parent);
520
521        verify(toggle).toggle();
522        verify(mBackend).setNotificationsEnabledForPackage(
523                entry.info.packageName, entry.info.uid, true);
524        assertThat(((NotificationsSentState) entry.extraInfo).blocked).isFalse();
525    }
526
527    @Test
528    public void testSwitchViews_nullDoesNotCrash() {
529        mBridge.enableSwitch(null);
530        mBridge.checkSwitch(null);
531    }
532}
533