[Media TTT] Add a chip view and controller for the receiving device.

Screenshot of chip is attached to the bug.

adb command to trigger receiver chip: `adb shell cmd statusbar
media-ttt-chip-add-receiver`. The command to trigger the sender chip is
now `adb shell cmd statusbar media-ttt-chip-add-sender Tablet
MoveCloserToTransfer`.

Fixes: 203800646
Test: manual
Test: MediaTttChipControllerReceiverTest
Change-Id: I32843a8b6f60b563bca192cbd2456a5d88a6ed24
This commit is contained in:
Caitlin Cassidy
2021-12-21 20:55:19 +00:00
parent 5fae789837
commit 05898447fc
10 changed files with 325 additions and 25 deletions

View File

@@ -0,0 +1,26 @@
<!--
~ 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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="oval">
<size
android:height="@dimen/media_ttt_chip_size_receiver"
android:width="@dimen/media_ttt_chip_size_receiver"
/>
<solid android:color="?androidprv:attr/colorSurface" />
</shape>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<!-- TODO(b/203800646): layout_marginTop doesn't seem to work on some large screens. -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/media_ttt_chip_background_receiver"
>
<com.android.internal.widget.CachingIconView
android:id="@+id/app_icon"
android:layout_width="@dimen/media_ttt_icon_size_receiver"
android:layout_height="@dimen/media_ttt_icon_size_receiver"
android:layout_gravity="center"
/>
</FrameLayout>

View File

@@ -977,7 +977,7 @@
<dimen name="qs_aa_media_rec_album_margin_vert">4dp</dimen>
<dimen name="qq_aa_media_rec_header_text_size">16sp</dimen>
<!-- Media tap-to-transfer chip -->
<!-- Media tap-to-transfer chip for sender device -->
<dimen name="media_ttt_chip_outer_padding">16dp</dimen>
<dimen name="media_ttt_text_size">16sp</dimen>
<dimen name="media_ttt_icon_size">24dp</dimen>
@@ -985,6 +985,10 @@
<dimen name="media_ttt_undo_button_vertical_padding">8dp</dimen>
<dimen name="media_ttt_undo_button_vertical_negative_margin">-8dp</dimen>
<!-- Media tap-to-transfer chip for receiver device -->
<dimen name="media_ttt_chip_size_receiver">100dp</dimen>
<dimen name="media_ttt_icon_size_receiver">95dp</dimen>
<!-- Window magnification -->
<dimen name="magnification_border_drag_size">35dp</dimen>
<dimen name="magnification_outer_border_margin">15dp</dimen>

View File

@@ -24,6 +24,7 @@ import com.android.systemui.SystemUIAppComponentFactory;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardSliceProvider;
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver;
import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender;
import com.android.systemui.people.PeopleProvider;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -135,6 +136,7 @@ public interface SysUIComponent {
getNaturalRotationUnfoldProgressProvider().ifPresent(o -> o.init());
// No init method needed, just needs to be gotten so that it's created.
getMediaTttChipControllerSender();
getMediaTttChipControllerReceiver();
getMediaTttCommandLineHelper();
getUnfoldLatencyTracker().init();
}
@@ -192,6 +194,9 @@ public interface SysUIComponent {
/** */
Optional<MediaTttChipControllerSender> getMediaTttChipControllerSender();
/** */
Optional<MediaTttChipControllerReceiver> getMediaTttChipControllerReceiver();
/** */
Optional<MediaTttCommandLineHelper> getMediaTttCommandLineHelper();

View File

@@ -28,6 +28,7 @@ import com.android.systemui.media.MediaHost;
import com.android.systemui.media.MediaHostStatesManager;
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
import com.android.systemui.media.taptotransfer.MediaTttFlags;
import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver;
import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender;
import com.android.systemui.statusbar.commandline.CommandRegistry;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -93,6 +94,19 @@ public interface MediaModule {
context, windowManager, mainExecutor, backgroundExecutor));
}
/** */
@Provides
@SysUISingleton
static Optional<MediaTttChipControllerReceiver> providesMediaTttChipControllerReceiver(
MediaTttFlags mediaTttFlags,
Context context,
WindowManager windowManager) {
if (!mediaTttFlags.isMediaTttEnabled()) {
return Optional.empty();
}
return Optional.of(new MediaTttChipControllerReceiver(context, windowManager));
}
/** */
@Provides
@SysUISingleton
@@ -101,11 +115,17 @@ public interface MediaModule {
CommandRegistry commandRegistry,
Context context,
MediaTttChipControllerSender mediaTttChipControllerSender,
MediaTttChipControllerReceiver mediaTttChipControllerReceiver,
@Main DelayableExecutor mainExecutor) {
if (!mediaTttFlags.isMediaTttEnabled()) {
return Optional.empty();
}
return Optional.of(new MediaTttCommandLineHelper(
commandRegistry, context, mediaTttChipControllerSender, mainExecutor));
return Optional.of(
new MediaTttCommandLineHelper(
commandRegistry,
context,
mediaTttChipControllerSender,
mediaTttChipControllerReceiver,
mainExecutor));
}
}

