diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 68ff116be4b05..c839263f7e5e1 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -901,6 +901,29 @@ + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 3e7b0f146b392..0b5737885004a 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2661,6 +2661,10 @@ %1$s, %2$s + + + Notetaking + Broadcasting diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index b964b76795b8e..6dd60d043a06b 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -17,10 +17,12 @@ package com.android.systemui.notetask import android.app.KeyguardManager +import android.content.ComponentName import android.content.Context +import android.content.pm.PackageManager import android.os.UserManager -import android.view.KeyEvent import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.util.kotlin.getOrNull import com.android.wm.shell.bubbles.Bubbles import java.util.Optional @@ -45,15 +47,22 @@ constructor( @NoteTaskEnabledKey private val isEnabled: Boolean, ) { - fun handleSystemKey(keyCode: Int) { + /** + * Shows a note task. How the task is shown will depend on when the method is invoked. + * + * If in multi-window mode, notes will open as a full screen experience. That is particularly + * important for Large screen devices. These devices may support a taskbar that let users to + * drag and drop a shortcut into multi-window mode, and notes should comply with this behaviour. + * + * If the keyguard is locked, notes will open as a full screen experience. A locked device has + * no contextual information which let us use the whole screen space available. + * + * If no in multi-window or the keyguard is unlocked, notes will open as a floating experience. + * That will let users open other apps in full screen, and take contextual notes. + */ + fun showNoteTask(isInMultiWindowMode: Boolean = false) { if (!isEnabled) return - if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) { - showNoteTask() - } - } - - private fun showNoteTask() { val bubbles = optionalBubbles.getOrNull() ?: return val keyguardManager = optionalKeyguardManager.getOrNull() ?: return val userManager = optionalUserManager.getOrNull() ?: return @@ -62,11 +71,35 @@ constructor( // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing. if (!userManager.isUserUnlocked) return - if (keyguardManager.isKeyguardLocked) { + if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) { context.startActivity(intent) } else { // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter. bubbles.showAppBubble(intent) } } + + /** + * Set `android:enabled` property in the `AndroidManifest` associated with the Shortcut + * component to [value]. + * + * If the shortcut entry `android:enabled` is set to `true`, the shortcut will be visible in the + * Widget Picker to all users. + */ + fun setNoteTaskShortcutEnabled(value: Boolean) { + val componentName = ComponentName(context, CreateNoteTaskShortcutActivity::class.java) + + val enabledState = + if (value) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + + context.packageManager.setComponentEnabledSetting( + componentName, + enabledState, + PackageManager.DONT_KILL_APP, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt index 0a5b6008981b5..d14b7a7667621 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -16,9 +16,10 @@ package com.android.systemui.notetask +import android.view.KeyEvent +import androidx.annotation.VisibleForTesting import com.android.systemui.statusbar.CommandQueue import com.android.wm.shell.bubbles.Bubbles -import dagger.Lazy import java.util.Optional import javax.inject.Inject @@ -27,15 +28,18 @@ internal class NoteTaskInitializer @Inject constructor( private val optionalBubbles: Optional, - private val lazyNoteTaskController: Lazy, + private val noteTaskController: NoteTaskController, private val commandQueue: CommandQueue, @NoteTaskEnabledKey private val isEnabled: Boolean, ) { - private val callbacks = + @VisibleForTesting + val callbacks = object : CommandQueue.Callbacks { override fun handleSystemKey(keyCode: Int) { - lazyNoteTaskController.get().handleSystemKey(keyCode) + if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) { + noteTaskController.showNoteTask() + } } } @@ -43,5 +47,6 @@ constructor( if (isEnabled && optionalBubbles.isPresent) { commandQueue.addCallback(callbacks) } + noteTaskController.setNoteTaskShortcutEnabled(isEnabled) } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt index 035396a6fc76c..8bdf3195d53b5 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -16,32 +16,47 @@ package com.android.systemui.notetask +import android.app.Activity import android.app.KeyguardManager import android.content.Context import android.os.UserManager import androidx.core.content.getSystemService import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity +import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity +import dagger.Binds import dagger.Module import dagger.Provides -import java.util.* +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import java.util.Optional /** Compose all dependencies required by Note Task feature. */ @Module -internal class NoteTaskModule { +internal interface NoteTaskModule { - @[Provides NoteTaskEnabledKey] - fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean { - return featureFlags.isEnabled(Flags.NOTE_TASKS) - } + @[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)] + fun bindNoteTaskLauncherActivity(activity: LaunchNoteTaskActivity): Activity? - @Provides - fun provideOptionalKeyguardManager(context: Context): Optional { - return Optional.ofNullable(context.getSystemService()) - } + @[Binds IntoMap ClassKey(CreateNoteTaskShortcutActivity::class)] + fun bindNoteTaskShortcutActivity(activity: CreateNoteTaskShortcutActivity): Activity? - @Provides - fun provideOptionalUserManager(context: Context): Optional { - return Optional.ofNullable(context.getSystemService()) + companion object { + + @[Provides NoteTaskEnabledKey] + fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean { + return featureFlags.isEnabled(Flags.NOTE_TASKS) + } + + @Provides + fun provideOptionalKeyguardManager(context: Context): Optional { + return Optional.ofNullable(context.getSystemService()) + } + + @Provides + fun provideOptionalUserManager(context: Context): Optional { + return Optional.ofNullable(context.getSystemService()) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt new file mode 100644 index 0000000000000..f6a623e4f0013 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask.shortcut + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.annotation.DrawableRes +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import com.android.systemui.R +import javax.inject.Inject + +/** + * Activity responsible for create a shortcut for notes action. If the shortcut is enabled, a new + * shortcut will appear in the widget picker. If the shortcut is selected, the Activity here will be + * launched, creating a new shortcut for [CreateNoteTaskShortcutActivity], and will finish. + * + * @see Creating + * a custom shortcut activity + */ +internal class CreateNoteTaskShortcutActivity @Inject constructor() : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val intent = + createShortcutIntent( + id = SHORTCUT_ID, + shortLabel = getString(R.string.note_task_button_label), + intent = LaunchNoteTaskActivity.newIntent(context = this), + iconResource = R.drawable.ic_note_task_button, + ) + setResult(Activity.RESULT_OK, intent) + + finish() + } + + private fun createShortcutIntent( + id: String, + shortLabel: String, + intent: Intent, + @DrawableRes iconResource: Int, + ): Intent { + val shortcutInfo = + ShortcutInfoCompat.Builder(this, id) + .setIntent(intent) + .setShortLabel(shortLabel) + .setLongLived(true) + .setIcon(IconCompat.createWithResource(this, iconResource)) + .build() + + return ShortcutManagerCompat.createShortcutResultIntent( + this, + shortcutInfo, + ) + } + + private companion object { + private const val SHORTCUT_ID = "note-task-shortcut-id" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt new file mode 100644 index 0000000000000..47fe67638cd05 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask.shortcut + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import com.android.systemui.notetask.NoteTaskController +import com.android.systemui.notetask.NoteTaskIntentResolver +import javax.inject.Inject + +/** Activity responsible for launching the note experience, and finish. */ +internal class LaunchNoteTaskActivity +@Inject +constructor( + private val noteTaskController: NoteTaskController, +) : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + noteTaskController.showNoteTask(isInMultiWindowMode) + + finish() + } + + companion object { + + /** Creates a new [Intent] set to start [LaunchNoteTaskActivity]. */ + fun newIntent(context: Context): Intent { + return Intent(context, LaunchNoteTaskActivity::class.java).apply { + // Intent's action must be set in shortcuts, or an exception will be thrown. + // TODO(b/254606432): Use Intent.ACTION_NOTES instead. + action = NoteTaskIntentResolver.NOTES_ACTION + } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index 9758842d1e353..4a9c7508b1b3a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -16,16 +16,21 @@ package com.android.systemui.notetask import android.app.KeyguardManager +import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.os.UserManager import android.test.suitebuilder.annotation.SmallTest -import android.view.KeyEvent import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.android.wm.shell.bubbles.Bubbles +import com.google.common.truth.Truth.assertThat import java.util.Optional import org.junit.Before import org.junit.Test @@ -48,6 +53,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { private val notesIntent = Intent(NOTES_ACTION) @Mock lateinit var context: Context + @Mock lateinit var packageManager: PackageManager @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver @Mock lateinit var bubbles: Bubbles @Mock lateinit var optionalBubbles: Optional @@ -60,6 +66,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + whenever(context.packageManager).thenReturn(packageManager) whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent) whenever(optionalBubbles.orElse(null)).thenReturn(bubbles) whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager) @@ -78,89 +85,125 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) } + // region showNoteTask @Test - fun handleSystemKey_keyguardIsLocked_shouldStartActivity() { + fun showNoteTask_keyguardIsLocked_shouldStartActivity() { whenever(keyguardManager.isKeyguardLocked).thenReturn(true) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_keyguardIsUnlocked_shouldStartBubbles() { + fun showNoteTask_keyguardIsUnlocked_shouldStartBubbles() { whenever(keyguardManager.isKeyguardLocked).thenReturn(false) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(bubbles).showAppBubble(notesIntent) verify(context, never()).startActivity(notesIntent) } @Test - fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() { - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_UNKNOWN) + fun showNoteTask_isInMultiWindowMode_shouldStartActivity() { + whenever(keyguardManager.isKeyguardLocked).thenReturn(false) - verify(context, never()).startActivity(notesIntent) + createNoteTaskController().showNoteTask(isInMultiWindowMode = true) + + verify(context).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_bubblesIsNull_shouldDoNothing() { + fun showNoteTask_bubblesIsNull_shouldDoNothing() { whenever(optionalBubbles.orElse(null)).thenReturn(null) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_keyguardManagerIsNull_shouldDoNothing() { + fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() { whenever(optionalKeyguardManager.orElse(null)).thenReturn(null) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_userManagerIsNull_shouldDoNothing() { + fun showNoteTask_userManagerIsNull_shouldDoNothing() { whenever(optionalUserManager.orElse(null)).thenReturn(null) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_intentResolverReturnsNull_shouldDoNothing() { + fun showNoteTask_intentResolverReturnsNull_shouldDoNothing() { whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_flagDisabled_shouldDoNothing() { - createNoteTaskController(isEnabled = false).handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + fun showNoteTask_flagDisabled_shouldDoNothing() { + createNoteTaskController(isEnabled = false).showNoteTask() verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_userIsLocked_shouldDoNothing() { + fun showNoteTask_userIsLocked_shouldDoNothing() { whenever(userManager.isUserUnlocked).thenReturn(false) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } + // endregion + + // region setNoteTaskShortcutEnabled + @Test + fun setNoteTaskShortcutEnabled_setTrue() { + createNoteTaskController().setNoteTaskShortcutEnabled(value = true) + + val argument = argumentCaptor() + verify(context.packageManager) + .setComponentEnabledSetting( + argument.capture(), + eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), + eq(PackageManager.DONT_KILL_APP), + ) + val expected = ComponentName(context, CreateNoteTaskShortcutActivity::class.java) + assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString()) + } + + @Test + fun setNoteTaskShortcutEnabled_setFalse() { + createNoteTaskController().setNoteTaskShortcutEnabled(value = false) + + val argument = argumentCaptor() + verify(context.packageManager) + .setComponentEnabledSetting( + argument.capture(), + eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), + eq(PackageManager.DONT_KILL_APP), + ) + val expected = ComponentName(context, CreateNoteTaskShortcutActivity::class.java) + assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString()) + } + // endregion } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt index 334089c43e270..538131a4dd733 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -16,10 +16,10 @@ package com.android.systemui.notetask import android.test.suitebuilder.annotation.SmallTest +import android.view.KeyEvent import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.CommandQueue -import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.wm.shell.bubbles.Bubbles import java.util.Optional @@ -45,6 +45,7 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { @Mock lateinit var commandQueue: CommandQueue @Mock lateinit var bubbles: Bubbles @Mock lateinit var optionalBubbles: Optional + @Mock lateinit var noteTaskController: NoteTaskController @Before fun setUp() { @@ -57,12 +58,13 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer { return NoteTaskInitializer( optionalBubbles = optionalBubbles, - lazyNoteTaskController = mock(), + noteTaskController = noteTaskController, commandQueue = commandQueue, isEnabled = isEnabled, ) } + // region initializer @Test fun initialize_shouldAddCallbacks() { createNoteTaskInitializer().initialize() @@ -85,4 +87,35 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { verify(commandQueue, never()).addCallback(any()) } + + @Test + fun initialize_flagEnabled_shouldEnableShortcut() { + createNoteTaskInitializer().initialize() + + verify(noteTaskController).setNoteTaskShortcutEnabled(true) + } + + @Test + fun initialize_flagDisabled_shouldDisableShortcut() { + createNoteTaskInitializer(isEnabled = false).initialize() + + verify(noteTaskController).setNoteTaskShortcutEnabled(false) + } + // endregion + + // region handleSystemKey + @Test + fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() { + createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(noteTaskController).showNoteTask() + } + + @Test + fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() { + createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN) + + verify(noteTaskController, never()).showNoteTask() + } + // endregion }