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