Merge "[Media TTT] Add the aidl interface for the sender and a service that implements it."

This commit is contained in:
TreeHugger Robot
2022-01-13 21:37:03 +00:00
committed by Android (Google) Code Review
10 changed files with 271 additions and 31 deletions

View File

@@ -855,6 +855,12 @@
android:singleUser="true"
android:permission="android.permission.BIND_DREAM_SERVICE" />
<!-- Service for external clients to do media transfer -->
<!-- TODO(b/203800643): Export and guard with a permission. -->
<service
android:name=".media.taptotransfer.sender.MediaTttSenderService"
/>
<receiver
android:name=".tuner.TunerService$ClearReceiver"
android:exported="false">

View File

@@ -2152,8 +2152,8 @@
<!--- ****** Media tap-to-transfer ****** -->
<!-- Text for a button to undo the media transfer. [CHAR LIMIT=20] -->
<string name="media_transfer_undo">Undo</string>
<!-- Text to ask the user to move their device closer to a different device (deviceName) in order to play music on the different device. [CHAR LIMIT=75] -->
<string name="media_move_closer_to_transfer">Move closer to play on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
<!-- Text to ask the user to move their device closer to a different device (deviceName) in order to play media on the different device. [CHAR LIMIT=75] -->
<string name="media_move_closer_to_start_cast">Move closer to play on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
<!-- Text informing the user that their media is now playing on a different device (deviceName). [CHAR LIMIT=50] -->
<string name="media_transfer_playing">Playing on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>

View File

@@ -0,0 +1,45 @@
/*
* 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.shared.mediattt;
import android.media.MediaRoute2Info;
/**
* A callback interface that can be invoked to trigger media transfer events on System UI.
*
* This interface is for the *sender* device, which is the device currently playing media. This
* sender device can transfer the media to a different device, called the receiver.
*
* System UI will implement this interface and other services will invoke it.
*/
interface IDeviceSenderCallback {
/**
* Invoke to notify System UI that this device (the sender) is close to a receiver device, so
* the user can potentially *start* a cast to the receiver device if the user moves their device
* a bit closer.
*
* Important notes:
* - When this callback triggers, the device is close enough to inform the user that
* transferring is an option, but the device is *not* close enough to actually initiate a
* transfer yet.
* - This callback is for *starting* a cast. It should be used when this device is currently
* playing media locally and the media should be transferred to be played on the receiver
* device instead.
*/
// TODO(b/203800643): Add the otherDeviceInfo parameter.
oneway void closeToReceiverToStartCast(in MediaRoute2Info mediaInfo);
}

View File

@@ -16,6 +16,7 @@
package com.android.systemui.media.dagger;
import android.app.Service;
import android.content.Context;
import android.view.WindowManager;
@@ -30,6 +31,7 @@ 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.media.taptotransfer.sender.MediaTttSenderService;
import com.android.systemui.statusbar.commandline.CommandRegistry;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -38,8 +40,11 @@ import java.util.concurrent.Executor;
import javax.inject.Named;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ClassKey;
import dagger.multibindings.IntoMap;
/** Dagger module for the media package. */
@Module
@@ -128,4 +133,10 @@ public interface MediaModule {
mediaTttChipControllerReceiver,
mainExecutor));
}
/** Inject into MediaTttSenderService. */
@Binds
@IntoMap
@ClassKey(MediaTttSenderService.class)
Service bindMediaTttSenderService(MediaTttSenderService service);
}

View File

