diff options
14 files changed, 785 insertions, 11 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index b5145f926abd..4267ba2ff0b7 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -289,6 +289,12 @@ <!-- Query all packages on device on R+ --> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> + <queries> + <intent> + <action android:name="android.intent.action.NOTES" /> + </intent> + </queries> + <!-- Permission to register process observer --> <uses-permission android:name="android.permission.SET_ACTIVITY_WATCHER"/> diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 7e31626983e7..e47e636fa445 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -48,6 +48,7 @@ import com.android.systemui.log.dagger.LogModule; import com.android.systemui.mediaprojection.appselector.MediaProjectionModule; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationBarComponent; +import com.android.systemui.notetask.NoteTaskModule; import com.android.systemui.people.PeopleModule; import com.android.systemui.plugins.BcSmartspaceDataPlugin; import com.android.systemui.privacy.PrivacyModule; @@ -152,6 +153,7 @@ import dagger.Provides; TunerModule.class, UserModule.class, UtilModule.class, + NoteTaskModule.class, WalletModule.class }, subcomponents = { diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 68c18db167c5..e76247c20336 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -328,6 +328,9 @@ object Flags { // 1800 - shade container @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, true) + // 1900 - note task + @JvmField val NOTE_TASKS = SysPropBooleanFlag(1900, "persist.sysui.debug.note_tasks") + // Pay no attention to the reflection behind the curtain. // ========================== Curtain ========================== // | | diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt new file mode 100644 index 000000000000..d247f249e2fd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -0,0 +1,72 @@ +/* + * 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 + +import android.app.KeyguardManager +import android.content.Context +import android.os.UserManager +import android.view.KeyEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.util.kotlin.getOrNull +import com.android.wm.shell.floating.FloatingTasks +import java.util.Optional +import javax.inject.Inject + +/** + * Entry point for creating and managing note. + * + * The controller decides how a note is launched based in the device state: locked or unlocked. + * + * Currently, we only support a single task per time. + */ +@SysUISingleton +internal class NoteTaskController +@Inject +constructor( + private val context: Context, + private val intentResolver: NoteTaskIntentResolver, + private val optionalFloatingTasks: Optional<FloatingTasks>, + private val optionalKeyguardManager: Optional<KeyguardManager>, + private val optionalUserManager: Optional<UserManager>, + @NoteTaskEnabledKey private val isEnabled: Boolean, +) { + + fun handleSystemKey(keyCode: Int) { + if (!isEnabled) return + + if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) { + showNoteTask() + } + } + + private fun showNoteTask() { + val floatingTasks = optionalFloatingTasks.getOrNull() ?: return + val keyguardManager = optionalKeyguardManager.getOrNull() ?: return + val userManager = optionalUserManager.getOrNull() ?: return + val intent = intentResolver.resolveIntent() ?: return + + // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing. + if (!userManager.isUserUnlocked) return + + if (keyguardManager.isKeyguardLocked) { + context.startActivity(intent) + } else { + // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter. + floatingTasks.showOrSetStashed(intent) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt new file mode 100644 index 000000000000..e0bf1da2f652 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt @@ -0,0 +1,22 @@ +/* + * 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 + +import javax.inject.Qualifier + +/** Key associated with a [Boolean] flag that enables or disables the note task feature. */ +@Qualifier internal annotation class NoteTaskEnabledKey diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt new file mode 100644 index 000000000000..d84717da3a21 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -0,0 +1,47 @@ +/* + * 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 + +import com.android.systemui.statusbar.CommandQueue +import com.android.wm.shell.floating.FloatingTasks +import dagger.Lazy +import java.util.Optional +import javax.inject.Inject + +/** Class responsible to "glue" all note task dependencies. */ +internal class NoteTaskInitializer +@Inject +constructor( + private val optionalFloatingTasks: Optional<FloatingTasks>, + private val lazyNoteTaskController: Lazy<NoteTaskController>, + private val commandQueue: CommandQueue, + @NoteTaskEnabledKey private val isEnabled: Boolean, +) { + + private val callbacks = + object : CommandQueue.Callbacks { + override fun handleSystemKey(keyCode: Int) { + lazyNoteTaskController.get().handleSystemKey(keyCode) + } + } + + fun initialize() { + if (isEnabled && optionalFloatingTasks.isPresent) { + commandQueue.addCallback(callbacks) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt new file mode 100644 index 000000000000..98d69910aac3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt @@ -0,0 +1,81 @@ +/* + * 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 + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ResolveInfoFlags +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import javax.inject.Inject + +/** + * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an + * [Intent] ready for be launched will be returned. Otherwise, returns null. + * + * TODO(b/248274123): should be revisited once the notes role is implemented. + */ +internal class NoteTaskIntentResolver +@Inject +constructor( + private val packageManager: PackageManager, +) { + + fun resolveIntent(): Intent? { + val intent = Intent(NOTES_ACTION) + val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) + val infoList = packageManager.queryIntentActivities(intent, flags) + + for (info in infoList) { + val packageName = info.serviceInfo.applicationInfo.packageName ?: continue + val activityName = resolveActivityNameForNotesAction(packageName) ?: continue + + return Intent(NOTES_ACTION) + .setComponent(ComponentName(packageName, activityName)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + return null + } + + private fun resolveActivityNameForNotesAction(packageName: String): String? { + val intent = Intent(NOTES_ACTION).setPackage(packageName) + val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) + val resolveInfo = packageManager.resolveActivity(intent, flags) + + val activityInfo = resolveInfo?.activityInfo ?: return null + if (activityInfo.name.isNullOrBlank()) return null + if (!activityInfo.exported) return null + if (!activityInfo.enabled) return null + if (!activityInfo.showWhenLocked) return null + if (!activityInfo.turnScreenOn) return null + + return activityInfo.name + } + + companion object { + // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead. + const val NOTES_ACTION = "android.intent.action.NOTES" + } +} + +private val ActivityInfo.showWhenLocked: Boolean + get() = flags and ActivityInfo.FLAG_SHOW_WHEN_LOCKED != 0 + +private val ActivityInfo.turnScreenOn: Boolean + get() = flags and ActivityInfo.FLAG_TURN_SCREEN_ON != 0 diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt new file mode 100644 index 000000000000..035396a6fc76 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -0,0 +1,47 @@ +/* + * 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 + +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 dagger.Module +import dagger.Provides +import java.util.* + +/** Compose all dependencies required by Note Task feature. */ +@Module +internal class NoteTaskModule { + + @[Provides NoteTaskEnabledKey] + fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean { + return featureFlags.isEnabled(Flags.NOTE_TASKS) + } + + @Provides + fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> { + return Optional.ofNullable(context.getSystemService()) + } + + @Provides + fun provideOptionalUserManager(context: Context): Optional<UserManager> { + return Optional.ofNullable(context.getSystemService()) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 309f1681b964..02738d5ae48b 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -49,6 +49,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; +import com.android.systemui.notetask.NoteTaskInitializer; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.tracing.ProtoTraceable; import com.android.systemui.statusbar.CommandQueue; @@ -58,7 +59,6 @@ import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tracing.nano.SystemUiTraceProto; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; -import com.android.wm.shell.floating.FloatingTasks; import com.android.wm.shell.nano.WmShellTraceProto; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEventCallback; @@ -113,7 +113,6 @@ public final class WMShell implements private final Optional<Pip> mPipOptional; private final Optional<SplitScreen> mSplitScreenOptional; private final Optional<OneHanded> mOneHandedOptional; - private final Optional<FloatingTasks> mFloatingTasksOptional; private final Optional<DesktopMode> mDesktopModeOptional; private final CommandQueue mCommandQueue; @@ -125,6 +124,7 @@ public final class WMShell implements private final WakefulnessLifecycle mWakefulnessLifecycle; private final ProtoTracer mProtoTracer; private final UserTracker mUserTracker; + private final NoteTaskInitializer mNoteTaskInitializer; private final Executor mSysUiMainExecutor; // Listeners and callbacks. Note that we prefer member variable over anonymous class here to @@ -176,7 +176,6 @@ public final class WMShell implements Optional<Pip> pipOptional, Optional<SplitScreen> splitScreenOptional, Optional<OneHanded> oneHandedOptional, - Optional<FloatingTasks> floatingTasksOptional, Optional<DesktopMode> desktopMode, CommandQueue commandQueue, ConfigurationController configurationController, @@ -187,6 +186,7 @@ public final class WMShell implements ProtoTracer protoTracer, WakefulnessLifecycle wakefulnessLifecycle, UserTracker userTracker, + NoteTaskInitializer noteTaskInitializer, @Main Executor sysUiMainExecutor) { mContext = context; mShell = shell; @@ -203,7 +203,7 @@ public final class WMShell implements mWakefulnessLifecycle = wakefulnessLifecycle; mProtoTracer = protoTracer; mUserTracker = userTracker; - mFloatingTasksOptional = floatingTasksOptional; + mNoteTaskInitializer = noteTaskInitializer; mSysUiMainExecutor = sysUiMainExecutor; } @@ -226,6 +226,8 @@ public final class WMShell implements mSplitScreenOptional.ifPresent(this::initSplitScreen); mOneHandedOptional.ifPresent(this::initOneHanded); mDesktopModeOptional.ifPresent(this::initDesktopMode); + + mNoteTaskInitializer.initialize(); } @VisibleForTesting diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt new file mode 100644 index 000000000000..f20c6a29b840 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -0,0 +1,166 @@ +/* + * 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 + +import android.app.KeyguardManager +import android.content.Context +import android.content.Intent +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.util.mockito.whenever +import com.android.wm.shell.floating.FloatingTasks +import java.util.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * Tests for [NoteTaskController]. + * + * Build/Install/Run: + * - atest SystemUITests:NoteTaskControllerTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +internal class NoteTaskControllerTest : SysuiTestCase() { + + private val notesIntent = Intent(NOTES_ACTION) + + @Mock lateinit var context: Context + @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver + @Mock lateinit var floatingTasks: FloatingTasks + @Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks> + @Mock lateinit var keyguardManager: KeyguardManager + @Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager> + @Mock lateinit var optionalUserManager: Optional<UserManager> + @Mock lateinit var userManager: UserManager + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent) + whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks) + whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager) + whenever(optionalUserManager.orElse(null)).thenReturn(userManager) + whenever(userManager.isUserUnlocked).thenReturn(true) + } + + private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController { + return NoteTaskController( + context = context, + intentResolver = noteTaskIntentResolver, + optionalFloatingTasks = optionalFloatingTasks, + optionalKeyguardManager = optionalKeyguardManager, + optionalUserManager = optionalUserManager, + isEnabled = isEnabled, + ) + } + + @Test + fun handleSystemKey_keyguardIsLocked_shouldStartActivity() { + whenever(keyguardManager.isKeyguardLocked).thenReturn(true) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_keyguardIsUnlocked_shouldStartFloatingTask() { + whenever(keyguardManager.isKeyguardLocked).thenReturn(false) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(floatingTasks).showOrSetStashed(notesIntent) + verify(context, never()).startActivity(notesIntent) + } + + @Test + fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() { + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_UNKNOWN) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_floatingTasksIsNull_shouldDoNothing() { + whenever(optionalFloatingTasks.orElse(null)).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_keyguardManagerIsNull_shouldDoNothing() { + whenever(optionalKeyguardManager.orElse(null)).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_userManagerIsNull_shouldDoNothing() { + whenever(optionalUserManager.orElse(null)).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_intentResolverReturnsNull_shouldDoNothing() { + whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_flagDisabled_shouldDoNothing() { + createNoteTaskController(isEnabled = false).handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_userIsLocked_shouldDoNothing() { + whenever(userManager.isUserUnlocked).thenReturn(false) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt new file mode 100644 index 000000000000..f344c8d9eec4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -0,0 +1,88 @@ +/* + * 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 + +import android.test.suitebuilder.annotation.SmallTest +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.floating.FloatingTasks +import java.util.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * Tests for [NoteTaskController]. + * + * Build/Install/Run: + * - atest SystemUITests:NoteTaskInitializerTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +internal class NoteTaskInitializerTest : SysuiTestCase() { + + @Mock lateinit var commandQueue: CommandQueue + @Mock lateinit var floatingTasks: FloatingTasks + @Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks> + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(optionalFloatingTasks.isPresent).thenReturn(true) + whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks) + } + + private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer { + return NoteTaskInitializer( + optionalFloatingTasks = optionalFloatingTasks, + lazyNoteTaskController = mock(), + commandQueue = commandQueue, + isEnabled = isEnabled, + ) + } + + @Test + fun initialize_shouldAddCallbacks() { + createNoteTaskInitializer().initialize() + + verify(commandQueue).addCallback(any()) + } + + @Test + fun initialize_flagDisabled_shouldDoNothing() { + createNoteTaskInitializer(isEnabled = false).initialize() + + verify(commandQueue, never()).addCallback(any()) + } + + @Test + fun initialize_floatingTasksNotPresent_shouldDoNothing() { + whenever(optionalFloatingTasks.isPresent).thenReturn(false) + + createNoteTaskInitializer().initialize() + + verify(commandQueue, never()).addCallback(any()) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt new file mode 100644 index 000000000000..dd2cc2ffc9db --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt @@ -0,0 +1,222 @@ +/* + * 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 + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ResolveInfoFlags +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.test.suitebuilder.annotation.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.MockitoAnnotations + +/** + * Tests for [NoteTaskIntentResolver]. + * + * Build/Install/Run: + * - atest SystemUITests:NoteTaskIntentResolverTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +internal class NoteTaskIntentResolverTest : SysuiTestCase() { + + @Mock lateinit var packageManager: PackageManager + + private lateinit var resolver: NoteTaskIntentResolver + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + resolver = NoteTaskIntentResolver(packageManager) + } + + private fun createResolveInfo( + packageName: String = "PackageName", + activityInfo: ActivityInfo? = null, + ): ResolveInfo { + return ResolveInfo().apply { + serviceInfo = + ServiceInfo().apply { + applicationInfo = ApplicationInfo().apply { this.packageName = packageName } + } + this.activityInfo = activityInfo + } + } + + private fun createActivityInfo( + name: String? = "ActivityName", + exported: Boolean = true, + enabled: Boolean = true, + showWhenLocked: Boolean = true, + turnScreenOn: Boolean = true, + ): ActivityInfo { + return ActivityInfo().apply { + this.name = name + this.exported = exported + this.enabled = enabled + if (showWhenLocked) { + flags = flags or ActivityInfo.FLAG_SHOW_WHEN_LOCKED + } + if (turnScreenOn) { + flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON + } + } + } + + private fun givenQueryIntentActivities(block: () -> List<ResolveInfo>) { + whenever(packageManager.queryIntentActivities(any(), any<ResolveInfoFlags>())) + .thenReturn(block()) + } + + private fun givenResolveActivity(block: () -> ResolveInfo?) { + whenever(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(block()) + } + + @Test + fun resolveIntent_shouldReturnNotesIntent() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo()) } + + val actual = resolver.resolveIntent() + + val expected = + Intent(NOTES_ACTION) + .setComponent(ComponentName("PackageName", "ActivityName")) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // Compares the string representation of both intents, as they are different instances. + assertThat(actual.toString()).isEqualTo(expected.toString()) + } + + @Test + fun resolveIntent_activityInfoEnabledIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(enabled = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoExportedIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(exported = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoShowWhenLockedIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(showWhenLocked = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoTurnScreenOnIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(turnScreenOn = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoNameIsBlank_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = "")) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoNameIsNull_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = null)) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoIsNull_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = null) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_resolveActivityIsNull_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { null } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_packageNameIsBlank_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityNotFoundForAction_shouldReturnNull() { + givenQueryIntentActivities { emptyList() } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java index 7af66f641837..7ae47b41d5ae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java @@ -28,6 +28,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; +import com.android.systemui.notetask.NoteTaskInitializer; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -36,7 +37,6 @@ import com.android.systemui.tracing.ProtoTracer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; -import com.android.wm.shell.floating.FloatingTasks; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEventCallback; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; @@ -78,18 +78,31 @@ public class WMShellTest extends SysuiTestCase { @Mock ProtoTracer mProtoTracer; @Mock UserTracker mUserTracker; @Mock ShellExecutor mSysUiMainExecutor; - @Mock FloatingTasks mFloatingTasks; + @Mock NoteTaskInitializer mNoteTaskInitializer; @Mock DesktopMode mDesktopMode; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mWMShell = new WMShell(mContext, mShellInterface, Optional.of(mPip), - Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mFloatingTasks), + mWMShell = new WMShell( + mContext, + mShellInterface, + Optional.of(mPip), + Optional.of(mSplitScreen), + Optional.of(mOneHanded), Optional.of(mDesktopMode), - mCommandQueue, mConfigurationController, mKeyguardStateController, - mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer, - mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor); + mCommandQueue, + mConfigurationController, + mKeyguardStateController, + mKeyguardUpdateMonitor, + mScreenLifecycle, + mSysUiState, + mProtoTracer, + mWakefulnessLifecycle, + mUserTracker, + mNoteTaskInitializer, + mSysUiMainExecutor + ); } @Test diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 32ef014bbad7..aec1728ec46a 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4052,6 +4052,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_DEMO_APP_2: case KeyEvent.KEYCODE_DEMO_APP_3: case KeyEvent.KEYCODE_DEMO_APP_4: { + // TODO(b/254604589): Dispatch KeyEvent to System UI. + sendSystemKeyToStatusBarAsync(keyCode); + // Just drop if keys are not intercepted for direct key. result &= ~ACTION_PASS_TO_USER; break; |