[Media TTT] Add app icon and name to the receiver SystemApi.

We typically get the app icon and name using
`context.packageManager.getApplicationIcon(mediaRoute2Info.packageName)`.
However, this may not work on the receiver device if the receiver
doesn't have the app installed.

The API will allow the caller to provide the app icon and name directly
so that we can ensure the chip on the receiver device is displaying the
necessary app information.

Bug: 216318437
Bug: 217418566
Test: atest MediaTttChipControllerReceiverTest
Test: CTS tests updated in different CL in same topic
Change-Id: Ib5150355a08ae389b218500b235b4bd9c686f919
This commit is contained in:
Caitlin Cassidy
2022-02-08 18:48:22 +00:00
parent 2e21b11a25
commit 27378ee368
14 changed files with 153 additions and 35 deletions

View File

@@ -911,7 +911,7 @@ package android.app {
method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public int getNavBarModeOverride();
method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setDisabledForSetup(boolean);
method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setNavBarModeOverride(int);
method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void updateMediaTapToTransferReceiverDisplay(int, @NonNull android.media.MediaRoute2Info);
method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void updateMediaTapToTransferReceiverDisplay(int, @NonNull android.media.MediaRoute2Info, @Nullable android.graphics.drawable.Icon, @Nullable CharSequence);
method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void updateMediaTapToTransferSenderDisplay(int, @NonNull android.media.MediaRoute2Info, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
field public static final int MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER = 0; // 0x0
field public static final int MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER = 1; // 0x1

View File

@@ -1012,6 +1012,8 @@ public class StatusBarManager {
*
* @param displayState the new state for media tap-to-transfer.
* @param routeInfo the media route information for the media being transferred.
* @param appIcon the icon of the app playing the media.
* @param appName the name of the app playing the media.
*
* @hide
*/
@@ -1019,11 +1021,13 @@ public class StatusBarManager {
@RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
public void updateMediaTapToTransferReceiverDisplay(
@MediaTransferReceiverState int displayState,
@NonNull MediaRoute2Info routeInfo) {
@NonNull MediaRoute2Info routeInfo,
@Nullable Icon appIcon,
@Nullable CharSequence appName) {
Objects.requireNonNull(routeInfo);
IStatusBarService svc = getService();
try {
svc.updateMediaTapToTransferReceiverDisplay(displayState, routeInfo);
svc.updateMediaTapToTransferReceiverDisplay(displayState, routeInfo, appIcon, appName);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}

View File

@@ -308,5 +308,7 @@ oneway interface IStatusBar
/** Notifies System UI about an update to the media tap-to-transfer receiver state. */
void updateMediaTapToTransferReceiverDisplay(
int displayState,
in MediaRoute2Info routeInfo);
in MediaRoute2Info routeInfo,
in Icon appIcon,
in CharSequence appName);
}

View File

@@ -208,5 +208,7 @@ interface IStatusBarService
/** Notifies System UI about an update to the media tap-to-transfer receiver state. */
void updateMediaTapToTransferReceiverDisplay(
int displayState,
in MediaRoute2Info routeInfo);
in MediaRoute2Info routeInfo,
in Icon appIcon,
in CharSequence appName);
}

View File

@@ -18,6 +18,7 @@ package com.android.systemui.media.dagger;
import android.app.Service;
import android.content.Context;
import android.os.Handler;
import android.view.WindowManager;
import com.android.systemui.dagger.SysUISingleton;
@@ -117,12 +118,14 @@ public interface MediaModule {
MediaTttFlags mediaTttFlags,
CommandQueue commandQueue,
Context context,
WindowManager windowManager) {
WindowManager windowManager,
@Main Handler mainHandler) {
if (!mediaTttFlags.isMediaTttEnabled()) {
return Optional.empty();
}
return Optional.of(
new MediaTttChipControllerReceiver(commandQueue, context, windowManager));
new MediaTttChipControllerReceiver(
commandQueue, context, windowManager, mainHandler));
}
/** */

View File

