diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt index d0d3052bc544b..31ab24748c93b 100644 --- a/packages/SystemUI/ktfmt_includes.txt +++ b/packages/SystemUI/ktfmt_includes.txt @@ -832,7 +832,6 @@ -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/WalletControllerImplTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/window/StatusBarWindowStateControllerTest.kt -packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateLoggingProviderTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml index 4da77118f00bf..bc97e511e7f41 100644 --- a/packages/SystemUI/res/layout/chipbar.xml +++ b/packages/SystemUI/res/layout/chipbar.xml @@ -19,12 +19,12 @@ + null + is Loaded -> this.description + is Resource -> context.getString(this.res) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt index 5d0e08ffc3078..4a5693202dba1 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt @@ -18,6 +18,7 @@ package com.android.systemui.common.shared.model import android.annotation.StringRes +import android.content.Context /** * Models a text, that can either be already [loaded][Text.Loaded] or be a [reference] @@ -31,4 +32,20 @@ sealed class Text { data class Resource( @StringRes val res: Int, ) : Text() + + companion object { + /** + * Returns the loaded test string, or null if we don't have one. + * + * Prefer [com.android.systemui.common.ui.binder.TextViewBinder.bind] over this method. This + * should only be used for testing or concatenation purposes. + */ + fun Text?.loadText(context: Context): String? { + return when (this) { + null -> null + is Loaded -> this.text + is Resource -> context.getString(this.res) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt index c3de94f28aea1..0a6043793ef66 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt @@ -21,6 +21,8 @@ import android.content.pm.PackageManager import android.graphics.drawable.Drawable import com.android.settingslib.Utils import com.android.systemui.R +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon /** Utility methods for media tap-to-transfer. */ class MediaTttUtils { @@ -30,6 +32,23 @@ class MediaTttUtils { const val WINDOW_TITLE = "Media Transfer Chip View" const val WAKE_REASON = "MEDIA_TRANSFER_ACTIVATED" + /** + * Returns the information needed to display the icon in [Icon] form. + * + * See [getIconInfoFromPackageName]. + */ + fun getIconFromPackageName( + context: Context, + appPackageName: String?, + logger: MediaTttLogger, + ): Icon { + val iconInfo = getIconInfoFromPackageName(context, appPackageName, logger) + return Icon.Loaded( + iconInfo.drawable, + ContentDescription.Loaded(iconInfo.contentDescription) + ) + } + /** * Returns the information needed to display the icon. * 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 c24b0307fcd17..6e596ee1f473b 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 @@ -18,17 +18,12 @@ package com.android.systemui.media.taptotransfer.sender import android.app.StatusBarManager import android.content.Context -import android.media.MediaRoute2Info import android.util.Log -import android.view.View import androidx.annotation.StringRes import com.android.internal.logging.UiEventLogger -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R -import com.android.systemui.plugins.FalsingManager +import com.android.systemui.common.shared.model.Text import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo -import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator /** * A class enumerating all the possible states of the media tap-to-transfer chip on the sender @@ -38,6 +33,7 @@ import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator * @property stringResId the res ID of the string that should be displayed in the chip. Null if the * state should not have the chip be displayed. * @property transferStatus the transfer status that the chip state represents. + * @property endItem the item that should be displayed in the end section of the chip. * @property timeout the amount of time this chip should display on the screen before it times out * and disappears. */ @@ -46,6 +42,7 @@ enum class ChipStateSender( val uiEvent: UiEventLogger.UiEventEnum, @StringRes val stringResId: Int?, val transferStatus: TransferStatus, + val endItem: SenderEndItem?, val timeout: Long = DEFAULT_TIMEOUT_MILLIS ) { /** @@ -58,6 +55,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST, R.string.media_move_closer_to_start_cast, transferStatus = TransferStatus.NOT_STARTED, + endItem = null, ), /** @@ -71,6 +69,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST, R.string.media_move_closer_to_end_cast, transferStatus = TransferStatus.NOT_STARTED, + endItem = null, ), /** @@ -82,6 +81,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED, R.string.media_transfer_playing_different_device, transferStatus = TransferStatus.IN_PROGRESS, + endItem = SenderEndItem.Loading, timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS ), @@ -94,6 +94,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED, R.string.media_transfer_playing_this_device, transferStatus = TransferStatus.IN_PROGRESS, + endItem = SenderEndItem.Loading, timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS ), @@ -105,36 +106,13 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED, R.string.media_transfer_playing_different_device, transferStatus = TransferStatus.SUCCEEDED, - ) { - override fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? { - if (undoCallback == null) { - return null - } - return View.OnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener - - uiEventLogger.logUndoClicked( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED - ) - undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToThisDeviceTriggered - // state, but that may take too long to go through the binder and the user may be - // confused as to why the UI hasn't changed yet. So, we immediately change the UI - // here. - chipbarCoordinator.displayView( - ChipSenderInfo( - TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo, undoCallback - ) - ) - } - } - }, + endItem = SenderEndItem.UndoButton( + uiEventOnClick = + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED, + newState = + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED + ), + ), /** * A state representing that a transfer back to this device has been successfully completed. @@ -144,36 +122,13 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, R.string.media_transfer_playing_this_device, transferStatus = TransferStatus.SUCCEEDED, - ) { - override fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? { - if (undoCallback == null) { - return null - } - return View.OnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener - - uiEventLogger.logUndoClicked( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED - ) - undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToReceiverTriggered - // state, but that may take too long to go through the binder and the user may be - // confused as to why the UI hasn't changed yet. So, we immediately change the UI - // here. - chipbarCoordinator.displayView( - ChipSenderInfo( - TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo, undoCallback - ) - ) - } - } - }, + endItem = SenderEndItem.UndoButton( + uiEventOnClick = + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED, + newState = + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED + ), + ), /** A state representing that a transfer to the receiver device has failed. */ TRANSFER_TO_RECEIVER_FAILED( @@ -181,6 +136,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED, R.string.media_transfer_failed, transferStatus = TransferStatus.FAILED, + endItem = SenderEndItem.Error, ), /** A state representing that a transfer back to this device has failed. */ @@ -189,6 +145,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED, R.string.media_transfer_failed, transferStatus = TransferStatus.FAILED, + endItem = SenderEndItem.Error, ), /** A state representing that this device is far away from any receiver device. */ @@ -197,37 +154,27 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER, stringResId = null, transferStatus = TransferStatus.TOO_FAR, - ); + // We shouldn't be displaying the chipbar anyway + endItem = null, + ) { + override fun getChipTextString(context: Context, otherDeviceName: String): Text { + // TODO(b/245610654): Better way to handle this. + throw IllegalArgumentException("FAR_FROM_RECEIVER should never be displayed, " + + "so its string should never be fetched") + } + }; /** * Returns a fully-formed string with the text that the chip should display. * + * Throws an NPE if [stringResId] is null. + * * @param otherDeviceName the name of the other device involved in the transfer. */ - fun getChipTextString(context: Context, otherDeviceName: String): String? { - if (stringResId == null) { - return null - } - return context.getString(stringResId, otherDeviceName) + open fun getChipTextString(context: Context, otherDeviceName: String): Text { + return Text.Loaded(context.getString(stringResId!!, otherDeviceName)) } - /** - * Returns a click listener for the undo button on the chip. Returns null if this chip state - * doesn't have an undo button. - * - * @param chipbarCoordinator passed as a parameter in case we want to display a new chipbar - * when undo is clicked. - * @param undoCallback if present, the callback that should be called when the user clicks the - * undo button. The undo button will only be shown if this is non-null. - */ - open fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? = null - companion object { /** * Returns the sender state enum associated with the given [displayState] from @@ -253,6 +200,26 @@ enum class ChipStateSender( } } +/** Represents the item that should be displayed in the end section of the chip. */ +sealed class SenderEndItem { + /** A loading icon should be displayed. */ + object Loading : SenderEndItem() + + /** An error icon should be displayed. */ + object Error : SenderEndItem() + + /** + * An undo button should be displayed. + * + * @property uiEventOnClick the UI event to log when this button is clicked. + * @property newState the state that should immediately be transitioned to. + */ + data class UndoButton( + val uiEventOnClick: UiEventLogger.UiEventEnum, + @StatusBarManager.MediaTransferSenderState val newState: Int, + ) : SenderEndItem() +} + // Give the Transfer*Triggered states a longer timeout since those states represent an active // process and we should keep the user informed about it as long as possible (but don't allow it to // continue indefinitely). diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index 224303ac098c4..fe2eed9c6079e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -20,14 +20,20 @@ import android.app.StatusBarManager import android.content.Context import android.media.MediaRoute2Info import android.util.Log +import android.view.View +import com.android.internal.logging.UiEventLogger import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.CoreStartable +import com.android.systemui.R +import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.media.taptotransfer.common.MediaTttUtils import com.android.systemui.statusbar.CommandQueue -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem +import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo import com.android.systemui.temporarydisplay.chipbar.SENDER_TAG import javax.inject.Inject @@ -107,7 +113,89 @@ constructor( chipbarCoordinator.removeView(removalReason) } else { displayedState = chipState - chipbarCoordinator.displayView(ChipSenderInfo(chipState, routeInfo, undoCallback)) + chipbarCoordinator.displayView( + createChipbarInfo( + chipState, + routeInfo, + undoCallback, + context, + logger, + ) + ) } } + + /** + * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display. + */ + private fun createChipbarInfo( + chipStateSender: ChipStateSender, + routeInfo: MediaRoute2Info, + undoCallback: IUndoMediaTransferCallback?, + context: Context, + logger: MediaTttLogger, + ): ChipbarInfo { + val packageName = routeInfo.clientPackageName + val otherDeviceName = routeInfo.name.toString() + + return ChipbarInfo( + // Display the app's icon as the start icon + startIcon = MediaTttUtils.getIconFromPackageName(context, packageName, logger), + text = chipStateSender.getChipTextString(context, otherDeviceName), + endItem = + when (chipStateSender.endItem) { + null -> null + is SenderEndItem.Loading -> ChipbarEndItem.Loading + is SenderEndItem.Error -> ChipbarEndItem.Error + is SenderEndItem.UndoButton -> { + if (undoCallback != null) { + getUndoButton( + undoCallback, + chipStateSender.endItem.uiEventOnClick, + chipStateSender.endItem.newState, + routeInfo, + ) + } else { + null + } + } + } + ) + } + + /** + * Returns an undo button for the chip. + * + * When the button is clicked: [undoCallback] will be triggered, [uiEvent] will be logged, and + * this coordinator will transition to [newState]. + */ + private fun getUndoButton( + undoCallback: IUndoMediaTransferCallback, + uiEvent: UiEventLogger.UiEventEnum, + @StatusBarManager.MediaTransferSenderState newState: Int, + routeInfo: MediaRoute2Info, + ): ChipbarEndItem.Button { + val onClickListener = + View.OnClickListener { + uiEventLogger.logUndoClicked(uiEvent) + undoCallback.onUndoTriggered() + + // The external service should eventually send us a new TransferTriggered state, but + // but that may take too long to go through the binder and the user may be confused + // as to why the UI hasn't changed yet. So, we immediately change the UI here. + updateMediaTapToTransferSenderDisplay( + newState, + routeInfo, + // Since we're force-updating the UI, we don't have any [undoCallback] from the + // external service (and TransferTriggered states don't have undo callbacks + // anyway). + undoCallback = null, + ) + } + + return ChipbarEndItem.Button( + Text.Resource(R.string.media_transfer_undo), + onClickListener, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index 1a25e4df3a61e..45ce687a1a4d4 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -18,7 +18,6 @@ package com.android.systemui.temporarydisplay.chipbar import android.content.Context import android.graphics.Rect -import android.media.MediaRoute2Info import android.os.PowerManager import android.view.Gravity import android.view.MotionEvent @@ -27,25 +26,24 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.widget.TextView -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.internal.widget.CachingIconView import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.animation.Interpolators import com.android.systemui.animation.ViewHierarchyAnimator import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Text.Companion.loadText +import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.common.ui.binder.TextViewBinder import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.common.MediaTttUtils -import com.android.systemui.media.taptotransfer.sender.ChipStateSender import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger -import com.android.systemui.media.taptotransfer.sender.TransferStatus import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.TemporaryViewDisplayController -import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.view.ViewUtil import javax.inject.Inject @@ -78,11 +76,10 @@ open class ChipbarCoordinator @Inject constructor( accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, - private val uiEventLogger: MediaTttSenderUiEventLogger, private val falsingManager: FalsingManager, private val falsingCollector: FalsingCollector, private val viewUtil: ViewUtil, -) : TemporaryViewDisplayController( +) : TemporaryViewDisplayController( context, logger, windowManager, @@ -104,15 +101,13 @@ open class ChipbarCoordinator @Inject constructor( override fun start() {} override fun updateView( - newInfo: ChipSenderInfo, + newInfo: ChipbarInfo, currentView: ViewGroup ) { // TODO(b/245610654): Adding logging here. - val chipState = newInfo.state - // Detect falsing touches on the chip. - parent = currentView.requireViewById(R.id.media_ttt_sender_chip) + parent = currentView.requireViewById(R.id.chipbar_root_view) parent.touchHandler = object : Gefingerpoken { override fun onTouchEvent(ev: MotionEvent?): Boolean { falsingCollector.onTouchEvent(ev) @@ -120,47 +115,49 @@ open class ChipbarCoordinator @Inject constructor( } } - // App icon - val iconInfo = MediaTttUtils.getIconInfoFromPackageName( - context, newInfo.routeInfo.clientPackageName, logger - ) - val iconView = currentView.requireViewById(R.id.app_icon) - iconView.setImageDrawable(iconInfo.drawable) - iconView.contentDescription = iconInfo.contentDescription + // ---- Start icon ---- + val iconView = currentView.requireViewById(R.id.start_icon) + IconViewBinder.bind(newInfo.startIcon, iconView) - // Text - val otherDeviceName = newInfo.routeInfo.name.toString() - val chipText = chipState.getChipTextString(context, otherDeviceName) - currentView.requireViewById(R.id.text).text = chipText + // ---- Text ---- + val textView = currentView.requireViewById(R.id.text) + TextViewBinder.bind(textView, newInfo.text) + // ---- End item ---- // Loading currentView.requireViewById(R.id.loading).visibility = - (chipState.transferStatus == TransferStatus.IN_PROGRESS).visibleIfTrue() + (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue() - // Undo - val undoView = currentView.requireViewById(R.id.undo) - val undoClickListener = chipState.undoClickListener( - this, - newInfo.routeInfo, - newInfo.undoCallback, - uiEventLogger, - falsingManager, - ) - undoView.setOnClickListener(undoClickListener) - undoView.visibility = (undoClickListener != null).visibleIfTrue() + // Error + currentView.requireViewById(R.id.error).visibility = + (newInfo.endItem == ChipbarEndItem.Error).visibleIfTrue() - // Failure - currentView.requireViewById(R.id.failure_icon).visibility = - (chipState.transferStatus == TransferStatus.FAILED).visibleIfTrue() + // Button + val buttonView = currentView.requireViewById(R.id.end_button) + if (newInfo.endItem is ChipbarEndItem.Button) { + TextViewBinder.bind(buttonView, newInfo.endItem.text) - // For accessibility + val onClickListener = View.OnClickListener { clickedView -> + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener + newInfo.endItem.onClickListener.onClick(clickedView) + } + + buttonView.setOnClickListener(onClickListener) + buttonView.visibility = View.VISIBLE + } else { + buttonView.visibility = View.GONE + } + + // ---- Overall accessibility ---- currentView.requireViewById( - R.id.media_ttt_sender_chip_inner - ).contentDescription = "${iconInfo.contentDescription} $chipText" + R.id.chipbar_inner + ).contentDescription = + "${newInfo.startIcon.contentDescription.loadContentDescription(context)} " + + "${newInfo.text.loadText(context)}" } override fun animateViewIn(view: ViewGroup) { - val chipInnerView = view.requireViewById(R.id.media_ttt_sender_chip_inner) + val chipInnerView = view.requireViewById(R.id.chipbar_inner) ViewHierarchyAnimator.animateAddition( chipInnerView, ViewHierarchyAnimator.Hotspot.TOP, @@ -175,7 +172,7 @@ open class ChipbarCoordinator @Inject constructor( override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { ViewHierarchyAnimator.animateRemoval( - view.requireViewById(R.id.media_ttt_sender_chip_inner), + view.requireViewById(R.id.chipbar_inner), ViewHierarchyAnimator.Hotspot.TOP, Interpolators.EMPHASIZED_ACCELERATE, ANIMATION_DURATION, @@ -197,13 +194,5 @@ open class ChipbarCoordinator @Inject constructor( } } -data class ChipSenderInfo( - val state: ChipStateSender, - val routeInfo: MediaRoute2Info, - val undoCallback: IUndoMediaTransferCallback? = null -) : TemporaryViewInfo { - override fun getTimeoutMs() = state.timeout -} - const val SENDER_TAG = "MediaTapToTransferSender" private const val ANIMATION_DURATION = 500L diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt new file mode 100644 index 0000000000000..211a663879664 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt @@ -0,0 +1,53 @@ +/* + * 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.temporarydisplay.chipbar + +import android.view.View +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.temporarydisplay.TemporaryViewInfo + +/** + * A container for all the state needed to display a chipbar via [ChipbarCoordinator]. + * + * @property startIcon the icon to display at the start of the chipbar (on the left in LTR locales; + * on the right in RTL locales). + * @property text the text to display. + * @property endItem an optional end item to display at the end of the chipbar (on the right in LTR + * locales; on the left in RTL locales). + */ +data class ChipbarInfo( + val startIcon: Icon, + val text: Text, + val endItem: ChipbarEndItem?, +) : TemporaryViewInfo + +/** The possible items to display at the end of the chipbar. */ +sealed class ChipbarEndItem { + /** A loading icon should be displayed. */ + object Loading : ChipbarEndItem() + + /** An error icon should be displayed. */ + object Error : ChipbarEndItem() + + /** + * A button with the provided [text] and [onClickListener] functionality should be displayed. + */ + data class Button(val text: Text, val onClickListener: View.OnClickListener) : ChipbarEndItem() + + // TODO(b/245610654): Add support for a generic icon. +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt index 7c83cb74bb773..6a4c0f60466d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt @@ -22,6 +22,9 @@ import android.graphics.drawable.Drawable import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -61,6 +64,34 @@ class MediaTttUtilsTest : SysuiTestCase() { context.setMockPackageManager(packageManager) } + @Test + fun getIconFromPackageName_nullPackageName_returnsDefault() { + val icon = MediaTttUtils.getIconFromPackageName(context, appPackageName = null, logger) + + val expectedDesc = + ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name) + .loadContentDescription(context) + assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc) + } + + @Test + fun getIconFromPackageName_invalidPackageName_returnsDefault() { + val icon = MediaTttUtils.getIconFromPackageName(context, "fakePackageName", logger) + + val expectedDesc = + ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name) + .loadContentDescription(context) + assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc) + } + + @Test + fun getIconFromPackageName_validPackageName_returnsAppInfo() { + val icon = MediaTttUtils.getIconFromPackageName(context, PACKAGE_NAME, logger) + + assertThat(icon) + .isEqualTo(Icon.Loaded(appIconFromPackageName, ContentDescription.Loaded(APP_NAME))) + } + @Test fun getIconInfoFromPackageName_nullPackageName_returnsDefault() { val iconInfo = diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index 110bbb80df3a0..f977f55db4830 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -17,6 +17,9 @@ package com.android.systemui.media.taptotransfer.sender import android.app.StatusBarManager +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable import android.media.MediaRoute2Info import android.os.PowerManager import android.testing.AndroidTestingRunner @@ -25,6 +28,7 @@ import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager +import android.widget.ImageView import android.widget.TextView import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake @@ -32,16 +36,17 @@ import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.Text.Companion.loadText import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat @@ -60,20 +65,29 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class MediaTttSenderCoordinatorTest : SysuiTestCase() { + + // Note: This tests are a bit like integration tests because they use a real instance of + // [ChipbarCoordinator] and verify that the coordinator displays the correct view, based on + // the inputs from [MediaTttSenderCoordinator]. + private lateinit var underTest: MediaTttSenderCoordinator @Mock private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var applicationInfo: ApplicationInfo @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var falsingCollector: FalsingCollector @Mock private lateinit var logger: MediaTttLogger @Mock private lateinit var mediaTttFlags: MediaTttFlags + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var powerManager: PowerManager @Mock private lateinit var viewUtil: ViewUtil @Mock private lateinit var windowManager: WindowManager private lateinit var chipbarCoordinator: ChipbarCoordinator private lateinit var commandQueueCallback: CommandQueue.Callbacks + private lateinit var fakeAppIconDrawable: Drawable private lateinit var fakeClock: FakeSystemClock private lateinit var fakeExecutor: FakeExecutor private lateinit var uiEventLoggerFake: UiEventLoggerFake @@ -85,6 +99,18 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true) whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) + fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! + whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) + whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) + whenever( + packageManager.getApplicationInfo( + eq(PACKAGE_NAME), + any() + ) + ) + .thenReturn(applicationInfo) + context.setMockPackageManager(packageManager) + fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) @@ -100,7 +126,6 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { accessibilityManager, configurationController, powerManager, - uiEventLogger, falsingManager, falsingCollector, viewUtil, @@ -149,8 +174,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(almostCloseToStartCast().state.getChipTextString(context, OTHER_DEVICE_NAME)) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id) } @@ -163,8 +195,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(almostCloseToEndCast().state.getChipTextString(context, OTHER_DEVICE_NAME)) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id) } @@ -177,10 +216,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id) } @@ -193,10 +237,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id) } @@ -209,14 +258,68 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id) } + @Test + fun transferToReceiverSucceeded_nullUndoCallback_noUndo() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun transferToReceiverSucceeded_withUndoRunnable_undoVisible() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() + } + + @Test + fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { + var undoCallbackCalled = false + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + }, + ) + + getChipbarView().getUndoButton().performClick() + + // Event index 1 since initially displaying the succeeded chip would also log an event + assertThat(uiEventLoggerFake.eventId(1)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id) + assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + } + @Test fun commandQueueCallback_transferToThisDeviceSucceeded_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( @@ -225,14 +328,70 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id) } + @Test + fun transferToThisDeviceSucceeded_nullUndoCallback_noUndo() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun transferToThisDeviceSucceeded_withUndoRunnable_undoVisible() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() + } + + @Test + fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { + var undoCallbackCalled = false + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + }, + ) + + getChipbarView().getUndoButton().performClick() + + // Event index 1 since initially displaying the succeeded chip would also log an event + assertThat(uiEventLoggerFake.eventId(1)) + .isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id + ) + assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + } + @Test fun commandQueueCallback_transferToReceiverFailed_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( @@ -241,10 +400,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id) } @@ -257,10 +421,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id) } @@ -407,53 +576,113 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { verify(windowManager).removeView(any()) } - private fun getChipView(): ViewGroup { + @Test + fun transferToReceiverSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) + + // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should + // verify that the new state it triggers operates just like any other state. + getChipbarView().getUndoButton().performClick() + fakeExecutor.runAllReady() + + // Verify that the click updated us to the triggered state + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + // Verify that we didn't remove the chipbar because it's in the triggered state + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + // Verify we eventually remove the chipbar + verify(windowManager).removeView(any()) + } + + @Test + fun transferToThisDeviceSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) + + // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should + // verify that the new state it triggers operates just like any other state. + getChipbarView().getUndoButton().performClick() + fakeExecutor.runAllReady() + + // Verify that the click updated us to the triggered state + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + // Verify that we didn't remove the chipbar because it's in the triggered state + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + // Verify we eventually remove the chipbar + verify(windowManager).removeView(any()) + } + + private fun getChipbarView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) verify(windowManager).addView(viewCaptor.capture(), any()) return viewCaptor.value as ViewGroup } + private fun ViewGroup.getAppIconView() = this.requireViewById(R.id.start_icon) + private fun ViewGroup.getChipText(): String = (this.requireViewById(R.id.text)).text as String - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToStartCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo) + private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToEndCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo) + private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo) + private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) + private fun ChipStateSender.getExpectedStateText(): String? { + return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context) + } } +private const val APP_NAME = "Fake app name" private const val OTHER_DEVICE_NAME = "My Tablet" +private const val PACKAGE_NAME = "com.android.systemui" private const val TIMEOUT = 10000 private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) .addFeature("feature") - .setClientPackageName("com.android.systemui") + .setClientPackageName(PACKAGE_NAME) .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt index a3ad028519bbc..e56623f9fea28 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt @@ -22,7 +22,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK @@ -125,19 +125,12 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase } else { testCase.expected.contentDescription.invoke(context) } - assertThat(iconFlow.value?.contentDescription?.getAsString()) + assertThat(iconFlow.value?.contentDescription?.loadContentDescription(context)) .isEqualTo(expectedContentDescription) job.cancel() } - private fun ContentDescription.getAsString(): String? { - return when (this) { - is ContentDescription.Loaded -> this.description - is ContentDescription.Resource -> context.getString(this.res) - } - } - internal data class Expected( /** The resource that should be used for the icon. */ @DrawableRes val iconResource: Int, diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index 6225d0c722ae8..fa78b3832158d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -16,10 +16,6 @@ package com.android.systemui.temporarydisplay.chipbar -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.media.MediaRoute2Info import android.os.PowerManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -31,19 +27,18 @@ import android.widget.ImageView import android.widget.TextView import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text import com.android.systemui.media.taptotransfer.common.MediaTttLogger -import com.android.systemui.media.taptotransfer.sender.ChipStateSender -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEvents import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat @@ -53,7 +48,6 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -64,437 +58,277 @@ import org.mockito.MockitoAnnotations class ChipbarCoordinatorTest : SysuiTestCase() { private lateinit var underTest: FakeChipbarCoordinator - @Mock - private lateinit var packageManager: PackageManager - @Mock - private lateinit var applicationInfo: ApplicationInfo - @Mock - private lateinit var logger: MediaTttLogger - @Mock - private lateinit var accessibilityManager: AccessibilityManager - @Mock - private lateinit var configurationController: ConfigurationController - @Mock - private lateinit var powerManager: PowerManager - @Mock - private lateinit var windowManager: WindowManager - @Mock - private lateinit var falsingManager: FalsingManager - @Mock - private lateinit var falsingCollector: FalsingCollector - @Mock - private lateinit var viewUtil: ViewUtil - private lateinit var fakeAppIconDrawable: Drawable + @Mock private lateinit var logger: MediaTttLogger + @Mock private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var powerManager: PowerManager + @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var falsingManager: FalsingManager + @Mock private lateinit var falsingCollector: FalsingCollector + @Mock private lateinit var viewUtil: ViewUtil private lateinit var fakeClock: FakeSystemClock private lateinit var fakeExecutor: FakeExecutor private lateinit var uiEventLoggerFake: UiEventLoggerFake - private lateinit var senderUiEventLogger: MediaTttSenderUiEventLogger @Before fun setUp() { MockitoAnnotations.initMocks(this) - - fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! - whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) - whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) - whenever(packageManager.getApplicationInfo( - eq(PACKAGE_NAME), any() - )).thenReturn(applicationInfo) - context.setMockPackageManager(packageManager) + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) uiEventLoggerFake = UiEventLoggerFake() - senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake) - whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) - - underTest = FakeChipbarCoordinator( - context, - logger, - windowManager, - fakeExecutor, - accessibilityManager, - configurationController, - powerManager, - senderUiEventLogger, - falsingManager, - falsingCollector, - viewUtil, - ) + underTest = + FakeChipbarCoordinator( + context, + logger, + windowManager, + fakeExecutor, + accessibilityManager, + configurationController, + powerManager, + falsingManager, + falsingCollector, + viewUtil, + ) underTest.start() } @Test - fun almostCloseToStartCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() { - val state = almostCloseToStartCast() - underTest.displayView(state) + fun displayView_loadedIcon_correctlyRendered() { + val drawable = context.getDrawable(R.drawable.ic_celebration)!! - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun almostCloseToEndCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() { - val state = almostCloseToEndCast() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToReceiverTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() { - val state = transferToReceiverTriggered() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToThisDeviceTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() { - val state = transferToThisDeviceTriggered() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToReceiverSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() { - val state = transferToReceiverSucceeded() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToReceiverSucceeded_nullUndoRunnable_noUndo() { - underTest.displayView(transferToReceiverSucceeded(undoCallback = null)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToReceiverSucceeded_withUndoRunnable_undoWithClick() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue() - } - - @Test - fun transferToReceiverSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() { - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() - } - - @Test - fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() { - whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isFalse() - } - - @Test - fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() { - whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false) - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() - } - - @Test - fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - - getChipView().getUndoButton().performClick() - - assertThat(getChipView().getChipText()).isEqualTo( - transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id - ) - } - - @Test - fun transferToThisDeviceSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() { - val state = transferToThisDeviceSucceeded() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToThisDeviceSucceeded_nullUndoRunnable_noUndo() { - underTest.displayView(transferToThisDeviceSucceeded(undoCallback = null)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToThisDeviceSucceeded_withUndoRunnable_undoWithClick() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue() - } - - @Test - fun transferToThisDeviceSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() { - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() - } - - @Test - fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToReceiverTriggered() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) - - getChipView().getUndoButton().performClick() - - assertThat(getChipView().getChipText()).isEqualTo( - transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id - ) - } - - @Test - fun transferToReceiverFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { - val state = transferToReceiverFailed() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(getChipView().getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) - } - - @Test - fun transferToThisDeviceFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { - val state = transferToThisDeviceFailed() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(getChipView().getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) - } - - @Test - fun changeFromAlmostCloseToStartToTransferTriggered_loadingIconAppears() { - underTest.displayView(almostCloseToStartCast()) - underTest.displayView(transferToReceiverTriggered()) - - assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - } - - @Test - fun changeFromTransferTriggeredToTransferSucceeded_loadingIconDisappears() { - underTest.displayView(transferToReceiverTriggered()) - underTest.displayView(transferToReceiverSucceeded()) - - assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE) - } - - @Test - fun changeFromTransferTriggeredToTransferSucceeded_undoButtonAppears() { - underTest.displayView(transferToReceiverTriggered()) underTest.displayView( - transferToReceiverSucceeded( - object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } + ChipbarInfo( + Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), + Text.Loaded("text"), + endItem = null, ) ) - assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE) + val iconView = getChipbarView().getStartIconView() + assertThat(iconView.drawable).isEqualTo(drawable) + assertThat(iconView.contentDescription).isEqualTo("loadedCD") } @Test - fun changeFromTransferSucceededToAlmostCloseToStart_undoButtonDisappears() { - underTest.displayView(transferToReceiverSucceeded()) - underTest.displayView(almostCloseToStartCast()) + fun displayView_resourceIcon_correctlyRendered() { + val contentDescription = ContentDescription.Resource(R.string.controls_error_timeout) + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.drawable.ic_cake, contentDescription), + Text.Loaded("text"), + endItem = null, + ) + ) - assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE) + val iconView = getChipbarView().getStartIconView() + assertThat(iconView.contentDescription) + .isEqualTo(contentDescription.loadContentDescription(context)) } @Test - fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() { - underTest.displayView(transferToReceiverTriggered()) - underTest.displayView(transferToReceiverFailed()) + fun displayView_loadedText_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("display view text here"), + endItem = null, + ) + ) - assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(getChipbarView().getChipText()).isEqualTo("display view text here") } - private fun ViewGroup.getAppIconView() = this.requireViewById(R.id.app_icon) + @Test + fun displayView_resourceText_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Resource(R.string.screenrecord_start_error), + endItem = null, + ) + ) + + assertThat(getChipbarView().getChipText()) + .isEqualTo(context.getString(R.string.screenrecord_start_error)) + } + + @Test + fun displayView_endItemNull_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = null, + ) + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun displayView_endItemLoading_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Loading, + ) + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun displayView_endItemError_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Error, + ) + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun displayView_endItemButton_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + onClickListener = {}, + ), + ) + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().text).isEqualTo("button text") + assertThat(chipbarView.getEndButton().hasOnClickListeners()).isTrue() + } + + @Test + fun displayView_endItemButtonClicked_falseTap_listenerNotRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) + var isClicked = false + val buttonClickListener = View.OnClickListener { isClicked = true } + + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + buttonClickListener, + ), + ) + ) + + getChipbarView().getEndButton().performClick() + + assertThat(isClicked).isFalse() + } + + @Test + fun displayView_endItemButtonClicked_notFalseTap_listenerRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false) + var isClicked = false + val buttonClickListener = View.OnClickListener { isClicked = true } + + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + buttonClickListener, + ), + ) + ) + + getChipbarView().getEndButton().performClick() + + assertThat(isClicked).isTrue() + } + + @Test + fun updateView_viewUpdated() { + // First, display a view + val drawable = context.getDrawable(R.drawable.ic_celebration)!! + + underTest.displayView( + ChipbarInfo( + Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), + Text.Loaded("title text"), + endItem = ChipbarEndItem.Loading, + ) + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getStartIconView().drawable).isEqualTo(drawable) + assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("loadedCD") + assertThat(chipbarView.getChipText()).isEqualTo("title text") + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + + // WHEN the view is updated + val newDrawable = context.getDrawable(R.drawable.ic_cake)!! + underTest.updateView( + ChipbarInfo( + Icon.Loaded(newDrawable, ContentDescription.Loaded("new CD")), + Text.Loaded("new title text"), + endItem = ChipbarEndItem.Error, + ), + chipbarView + ) + + // THEN we display the new view + assertThat(chipbarView.getStartIconView().drawable).isEqualTo(newDrawable) + assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("new CD") + assertThat(chipbarView.getChipText()).isEqualTo("new title text") + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + } + + private fun ViewGroup.getStartIconView() = this.requireViewById(R.id.start_icon) private fun ViewGroup.getChipText(): String = (this.requireViewById(R.id.text)).text as String - private fun ViewGroup.getLoadingIconVisibility(): Int = - this.requireViewById(R.id.loading).visibility + private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading) - private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.undo) + private fun ViewGroup.getEndButton(): TextView = this.requireViewById(R.id.end_button) - private fun ViewGroup.getFailureIcon(): View = this.requireViewById(R.id.failure_icon) + private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error) - private fun getChipView(): ViewGroup { + private fun getChipbarView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) verify(windowManager).addView(viewCaptor.capture(), any()) return viewCaptor.value as ViewGroup } - - // TODO(b/245610654): For now, the below methods are duplicated between this test and - // [MediaTttSenderCoordinatorTest]. Once we define a generic API for [ChipbarCoordinator], - // these will no longer be duplicated. - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToStartCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToEndCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) } -private const val APP_NAME = "Fake app name" -private const val OTHER_DEVICE_NAME = "My Tablet" -private const val PACKAGE_NAME = "com.android.systemui" private const val TIMEOUT = 10000 - -private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) - .addFeature("feature") - .setClientPackageName(PACKAGE_NAME) - .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt index 10704ac8fc679..8f32e0fd1de54 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt @@ -24,7 +24,6 @@ import android.view.accessibility.AccessibilityManager import com.android.systemui.classifier.FalsingCollector import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor @@ -39,7 +38,6 @@ class FakeChipbarCoordinator( accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, - uiEventLogger: MediaTttSenderUiEventLogger, falsingManager: FalsingManager, falsingCollector: FalsingCollector, viewUtil: ViewUtil, @@ -52,7 +50,6 @@ class FakeChipbarCoordinator( accessibilityManager, configurationController, powerManager, - uiEventLogger, falsingManager, falsingCollector, viewUtil,