diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index a9fb743bff8d0..63392c9fed26b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -23,6 +23,7 @@ import com.android.systemui.InitController; import com.android.systemui.SystemUIAppComponentFactory; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardSliceProvider; +import com.android.systemui.media.taptotransfer.MediaTttChipController; import com.android.systemui.people.PeopleProvider; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.unfold.SysUIUnfoldComponent; @@ -122,6 +123,8 @@ public interface SysUIComponent { c.getUnfoldTransitionWallpaperController().init(); }); getNaturalRotationUnfoldProgressProvider().ifPresent(o -> o.init()); + // No init method needed, just needs to be gotten so that it's created. + getMediaTttChipController(); } /** @@ -168,6 +171,9 @@ public interface SysUIComponent { */ Optional getNaturalRotationUnfoldProgressProvider(); + /** */ + Optional getMediaTttChipController(); + /** * Member injection into the supplied argument. */ diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index a1413f9e1b744..7dc4eb17d65c7 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -114,6 +114,10 @@ public class Flags { public static final BooleanFlag MONET = new BooleanFlag(800, true, R.bool.flag_monet); + /***************************************/ + // 900 - media + public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, false); + // Pay no attention to the reflection behind the curtain. // ========================== Curtain ========================== // | | diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index 57ac9dfb52cd2..237d0771bec20 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -16,11 +16,19 @@ package com.android.systemui.media.dagger; +import android.content.Context; +import android.view.WindowManager; + import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.media.MediaDataManager; import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.media.MediaHost; import com.android.systemui.media.MediaHostStatesManager; +import com.android.systemui.media.taptotransfer.MediaTttChipController; +import com.android.systemui.media.taptotransfer.MediaTttFlags; +import com.android.systemui.statusbar.commandline.CommandRegistry; + +import java.util.Optional; import javax.inject.Named; @@ -63,4 +71,18 @@ public interface MediaModule { MediaHostStatesManager statesManager) { return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); } + + /** */ + @Provides + @SysUISingleton + static Optional providesMediaTttChipController( + MediaTttFlags mediaTttFlags, + Context context, + CommandRegistry commandRegistry, + WindowManager windowManager) { + if (!mediaTttFlags.isMediaTttEnabled()) { + return Optional.empty(); + } + return Optional.of(new MediaTttChipController(context, commandRegistry, windowManager)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttChipController.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttChipController.kt new file mode 100644 index 0000000000000..85e5b336575b9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttChipController.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2021 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.media.taptotransfer + +import android.content.Context +import android.graphics.PixelFormat +import android.view.Gravity +import android.view.WindowManager +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry +import java.io.PrintWriter +import javax.inject.Inject + +/** + * A controller to display and hide the Media Tap-To-Transfer chip. This chip is shown when a user + * is currently playing media on a local "media cast sender" device (e.g. a phone) and gets close + * enough to a "media cast receiver" device (e.g. a tablet). This chip encourages the user to + * transfer the media from the sender device to the receiver device. + */ +@SysUISingleton +class MediaTttChipController @Inject constructor( + context: Context, + commandRegistry: CommandRegistry, + private val windowManager: WindowManager, +) { + init { + commandRegistry.registerCommand(ADD_CHIP_COMMAND_TAG) { AddChipCommand() } + commandRegistry.registerCommand(REMOVE_CHIP_COMMAND_TAG) { RemoveChipCommand() } + } + + private val windowLayoutParams = WindowManager.LayoutParams().apply { + width = WindowManager.LayoutParams.MATCH_PARENT + height = WindowManager.LayoutParams.MATCH_PARENT + gravity = Gravity.CENTER_HORIZONTAL + type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY + title = "Media Tap-To-Transfer Chip View" + flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) + format = PixelFormat.TRANSLUCENT + setTrustedOverlay() + } + + // TODO(b/203800327): Create a layout that matches UX. + private val chipView: TextView = TextView(context).apply { + text = "Media Tap-To-Transfer Chip" + } + + private var chipDisplaying: Boolean = false + + private fun addChip() { + if (chipDisplaying) { return } + windowManager.addView(chipView, windowLayoutParams) + chipDisplaying = true + } + + private fun removeChip() { + if (!chipDisplaying) { return } + windowManager.removeView(chipView) + chipDisplaying = false + } + + inner class AddChipCommand : Command { + override fun execute(pw: PrintWriter, args: List) = addChip() + override fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar $ADD_CHIP_COMMAND_TAG") + } + } + + inner class RemoveChipCommand : Command { + override fun execute(pw: PrintWriter, args: List) = removeChip() + override fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar $REMOVE_CHIP_COMMAND_TAG") + } + } + + companion object { + @VisibleForTesting + const val ADD_CHIP_COMMAND_TAG = "media-ttt-chip-add" + @VisibleForTesting + const val REMOVE_CHIP_COMMAND_TAG = "media-ttt-chip-remove" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt new file mode 100644 index 0000000000000..03bc9350674b5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 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.media.taptotransfer + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import javax.inject.Inject + +/** Flags related to media tap-to-transfer. */ +@SysUISingleton +class MediaTttFlags @Inject constructor(private val featureFlags: FeatureFlags) { + /** */ + fun isMediaTttEnabled(): Boolean = featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttChipControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttChipControllerTest.kt new file mode 100644 index 0000000000000..efb493123a338 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttChipControllerTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2021 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.media.taptotransfer + +import android.view.WindowManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.util.mockito.any +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import java.io.PrintWriter +import java.io.StringWriter +import java.util.concurrent.Executor + +@SmallTest +class MediaTttChipControllerTest : SysuiTestCase() { + + private lateinit var mediaTttChipController: MediaTttChipController + + private val inlineExecutor = Executor { command -> command.run() } + private val commandRegistry = CommandRegistry(context, inlineExecutor) + private val pw = PrintWriter(StringWriter()) + + @Mock + private lateinit var windowManager: WindowManager + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mediaTttChipController = MediaTttChipController(context, commandRegistry, windowManager) + } + + @Test(expected = IllegalStateException::class) + fun constructor_addCommmandAlreadyRegistered() { + // Since creating the chip controller should automatically register the add command, it + // should throw when registering it again. + commandRegistry.registerCommand( + MediaTttChipController.ADD_CHIP_COMMAND_TAG + ) { EmptyCommand() } + } + + @Test(expected = IllegalStateException::class) + fun constructor_removeCommmandAlreadyRegistered() { + // Since creating the chip controller should automatically register the remove command, it + // should throw when registering it again. + commandRegistry.registerCommand( + MediaTttChipController.REMOVE_CHIP_COMMAND_TAG + ) { EmptyCommand() } + } + + @Test + fun addChipCommand_chipAdded() { + commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.ADD_CHIP_COMMAND_TAG)) + + verify(windowManager).addView(any(), any()) + } + + @Test + fun addChipCommand_twice_chipNotAddedTwice() { + commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.ADD_CHIP_COMMAND_TAG)) + reset(windowManager) + + commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.ADD_CHIP_COMMAND_TAG)) + verify(windowManager, never()).addView(any(), any()) + } + + @Test + fun removeChipCommand_chipRemoved() { + // First, add the chip + commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.ADD_CHIP_COMMAND_TAG)) + + // Then, remove it + commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.REMOVE_CHIP_COMMAND_TAG)) + + verify(windowManager).removeView(any()) + } + + @Test + fun removeChipCommand_noAdd_viewNotRemoved() { + commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.REMOVE_CHIP_COMMAND_TAG)) + + verify(windowManager, never()).removeView(any()) + } + + class EmptyCommand : Command { + override fun execute(pw: PrintWriter, args: List) { + } + + override fun help(pw: PrintWriter) { + } + } +}