View File

@@ -17,12 +17,15 @@
package com.android.systemui.media.taptotransfer
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Icon
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
import com.android.systemui.media.taptotransfer.receiver.ChipStateReceiver
import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender
import com.android.systemui.media.taptotransfer.sender.MoveCloserToTransfer
import com.android.systemui.media.taptotransfer.sender.TransferInitiated
@@ -43,17 +46,26 @@ class MediaTttCommandLineHelper @Inject constructor(
commandRegistry: CommandRegistry,
context: Context,
private val mediaTttChipControllerSender: MediaTttChipControllerSender,
private val mediaTttChipControllerReceiver: MediaTttChipControllerReceiver,
@Main private val mainExecutor: DelayableExecutor,
) {
private val appIconDrawable =
Icon.createWithResource(context, R.drawable.ic_cake).loadDrawable(context)
Icon.createWithResource(context, R.drawable.ic_avatar_user).loadDrawable(context).also {
it.setTint(Color.YELLOW)
}
init {
commandRegistry.registerCommand(ADD_CHIP_COMMAND_TAG) { AddChipCommand() }
commandRegistry.registerCommand(REMOVE_CHIP_COMMAND_TAG) { RemoveChipCommand() }
commandRegistry.registerCommand(
ADD_CHIP_COMMAND_SENDER_TAG) { AddChipCommandSender() }
commandRegistry.registerCommand(
REMOVE_CHIP_COMMAND_SENDER_TAG) { RemoveChipCommandSender() }
commandRegistry.registerCommand(
ADD_CHIP_COMMAND_RECEIVER_TAG) { AddChipCommandReceiver() }
commandRegistry.registerCommand(
REMOVE_CHIP_COMMAND_RECEIVER_TAG) { RemoveChipCommandReceiver() }
}
inner class AddChipCommand : Command {
inner class AddChipCommandSender : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
val otherDeviceName = args[0]
when (args[1]) {
@@ -86,18 +98,40 @@ class MediaTttCommandLineHelper @Inject constructor(
}
override fun help(pw: PrintWriter) {
pw.println(
"Usage: adb shell cmd statusbar $ADD_CHIP_COMMAND_TAG <deviceName> <chipStatus>"
pw.println("Usage: adb shell cmd statusbar " +
"$ADD_CHIP_COMMAND_SENDER_TAG <deviceName> <chipStatus>"
)
}
}
inner class RemoveChipCommand : Command {
/** A command to REMOVE the media ttt chip on the SENDER device. */
inner class RemoveChipCommandSender : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
mediaTttChipControllerSender.removeChip()
}
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar $REMOVE_CHIP_COMMAND_TAG")
pw.println("Usage: adb shell cmd statusbar $REMOVE_CHIP_COMMAND_SENDER_TAG")
}
}
/** A command to DISPLAY the media ttt chip on the RECEIVER device. */
inner class AddChipCommandReceiver : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
mediaTttChipControllerReceiver.displayChip(ChipStateReceiver(appIconDrawable))
}
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar $ADD_CHIP_COMMAND_RECEIVER_TAG")
}
}
/** A command to REMOVE the media ttt chip on the RECEIVER device. */
inner class RemoveChipCommandReceiver : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
mediaTttChipControllerReceiver.removeChip()
}
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar $REMOVE_CHIP_COMMAND_RECEIVER_TAG")
}
}
@@ -107,9 +141,13 @@ class MediaTttCommandLineHelper @Inject constructor(
}
@VisibleForTesting
const val ADD_CHIP_COMMAND_TAG = "media-ttt-chip-add"
const val ADD_CHIP_COMMAND_SENDER_TAG = "media-ttt-chip-add-sender"
@VisibleForTesting
const val REMOVE_CHIP_COMMAND_TAG = "media-ttt-chip-remove"
const val REMOVE_CHIP_COMMAND_SENDER_TAG = "media-ttt-chip-remove-sender"
@VisibleForTesting
const val ADD_CHIP_COMMAND_RECEIVER_TAG = "media-ttt-chip-add-receiver"
@VisibleForTesting
const val REMOVE_CHIP_COMMAND_RECEIVER_TAG = "media-ttt-chip-remove-receiver"
@VisibleForTesting
val MOVE_CLOSER_TO_TRANSFER_COMMAND_NAME = MoveCloserToTransfer::class.simpleName!!
@VisibleForTesting

