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