Merge "[Media TTT] Add the aidl interface for the sender and a service that implements it."
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index a741514..e9e85f1 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -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">
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2b6c9f5..08fb2c6 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -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>
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/mediattt/IDeviceSenderCallback.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/mediattt/IDeviceSenderCallback.aidl
new file mode 100644
index 0000000..f34b8d6
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/mediattt/IDeviceSenderCallback.aidl
@@ -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);
+}
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 558f0e6..d1fe7d4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -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.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 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 @@
                         mediaTttChipControllerReceiver,
                         mainExecutor));
     }
+
+    /** Inject into MediaTttSenderService. */
+    @Binds
+    @IntoMap
+    @ClassKey(MediaTttSenderService.class)
+    Service bindMediaTttSenderService(MediaTttSenderService service);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
index 5a86723..0078b95 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
@@ -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.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 @@
 @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 @@
     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 @@
                 }
                 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 @@
                     "$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 @@
         }
     }
 
+    /** 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 @@
 @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
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index b1f6faa..dd434e7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -40,17 +40,18 @@
 ) : 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
 )
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderService.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderService.kt
new file mode 100644
index 0000000..a7e70fa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderService.kt
@@ -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"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt
index dec5a10..abf53d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt
@@ -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 @@
     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 @@
     }
 
     @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 @@
         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> =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
index caef5b9..ecc4c46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
@@ -66,8 +66,8 @@
     }
 
     @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 @@
     }
 
     @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 @@
     }
 
     @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 @@
     }
 
     /** 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(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderServiceTest.kt
new file mode 100644
index 0000000..5849ed5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderServiceTest.kt
@@ -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>())
+    }
+}