1 /*
2  * Copyright (C) 2024 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.modes;
18 
19 import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
20 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
21 import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
22 
23 import static com.google.common.base.Preconditions.checkNotNull;
24 import static com.google.common.truth.Truth.assertThat;
25 
26 import static org.mockito.ArgumentMatchers.any;
27 import static org.mockito.ArgumentMatchers.anyBoolean;
28 import static org.mockito.ArgumentMatchers.anyInt;
29 import static org.mockito.ArgumentMatchers.eq;
30 import static org.mockito.Mockito.mock;
31 import static org.mockito.Mockito.never;
32 import static org.mockito.Mockito.times;
33 import static org.mockito.Mockito.verify;
34 import static org.mockito.Mockito.verifyNoMoreInteractions;
35 import static org.mockito.Mockito.when;
36 import static org.robolectric.Shadows.shadowOf;
37 
38 import android.app.Flags;
39 import android.app.settings.SettingsEnums;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.pm.ApplicationInfo;
43 import android.content.pm.UserInfo;
44 import android.graphics.drawable.ColorDrawable;
45 import android.os.Bundle;
46 import android.os.UserHandle;
47 import android.os.UserManager;
48 import android.platform.test.annotations.EnableFlags;
49 import android.platform.test.flag.junit.SetFlagsRule;
50 import android.service.notification.ZenPolicy;
51 import android.view.LayoutInflater;
52 import android.view.View;
53 
54 import androidx.fragment.app.Fragment;
55 import androidx.preference.PreferenceViewHolder;
56 
57 import com.android.settings.R;
58 import com.android.settings.SettingsActivity;
59 import com.android.settingslib.applications.ApplicationsState;
60 import com.android.settingslib.applications.ApplicationsState.AppEntry;
61 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
62 import com.android.settingslib.notification.modes.TestModeBuilder;
63 import com.android.settingslib.notification.modes.ZenMode;
64 import com.android.settingslib.notification.modes.ZenModesBackend;
65 
66 import com.google.common.collect.ImmutableList;
67 import com.google.common.util.concurrent.MoreExecutors;
68 
69 import org.junit.Before;
70 import org.junit.Rule;
71 import org.junit.Test;
72 import org.junit.runner.RunWith;
73 import org.mockito.Mock;
74 import org.mockito.MockitoAnnotations;
75 import org.robolectric.RobolectricTestRunner;
76 import org.robolectric.RuntimeEnvironment;
77 
78 import java.util.ArrayList;
79 import java.util.List;
80 import java.util.Map;
81 import java.util.Random;
82 
83 @RunWith(RobolectricTestRunner.class)
84 @EnableFlags(Flags.FLAG_MODES_UI)
85 public final class ZenModeAppsLinkPreferenceControllerTest {
86 
87     private ZenModeAppsLinkPreferenceController mController;
88     private CircularIconsPreference mPreference;
89     private CircularIconsView mIconsView;
90 
91     private Context mContext;
92     @Mock
93     private ZenModesBackend mZenModesBackend;
94 
95     @Mock
96     private ZenHelperBackend mHelperBackend;
97 
98     @Mock
99     private ApplicationsState mApplicationsState;
100     @Mock
101     private ApplicationsState.Session mSession;
102 
103     @Rule
104     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
105 
106     @Before
setup()107     public void setup() {
108         MockitoAnnotations.initMocks(this);
109         mContext = RuntimeEnvironment.application;
110         CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService();
111         mPreference = new TestableCircularIconsPreference(mContext);
112         when(mApplicationsState.newSession(any(), any())).thenReturn(mSession);
113 
114         mController = new ZenModeAppsLinkPreferenceController(
115                 mContext, "controller_key", mock(Fragment.class), mApplicationsState,
116                 mZenModesBackend, mHelperBackend,
117                 /* appIconRetriever= */ appInfo -> new ColorDrawable());
118 
119         // Ensure the preference view is bound & measured (needed to add child ImageViews).
120         View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(),
121                 null);
122         mIconsView = checkNotNull(preferenceView.findViewById(R.id.circles_container));
123         mIconsView.setUiExecutor(MoreExecutors.directExecutor());
124         preferenceView.measure(View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY),
125                 View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY));
126         PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(preferenceView);
127         mPreference.onBindViewHolder(holder);
128     }
129 
createAppEntry(String packageName, int userId)130     private AppEntry createAppEntry(String packageName, int userId) {
131         ApplicationInfo applicationInfo = new ApplicationInfo();
132         applicationInfo.packageName = packageName;
133         applicationInfo.uid = UserHandle.getUid(userId, new Random().nextInt(100));
134         AppEntry appEntry = new AppEntry(mContext, applicationInfo, 1);
135         appEntry.label = packageName;
136         return appEntry;
137     }
138 
createPriorityChannelsZenMode()139     private ZenMode createPriorityChannelsZenMode() {
140         return new TestModeBuilder()
141                 .setId("id")
142                 .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
143                 .setZenPolicy(new ZenPolicy.Builder()
144                         .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
145                         .build())
146                 .build();
147     }
148 
149     @Test
testIsAvailable()150     public void testIsAvailable() {
151         assertThat(mController.isAvailable()).isTrue();
152     }
153 
154     @Test
updateState_dnd_enabled()155     public void updateState_dnd_enabled() {
156         ZenMode dnd = TestModeBuilder.MANUAL_DND_ACTIVE;
157         mController.updateState(mPreference, dnd);
158         assertThat(mPreference.isEnabled()).isTrue();
159     }
160 
161     @Test
updateState_specialDnd_disabled()162     public void updateState_specialDnd_disabled() {
163         ZenMode specialDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, true);
164         mController.updateState(mPreference, specialDnd);
165         assertThat(mPreference.isEnabled()).isFalse();
166     }
167 
168     @Test
testUpdateState_disabled()169     public void testUpdateState_disabled() {
170         ZenMode zenMode = new TestModeBuilder()
171                 .setEnabled(false)
172                 .build();
173 
174         mController.updateState(mPreference, zenMode);
175 
176         assertThat(mPreference.isEnabled()).isFalse();
177     }
178 
179     @Test
testUpdateSetsIntent()180     public void testUpdateSetsIntent() {
181         // Create a zen mode that allows priority channels to breakthrough.
182         ZenMode zenMode = createPriorityChannelsZenMode();
183 
184         mController.updateState(mPreference, zenMode);
185         Intent launcherIntent = mPreference.getIntent();
186 
187         assertThat(launcherIntent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
188                 .isEqualTo("com.android.settings.notification.modes.ZenModeAppsFragment");
189         assertThat(launcherIntent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
190                 -1)).isEqualTo(SettingsEnums.ZEN_PRIORITY_MODE);
191 
192         Bundle bundle = launcherIntent.getBundleExtra(
193                 SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
194         assertThat(bundle).isNotNull();
195         assertThat(bundle.getString(EXTRA_AUTOMATIC_ZEN_RULE_ID)).isEqualTo("id");
196     }
197 
198     @Test
testGetAppsBypassingDnd()199     public void testGetAppsBypassingDnd() {
200         ApplicationsState.AppEntry app1 = createAppEntry("app1", mContext.getUserId());
201         ApplicationsState.AppEntry app2 = createAppEntry("app2", mContext.getUserId());
202         List<ApplicationsState.AppEntry> allApps = List.of(app1, app2);
203 
204         when(mHelperBackend.getPackagesBypassingDnd(mContext.getUserId())).thenReturn(
205                 Map.of("app1", true));
206 
207         assertThat(mController.getAppsBypassingDndSortedByName(allApps)).containsExactly(app1);
208     }
209 
210     @Test
testGetAppsBypassingDnd_sortsByName()211     public void testGetAppsBypassingDnd_sortsByName() {
212         ApplicationsState.AppEntry appC = createAppEntry("C", mContext.getUserId());
213         ApplicationsState.AppEntry appA = createAppEntry("A", mContext.getUserId());
214         ApplicationsState.AppEntry appB = createAppEntry("B", mContext.getUserId());
215         List<ApplicationsState.AppEntry> allApps = List.of(appC, appA, appB);
216 
217         when(mHelperBackend.getPackagesBypassingDnd(eq(mContext.getUserId())))
218                 .thenReturn(Map.of("B", true, "C", false, "A", true));
219 
220         assertThat(mController.getAppsBypassingDndSortedByName(allApps))
221                 .containsExactly(appA, appB, appC).inOrder();
222     }
223 
224     @Test
testGetAppsBypassingDnd_withWorkProfile_includesProfileAndSorts()225     public void testGetAppsBypassingDnd_withWorkProfile_includesProfileAndSorts() {
226         UserInfo workProfile = new UserInfo(10, "Work Profile", 0);
227         workProfile.userType = UserManager.USER_TYPE_PROFILE_MANAGED;
228         UserManager userManager = mContext.getSystemService(UserManager.class);
229         shadowOf(userManager).addProfile(mContext.getUserId(), 10, workProfile);
230 
231         ApplicationsState.AppEntry personalCopy = createAppEntry("app", mContext.getUserId());
232         ApplicationsState.AppEntry workCopy = createAppEntry("app", 10);
233         ApplicationsState.AppEntry otherPersonal = createAppEntry("p2", mContext.getUserId());
234         ApplicationsState.AppEntry otherWork = createAppEntry("w2", 10);
235         List<ApplicationsState.AppEntry> allApps = List.of(workCopy, personalCopy, otherPersonal,
236                 otherWork);
237 
238         when(mHelperBackend.getPackagesBypassingDnd(eq(mContext.getUserId())))
239                 .thenReturn(Map.of("app", true, "p2", true));
240         when(mHelperBackend.getPackagesBypassingDnd(eq(10)))
241                 .thenReturn(Map.of("app", false));
242 
243         // Personal copy before work copy (names match).
244         assertThat(mController.getAppsBypassingDndSortedByName(allApps))
245                 .containsExactly(personalCopy, workCopy, otherPersonal).inOrder();
246     }
247 
248     @Test
updateState_withPolicyAllowingNoChannels_doesNotLoadPriorityApps()249     public void updateState_withPolicyAllowingNoChannels_doesNotLoadPriorityApps() {
250         ZenMode zenMode = new TestModeBuilder()
251                 .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
252                 .build();
253 
254         mController.updateState(mPreference, zenMode);
255 
256         verifyNoMoreInteractions(mSession);
257         verify(mHelperBackend, never()).getPackagesBypassingDnd(anyInt());
258         assertThat(String.valueOf(mPreference.getSummary())).isEqualTo("None");
259     }
260 
261     @Test
updateState_withPolicyAllowingPriorityChannels_triggersRebuild()262     public void updateState_withPolicyAllowingPriorityChannels_triggersRebuild() {
263         // Create a zen mode that allows priority channels to breakthrough.
264         ZenMode zenMode = createPriorityChannelsZenMode();
265 
266         // Create some applications.
267         ArrayList<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
268         appEntries.add(createAppEntry("test", mContext.getUserId()));
269 
270         when(mHelperBackend.getPackagesBypassingDnd(mContext.getUserId()))
271                 .thenReturn(Map.of("test", false));
272 
273         // Updates the preference with the zen mode. We expect that this causes the app session
274         // to trigger a rebuild (and display a temporary text in the meantime).
275         mController.updateZenMode(mPreference, zenMode);
276         verify(mSession).rebuild(any(), any(), eq(false));
277         assertThat(String.valueOf(mPreference.getSummary())).isEqualTo("Calculating…");
278 
279         // Manually triggers the callback that will happen on rebuild.
280         mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
281         assertThat(String.valueOf(mPreference.getSummary())).isEqualTo("test can interrupt");
282     }
283 
284     @Test
updateState_withPolicyAllowingPriorityChannels_loadsIcons()285     public void updateState_withPolicyAllowingPriorityChannels_loadsIcons() {
286         ZenMode zenMode = createPriorityChannelsZenMode();
287 
288         mController.updateState(mPreference, zenMode);
289         when(mHelperBackend.getPackagesBypassingDnd(anyInt()))
290                 .thenReturn(Map.of("test1", false, "test2", false));
291         ArrayList<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
292         appEntries.add(createAppEntry("test1", mContext.getUserId()));
293         appEntries.add(createAppEntry("test2", mContext.getUserId()));
294         mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
295 
296         assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(2);
297     }
298 
299     @Test
testOnPackageListChangedTriggersRebuild()300     public void testOnPackageListChangedTriggersRebuild() {
301         // Create a zen mode that allows priority channels to breakthrough.
302         ZenMode zenMode = createPriorityChannelsZenMode();
303         mController.updateState(mPreference, zenMode);
304         verify(mSession).rebuild(any(), any(), eq(false));
305 
306         mController.mAppSessionCallbacks.onPackageListChanged();
307         verify(mSession, times(2)).rebuild(any(), any(), eq(false));
308     }
309 
310     @Test
testOnLoadEntriesCompletedTriggersRebuild()311     public void testOnLoadEntriesCompletedTriggersRebuild() {
312         // Create a zen mode that allows priority channels to breakthrough.
313         ZenMode zenMode = createPriorityChannelsZenMode();
314         mController.updateState(mPreference, zenMode);
315         verify(mSession).rebuild(any(), any(), eq(false));
316 
317         mController.mAppSessionCallbacks.onLoadEntriesCompleted();
318         verify(mSession, times(2)).rebuild(any(), any(), eq(false));
319     }
320 
321     @Test
updateState_noneToPriority_loadsBypassingAppsAndListensForChanges()322     public void updateState_noneToPriority_loadsBypassingAppsAndListensForChanges() {
323         ZenMode zenModeWithNone = new TestModeBuilder()
324                 .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
325                 .build();
326         ZenMode zenModeWithPriority = new TestModeBuilder()
327                 .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build())
328                 .build();
329         ArrayList<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
330         appEntries.add(createAppEntry("test", mContext.getUserId()));
331         when(mHelperBackend.getPackagesBypassingDnd(mContext.getUserId()))
332                 .thenReturn(Map.of("test", true));
333 
334         mController.updateState(mPreference, zenModeWithNone);
335 
336         assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(0);
337         verifyNoMoreInteractions(mApplicationsState);
338         verifyNoMoreInteractions(mSession);
339 
340         mController.updateState(mPreference, zenModeWithPriority);
341 
342         verify(mApplicationsState).newSession(any(), any());
343         verify(mSession).rebuild(any(), any(), anyBoolean());
344         mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
345         assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(1);
346     }
347 
348     @Test
updateState_priorityToNone_clearsBypassingAppsAndStopsListening()349     public void updateState_priorityToNone_clearsBypassingAppsAndStopsListening() {
350         ZenMode zenModeWithNone = new TestModeBuilder()
351                 .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
352                 .build();
353         ZenMode zenModeWithPriority = new TestModeBuilder()
354                 .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build())
355                 .build();
356         ArrayList<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
357         appEntries.add(createAppEntry("test", mContext.getUserId()));
358         when(mHelperBackend.getPackagesBypassingDnd(mContext.getUserId()))
359                 .thenReturn(Map.of("test", true));
360 
361         mController.updateState(mPreference, zenModeWithPriority);
362 
363         verify(mApplicationsState).newSession(any(), any());
364         verify(mSession).rebuild(any(), any(), anyBoolean());
365         mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
366         assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(1);
367 
368         mController.updateState(mPreference, zenModeWithNone);
369 
370         assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(0);
371         verify(mSession).deactivateSession();
372         verifyNoMoreInteractions(mSession);
373         verifyNoMoreInteractions(mApplicationsState);
374 
375         // An errant callback (triggered by onResume and received asynchronously after
376         // updateState()) is ignored.
377         mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
378 
379         assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(0);
380     }
381 
382     @Test
updateState_priorityToNoneToPriority_restartsListening()383     public void updateState_priorityToNoneToPriority_restartsListening() {
384         ZenMode zenModeWithNone = new TestModeBuilder()
385                 .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
386                 .build();
387         ZenMode zenModeWithPriority = new TestModeBuilder()
388                 .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build())
389                 .build();
390 
391         mController.updateState(mPreference, zenModeWithPriority);
392         verify(mApplicationsState).newSession(any(), any());
393         verify(mSession).rebuild(any(), any(), anyBoolean());
394 
395         mController.updateState(mPreference, zenModeWithNone);
396         verifyNoMoreInteractions(mApplicationsState);
397         verify(mSession).deactivateSession();
398 
399         mController.updateState(mPreference, zenModeWithPriority);
400         verifyNoMoreInteractions(mApplicationsState);
401         verify(mSession).activateSession();
402     }
403 
404     @Test
testNoCrashIfAppsReadyBeforeRuleAvailable()405     public void testNoCrashIfAppsReadyBeforeRuleAvailable() {
406         mController.mAppSessionCallbacks.onLoadEntriesCompleted();
407     }
408 }
409