Open Notes from a KeyEvent dispatch
Dispatches any system `KeyEvent` from `PhoneWindowManager` to SystemUI, allowing SystemUI to handle the KeyEvent with custom behaviour. How the `KeyEvent` is handled will depend on the context: - If user is in the lockscreen, it will launch a full screen experience. - Otherwise, it will launch a floating task. Once the context is decided, we will query for any Activity that can handle the new action `android.intent.action.NOTES` to execute the launch. If no `Activity` is available, nothing will happen. To enable the feature, one must run the following ADB commands: - adb shell setprop "persist.wm.debug.floating_tasks" 1 # enables floating windows - adb shell setprop "persist.wm.debug.floating_tasks_as_bubbles" 1 # optional: enables bubble windows - adb shell setprop ""persist.sysui.debug.note_tasks"" 1 # enables note tasking experience - adb reboot # restart to load the new configs Test: atest SystemUITests:WMShellTest Test: atest SystemUITests:NoteTaskControllerTest Test: atest SystemUITests:NoteTaskInitializerTest Test: atest SystemUITests:NoteTaskIntentResolverTest Fixes: 248274123 Change-Id: I36f30b3cbfe9e538729471d531b1b4b770738544
This commit is contained in:
@@ -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"/>
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 ==========================
|
||||
// | |
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user