1 /* 2 * Copyright (C) 2022 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 @file:OptIn(InternalNoteTaskApi::class) 17 18 package com.android.systemui.notetask 19 20 import android.app.ActivityManager 21 import android.app.KeyguardManager 22 import android.app.admin.DevicePolicyManager 23 import android.app.role.RoleManager 24 import android.app.role.RoleManager.ROLE_NOTES 25 import android.content.ComponentName 26 import android.content.Context 27 import android.content.Intent 28 import android.content.Intent.ACTION_CREATE_NOTE 29 import android.content.Intent.ACTION_MAIN 30 import android.content.Intent.ACTION_MANAGE_DEFAULT_APP 31 import android.content.Intent.CATEGORY_HOME 32 import android.content.Intent.EXTRA_USE_STYLUS_MODE 33 import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK 34 import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT 35 import android.content.Intent.FLAG_ACTIVITY_NEW_TASK 36 import android.content.pm.PackageManager 37 import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED 38 import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED 39 import android.content.pm.ShortcutManager 40 import android.content.pm.UserInfo 41 import android.graphics.drawable.Icon 42 import android.os.UserHandle 43 import android.os.UserManager 44 import android.provider.Settings 45 import androidx.test.ext.truth.content.IntentSubject.assertThat 46 import androidx.test.filters.SmallTest 47 import androidx.test.runner.AndroidJUnit4 48 import com.android.systemui.R 49 import com.android.systemui.SysuiTestCase 50 import com.android.systemui.notetask.NoteTaskController.Companion.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE 51 import com.android.systemui.notetask.NoteTaskController.Companion.SHORTCUT_ID 52 import com.android.systemui.notetask.NoteTaskEntryPoint.APP_CLIPS 53 import com.android.systemui.notetask.NoteTaskEntryPoint.KEYBOARD_SHORTCUT 54 import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE 55 import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON 56 import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT 57 import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity 58 import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity 59 import com.android.systemui.settings.FakeUserTracker 60 import com.android.systemui.util.mockito.any 61 import com.android.systemui.util.mockito.argumentCaptor 62 import com.android.systemui.util.mockito.capture 63 import com.android.systemui.util.mockito.eq 64 import com.android.systemui.util.mockito.mock 65 import com.android.systemui.util.mockito.whenever 66 import com.android.systemui.util.mockito.withArgCaptor 67 import com.android.systemui.util.settings.SecureSettings 68 import com.android.wm.shell.bubbles.Bubble 69 import com.android.wm.shell.bubbles.Bubbles 70 import com.google.common.truth.Truth.assertThat 71 import java.util.Optional 72 import kotlin.test.assertNotNull 73 import kotlinx.coroutines.ExperimentalCoroutinesApi 74 import kotlinx.coroutines.test.TestScope 75 import kotlinx.coroutines.test.UnconfinedTestDispatcher 76 import org.junit.Before 77 import org.junit.Test 78 import org.junit.runner.RunWith 79 import org.mockito.ArgumentMatchers.anyInt 80 import org.mockito.Mock 81 import org.mockito.Mockito.atLeastOnce 82 import org.mockito.Mockito.doNothing 83 import org.mockito.Mockito.never 84 import org.mockito.Mockito.spy 85 import org.mockito.Mockito.verify 86 import org.mockito.Mockito.verifyZeroInteractions 87 import org.mockito.MockitoAnnotations 88 89 /** atest SystemUITests:NoteTaskControllerTest */ 90 @OptIn(ExperimentalCoroutinesApi::class) 91 @SmallTest 92 @RunWith(AndroidJUnit4::class) 93 internal class NoteTaskControllerTest : SysuiTestCase() { 94 95 @Mock private lateinit var context: Context 96 @Mock private lateinit var workProfileContext: Context 97 @Mock private lateinit var packageManager: PackageManager 98 @Mock private lateinit var workProfilePackageManager: PackageManager 99 @Mock private lateinit var resolver: NoteTaskInfoResolver 100 @Mock private lateinit var bubbles: Bubbles 101 @Mock private lateinit var keyguardManager: KeyguardManager 102 @Mock private lateinit var userManager: UserManager 103 @Mock private lateinit var eventLogger: NoteTaskEventLogger 104 @Mock private lateinit var roleManager: RoleManager 105 @Mock private lateinit var shortcutManager: ShortcutManager 106 @Mock private lateinit var activityManager: ActivityManager 107 @Mock private lateinit var devicePolicyManager: DevicePolicyManager 108 @Mock private lateinit var secureSettings: SecureSettings 109 private val userTracker = FakeUserTracker() 110 private val testDispatcher = UnconfinedTestDispatcher() 111 private val testScope = TestScope(testDispatcher) 112 113 @Before 114 fun setUp() { 115 MockitoAnnotations.initMocks(this) 116 117 whenever(context.getString(R.string.note_task_button_label)) 118 .thenReturn(NOTE_TASK_SHORT_LABEL) 119 whenever(context.getString(eq(R.string.note_task_shortcut_long_label), any())) 120 .thenReturn(NOTE_TASK_LONG_LABEL) 121 whenever(context.packageManager).thenReturn(packageManager) 122 whenever(context.createContextAsUser(any(), any())).thenReturn(context) 123 whenever(packageManager.getApplicationInfo(any(), any<Int>())).thenReturn(mock()) 124 whenever(packageManager.getApplicationLabel(any())).thenReturn(NOTE_TASK_LONG_LABEL) 125 whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(NOTE_TASK_INFO) 126 whenever(userManager.isUserUnlocked).thenReturn(true) 127 whenever(userManager.isUserUnlocked(any<Int>())).thenReturn(true) 128 whenever(userManager.isUserUnlocked(any<UserHandle>())).thenReturn(true) 129 whenever( 130 devicePolicyManager.getKeyguardDisabledFeatures( 131 /* admin= */ eq(null), 132 /* userHandle= */ anyInt() 133 ) 134 ) 135 .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE) 136 whenever(roleManager.getRoleHoldersAsUser(ROLE_NOTES, userTracker.userHandle)) 137 .thenReturn(listOf(NOTE_TASK_PACKAGE_NAME)) 138 whenever(activityManager.getRunningTasks(anyInt())).thenReturn(emptyList()) 139 whenever(userManager.isManagedProfile(workUserInfo.id)).thenReturn(true) 140 whenever(context.resources).thenReturn(getContext().resources) 141 whenever(secureSettings.userTracker).thenReturn(userTracker) 142 } 143 144 private fun createNoteTaskController( 145 isEnabled: Boolean = true, 146 bubbles: Bubbles? = this.bubbles, 147 ): NoteTaskController = 148 NoteTaskController( 149 context = context, 150 resolver = resolver, 151 eventLogger = eventLogger, 152 userManager = userManager, 153 keyguardManager = keyguardManager, 154 isEnabled = isEnabled, 155 devicePolicyManager = devicePolicyManager, 156 userTracker = userTracker, 157 roleManager = roleManager, 158 shortcutManager = shortcutManager, 159 activityManager = activityManager, 160 secureSettings = secureSettings, 161 noteTaskBubblesController = 162 FakeNoteTaskBubbleController(context, testDispatcher, Optional.ofNullable(bubbles)), 163 applicationScope = testScope, 164 ) 165 166 // region onBubbleExpandChanged 167 @Test 168 fun onBubbleExpandChanged_expanding_logNoteTaskOpened() { 169 val expectedInfo = NOTE_TASK_INFO.copy(isKeyguardLocked = false) 170 171 createNoteTaskController() 172 .apply { infoReference.set(expectedInfo) } 173 .onBubbleExpandChanged( 174 isExpanding = true, 175 key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), 176 ) 177 178 verify(eventLogger).logNoteTaskOpened(expectedInfo) 179 verifyZeroInteractions(bubbles, keyguardManager, userManager) 180 } 181 182 @Test 183 fun onBubbleExpandChanged_collapsing_logNoteTaskClosed() { 184 val expectedInfo = NOTE_TASK_INFO.copy(isKeyguardLocked = false) 185 186 createNoteTaskController() 187 .apply { infoReference.set(expectedInfo) } 188 .onBubbleExpandChanged( 189 isExpanding = false, 190 key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), 191 ) 192 193 verify(eventLogger).logNoteTaskClosed(expectedInfo) 194 verifyZeroInteractions(bubbles, keyguardManager, userManager) 195 } 196 197 @Test 198 fun onBubbleExpandChanged_expandingAndKeyguardLocked_shouldDoNothing() { 199 val expectedInfo = NOTE_TASK_INFO.copy(isKeyguardLocked = true) 200 201 createNoteTaskController() 202 .apply { infoReference.set(expectedInfo) } 203 .onBubbleExpandChanged( 204 isExpanding = true, 205 key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), 206 ) 207 208 verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) 209 } 210 211 @Test 212 fun onBubbleExpandChanged_notExpandingAndKeyguardLocked_shouldDoNothing() { 213 val expectedInfo = NOTE_TASK_INFO.copy(isKeyguardLocked = true) 214 215 createNoteTaskController() 216 .apply { infoReference.set(expectedInfo) } 217 .onBubbleExpandChanged( 218 isExpanding = false, 219 key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), 220 ) 221 222 verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) 223 } 224 225 @Test 226 fun onBubbleExpandChanged_notKeyAppBubble_shouldDoNothing() { 227 createNoteTaskController() 228 .onBubbleExpandChanged( 229 isExpanding = true, 230 key = "any other key", 231 ) 232 233 verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) 234 } 235 236 @Test 237 fun onBubbleExpandChanged_flagDisabled_shouldDoNothing() { 238 createNoteTaskController(isEnabled = false) 239 .onBubbleExpandChanged( 240 isExpanding = true, 241 key = Bubble.getAppBubbleKeyForApp(NOTE_TASK_INFO.packageName, NOTE_TASK_INFO.user), 242 ) 243 244 verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) 245 } 246 // endregion 247 248 // region showNoteTask 249 fun showNoteTaskAsUser_keyguardIsLocked_shouldStartActivityWithExpectedUserAndLogUiEvent() { 250 val user10 = UserHandle.of(/* userId= */ 10) 251 val expectedInfo = 252 NOTE_TASK_INFO.copy( 253 entryPoint = TAIL_BUTTON, 254 isKeyguardLocked = true, 255 user = user10, 256 ) 257 whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) 258 whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) 259 260 createNoteTaskController() 261 .showNoteTaskAsUser(entryPoint = expectedInfo.entryPoint!!, user = user10) 262 263 val intentCaptor = argumentCaptor<Intent>() 264 val userCaptor = argumentCaptor<UserHandle>() 265 verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) 266 assertThat(intentCaptor.value).run { 267 hasAction(ACTION_CREATE_NOTE) 268 hasPackage(NOTE_TASK_PACKAGE_NAME) 269 hasFlags(FLAG_ACTIVITY_NEW_TASK) 270 hasFlags(FLAG_ACTIVITY_MULTIPLE_TASK) 271 hasFlags(FLAG_ACTIVITY_NEW_DOCUMENT) 272 extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() 273 } 274 assertThat(userCaptor.value).isEqualTo(user10) 275 verify(eventLogger).logNoteTaskOpened(expectedInfo) 276 verifyZeroInteractions(bubbles) 277 } 278 279 @Test 280 fun showNoteTask_keyguardIsLocked_notesIsClosed_shouldStartActivityAndLogUiEvent() { 281 val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true) 282 whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) 283 whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) 284 285 createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) 286 287 val intentCaptor = argumentCaptor<Intent>() 288 val userCaptor = argumentCaptor<UserHandle>() 289 verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) 290 assertThat(intentCaptor.value).run { 291 hasAction(ACTION_CREATE_NOTE) 292 hasPackage(NOTE_TASK_PACKAGE_NAME) 293 hasFlags(FLAG_ACTIVITY_NEW_TASK) 294 hasFlags(FLAG_ACTIVITY_MULTIPLE_TASK) 295 hasFlags(FLAG_ACTIVITY_NEW_DOCUMENT) 296 extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() 297 } 298 assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) 299 verify(eventLogger).logNoteTaskOpened(expectedInfo) 300 verifyZeroInteractions(bubbles) 301 } 302 303 @Test 304 fun showNoteTask_keyguardIsLocked_noteIsOpen_shouldCloseActivityAndLogUiEvent() { 305 val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true) 306 whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) 307 whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) 308 whenever(activityManager.getRunningTasks(anyInt())) 309 .thenReturn(listOf(NOTE_RUNNING_TASK_INFO)) 310 311 createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) 312 313 val intentCaptor = argumentCaptor<Intent>() 314 val userCaptor = argumentCaptor<UserHandle>() 315 verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) 316 assertThat(intentCaptor.value).run { 317 hasAction(ACTION_MAIN) 318 categories().contains(CATEGORY_HOME) 319 hasFlags(FLAG_ACTIVITY_NEW_TASK) 320 } 321 assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) 322 verify(eventLogger).logNoteTaskClosed(expectedInfo) 323 verifyZeroInteractions(bubbles) 324 } 325 326 @Test 327 fun showNoteTask_keyguardIsUnlocked_noteIsClosed_shouldStartBubblesWithoutLoggingUiEvent() { 328 val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = false) 329 whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) 330 whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) 331 332 createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) 333 334 // Context package name used to create bubble icon from drawable resource id 335 verify(context, atLeastOnce()).packageName 336 verifyNoteTaskOpenInBubbleInUser(userTracker.userHandle) 337 verifyZeroInteractions(eventLogger) 338 } 339 340 @Test 341 fun showNoteTask_keyguardIsUnlocked_noteIsOpen_shouldStartBubblesWithoutLoggingUiEvent() { 342 val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = false) 343 whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) 344 whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) 345 whenever(activityManager.getRunningTasks(anyInt())) 346 .thenReturn(listOf(NOTE_RUNNING_TASK_INFO)) 347 348 createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) 349 350 // Context package name used to create bubble icon from drawable resource id 351 verify(context, atLeastOnce()).packageName 352 verifyNoteTaskOpenInBubbleInUser(userTracker.userHandle) 353 verifyZeroInteractions(eventLogger) 354 } 355 356 @Test 357 fun showNoteTask_defaultUserSet_shouldStartActivityWithExpectedUserAndLogUiEvent() { 358 whenever( 359 secureSettings.getIntForUser( 360 /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), 361 /* def= */ any(), 362 /* userHandle= */ any() 363 ) 364 ) 365 .thenReturn(10) 366 val user10 = UserHandle.of(/* userId= */ 10) 367 368 val expectedInfo = 369 NOTE_TASK_INFO.copy( 370 entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, 371 isKeyguardLocked = true, 372 user = user10, 373 ) 374 whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) 375 whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) 376 377 createNoteTaskController() 378 .showNoteTask( 379 entryPoint = expectedInfo.entryPoint!!, 380 ) 381 382 val intentCaptor = argumentCaptor<Intent>() 383 val userCaptor = argumentCaptor<UserHandle>() 384 verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) 385 intentCaptor.value.let { intent -> 386 assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) 387 assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME) 388 assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) 389 assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK) 390 .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK) 391 assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT) 392 .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT) 393 assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() 394 } 395 assertThat(userCaptor.value).isEqualTo(user10) 396 verify(eventLogger).logNoteTaskOpened(expectedInfo) 397 verifyZeroInteractions(bubbles) 398 } 399 400 @Test 401 fun showNoteTask_bubblesIsNull_shouldDoNothing() { 402 createNoteTaskController(bubbles = null).showNoteTask(entryPoint = TAIL_BUTTON) 403 404 verifyZeroInteractions(bubbles, eventLogger) 405 } 406 407 @Test 408 fun showNoteTask_intentResolverReturnsNull_shouldShowToast() { 409 whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(null) 410 val noteTaskController = spy(createNoteTaskController()) 411 doNothing().whenever(noteTaskController).showNoDefaultNotesAppToast() 412 413 noteTaskController.showNoteTask(entryPoint = TAIL_BUTTON) 414 415 verify(noteTaskController).showNoDefaultNotesAppToast() 416 verifyZeroInteractions(bubbles, eventLogger) 417 } 418 419 @Test 420 fun showNoteTask_flagDisabled_shouldDoNothing() { 421 createNoteTaskController(isEnabled = false).showNoteTask(entryPoint = TAIL_BUTTON) 422 423 verifyZeroInteractions(bubbles, eventLogger) 424 } 425 426 @Test 427 fun showNoteTask_userIsLocked_shouldDoNothing() { 428 whenever(userManager.isUserUnlocked).thenReturn(false) 429 430 createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON) 431 432 verifyZeroInteractions(bubbles, eventLogger) 433 } 434 435 @Test 436 fun showNoteTask_keyboardShortcut_shouldStartActivity() { 437 val expectedInfo = 438 NOTE_TASK_INFO.copy(entryPoint = KEYBOARD_SHORTCUT, isKeyguardLocked = true) 439 whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) 440 whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) 441 442 createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) 443 444 val intentCaptor = argumentCaptor<Intent>() 445 val userCaptor = argumentCaptor<UserHandle>() 446 verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) 447 assertThat(intentCaptor.value).run { 448 hasAction(ACTION_CREATE_NOTE) 449 hasPackage(NOTE_TASK_PACKAGE_NAME) 450 hasFlags(FLAG_ACTIVITY_NEW_TASK) 451 hasFlags(FLAG_ACTIVITY_MULTIPLE_TASK) 452 hasFlags(FLAG_ACTIVITY_NEW_DOCUMENT) 453 extras().bool(EXTRA_USE_STYLUS_MODE).isFalse() 454 } 455 assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) 456 verify(eventLogger).logNoteTaskOpened(expectedInfo) 457 verifyZeroInteractions(bubbles) 458 } 459 // endregion 460 461 // region setNoteTaskShortcutEnabled 462 @Test 463 fun setNoteTaskShortcutEnabled_setTrue() { 464 createNoteTaskController().setNoteTaskShortcutEnabled(value = true, userTracker.userHandle) 465 466 val argument = argumentCaptor<ComponentName>() 467 verify(context.packageManager) 468 .setComponentEnabledSetting( 469 argument.capture(), 470 eq(COMPONENT_ENABLED_STATE_ENABLED), 471 eq(PackageManager.DONT_KILL_APP), 472 ) 473 474 assertThat(argument.value.className) 475 .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) 476 } 477 478 @Test 479 fun setNoteTaskShortcutEnabled_setFalse() { 480 createNoteTaskController().setNoteTaskShortcutEnabled(value = false, userTracker.userHandle) 481 482 val argument = argumentCaptor<ComponentName>() 483 verify(context.packageManager) 484 .setComponentEnabledSetting( 485 argument.capture(), 486 eq(COMPONENT_ENABLED_STATE_DISABLED), 487 eq(PackageManager.DONT_KILL_APP), 488 ) 489 490 assertThat(argument.value.className) 491 .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) 492 } 493 494 @Test 495 fun setNoteTaskShortcutEnabled_workProfileUser_setTrue() { 496 whenever(context.createContextAsUser(eq(workUserInfo.userHandle), any())) 497 .thenReturn(workProfileContext) 498 whenever(workProfileContext.packageManager).thenReturn(workProfilePackageManager) 499 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 500 501 createNoteTaskController().setNoteTaskShortcutEnabled(value = true, workUserInfo.userHandle) 502 503 val argument = argumentCaptor<ComponentName>() 504 verify(workProfilePackageManager) 505 .setComponentEnabledSetting( 506 argument.capture(), 507 eq(COMPONENT_ENABLED_STATE_ENABLED), 508 eq(PackageManager.DONT_KILL_APP), 509 ) 510 511 assertThat(argument.value.className) 512 .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) 513 } 514 515 @Test 516 fun setNoteTaskShortcutEnabled_workProfileUser_setFalse() { 517 whenever(context.createContextAsUser(eq(workUserInfo.userHandle), any())) 518 .thenReturn(workProfileContext) 519 whenever(workProfileContext.packageManager).thenReturn(workProfilePackageManager) 520 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 521 522 createNoteTaskController() 523 .setNoteTaskShortcutEnabled(value = false, workUserInfo.userHandle) 524 525 val argument = argumentCaptor<ComponentName>() 526 verify(workProfilePackageManager) 527 .setComponentEnabledSetting( 528 argument.capture(), 529 eq(COMPONENT_ENABLED_STATE_DISABLED), 530 eq(PackageManager.DONT_KILL_APP), 531 ) 532 533 assertThat(argument.value.className) 534 .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) 535 } 536 // endregion 537 538 // region keyguard policy 539 @Test 540 fun showNoteTask_keyguardLocked_keyguardDisableShortcutsAll_shouldDoNothing() { 541 whenever(keyguardManager.isKeyguardLocked).thenReturn(true) 542 whenever( 543 devicePolicyManager.getKeyguardDisabledFeatures( 544 /* admin= */ eq(null), 545 /* userHandle= */ anyInt() 546 ) 547 ) 548 .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL) 549 550 createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) 551 552 verifyZeroInteractions(bubbles, eventLogger) 553 } 554 555 @Test 556 fun showNoteTask_keyguardLocked_keyguardDisableFeaturesAll_shouldDoNothing() { 557 whenever(keyguardManager.isKeyguardLocked).thenReturn(true) 558 whenever( 559 devicePolicyManager.getKeyguardDisabledFeatures( 560 /* admin= */ eq(null), 561 /* userHandle= */ anyInt() 562 ) 563 ) 564 .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL) 565 566 createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) 567 568 verifyZeroInteractions(bubbles, eventLogger) 569 } 570 571 @Test 572 fun showNoteTask_keyguardUnlocked_keyguardDisableShortcutsAll_shouldStartBubble() { 573 whenever(keyguardManager.isKeyguardLocked).thenReturn(false) 574 whenever( 575 devicePolicyManager.getKeyguardDisabledFeatures( 576 /* admin= */ eq(null), 577 /* userHandle= */ anyInt() 578 ) 579 ) 580 .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL) 581 582 createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) 583 584 verifyNoteTaskOpenInBubbleInUser(userTracker.userHandle) 585 } 586 587 @Test 588 fun showNoteTask_keyguardUnlocked_keyguardDisableFeaturesAll_shouldStartBubble() { 589 whenever(keyguardManager.isKeyguardLocked).thenReturn(false) 590 whenever( 591 devicePolicyManager.getKeyguardDisabledFeatures( 592 /* admin= */ eq(null), 593 /* userHandle= */ anyInt() 594 ) 595 ) 596 .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL) 597 598 createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) 599 600 verifyNoteTaskOpenInBubbleInUser(userTracker.userHandle) 601 } 602 // endregion 603 604 // region showNoteTask, COPE devices 605 @Test 606 fun showNoteTask_copeDevices_quickAffordanceEntryPoint_managedProfileNotFound_shouldStartBubbleInTheMainProfile() { // ktlint-disable max-line-length 607 whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) 608 userTracker.set(listOf(mainUserInfo), mainAndWorkProfileUsers.indexOf(mainUserInfo)) 609 610 createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) 611 612 verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle) 613 } 614 615 @Test 616 fun showNoteTask_copeDevices_quickAffordanceEntryPoint_shouldStartBubbleInWorkProfile() { 617 whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) 618 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 619 620 createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) 621 622 verifyNoteTaskOpenInBubbleInUser(workUserInfo.userHandle) 623 } 624 625 @Test 626 fun showNoteTask_copeDevices_tailButtonEntryPoint_shouldStartBubbleInTheUserSelectedUser() { 627 whenever( 628 secureSettings.getIntForUser( 629 /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), 630 /* def= */ any(), 631 /* userHandle= */ any() 632 ) 633 ) 634 .thenReturn(mainUserInfo.id) 635 whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) 636 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 637 638 createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON) 639 640 verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle) 641 } 642 643 @Test 644 fun showNoteTask_copeDevices_shortcutsEntryPoint_shouldStartBubbleInTheSelectedUser() { 645 whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) 646 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 647 648 createNoteTaskController().showNoteTask(entryPoint = WIDGET_PICKER_SHORTCUT) 649 650 verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle) 651 } 652 653 @Test 654 fun showNoteTask_copeDevices_appClipsEntryPoint_shouldStartBubbleInTheSelectedUser() { 655 whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) 656 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 657 658 createNoteTaskController().showNoteTask(entryPoint = APP_CLIPS) 659 660 verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle) 661 } 662 // endregion 663 664 private fun verifyNoteTaskOpenInBubbleInUser(userHandle: UserHandle) { 665 val intentCaptor = argumentCaptor<Intent>() 666 val iconCaptor = argumentCaptor<Icon>() 667 verify(bubbles) 668 .showOrHideAppBubble(capture(intentCaptor), eq(userHandle), capture(iconCaptor)) 669 assertThat(intentCaptor.value).run { 670 hasAction(ACTION_CREATE_NOTE) 671 hasPackage(NOTE_TASK_PACKAGE_NAME) 672 hasFlags(FLAG_ACTIVITY_NEW_TASK) 673 extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() 674 } 675 iconCaptor.value?.let { icon -> 676 assertNotNull(icon) 677 assertThat(icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget) 678 } 679 } 680 681 // region onRoleHoldersChanged 682 @Test 683 fun onRoleHoldersChanged_notNotesRole_doNothing() { 684 val user = UserHandle.of(0) 685 686 createNoteTaskController(isEnabled = true).onRoleHoldersChanged("NOT_NOTES", user) 687 688 verify(context, never()).startActivityAsUser(any(), any()) 689 } 690 691 @Test 692 fun onRoleHoldersChanged_notesRole_shouldUpdateShortcuts() { 693 val user = userTracker.userHandle 694 val controller = spy(createNoteTaskController()) 695 doNothing().whenever(controller).updateNoteTaskAsUser(any()) 696 697 controller.onRoleHoldersChanged(ROLE_NOTES, user) 698 699 verify(controller).updateNoteTaskAsUser(user) 700 } 701 // endregion 702 703 // region updateNoteTaskAsUser 704 @Test 705 fun updateNoteTaskAsUser_sameUser_shouldUpdateShortcuts() { 706 val user = UserHandle.CURRENT 707 val controller = spy(createNoteTaskController()) 708 doNothing().whenever(controller).updateNoteTaskAsUserInternal(any()) 709 whenever(controller.getCurrentRunningUser()).thenReturn(user) 710 711 controller.updateNoteTaskAsUser(user) 712 713 verify(controller).updateNoteTaskAsUserInternal(user) 714 verify(context, never()).startServiceAsUser(any(), any()) 715 } 716 717 @Test 718 fun updateNoteTaskAsUser_differentUser_shouldUpdateShortcutsInUserProcess() { 719 val user = UserHandle.CURRENT 720 val controller = spy(createNoteTaskController(isEnabled = true)) 721 doNothing().whenever(controller).updateNoteTaskAsUserInternal(any()) 722 whenever(controller.getCurrentRunningUser()).thenReturn(UserHandle.SYSTEM) 723 724 controller.updateNoteTaskAsUser(user) 725 726 verify(controller, never()).updateNoteTaskAsUserInternal(any()) 727 val intent = withArgCaptor { verify(context).startServiceAsUser(capture(), eq(user)) } 728 assertThat(intent).hasComponentClass(NoteTaskControllerUpdateService::class.java) 729 } 730 // endregion 731 732 // region internalUpdateNoteTaskAsUser 733 @Test 734 fun updateNoteTaskAsUserInternal_withNotesRole_withShortcuts_shouldUpdateShortcuts() { 735 createNoteTaskController(isEnabled = true) 736 .updateNoteTaskAsUserInternal(userTracker.userHandle) 737 738 val actualComponent = argumentCaptor<ComponentName>() 739 verify(context.packageManager) 740 .setComponentEnabledSetting( 741 actualComponent.capture(), 742 eq(COMPONENT_ENABLED_STATE_ENABLED), 743 eq(PackageManager.DONT_KILL_APP), 744 ) 745 assertThat(actualComponent.value.className) 746 .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) 747 verify(shortcutManager, never()).disableShortcuts(any()) 748 verify(shortcutManager).enableShortcuts(listOf(SHORTCUT_ID)) 749 val shortcutInfo = withArgCaptor { verify(shortcutManager).updateShortcuts(capture()) } 750 with(shortcutInfo.first()) { 751 assertThat(id).isEqualTo(SHORTCUT_ID) 752 assertThat(intent).run { 753 hasComponentClass(LaunchNoteTaskActivity::class.java) 754 hasAction(ACTION_CREATE_NOTE) 755 } 756 assertThat(shortLabel).isEqualTo(NOTE_TASK_SHORT_LABEL) 757 assertThat(longLabel).isEqualTo(NOTE_TASK_LONG_LABEL) 758 assertThat(isLongLived).isEqualTo(true) 759 assertThat(icon?.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget) 760 assertThat(extras?.getString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE)) 761 .isEqualTo(NOTE_TASK_PACKAGE_NAME) 762 } 763 } 764 765 @Test 766 fun updateNoteTaskAsUserInternal_noNotesRole_shouldDisableShortcuts() { 767 whenever(roleManager.getRoleHoldersAsUser(ROLE_NOTES, userTracker.userHandle)) 768 .thenReturn(emptyList()) 769 770 createNoteTaskController(isEnabled = true) 771 .updateNoteTaskAsUserInternal(userTracker.userHandle) 772 773 val argument = argumentCaptor<ComponentName>() 774 verify(context.packageManager) 775 .setComponentEnabledSetting( 776 argument.capture(), 777 eq(COMPONENT_ENABLED_STATE_DISABLED), 778 eq(PackageManager.DONT_KILL_APP), 779 ) 780 assertThat(argument.value.className) 781 .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) 782 verify(shortcutManager).disableShortcuts(listOf(SHORTCUT_ID)) 783 verify(shortcutManager, never()).enableShortcuts(any()) 784 verify(shortcutManager, never()).updateShortcuts(any()) 785 } 786 787 @Test 788 fun updateNoteTaskAsUserInternal_flagDisabled_shouldDisableShortcuts() { 789 createNoteTaskController(isEnabled = false) 790 .updateNoteTaskAsUserInternal(userTracker.userHandle) 791 792 val argument = argumentCaptor<ComponentName>() 793 verify(context.packageManager) 794 .setComponentEnabledSetting( 795 argument.capture(), 796 eq(COMPONENT_ENABLED_STATE_DISABLED), 797 eq(PackageManager.DONT_KILL_APP), 798 ) 799 assertThat(argument.value.className) 800 .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) 801 verify(shortcutManager).disableShortcuts(listOf(SHORTCUT_ID)) 802 verify(shortcutManager, never()).enableShortcuts(any()) 803 verify(shortcutManager, never()).updateShortcuts(any()) 804 } 805 // endregion 806 807 // startregion updateNoteTaskForAllUsers 808 @Test 809 fun updateNoteTaskForAllUsers_shouldRunUpdateForCurrentUserAndProfiles() { 810 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 811 val controller = spy(createNoteTaskController()) 812 doNothing().whenever(controller).updateNoteTaskAsUser(any()) 813 814 controller.updateNoteTaskForCurrentUserAndManagedProfiles() 815 816 verify(controller).updateNoteTaskAsUser(mainUserInfo.userHandle) 817 verify(controller).updateNoteTaskAsUser(workUserInfo.userHandle) 818 } 819 // endregion 820 821 // region getUserForHandlingNotesTaking 822 @Test 823 fun getUserForHandlingNotesTaking_cope_quickAffordance_shouldReturnWorkProfileUser() { 824 whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) 825 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 826 827 val user = createNoteTaskController().getUserForHandlingNotesTaking(QUICK_AFFORDANCE) 828 829 assertThat(user).isEqualTo(UserHandle.of(workUserInfo.id)) 830 } 831 832 @Test 833 fun getUserForHandlingNotesTaking_cope_userSelectedWorkProfile_tailButton_shouldReturnWorkProfileUser() { // ktlint-disable max-line-length 834 whenever( 835 secureSettings.getIntForUser( 836 /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), 837 /* def= */ any(), 838 /* userHandle= */ any() 839 ) 840 ) 841 .thenReturn(workUserInfo.id) 842 whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) 843 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 844 845 val user = createNoteTaskController().getUserForHandlingNotesTaking(TAIL_BUTTON) 846 847 assertThat(user).isEqualTo(UserHandle.of(workUserInfo.id)) 848 } 849 850 @Test 851 fun getUserForHandlingNotesTaking_cope_userSelectedMainProfile_tailButton_shouldReturnMainProfileUser() { // ktlint-disable max-line-length 852 whenever( 853 secureSettings.getIntForUser( 854 /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), 855 /* def= */ any(), 856 /* userHandle= */ any() 857 ) 858 ) 859 .thenReturn(mainUserInfo.id) 860 whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) 861 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 862 863 val user = createNoteTaskController().getUserForHandlingNotesTaking(TAIL_BUTTON) 864 865 assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id)) 866 } 867 868 @Test 869 fun getUserForHandlingNotesTaking_cope_appClip_shouldReturnCurrentUser() { 870 whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) 871 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 872 873 val user = createNoteTaskController().getUserForHandlingNotesTaking(APP_CLIPS) 874 875 assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id)) 876 } 877 878 @Test 879 fun getUserForHandlingNotesTaking_noManagement_quickAffordance_shouldReturnCurrentUser() { 880 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 881 882 val user = createNoteTaskController().getUserForHandlingNotesTaking(QUICK_AFFORDANCE) 883 884 assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id)) 885 } 886 887 @Test 888 fun getUserForHandlingNotesTaking_noManagement_tailButton_shouldReturnCurrentUser() { 889 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 890 891 val user = createNoteTaskController().getUserForHandlingNotesTaking(TAIL_BUTTON) 892 893 assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id)) 894 } 895 896 @Test 897 fun getUserForHandlingNotesTaking_noManagement_appClip_shouldReturnCurrentUser() { 898 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 899 900 val user = createNoteTaskController().getUserForHandlingNotesTaking(APP_CLIPS) 901 902 assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id)) 903 } 904 // endregion 905 906 // startregion startNotesRoleSetting 907 @Test 908 fun startNotesRoleSetting_cope_quickAffordance_shouldStartNoteRoleIntentWithWorkProfileUser() { 909 whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) 910 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 911 912 createNoteTaskController().startNotesRoleSetting(context, QUICK_AFFORDANCE) 913 914 val intentCaptor = argumentCaptor<Intent>() 915 val userCaptor = argumentCaptor<UserHandle>() 916 verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) 917 assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) 918 assertThat(userCaptor.value).isEqualTo(UserHandle.of(workUserInfo.id)) 919 } 920 921 @Test 922 fun startNotesRoleSetting_cope_nullEntryPoint_shouldStartNoteRoleIntentWithCurrentUser() { 923 whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) 924 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 925 926 createNoteTaskController().startNotesRoleSetting(context, entryPoint = null) 927 928 val intentCaptor = argumentCaptor<Intent>() 929 val userCaptor = argumentCaptor<UserHandle>() 930 verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) 931 assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) 932 assertThat(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) 933 } 934 935 @Test 936 fun startNotesRoleSetting_noManagement_quickAffordance_shouldStartNoteRoleIntentWithCurrentUser() { // ktlint-disable max-line-length 937 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 938 939 createNoteTaskController().startNotesRoleSetting(context, QUICK_AFFORDANCE) 940 941 val intentCaptor = argumentCaptor<Intent>() 942 val userCaptor = argumentCaptor<UserHandle>() 943 verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) 944 assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) 945 assertThat(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) 946 } 947 948 @Test 949 fun startNotesRoleSetting_noManagement_nullEntryPoint_shouldStartNoteRoleIntentWithCurrentUser() { // ktlint-disable max-line-length 950 userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) 951 952 createNoteTaskController().startNotesRoleSetting(context, entryPoint = null) 953 954 val intentCaptor = argumentCaptor<Intent>() 955 val userCaptor = argumentCaptor<UserHandle>() 956 verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) 957 assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) 958 assertThat(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) 959 } 960 // endregion 961 962 private companion object { 963 const val NOTE_TASK_SHORT_LABEL = "Note-taking" 964 const val NOTE_TASK_LONG_LABEL = "Note-taking, App" 965 const val NOTE_TASK_ACTIVITY_NAME = "NoteTaskActivity" 966 const val NOTE_TASK_PACKAGE_NAME = "com.android.note.app" 967 const val NOTE_TASK_UID = 123456 968 969 private val NOTE_TASK_INFO = 970 NoteTaskInfo( 971 packageName = NOTE_TASK_PACKAGE_NAME, 972 uid = NOTE_TASK_UID, 973 user = UserHandle.of(0), 974 ) 975 private val NOTE_RUNNING_TASK_INFO = 976 ActivityManager.RunningTaskInfo().apply { 977 topActivity = ComponentName(NOTE_TASK_PACKAGE_NAME, NOTE_TASK_ACTIVITY_NAME) 978 } 979 980 val mainUserInfo = 981 UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN) 982 val workUserInfo = 983 UserInfo(/* id= */ 10, /* name= */ "work", /* flags= */ UserInfo.FLAG_PROFILE) 984 val mainAndWorkProfileUsers = listOf(mainUserInfo, workUserInfo) 985 } 986 } 987