@@ -16,9 +16,14 @@
package com.android.systemui.media.taptotransfer
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.graphics.Color
import android.graphics.drawable.Icon
import android.media.MediaRoute2Info
import android.os.IBinder
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.systemui.R
@@ -27,9 +32,11 @@ 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.MediaTttSenderService
import com.android.systemui.media.taptotransfer.sender.MoveCloserToStartCast
import com.android.systemui.media.taptotransfer.sender.TransferInitiated
import com.android.systemui.media.taptotransfer.sender.TransferSucceeded
import com.android.systemui.shared.mediattt.IDeviceSenderCallback
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.util.concurrency.DelayableExecutor
@@ -44,11 +51,14 @@ import javax.inject.Inject
@SysUISingleton
class MediaTttCommandLineHelper @Inject constructor(
commandRegistry: CommandRegistry,
context: Context,
private val context: Context,
private val mediaTttChipControllerSender: MediaTttChipControllerSender,
private val mediaTttChipControllerReceiver: MediaTttChipControllerReceiver,
@Main private val mainExecutor: DelayableExecutor,
) {
private var senderCallback: IDeviceSenderCallback? = null
private val senderServiceConnection = SenderServiceConnection()
private val appIconDrawable =
Icon.createWithResource(context, R.drawable.ic_avatar_user).loadDrawable(context).also {
it.setTint(Color.YELLOW)
@@ -68,14 +78,19 @@ class MediaTttCommandLineHelper @Inject constructor(
inner class AddChipCommandSender : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
val otherDeviceName = args[0]
val mediaInfo = MediaRoute2Info.Builder("id", "Test Name")
.addFeature("feature")
.build()
when (args[1]) {
MOVE_CLOSER_TO_TRANSFER_COMMAND_NAME -> {
mediaTttChipControllerSender.displayChip(
MoveCloserToTransfer(
appIconDrawable, APP_ICON_CONTENT_DESCRIPTION, otherDeviceName
)
)
MOVE_CLOSER_TO_START_CAST_COMMAND_NAME -> {
runOnService { senderCallback ->
senderCallback.closeToReceiverToStartCast(mediaInfo)
}
}
// TODO(b/203800643): Migrate other commands to invoke the service instead of the
// controller.
TRANSFER_INITIATED_COMMAND_NAME -> {
val futureTask = FutureTask { fakeUndoRunnable }
mediaTttChipControllerSender.displayChip(
@@ -101,7 +116,7 @@ class MediaTttCommandLineHelper @Inject constructor(
}
else -> {
pw.println("Chip type must be one of " +
"$MOVE_CLOSER_TO_TRANSFER_COMMAND_NAME, " +
"$MOVE_CLOSER_TO_START_CAST_COMMAND_NAME, " +
"$TRANSFER_INITIATED_COMMAND_NAME, " +
TRANSFER_SUCCEEDED_COMMAND_NAME
)
@@ -114,19 +129,40 @@ class MediaTttCommandLineHelper @Inject constructor(
"$ADD_CHIP_COMMAND_SENDER_TAG <deviceName> <chipStatus>"
)
}
private fun runOnService(command: SenderCallbackCommand) {
val currentServiceCallback = senderCallback
if (currentServiceCallback != null) {
command.run(currentServiceCallback)
} else {
bindService(command)
}
}
private fun bindService(command: SenderCallbackCommand) {
senderServiceConnection.pendingCommand = command
val binding = context.bindService(
Intent(context, MediaTttSenderService::class.java),
senderServiceConnection,
Context.BIND_AUTO_CREATE
)
Log.i(TAG, "Starting service binding? $binding")
}
}
/** 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()
if (senderCallback != null) {
context.unbindService(senderServiceConnection)
}
}
override fun help(pw: PrintWriter) {
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>) {
@@ -149,6 +185,29 @@ class MediaTttCommandLineHelper @Inject constructor(
}
}
/** A service connection for [IDeviceSenderCallback]. */
private inner class SenderServiceConnection : ServiceConnection {
// A command that should be run when the service gets connected.
var pendingCommand: SenderCallbackCommand? = null
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val newCallback = IDeviceSenderCallback.Stub.asInterface(service)
senderCallback = newCallback
pendingCommand?.run(newCallback)
pendingCommand = null
}
override fun onServiceDisconnected(className: ComponentName) {
senderCallback = null
}
}
/** An interface defining a command that should be run on the sender callback. */
private fun interface SenderCallbackCommand {
/** Runs the command on the provided [senderCallback]. */
fun run(senderCallback: IDeviceSenderCallback)
}
private val fakeUndoRunnable = Runnable {
Log.i(TAG, "Undo runnable triggered")
}
@@ -163,7 +222,7 @@ 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!!
val MOVE_CLOSER_TO_START_CAST_COMMAND_NAME = MoveCloserToStartCast::class.simpleName!!
@VisibleForTesting
val TRANSFER_INITIATED_COMMAND_NAME = TransferInitiated::class.simpleName!!
@VisibleForTesting

View File

@@ -40,17 +40,18 @@ sealed class ChipStateSender(
) : MediaTttChipState(appIconDrawable, appIconContentDescription)
/**
* A state representing that the two devices are close but not close enough to initiate a transfer.
* The chip will instruct the user to move closer in order to initiate the transfer.
* A state representing that the two devices are close but not close enough to *start* a cast to
* the receiver device. The chip will instruct the user to move closer in order to initiate the
* transfer to the receiver.
*/
class MoveCloserToTransfer(
class MoveCloserToStartCast(
appIconDrawable: Drawable,
appIconContentDescription: String,
otherDeviceName: String,
) : ChipStateSender(
appIconDrawable,
appIconContentDescription,
R.string.media_move_closer_to_transfer,
R.string.media_move_closer_to_start_cast,
otherDeviceName
)

View File

@@ -0,0 +1,63 @@
/*
* 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.media.taptotransfer.sender
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Icon
import android.media.MediaRoute2Info
import android.os.IBinder
import com.android.systemui.R
import com.android.systemui.shared.mediattt.IDeviceSenderCallback
import javax.inject.Inject
/**
* Service that allows external handlers to trigger the media chip on the sender device.
*/
class MediaTttSenderService @Inject constructor(
context: Context,
val controller: MediaTttChipControllerSender
) : Service() {
// TODO(b/203800643): Add logging when callbacks trigger.
private val binder: IBinder = object : IDeviceSenderCallback.Stub() {
override fun closeToReceiverToStartCast(mediaInfo: MediaRoute2Info) {
this@MediaTttSenderService.closeToReceiverToStartCast(mediaInfo)
}
}
// TODO(b/203800643): Use the app icon from the media info instead of a fake one.
private val fakeAppIconDrawable =
Icon.createWithResource(context, R.drawable.ic_avatar_user).loadDrawable(context).also {
it.setTint(Color.YELLOW)
}
override fun onBind(intent: Intent?): IBinder = binder
private fun closeToReceiverToStartCast(mediaInfo: MediaRoute2Info) {
val chipState = MoveCloserToStartCast(
appIconDrawable = fakeAppIconDrawable,
appIconContentDescription = mediaInfo.name.toString(),
otherDeviceName = FAKE_DEVICE_NAME
)
controller.displayChip(chipState)
}
}
private const val FAKE_DEVICE_NAME = "Fake Other Device Name"

View File

@@ -16,23 +16,28 @@
package com.android.systemui.media.taptotransfer
import android.content.ComponentName
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.MediaTttSenderService
import com.android.systemui.media.taptotransfer.sender.TransferInitiated
import com.android.systemui.media.taptotransfer.sender.TransferSucceeded
import com.android.systemui.shared.mediattt.IDeviceSenderCallback
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.anyString
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
import java.io.PrintWriter
import java.io.StringWriter
@@ -51,10 +56,19 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
private lateinit var mediaTttChipControllerSender: MediaTttChipControllerSender
@Mock
private lateinit var mediaTttChipControllerReceiver: MediaTttChipControllerReceiver
@Mock
private lateinit var mediaSenderService: IDeviceSenderCallback.Stub
private lateinit var mediaSenderServiceComponentName: ComponentName
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
mediaSenderServiceComponentName = ComponentName(context, MediaTttSenderService::class.java)
context.addMockService(mediaSenderServiceComponentName, mediaSenderService)
whenever(mediaSenderService.queryLocalInterface(anyString())).thenReturn(mediaSenderService)
whenever(mediaSenderService.asBinder()).thenReturn(mediaSenderService)
mediaTttCommandLineHelper =
MediaTttCommandLineHelper(
commandRegistry,
@@ -102,10 +116,11 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
}
@Test
fun sender_moveCloserToTransfer_chipDisplayWithCorrectState() {
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
fun sender_moveCloserToStartCast_serviceCallbackCalled() {
commandRegistry.onShellCommand(pw, getMoveCloserToStartCastCommand())
verify(mediaTttChipControllerSender).displayChip(any(MoveCloserToTransfer::class.java))
assertThat(context.isBound(mediaSenderServiceComponentName)).isTrue()
verify(mediaSenderService).closeToReceiverToStartCast(any())
}
@Test
@@ -143,11 +158,11 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
verify(mediaTttChipControllerReceiver).removeChip()
}
private fun getMoveCloserToTransferCommand(): Array<String> =
private fun getMoveCloserToStartCastCommand(): Array<String> =
arrayOf(
ADD_CHIP_COMMAND_SENDER_TAG,
DEVICE_NAME,
MOVE_CLOSER_TO_TRANSFER_COMMAND_NAME
MOVE_CLOSER_TO_START_CAST_COMMAND_NAME
)
private fun getTransferInitiatedCommand(): Array<String> =

View File

@@ -66,8 +66,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
}
@Test
fun moveCloserToTransfer_appIcon_chipTextContainsDeviceName_noLoadingIcon_noUndo() {
controllerSender.displayChip(moveCloserToTransfer())
fun moveCloserToStartCast_appIcon_chipTextContainsDeviceName_noLoadingIcon_noUndo() {
controllerSender.displayChip(moveCloserToStartCast())
val chipView = getChipView()
assertThat(chipView.getAppIconView().drawable).isEqualTo(appIconDrawable)
@@ -192,8 +192,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
}
@Test
fun changeFromCloserToTransferToTransferInitiated_loadingIconAppears() {
controllerSender.displayChip(moveCloserToTransfer())
fun changeFromCloserToStartToTransferInitiated_loadingIconAppears() {
controllerSender.displayChip(moveCloserToStartCast())
controllerSender.displayChip(transferInitiated())
assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
@@ -216,9 +216,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
}
@Test
fun changeFromTransferSucceededToMoveCloser_undoButtonDisappears() {
fun changeFromTransferSucceededToMoveCloserToStart_undoButtonDisappears() {
controllerSender.displayChip(transferSucceeded())
controllerSender.displayChip(moveCloserToTransfer())
controllerSender.displayChip(moveCloserToStartCast())
assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE)
}
@@ -240,8 +240,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
}
/** Helper method providing default parameters to not clutter up the tests. */
private fun moveCloserToTransfer() =
MoveCloserToTransfer(appIconDrawable, APP_ICON_CONTENT_DESC, DEVICE_NAME)
private fun moveCloserToStartCast() =
MoveCloserToStartCast(appIconDrawable, APP_ICON_CONTENT_DESC, DEVICE_NAME)
/** Helper method providing default parameters to not clutter up the tests. */
private fun transferInitiated(

View File

@@ -0,0 +1,40 @@
package com.android.systemui.media.taptotransfer.sender
import android.media.MediaRoute2Info
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.shared.mediattt.IDeviceSenderCallback
import com.android.systemui.util.mockito.any
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
class MediaTttSenderServiceTest : SysuiTestCase() {
private lateinit var service: MediaTttSenderService
private lateinit var callback: IDeviceSenderCallback
@Mock
private lateinit var controller: MediaTttChipControllerSender
private val mediaInfo = MediaRoute2Info.Builder("id", "Test Name")
.addFeature("feature")
.build()
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
service = MediaTttSenderService(context, controller)
callback = IDeviceSenderCallback.Stub.asInterface(service.onBind(null))
}
@Test
fun closeToReceiverToStartCast_controllerTriggeredWithMoveCloserToStartCastState() {
callback.closeToReceiverToStartCast(mediaInfo)
verify(controller).displayChip(any<MoveCloserToStartCast>())
}
}