View File

@@ -0,0 +1,28 @@
/*
* 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.receiver
import android.graphics.drawable.Drawable
import com.android.systemui.media.taptotransfer.common.MediaTttChipState
/**
* A class that stores all the information necessary to display the media tap-to-transfer chip on
* the receiver device.
*/
class ChipStateReceiver(
appIconDrawable: Drawable
) : MediaTttChipState(appIconDrawable)

View File

@@ -0,0 +1,43 @@
/*
* 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.receiver
import android.content.Context
import android.view.ViewGroup
import android.view.WindowManager
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCommon
import javax.inject.Inject
/**
* A controller to display and hide the Media Tap-To-Transfer chip on the **receiving** device.
*
* This chip is shown when a user is transferring media to/from a sending device and this device.
*/
@SysUISingleton
class MediaTttChipControllerReceiver @Inject constructor(
context: Context,
windowManager: WindowManager,
) : MediaTttChipControllerCommon<ChipStateReceiver>(
context, windowManager, R.layout.media_ttt_chip_receiver
) {
override fun updateChipView(chipState: ChipStateReceiver, currentChipView: ViewGroup) {
setIcon(chipState, currentChipView)
}
}

View File

@@ -18,6 +18,8 @@ package com.android.systemui.media.taptotransfer
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.media.taptotransfer.receiver.ChipStateReceiver
import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender
import com.android.systemui.media.taptotransfer.sender.MoveCloserToTransfer
import com.android.systemui.media.taptotransfer.sender.TransferInitiated
@@ -47,6 +49,8 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
@Mock
private lateinit var mediaTttChipControllerSender: MediaTttChipControllerSender
@Mock
private lateinit var mediaTttChipControllerReceiver: MediaTttChipControllerReceiver
@Before
fun setUp() {
@@ -56,73 +60,106 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
commandRegistry,
context,
mediaTttChipControllerSender,
mediaTttChipControllerReceiver,
FakeExecutor(FakeSystemClock())
)
}
@Test(expected = IllegalStateException::class)
fun constructor_addCommandAlreadyRegistered() {
fun constructor_addSenderCommandAlreadyRegistered() {
// Since creating the chip controller should automatically register the add command, it
// should throw when registering it again.
commandRegistry.registerCommand(
ADD_CHIP_COMMAND_TAG
ADD_CHIP_COMMAND_SENDER_TAG
) { EmptyCommand() }
}
@Test(expected = IllegalStateException::class)
fun constructor_removeCommandAlreadyRegistered() {
fun constructor_removeSenderCommandAlreadyRegistered() {
// Since creating the chip controller should automatically register the remove command, it
// should throw when registering it again.
commandRegistry.registerCommand(
REMOVE_CHIP_COMMAND_TAG
REMOVE_CHIP_COMMAND_SENDER_TAG
) { EmptyCommand() }
}
@Test(expected = IllegalStateException::class)
fun constructor_addReceiverCommandAlreadyRegistered() {
// Since creating the chip controller should automatically register the add command, it
// should throw when registering it again.
commandRegistry.registerCommand(
ADD_CHIP_COMMAND_RECEIVER_TAG
) { EmptyCommand() }
}
@Test(expected = IllegalStateException::class)
fun constructor_removeReceiverCommandAlreadyRegistered() {
// Since creating the chip controller should automatically register the remove command, it
// should throw when registering it again.
commandRegistry.registerCommand(
REMOVE_CHIP_COMMAND_RECEIVER_TAG
) { EmptyCommand() }
}
@Test
fun moveCloserToTransfer_chipDisplayWithCorrectState() {
fun sender_moveCloserToTransfer_chipDisplayWithCorrectState() {
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
verify(mediaTttChipControllerSender).displayChip(any(MoveCloserToTransfer::class.java))
}
@Test
fun transferInitiated_chipDisplayWithCorrectState() {
fun sender_transferInitiated_chipDisplayWithCorrectState() {
commandRegistry.onShellCommand(pw, getTransferInitiatedCommand())
verify(mediaTttChipControllerSender).displayChip(any(TransferInitiated::class.java))
}
@Test
fun transferSucceeded_chipDisplayWithCorrectState() {
fun sender_transferSucceeded_chipDisplayWithCorrectState() {
commandRegistry.onShellCommand(pw, getTransferSucceededCommand())
verify(mediaTttChipControllerSender).displayChip(any(TransferSucceeded::class.java))
}
@Test
fun removeCommand_chipRemoved() {
commandRegistry.onShellCommand(pw, arrayOf(REMOVE_CHIP_COMMAND_TAG))
fun sender_removeCommand_chipRemoved() {
commandRegistry.onShellCommand(pw, arrayOf(REMOVE_CHIP_COMMAND_SENDER_TAG))
verify(mediaTttChipControllerSender).removeChip()
}
@Test
fun receiver_addCommand_chipAdded() {
commandRegistry.onShellCommand(pw, arrayOf(ADD_CHIP_COMMAND_RECEIVER_TAG))
verify(mediaTttChipControllerReceiver).displayChip(any(ChipStateReceiver::class.java))
}
@Test
fun receiver_removeCommand_chipRemoved() {
commandRegistry.onShellCommand(pw, arrayOf(REMOVE_CHIP_COMMAND_RECEIVER_TAG))
verify(mediaTttChipControllerReceiver).removeChip()
}
private fun getMoveCloserToTransferCommand(): Array<String> =
arrayOf(
ADD_CHIP_COMMAND_TAG,
ADD_CHIP_COMMAND_SENDER_TAG,
DEVICE_NAME,
MOVE_CLOSER_TO_TRANSFER_COMMAND_NAME
)
private fun getTransferInitiatedCommand(): Array<String> =
arrayOf(
ADD_CHIP_COMMAND_TAG,
ADD_CHIP_COMMAND_SENDER_TAG,
DEVICE_NAME,
TRANSFER_INITIATED_COMMAND_NAME
)
private fun getTransferSucceededCommand(): Array<String> =
arrayOf(
ADD_CHIP_COMMAND_TAG,
ADD_CHIP_COMMAND_SENDER_TAG,
DEVICE_NAME,
TRANSFER_SUCCEEDED_COMMAND_NAME
)

View File

@@ -0,0 +1,67 @@
/*
* 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.receiver
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.ImageView
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
@SmallTest
class MediaTttChipControllerReceiverTest : SysuiTestCase() {
private lateinit var controllerReceiver: MediaTttChipControllerReceiver
@Mock
private lateinit var windowManager: WindowManager
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
controllerReceiver = MediaTttChipControllerReceiver(context, windowManager)
}
@Test
fun displayChip_chipContainsIcon() {
val drawable = Icon.createWithResource(context, R.drawable.ic_cake).loadDrawable(context)
controllerReceiver.displayChip(ChipStateReceiver(drawable))
assertThat(getChipView().getAppIconDrawable()).isEqualTo(drawable)
}
private fun getChipView(): ViewGroup {
val viewCaptor = ArgumentCaptor.forClass(View::class.java)
Mockito.verify(windowManager).addView(viewCaptor.capture(), any())
return viewCaptor.value as ViewGroup
}
private fun ViewGroup.getAppIconDrawable(): Drawable =
(this.requireViewById<ImageView>(R.id.app_icon)).drawable
}