1 /*
2  * 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.RestrictTo
24 import androidx.window.WindowSdkExtensions
25 import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
26 import androidx.window.core.BuildConfig
27 import androidx.window.core.ExperimentalWindowApi
28 import androidx.window.core.ExtensionsUtil
29 import androidx.window.core.VerificationMode
30 import java.util.concurrent.Executor
31 import kotlinx.coroutines.flow.Flow
32 
33 /**
34  * An interface to provide the information and behavior around moving windows between displays or
35  * display areas on a device.
36  */
37 @ExperimentalWindowApi
38 abstract class WindowAreaController @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor() {
39 
40     /**
41      * [Flow] of the list of current [WindowAreaInfo]s that are currently available to be interacted
42      * with.
43      *
44      * If [WindowSdkExtensions.extensionVersion] is less than 2, the flow will return empty
45      * [WindowAreaInfo] list flow.
46      */
47     abstract val windowAreaInfos: Flow<List<WindowAreaInfo>>
48 
49     /**
50      * Starts a transfer session where the calling [Activity] is moved to the window area identified
51      * by the [token]. Updates on the session are provided through the [WindowAreaSessionCallback].
52      * Attempting to start a transfer session when the [WindowAreaInfo] does not return
53      * [WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE] will result in
54      * [WindowAreaSessionCallback.onSessionEnded] containing an [IllegalStateException]
55      *
56      * Only the top visible application can request to start a transfer session.
57      *
58      * The calling [Activity] will likely go through a configuration change since the window area it
59      * will be transferred to is usually different from the current area the [Activity] is in. The
60      * callback is retained during the lifetime of the session. If an [Activity] is captured in the
61      * callback and it does not handle the configuration change then it will be leaked. Consider
62      * using an [androidx.lifecycle.ViewModel] since that is meant to outlive the [Activity]
63      * lifecycle. If the [Activity] does override configuration changes, it is safe to have the
64      * [Activity] handle the WindowAreaSessionCallback. This guarantees that the calling [Activity]
65      * will continue to receive [WindowAreaSessionCallback.onSessionEnded] and keep a handle to the
66      * [WindowAreaSession] provided through [WindowAreaSessionCallback.onSessionStarted].
67      *
68      * The [windowAreaSessionCallback] provided will receive a call to
69      * [WindowAreaSessionCallback.onSessionStarted] after the [Activity] has been transferred to the
70      * window area. The transfer session will stay active until the session provided through
71      * [WindowAreaSessionCallback.onSessionStarted] is closed. Depending on the
72      * [WindowAreaInfo.Type] there may be other triggers that end the session, such as if a device
73      * state change makes the window area unavailable. One example of this is if the [Activity] is
74      * currently transferred to the [TYPE_REAR_FACING] window area of a foldable device, the session
75      * will be ended when the device is closed. When this occurs,
76      * [WindowAreaSessionCallback.onSessionEnded] is called.
77      *
78      * @param token [Binder] token identifying the window area to be transferred to.
79      * @param activity Base Activity making the call to [transferActivityToWindowArea].
80      * @param executor Executor used to provide updates to [windowAreaSessionCallback].
81      * @param windowAreaSessionCallback to be notified when the rear display session is started and
82      *   ended.
83      * @see windowAreaInfos
84      */
transferActivityToWindowAreanull85     abstract fun transferActivityToWindowArea(
86         token: Binder,
87         activity: Activity,
88         executor: Executor,
89         // TODO(272064992) investigate how to make this safer from leaks
90         windowAreaSessionCallback: WindowAreaSessionCallback
91     )
92 
93     /**
94      * Starts a presentation session on the [WindowAreaInfo] identified by the [token] and sends
95      * updates through the [WindowAreaPresentationSessionCallback].
96      *
97      * If a presentation session is attempted to be started without it being available,
98      * [WindowAreaPresentationSessionCallback.onSessionEnded] will be called immediately with an
99      * [IllegalStateException].
100      *
101      * Only the top visible application can request to start a presentation session.
102      *
103      * The presentation session will stay active until the presentation provided through
104      * [WindowAreaPresentationSessionCallback.onSessionStarted] is closed. The [WindowAreaInfo.Type]
105      * may provide different triggers to close the session such as if the calling application is no
106      * longer in the foreground, or there is a device state change that makes the window area
107      * unavailable to be presented on. One example scenario is if a [TYPE_REAR_FACING] window area
108      * is being presented to on a foldable device that is open and has 2 screens. If the device is
109      * closed and the internal display is turned off, the session would be ended and
110      * [WindowAreaPresentationSessionCallback.onSessionEnded] is called to notify that the session
111      * has been ended. The session may end prematurely if the device gets to a critical thermal
112      * level, or if power saver mode is enabled.
113      *
114      * @param token [Binder] token to identify which [WindowAreaInfo] is to be presented on
115      * @param activity An [Activity] that will present content on the Rear Display.
116      * @param executor Executor used to provide updates to [windowAreaPresentationSessionCallback].
117      * @param windowAreaPresentationSessionCallback to be notified of updates to the lifecycle of
118      *   the currently enabled rear display presentation.
119      * @see windowAreaInfos
120      */
121     abstract fun presentContentOnWindowArea(
122         token: Binder,
123         activity: Activity,
124         executor: Executor,
125         windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback
126     )
127 
128     companion object {
129 
130         private val TAG = WindowAreaController::class.simpleName
131 
132         private var decorator: WindowAreaControllerDecorator = EmptyDecorator
133 
134         /** Provides an instance of [WindowAreaController]. */
135         @JvmName("getOrCreate")
136         @JvmStatic
137         fun getOrCreate(): WindowAreaController {
138             val windowAreaComponentExtensions =
139                 try {
140                     this::class.java.classLoader?.let {
141                         SafeWindowAreaComponentProvider(it).windowAreaComponent
142                     }
143                 } catch (t: Throwable) {
144                     if (BuildConfig.verificationMode == VerificationMode.LOG) {
145                         Log.d(TAG, "Failed to load WindowExtensions")
146                     }
147                     null
148                 }
149 
150             val deviceSupported =
151                 Build.VERSION.SDK_INT > Build.VERSION_CODES.Q &&
152                     windowAreaComponentExtensions != null &&
153                     ExtensionsUtil.safeVendorApiLevel >= 3
154 
155             val controller =
156                 if (deviceSupported) {
157                     WindowAreaControllerImpl(
158                         windowAreaComponent = windowAreaComponentExtensions!!,
159                     )
160                 } else {
161                     EmptyWindowAreaControllerImpl()
162                 }
163             return decorator.decorate(controller)
164         }
165 
166         @JvmStatic
167         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
168         fun overrideDecorator(overridingDecorator: WindowAreaControllerDecorator) {
169             decorator = overridingDecorator
170         }
171 
172         @JvmStatic
173         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
174         fun reset() {
175             decorator = EmptyDecorator
176         }
177     }
178 }
179 
180 /** Decorator that allows us to provide different functionality in our window-testing artifact. */
181 @ExperimentalWindowApi
182 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
183 interface WindowAreaControllerDecorator {
184     /** Returns an instance of [WindowAreaController] associated to the [Activity] */
185     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
decoratenull186     public fun decorate(controller: WindowAreaController): WindowAreaController
187 }
188 
189 @ExperimentalWindowApi
190 private object EmptyDecorator : WindowAreaControllerDecorator {
191     override fun decorate(controller: WindowAreaController): WindowAreaController {
192         return controller
193     }
194 }
195