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