1 /*
<lambda>null2  * Copyright 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 androidx.window.area
18 
19 import android.app.Activity
20 import android.os.Binder
21 import android.os.Build
22 import android.util.Log
23 import androidx.annotation.RequiresApi
24 import androidx.window.RequiresWindowSdkExtension
25 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_ACTIVE
26 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_AVAILABLE
27 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNKNOWN
28 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNSUPPORTED
29 import androidx.window.area.adapter.WindowAreaAdapter
30 import androidx.window.core.BuildConfig
31 import androidx.window.core.ExperimentalWindowApi
32 import androidx.window.core.ExtensionsUtil
33 import androidx.window.core.VerificationMode
34 import androidx.window.extensions.area.ExtensionWindowAreaStatus
35 import androidx.window.extensions.area.WindowAreaComponent
36 import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_ACTIVE
37 import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_CONTENT_VISIBLE
38 import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_INACTIVE
39 import androidx.window.extensions.area.WindowAreaComponent.WindowAreaSessionState
40 import androidx.window.layout.WindowMetrics
41 import androidx.window.layout.WindowMetricsCalculator
42 import androidx.window.reflection.Consumer2
43 import java.util.concurrent.Executor
44 import kotlinx.coroutines.CoroutineScope
45 import kotlinx.coroutines.asCoroutineDispatcher
46 import kotlinx.coroutines.channels.awaitClose
47 import kotlinx.coroutines.flow.Flow
48 import kotlinx.coroutines.flow.callbackFlow
49 import kotlinx.coroutines.flow.first
50 import kotlinx.coroutines.launch
51 
52 /**
53  * Implementation of WindowAreaController for devices that do implement the WindowAreaComponent on
54  * device.
55  *
56  * Requires [Build.VERSION_CODES.N] due to the use of [Consumer]. Will not be created though on API
57  * levels lower than [Build.VERSION_CODES.S] as that's the min level of support for this
58  * functionality.
59  */
60 @ExperimentalWindowApi
61 @RequiresWindowSdkExtension(3)
62 @RequiresApi(Build.VERSION_CODES.Q)
63 internal class WindowAreaControllerImpl(
64     private val windowAreaComponent: WindowAreaComponent,
65 ) : WindowAreaController() {
66 
67     private lateinit var rearDisplaySessionConsumer: Consumer2<Int>
68     private var currentRearDisplayModeStatus: WindowAreaCapability.Status =
69         WINDOW_AREA_STATUS_UNKNOWN
70     private var currentRearDisplayPresentationStatus: WindowAreaCapability.Status =
71         WINDOW_AREA_STATUS_UNKNOWN
72 
73     private var activeWindowAreaSession: Boolean = false
74     private var presentationSessionActive: Boolean = false
75 
76     private val currentWindowAreaInfoMap = HashMap<String, WindowAreaInfo>()
77 
78     override val windowAreaInfos: Flow<List<WindowAreaInfo>>
79         get() {
80             return callbackFlow {
81                 val rearDisplayListener =
82                     Consumer2<Int> { status ->
83                         updateRearDisplayAvailability(status)
84                         channel.trySend(currentWindowAreaInfoMap.values.toList())
85                     }
86                 val rearDisplayPresentationListener =
87                     Consumer2<ExtensionWindowAreaStatus> { extensionWindowAreaStatus ->
88                         updateRearDisplayPresentationAvailability(extensionWindowAreaStatus)
89                         channel.trySend(currentWindowAreaInfoMap.values.toList())
90                     }
91 
92                 windowAreaComponent.addRearDisplayStatusListener(rearDisplayListener)
93                 windowAreaComponent.addRearDisplayPresentationStatusListener(
94                     rearDisplayPresentationListener
95                 )
96 
97                 awaitClose {
98                     windowAreaComponent.removeRearDisplayStatusListener(rearDisplayListener)
99                     windowAreaComponent.removeRearDisplayPresentationStatusListener(
100                         rearDisplayPresentationListener
101                     )
102                 }
103             }
104         }
105 
106     private fun updateRearDisplayAvailability(status: @WindowAreaComponent.WindowAreaStatus Int) {
107         val windowMetrics =
108             WindowMetricsCalculator.fromDisplayMetrics(
109                 displayMetrics = windowAreaComponent.rearDisplayMetrics
110             )
111 
112         currentRearDisplayModeStatus = WindowAreaAdapter.translate(status, activeWindowAreaSession)
113         updateRearDisplayWindowArea(
114             WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA,
115             currentRearDisplayModeStatus,
116             windowMetrics
117         )
118     }
119 
120     private fun updateRearDisplayPresentationAvailability(
121         extensionWindowAreaStatus: ExtensionWindowAreaStatus
122     ) {
123         currentRearDisplayPresentationStatus =
124             WindowAreaAdapter.translate(
125                 extensionWindowAreaStatus.windowAreaStatus,
126                 presentationSessionActive
127             )
128         val windowMetrics =
129             WindowMetricsCalculator.fromDisplayMetrics(
130                 displayMetrics = extensionWindowAreaStatus.windowAreaDisplayMetrics
131             )
132 
133         updateRearDisplayWindowArea(
134             WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA,
135             currentRearDisplayPresentationStatus,
136             windowMetrics,
137         )
138     }
139 
140     /**
141      * Updates the [WindowAreaInfo] object with the [REAR_DISPLAY_BINDER_DESCRIPTOR] binder token
142      * with the updated [status] corresponding to the [operation] and with the updated [metrics]
143      * received from the device for this window area.
144      *
145      * @param operation Operation that we are updating the status of.
146      * @param status New status for the operation provided on this window area.
147      * @param metrics Updated [WindowMetrics] for this window area.
148      */
149     private fun updateRearDisplayWindowArea(
150         operation: WindowAreaCapability.Operation,
151         status: WindowAreaCapability.Status,
152         metrics: WindowMetrics,
153     ) {
154         var rearDisplayAreaInfo: WindowAreaInfo? =
155             currentWindowAreaInfoMap[REAR_DISPLAY_BINDER_DESCRIPTOR]
156         if (status == WINDOW_AREA_STATUS_UNSUPPORTED) {
157             rearDisplayAreaInfo?.let { info ->
158                 if (shouldRemoveWindowAreaInfo(info)) {
159                     currentWindowAreaInfoMap.remove(REAR_DISPLAY_BINDER_DESCRIPTOR)
160                 } else {
161                     val capability = WindowAreaCapability(operation, status)
162                     info.capabilityMap[operation] = capability
163                 }
164             }
165         } else {
166             if (rearDisplayAreaInfo == null) {
167                 rearDisplayAreaInfo =
168                     WindowAreaInfo(
169                         metrics = metrics,
170                         type = WindowAreaInfo.Type.TYPE_REAR_FACING,
171                         // TODO(b/273807238): Update extensions to send the binder token and type
172                         token = Binder(REAR_DISPLAY_BINDER_DESCRIPTOR),
173                         windowAreaComponent = windowAreaComponent
174                     )
175             }
176             val capability = WindowAreaCapability(operation, status)
177             rearDisplayAreaInfo.capabilityMap[operation] = capability
178             rearDisplayAreaInfo.metrics = metrics
179             currentWindowAreaInfoMap[REAR_DISPLAY_BINDER_DESCRIPTOR] = rearDisplayAreaInfo
180         }
181     }
182 
183     /**
184      * Determines if a [WindowAreaInfo] should be removed from [windowAreaInfos] if all
185      * [WindowAreaCapability] are currently [WINDOW_AREA_STATUS_UNSUPPORTED]
186      */
187     private fun shouldRemoveWindowAreaInfo(windowAreaInfo: WindowAreaInfo): Boolean {
188         for (capability: WindowAreaCapability in windowAreaInfo.capabilityMap.values) {
189             if (capability.status != WINDOW_AREA_STATUS_UNSUPPORTED) {
190                 return false
191             }
192         }
193         return true
194     }
195 
196     override fun transferActivityToWindowArea(
197         token: Binder,
198         activity: Activity,
199         executor: Executor,
200         windowAreaSessionCallback: WindowAreaSessionCallback
201     ) {
202         if (token.interfaceDescriptor != REAR_DISPLAY_BINDER_DESCRIPTOR) {
203             executor.execute {
204                 windowAreaSessionCallback.onSessionEnded(
205                     IllegalArgumentException("Invalid WindowAreaInfo token")
206                 )
207             }
208             return
209         }
210 
211         if (currentRearDisplayModeStatus == WINDOW_AREA_STATUS_UNKNOWN) {
212             Log.d(TAG, "Force updating currentRearDisplayModeStatus")
213             // currentRearDisplayModeStatus may be null if the client has not queried
214             // WindowAreaController.windowAreaInfos using this instance. In this case, we query
215             // it for a single value to force update currentRearDisplayModeStatus.
216             CoroutineScope(executor.asCoroutineDispatcher()).launch {
217                 windowAreaInfos.first()
218                 startRearDisplayMode(activity, executor, windowAreaSessionCallback)
219             }
220         } else {
221             startRearDisplayMode(activity, executor, windowAreaSessionCallback)
222         }
223     }
224 
225     override fun presentContentOnWindowArea(
226         token: Binder,
227         activity: Activity,
228         executor: Executor,
229         windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback
230     ) {
231         if (token.interfaceDescriptor != REAR_DISPLAY_BINDER_DESCRIPTOR) {
232             executor.execute {
233                 windowAreaPresentationSessionCallback.onSessionEnded(
234                     IllegalArgumentException("Invalid WindowAreaInfo token")
235                 )
236             }
237             return
238         }
239 
240         if (currentRearDisplayPresentationStatus == WINDOW_AREA_STATUS_UNKNOWN) {
241             Log.d(TAG, "Force updating currentRearDisplayPresentationStatus")
242             // currentRearDisplayModeStatus may be null if the client has not queried
243             // WindowAreaController.windowAreaInfos using this instance. In this case, we query
244             // it for a single value to force update currentRearDisplayPresentationStatus.
245             CoroutineScope(executor.asCoroutineDispatcher()).launch {
246                 windowAreaInfos.first()
247                 startRearDisplayPresentationMode(
248                     activity,
249                     executor,
250                     windowAreaPresentationSessionCallback
251                 )
252             }
253         } else {
254             startRearDisplayPresentationMode(
255                 activity,
256                 executor,
257                 windowAreaPresentationSessionCallback
258             )
259         }
260     }
261 
262     private fun startRearDisplayMode(
263         activity: Activity,
264         executor: Executor,
265         windowAreaSessionCallback: WindowAreaSessionCallback
266     ) {
267         // If the capability is currently active, provide an error pointing the developer on how to
268         // get access to the current session
269         if (currentRearDisplayModeStatus == WINDOW_AREA_STATUS_ACTIVE) {
270             windowAreaSessionCallback.onSessionEnded(
271                 IllegalStateException(
272                     "The WindowArea feature is currently active, WindowAreaInfo#getActiveSession" +
273                         "can be used to get an instance of the current active session"
274                 )
275             )
276             return
277         }
278 
279         // If we already have an availability value that is not
280         // [Availability.WINDOW_AREA_CAPABILITY_AVAILABLE] we should end the session and pass an
281         // exception to indicate they tried to enable rear display mode when it was not available.
282         if (currentRearDisplayModeStatus != WINDOW_AREA_STATUS_AVAILABLE) {
283             windowAreaSessionCallback.onSessionEnded(
284                 IllegalStateException(
285                     "The WindowArea feature is currently not available to be entered"
286                 )
287             )
288             return
289         }
290 
291         activeWindowAreaSession = true
292         rearDisplaySessionConsumer =
293             RearDisplaySessionConsumer(executor, windowAreaSessionCallback, windowAreaComponent)
294         windowAreaComponent.startRearDisplaySession(activity, rearDisplaySessionConsumer)
295     }
296 
297     private fun startRearDisplayPresentationMode(
298         activity: Activity,
299         executor: Executor,
300         windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback
301     ) {
302         if (currentRearDisplayPresentationStatus != WINDOW_AREA_STATUS_AVAILABLE) {
303             windowAreaPresentationSessionCallback.onSessionEnded(
304                 IllegalStateException(
305                     "The WindowArea feature is currently not available to be entered"
306                 )
307             )
308             return
309         }
310 
311         presentationSessionActive = true
312         windowAreaComponent.startRearDisplayPresentationSession(
313             activity,
314             RearDisplayPresentationSessionConsumer(
315                 executor,
316                 windowAreaPresentationSessionCallback,
317                 windowAreaComponent
318             )
319         )
320     }
321 
322     internal inner class RearDisplaySessionConsumer(
323         private val executor: Executor,
324         private val appCallback: WindowAreaSessionCallback,
325         private val extensionsComponent: WindowAreaComponent
326     ) : Consumer2<Int> {
327 
328         private var session: WindowAreaSession? = null
329 
330         override fun accept(value: Int) {
331             when (value) {
332                 SESSION_STATE_ACTIVE -> onSessionStarted()
333                 SESSION_STATE_INACTIVE -> onSessionFinished()
334                 else -> {
335                     if (BuildConfig.verificationMode == VerificationMode.STRICT) {
336                         Log.d(TAG, "Received an unknown session status value: $value")
337                     }
338                     onSessionFinished()
339                 }
340             }
341         }
342 
343         private fun onSessionStarted() {
344             session = RearDisplaySessionImpl(extensionsComponent)
345             session?.let { executor.execute { appCallback.onSessionStarted(it) } }
346         }
347 
348         private fun onSessionFinished() {
349             activeWindowAreaSession = false
350             session = null
351             executor.execute { appCallback.onSessionEnded(null) }
352         }
353     }
354 
355     internal inner class RearDisplayPresentationSessionConsumer(
356         private val executor: Executor,
357         private val windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback,
358         private val windowAreaComponent: WindowAreaComponent
359     ) : Consumer2<@WindowAreaSessionState Int> {
360 
361         private var lastReportedSessionStatus: @WindowAreaSessionState Int = SESSION_STATE_INACTIVE
362 
363         override fun accept(value: @WindowAreaSessionState Int) {
364             val previousStatus: @WindowAreaSessionState Int = lastReportedSessionStatus
365             lastReportedSessionStatus = value
366 
367             executor.execute {
368                 when (value) {
369                     SESSION_STATE_ACTIVE -> {
370                         // If the last status was visible, then ACTIVE infers the content is no
371                         // longer visible.
372                         if (previousStatus == SESSION_STATE_CONTENT_VISIBLE) {
373                             windowAreaPresentationSessionCallback.onContainerVisibilityChanged(
374                                 false /* isVisible */
375                             )
376                         } else {
377                             // Presentation should never be null if the session is active
378                             windowAreaPresentationSessionCallback.onSessionStarted(
379                                 RearDisplayPresentationSessionPresenterImpl(
380                                     windowAreaComponent,
381                                     windowAreaComponent.rearDisplayPresentation!!,
382                                     ExtensionsUtil.safeVendorApiLevel
383                                 )
384                             )
385                         }
386                     }
387                     SESSION_STATE_CONTENT_VISIBLE ->
388                         windowAreaPresentationSessionCallback.onContainerVisibilityChanged(true)
389                     SESSION_STATE_INACTIVE -> {
390                         presentationSessionActive = false
391                         windowAreaPresentationSessionCallback.onSessionEnded(null)
392                     }
393                     else -> {
394                         Log.e(TAG, "Invalid session state value received: $value")
395                     }
396                 }
397             }
398         }
399     }
400 
401     internal companion object {
402         private val TAG = WindowAreaControllerImpl::class.simpleName
403 
404         private const val REAR_DISPLAY_BINDER_DESCRIPTOR = "WINDOW_AREA_REAR_DISPLAY"
405     }
406 }
407