1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.media.taptotransfer.sender 18 19 import android.app.StatusBarManager 20 import android.content.Context 21 import android.util.Log 22 import androidx.annotation.StringRes 23 import com.android.internal.logging.UiEventLogger 24 import com.android.systemui.R 25 import com.android.systemui.common.shared.model.Text 26 27 /** 28 * A class enumerating all the possible states of the media tap-to-transfer chip on the sender 29 * device. 30 * 31 * @property stateInt the integer from [StatusBarManager] corresponding with this state. 32 * @property stringResId the res ID of the string that should be displayed in the chip. Null if the 33 * state should not have the chip be displayed. 34 * @property transferStatus the transfer status that the chip state represents. 35 * @property endItem the item that should be displayed in the end section of the chip. 36 * @property timeoutLength how long the chip should display on the screen before it times out and 37 * disappears. 38 */ 39 enum class ChipStateSender( 40 @StatusBarManager.MediaTransferSenderState val stateInt: Int, 41 val uiEvent: UiEventLogger.UiEventEnum, 42 @StringRes val stringResId: Int?, 43 val transferStatus: TransferStatus, 44 val endItem: SenderEndItem?, 45 val timeoutLength: TimeoutLength = TimeoutLength.DEFAULT, 46 ) { 47 /** 48 * A state representing that the two devices are close but not close enough to *start* a cast to 49 * the receiver device. The chip will instruct the user to move closer in order to initiate the 50 * transfer to the receiver. 51 */ 52 ALMOST_CLOSE_TO_START_CAST( 53 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 54 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST, 55 R.string.media_move_closer_to_start_cast, 56 transferStatus = TransferStatus.NOT_STARTED, 57 endItem = null, 58 // Give this view more time in case the loading view takes a bit to come in. (We don't want 59 // this view to disappear and then the loading view to appear quickly afterwards.) 60 timeoutLength = TimeoutLength.LONG, 61 ) { 62 override fun isValidNextState(nextState: ChipStateSender): Boolean { 63 return nextState == FAR_FROM_RECEIVER || 64 nextState == TRANSFER_TO_RECEIVER_TRIGGERED 65 } 66 }, 67 68 /** 69 * A state representing that the two devices are close but not close enough to *end* a cast 70 * that's currently occurring the receiver device. The chip will instruct the user to move 71 * closer in order to initiate the transfer from the receiver and back onto this device (the 72 * original sender). 73 */ 74 ALMOST_CLOSE_TO_END_CAST( 75 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 76 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST, 77 R.string.media_move_closer_to_end_cast, 78 transferStatus = TransferStatus.NOT_STARTED, 79 endItem = null, 80 timeoutLength = TimeoutLength.LONG, 81 ) { 82 override fun isValidNextState(nextState: ChipStateSender): Boolean { 83 return nextState == FAR_FROM_RECEIVER || 84 nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED 85 } 86 }, 87 88 /** 89 * A state representing that a transfer to the receiver device has been initiated (but not 90 * completed). 91 */ 92 TRANSFER_TO_RECEIVER_TRIGGERED( 93 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, 94 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED, 95 R.string.media_transfer_playing_different_device, 96 transferStatus = TransferStatus.IN_PROGRESS, 97 endItem = SenderEndItem.Loading, 98 // Give this view more time in case the succeeded/failed view takes a bit to come in. (We 99 // don't want this view to disappear and then the next view to appear quickly afterwards.) 100 timeoutLength = TimeoutLength.LONG, 101 ) { 102 override fun isValidNextState(nextState: ChipStateSender): Boolean { 103 return nextState == FAR_FROM_RECEIVER || 104 nextState == TRANSFER_TO_RECEIVER_SUCCEEDED || 105 nextState == TRANSFER_TO_RECEIVER_FAILED 106 } 107 }, 108 109 /** 110 * A state representing that a transfer from the receiver device and back to this device (the 111 * sender) has been initiated (but not completed). 112 */ 113 TRANSFER_TO_THIS_DEVICE_TRIGGERED( 114 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, 115 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED, 116 R.string.media_transfer_playing_this_device, 117 transferStatus = TransferStatus.IN_PROGRESS, 118 endItem = SenderEndItem.Loading, 119 timeoutLength = TimeoutLength.LONG, 120 ) { 121 override fun isValidNextState(nextState: ChipStateSender): Boolean { 122 return nextState == FAR_FROM_RECEIVER || 123 nextState == TRANSFER_TO_THIS_DEVICE_SUCCEEDED || 124 nextState == TRANSFER_TO_THIS_DEVICE_FAILED 125 } 126 }, 127 128 /** 129 * A state representing that a transfer to the receiver device has been successfully completed. 130 */ 131 TRANSFER_TO_RECEIVER_SUCCEEDED( 132 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 133 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED, 134 R.string.media_transfer_playing_different_device, 135 transferStatus = TransferStatus.SUCCEEDED, 136 endItem = SenderEndItem.UndoButton( 137 uiEventOnClick = 138 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED, 139 newState = 140 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED 141 ), 142 ) { 143 override fun isValidNextState(nextState: ChipStateSender): Boolean { 144 // Since _SUCCEEDED is the end of a transfer sequence, we should be able to move to any 145 // state that represents the beginning of a new sequence. 146 return stateIsStartOfSequence(nextState) 147 } 148 }, 149 150 /** 151 * A state representing that a transfer back to this device has been successfully completed. 152 */ 153 TRANSFER_TO_THIS_DEVICE_SUCCEEDED( 154 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 155 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 156 R.string.media_transfer_playing_this_device, 157 transferStatus = TransferStatus.SUCCEEDED, 158 endItem = SenderEndItem.UndoButton( 159 uiEventOnClick = 160 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED, 161 newState = 162 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED 163 ), 164 ) { 165 override fun isValidNextState(nextState: ChipStateSender): Boolean { 166 // Since _SUCCEEDED is the end of a transfer sequence, we should be able to move to any 167 // state that represents the beginning of a new sequence. 168 return stateIsStartOfSequence(nextState) 169 } 170 }, 171 172 /** A state representing that a transfer to the receiver device has failed. */ 173 TRANSFER_TO_RECEIVER_FAILED( 174 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED, 175 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED, 176 R.string.media_transfer_failed, 177 transferStatus = TransferStatus.FAILED, 178 endItem = SenderEndItem.Error, 179 ) { 180 override fun isValidNextState(nextState: ChipStateSender): Boolean { 181 // Since _FAILED is the end of a transfer sequence, we should be able to move to any 182 // state that represents the beginning of a new sequence. 183 return stateIsStartOfSequence(nextState) 184 } 185 }, 186 187 /** A state representing that a transfer back to this device has failed. */ 188 TRANSFER_TO_THIS_DEVICE_FAILED( 189 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED, 190 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED, 191 R.string.media_transfer_failed, 192 transferStatus = TransferStatus.FAILED, 193 endItem = SenderEndItem.Error, 194 ) { 195 override fun isValidNextState(nextState: ChipStateSender): Boolean { 196 // Since _FAILED is the end of a transfer sequence, we should be able to move to any 197 // state that represents the beginning of a new sequence. 198 return stateIsStartOfSequence(nextState) 199 } 200 }, 201 202 /** A state representing that this device is far away from any receiver device. */ 203 FAR_FROM_RECEIVER( 204 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 205 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER, 206 stringResId = null, 207 transferStatus = TransferStatus.TOO_FAR, 208 // We shouldn't be displaying the chipbar anyway 209 endItem = null, 210 ) { 211 override fun getChipTextString(context: Context, otherDeviceName: String): Text { 212 // TODO(b/245610654): Better way to handle this. 213 throw IllegalArgumentException("FAR_FROM_RECEIVER should never be displayed, " + 214 "so its string should never be fetched") 215 } 216 217 override fun isValidNextState(nextState: ChipStateSender): Boolean { 218 // When far away, we can go to any state that represents the start of a transfer 219 // sequence. 220 return stateIsStartOfSequence(nextState) 221 } 222 }; 223 224 /** 225 * Returns a fully-formed string with the text that the chip should display. 226 * 227 * Throws an NPE if [stringResId] is null. 228 * 229 * @param otherDeviceName the name of the other device involved in the transfer. 230 */ 231 open fun getChipTextString(context: Context, otherDeviceName: String): Text { 232 return Text.Loaded(context.getString(stringResId!!, otherDeviceName)) 233 } 234 235 /** 236 * Returns true if moving from this state to [nextState] is a valid transition. 237 * 238 * In general, we expect a media transfer go to through a sequence of states: 239 * For push-to-receiver: 240 * - ALMOST_CLOSE_TO_START_CAST => TRANSFER_TO_RECEIVER_TRIGGERED => 241 * TRANSFER_TO_RECEIVER_(SUCCEEDED|FAILED) 242 * - ALMOST_CLOSE_TO_END_CAST => TRANSFER_TO_THIS_DEVICE_TRIGGERED => 243 * TRANSFER_TO_THIS_DEVICE_(SUCCEEDED|FAILED) 244 * 245 * This method should validate that the states go through approximately that sequence. 246 * 247 * See b/221265848 for more details. 248 */ 249 abstract fun isValidNextState(nextState: ChipStateSender): Boolean 250 251 companion object { 252 /** 253 * Returns the sender state enum associated with the given [displayState] from 254 * [StatusBarManager]. 255 */ 256 fun getSenderStateFromId( 257 @StatusBarManager.MediaTransferSenderState displayState: Int, 258 ): ChipStateSender? = try { 259 values().first { it.stateInt == displayState } 260 } catch (e: NoSuchElementException) { 261 Log.e(TAG, "Could not find requested state $displayState", e) 262 null 263 } 264 265 /** 266 * Returns the state int from [StatusBarManager] associated with the given sender state 267 * name. 268 * 269 * @param name the name of one of the [ChipStateSender] enums. 270 */ 271 @StatusBarManager.MediaTransferSenderState 272 fun getSenderStateIdFromName(name: String): Int = valueOf(name).stateInt 273 274 /** 275 * Validates the transition from a chip state to another. 276 * 277 * @param currentState is the current state of the chip. 278 * @param desiredState is the desired state of the chip. 279 * @return true if the transition from [currentState] to [desiredState] is valid, and false 280 * otherwise. 281 */ 282 fun isValidStateTransition( 283 currentState: ChipStateSender?, 284 desiredState: ChipStateSender, 285 ): Boolean { 286 // Far from receiver is the default state. 287 if (currentState == null) { 288 return FAR_FROM_RECEIVER.isValidNextState(desiredState) 289 } 290 291 // No change in state is valid. 292 if (currentState == desiredState) { 293 return true 294 } 295 296 return currentState.isValidNextState(desiredState) 297 } 298 299 /** 300 * Returns true if [state] represents a state at the beginning of a sequence and false 301 * otherwise. 302 */ 303 private fun stateIsStartOfSequence(state: ChipStateSender): Boolean { 304 return state == FAR_FROM_RECEIVER || 305 state.transferStatus == TransferStatus.NOT_STARTED || 306 // It's possible to skip the NOT_STARTED phase and go immediately into the 307 // IN_PROGRESS phase. 308 state.transferStatus == TransferStatus.IN_PROGRESS 309 } 310 } 311 } 312 313 /** Represents the item that should be displayed in the end section of the chip. */ 314 sealed class SenderEndItem { 315 /** A loading icon should be displayed. */ 316 object Loading : SenderEndItem() 317 318 /** An error icon should be displayed. */ 319 object Error : SenderEndItem() 320 321 /** 322 * An undo button should be displayed. 323 * 324 * @property uiEventOnClick the UI event to log when this button is clicked. 325 * @property newState the state that should immediately be transitioned to. 326 */ 327 data class UndoButton( 328 val uiEventOnClick: UiEventLogger.UiEventEnum, 329 @StatusBarManager.MediaTransferSenderState val newState: Int, 330 ) : SenderEndItem() 331 } 332 333 /** Represents how long the chip should be visible before it times out. */ 334 enum class TimeoutLength { 335 /** A default timeout used for temporary displays at the top of the screen. */ 336 DEFAULT, 337 /** 338 * A longer timeout. Should be used when the status is pending (e.g. loading), so that the user 339 * remains informed about the process for longer and so that the UI has more time to resolve the 340 * pending state before disappearing. 341 */ 342 LONG, 343 } 344 345 private const val TAG = "ChipStateSender" 346