@@ -147,12 +147,16 @@ class MediaTttCommandLineHelper @Inject constructor(
CLOSE_TO_SENDER_STATE ->
statusBarManager.updateMediaTapToTransferReceiverDisplay(
StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
routeInfo
routeInfo,
null,
null
)
FAR_FROM_SENDER_STATE ->
statusBarManager.updateMediaTapToTransferReceiverDisplay(
StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER,
routeInfo
routeInfo,
null,
null
)
else ->
pw.println("Invalid command name $commandName")

View File

@@ -36,7 +36,7 @@ import com.android.systemui.R
* gets displayed to the user.
*/
abstract class MediaTttChipControllerCommon<T : MediaTttChipState>(
private val context: Context,
internal val context: Context,
private val windowManager: WindowManager,
@LayoutRes private val chipLayoutRes: Int
) {

View File

@@ -30,7 +30,7 @@ import android.util.Log
open class MediaTttChipState(
internal val appPackageName: String?,
) {
fun getAppIcon(context: Context): Drawable? {
open fun getAppIcon(context: Context): Drawable? {
appPackageName ?: return null
return try {
context.packageManager.getApplicationIcon(appPackageName)
@@ -41,7 +41,7 @@ open class MediaTttChipState(
}
/** Returns the name of the app playing the media or null if we can't find it. */
fun getAppName(context: Context): String? {
open fun getAppName(context: Context): String? {
appPackageName ?: return null
return try {
context.packageManager.getApplicationInfo(

View File

@@ -16,12 +16,35 @@
package com.android.systemui.media.taptotransfer.receiver
import android.content.Context
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.
*
* @property appIconDrawable a drawable representing the icon of the app playing the media. If
* present, this will be used in [this.getAppIcon] instead of [appPackageName].
* @property appName a name for the app playing the media. If present, this will be used in
* [this.getAppName] instead of [appPackageName].
*/
class ChipStateReceiver(
appPackageName: String?,
) : MediaTttChipState(appPackageName)
private val appIconDrawable: Drawable?,
private val appName: CharSequence?
) : MediaTttChipState(appPackageName) {
override fun getAppIcon(context: Context): Drawable? {
if (appIconDrawable != null) {
return appIconDrawable
}
return super.getAppIcon(context)
}
override fun getAppName(context: Context): String? {
if (appName != null) {
return appName.toString()
}
return super.getAppName(context)
}
}

View File

@@ -18,12 +18,17 @@ package com.android.systemui.media.taptotransfer.receiver
import android.app.StatusBarManager
import android.content.Context
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.media.MediaRoute2Info
import android.os.Handler
import android.util.Log
import android.view.ViewGroup
import android.view.WindowManager
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCommon
import com.android.systemui.statusbar.CommandQueue
import javax.inject.Inject
@@ -38,16 +43,19 @@ class MediaTttChipControllerReceiver @Inject constructor(
commandQueue: CommandQueue,
context: Context,
windowManager: WindowManager,
@Main private val mainHandler: Handler,
) : MediaTttChipControllerCommon<ChipStateReceiver>(
context, windowManager, R.layout.media_ttt_chip_receiver
) {
private val commandQueueCallbacks = object : CommandQueue.Callbacks {
override fun updateMediaTapToTransferReceiverDisplay(
@StatusBarManager.MediaTransferReceiverState displayState: Int,
routeInfo: MediaRoute2Info
routeInfo: MediaRoute2Info,
appIcon: Icon?,
appName: CharSequence?
) {
this@MediaTttChipControllerReceiver.updateMediaTapToTransferReceiverDisplay(
displayState, routeInfo
displayState, routeInfo, appIcon, appName
)
}
}
@@ -58,11 +66,28 @@ class MediaTttChipControllerReceiver @Inject constructor(
private fun updateMediaTapToTransferReceiverDisplay(
@StatusBarManager.MediaTransferReceiverState displayState: Int,
routeInfo: MediaRoute2Info
routeInfo: MediaRoute2Info,
appIcon: Icon?,
appName: CharSequence?
) {
when(displayState) {
StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER ->
displayChip(ChipStateReceiver(routeInfo.packageName))
StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER -> {
val packageName = routeInfo.packageName
if (appIcon == null) {
displayChip(ChipStateReceiver(packageName, null, appName))
} else {
appIcon.loadDrawableAsync(
context,
Icon.OnDrawableLoadedListener { drawable ->
displayChip(
ChipStateReceiver(packageName, drawable, appName)
)},
// Notify the listener on the main handler since the listener will update
// the UI.
mainHandler
)
}
}
StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER -> removeChip()
else ->
Log.e(RECEIVER_TAG, "Unhandled MediaTransferReceiverState $displayState")

View File

@@ -453,7 +453,9 @@ public class CommandQueue extends IStatusBar.Stub implements
/** @see IStatusBar#updateMediaTapToTransferReceiverDisplay */
default void updateMediaTapToTransferReceiverDisplay(
@StatusBarManager.MediaTransferReceiverState int displayState,
@NonNull MediaRoute2Info routeInfo) {}
@NonNull MediaRoute2Info routeInfo,
@Nullable Icon appIcon,
@Nullable CharSequence appName) {}
}
public CommandQueue(Context context) {
@@ -1208,10 +1210,14 @@ public class CommandQueue extends IStatusBar.Stub implements
@Override
public void updateMediaTapToTransferReceiverDisplay(
int displayState,
MediaRoute2Info routeInfo) {
@NonNull MediaRoute2Info routeInfo,
@Nullable Icon appIcon,
@Nullable CharSequence appName) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = displayState;
args.arg2 = routeInfo;
args.arg3 = appIcon;
args.arg4 = appName;
mHandler.obtainMessage(MSG_MEDIA_TRANSFER_RECEIVER_STATE, args).sendToTarget();
}
@@ -1629,9 +1635,11 @@ public class CommandQueue extends IStatusBar.Stub implements
args = (SomeArgs) msg.obj;
int receiverDisplayState = (int) args.arg1;
MediaRoute2Info receiverRouteInfo = (MediaRoute2Info) args.arg2;
Icon appIcon = (Icon) args.arg3;
appName = (CharSequence) args.arg4;
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).updateMediaTapToTransferReceiverDisplay(
receiverDisplayState, receiverRouteInfo);
receiverDisplayState, receiverRouteInfo, appIcon, appName);
}
args.recycle();
break;

View File

@@ -203,7 +203,9 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
verify(statusBarManager).updateMediaTapToTransferReceiverDisplay(
eq(StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER),
any()
any(),
nullable(),
nullable()
)
}
@@ -213,7 +215,9 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
verify(statusBarManager).updateMediaTapToTransferReceiverDisplay(
eq(StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER),
any()
any(),
nullable(),
nullable()
)
}

View File

@@ -17,7 +17,9 @@
package com.android.systemui.media.taptotransfer.receiver
import android.app.StatusBarManager
import android.graphics.drawable.Icon
import android.media.MediaRoute2Info
import android.os.Handler
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
@@ -51,7 +53,8 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
controllerReceiver = MediaTttChipControllerReceiver(commandQueue, context, windowManager)
controllerReceiver = MediaTttChipControllerReceiver(
commandQueue, context, windowManager, Handler.getMain())
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
verify(commandQueue).addCallback(callbackCaptor.capture())
@@ -60,19 +63,24 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
@Test
fun commandQueueCallback_closeToSender_triggersChip() {
val appName = "FakeAppName"
commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
routeInfo
routeInfo,
/* appIcon= */ null,
appName
)
assertThat(getChipView().getAppIconView().contentDescription).isEqualTo(ROUTE_NAME)
assertThat(getChipView().getAppIconView().contentDescription).isEqualTo(appName)
}
@Test
fun commandQueueCallback_farFromSender_noChipShown() {
commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER,
routeInfo
routeInfo,
null,
null
)
verify(windowManager, never()).addView(any(), any())
@@ -82,12 +90,16 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
fun commandQueueCallback_closeThenFar_chipShownThenHidden() {
commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
routeInfo
routeInfo,
null,
null
)
commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER,
routeInfo
routeInfo,
null,
null
)
val viewCaptor = ArgumentCaptor.forClass(View::class.java)
@@ -96,16 +108,45 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
}
@Test
fun displayChip_chipContainsIcon() {
val state = ChipStateReceiver(PACKAGE_NAME)
fun displayChip_nullAppIconDrawable_iconIsFromPackageName() {
val state = ChipStateReceiver(PACKAGE_NAME, appIconDrawable = null, "appName")
controllerReceiver.displayChip(state)
assertThat(getChipView().getAppIconView().drawable).isEqualTo(state.getAppIcon(context))
}
@Test
fun displayChip_hasAppIconDrawable_iconIsDrawable() {
val drawable = Icon.createWithResource(context, R.drawable.ic_cake).loadDrawable(context)
val state = ChipStateReceiver(PACKAGE_NAME, drawable, "appName")
controllerReceiver.displayChip(state)
assertThat(getChipView().getAppIconView().drawable).isEqualTo(drawable)
}
@Test
fun displayChip_nullAppName_iconContentDescriptionIsFromPackageName() {
val state = ChipStateReceiver(PACKAGE_NAME, appIconDrawable = null, appName = null)
controllerReceiver.displayChip(state)
assertThat(getChipView().getAppIconView().contentDescription)
.isEqualTo(state.getAppName(context))
}
@Test
fun displayChip_hasAppName_iconContentDescriptionIsAppNameOverride() {
val appName = "FakeAppName"
val state = ChipStateReceiver(PACKAGE_NAME, appIconDrawable = null, appName)
controllerReceiver.displayChip(state)
assertThat(getChipView().getAppIconView().contentDescription).isEqualTo(appName)
}
private fun getChipView(): ViewGroup {
val viewCaptor = ArgumentCaptor.forClass(View::class.java)
verify(windowManager).addView(viewCaptor.capture(), any())
@@ -115,10 +156,9 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
}
private const val ROUTE_NAME = "Test name"
private const val PACKAGE_NAME = "com.android.systemui"
private val routeInfo = MediaRoute2Info.Builder("id", ROUTE_NAME)
private val routeInfo = MediaRoute2Info.Builder("id", "Test route name")
.addFeature("feature")
.setPackageName(PACKAGE_NAME)
.build()

View File

@@ -2031,11 +2031,14 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
@Override
public void updateMediaTapToTransferReceiverDisplay(
@StatusBarManager.MediaTransferReceiverState int displayState,
MediaRoute2Info routeInfo) {
MediaRoute2Info routeInfo,
@Nullable Icon appIcon,
@Nullable CharSequence appName) {
enforceMediaContentControl();
if (mBar != null) {
try {
mBar.updateMediaTapToTransferReceiverDisplay(displayState, routeInfo);
mBar.updateMediaTapToTransferReceiverDisplay(
displayState, routeInfo, appIcon, appName);
} catch (RemoteException e) {
Slog.e(TAG, "updateMediaTapToTransferReceiverDisplay", e);
}