WindowAreaController.kt
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.window.area
import android.app.Activity
import android.os.Binder
import android.os.Build
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.window.WindowSdkExtensions
import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
import androidx.window.area.utils.DeviceUtils
import androidx.window.core.BuildConfig
import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.ExtensionsUtil
import androidx.window.core.VerificationMode
import java.util.concurrent.Executor
import kotlinx.coroutines.flow.Flow
/**
* An interface to provide the information and behavior around moving windows between
* displays or display areas on a device.
*
*/
@ExperimentalWindowApi
interface WindowAreaController {
/**
* [Flow] of the list of current [WindowAreaInfo]s that are currently available to be interacted
* with.
*
* If [WindowSdkExtensions.extensionVersion] is less than 2, the flow will return
* empty [WindowAreaInfo] list flow.
*/
val windowAreaInfos: Flow<List<WindowAreaInfo>>
/**
* Starts a transfer session where the calling [Activity] is moved to the window area identified
* by the [token]. Updates on the session are provided through the [WindowAreaSessionCallback].
* Attempting to start a transfer session when the [WindowAreaInfo] does not return
* [WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE] will result in
* [WindowAreaSessionCallback.onSessionEnded] containing an [IllegalStateException]
*
* Only the top visible application can request to start a transfer session.
*
* The calling [Activity] will likely go through a configuration change since the window area
* it will be transferred to is usually different from the current area the [Activity] is in.
* The callback is retained during the lifetime of the session. If an [Activity] is captured in
* the callback and it does not handle the configuration change then it will be leaked. Consider
* using an [androidx.lifecycle.ViewModel] since that is meant to outlive the [Activity]
* lifecycle. If the [Activity] does override configuration changes, it is safe to have the
* [Activity] handle the WindowAreaSessionCallback. This guarantees that the calling [Activity]
* will continue to receive [WindowAreaSessionCallback.onSessionEnded] and keep a handle to the
* [WindowAreaSession] provided through [WindowAreaSessionCallback.onSessionStarted].
*
* The [windowAreaSessionCallback] provided will receive a call to
* [WindowAreaSessionCallback.onSessionStarted] after the [Activity] has been transferred to the
* window area. The transfer session will stay active until the session provided through
* [WindowAreaSessionCallback.onSessionStarted] is closed. Depending on the
* [WindowAreaInfo.Type] there may be other triggers that end the session, such as if a device
* state change makes the window area unavailable. One example of this is if the [Activity] is
* currently transferred to the [TYPE_REAR_FACING] window area of a foldable device, the session
* will be ended when the device is closed. When this occurs,
* [WindowAreaSessionCallback.onSessionEnded] is called.
*
* @param token [Binder] token identifying the window area to be transferred to.
* @param activity Base Activity making the call to [transferActivityToWindowArea].
* @param executor Executor used to provide updates to [windowAreaSessionCallback].
* @param windowAreaSessionCallback to be notified when the rear display session is started and
* ended.
*
* @see windowAreaInfos
*/
fun transferActivityToWindowArea(
token: Binder,
activity: Activity,
executor: Executor,
// TODO(272064992) investigate how to make this safer from leaks
windowAreaSessionCallback: WindowAreaSessionCallback
)
/**
* Starts a presentation session on the [WindowAreaInfo] identified by the [token] and sends
* updates through the [WindowAreaPresentationSessionCallback].
*
* If a presentation session is attempted to be started without it being available,
* [WindowAreaPresentationSessionCallback.onSessionEnded] will be called immediately with an
* [IllegalStateException].
*
* Only the top visible application can request to start a presentation session.
*
* The presentation session will stay active until the presentation provided through
* [WindowAreaPresentationSessionCallback.onSessionStarted] is closed. The [WindowAreaInfo.Type]
* may provide different triggers to close the session such as if the calling application
* is no longer in the foreground, or there is a device state change that makes the window area
* unavailable to be presented on. One example scenario is if a [TYPE_REAR_FACING] window area
* is being presented to on a foldable device that is open and has 2 screens. If the device is
* closed and the internal display is turned off, the session would be ended and
* [WindowAreaPresentationSessionCallback.onSessionEnded] is called to notify that the session
* has been ended. The session may end prematurely if the device gets to a critical thermal
* level, or if power saver mode is enabled.
*
* @param token [Binder] token to identify which [WindowAreaInfo] is to be presented on
* @param activity An [Activity] that will present content on the Rear Display.
* @param executor Executor used to provide updates to [windowAreaPresentationSessionCallback].
* @param windowAreaPresentationSessionCallback to be notified of updates to the lifecycle of
* the currently enabled rear display presentation.
* @see windowAreaInfos
*/
fun presentContentOnWindowArea(
token: Binder,
activity: Activity,
executor: Executor,
windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback
)
public companion object {
private val TAG = WindowAreaController::class.simpleName
private var decorator: WindowAreaControllerDecorator = EmptyDecorator
/**
* Provides an instance of [WindowAreaController].
*/
@JvmName("getOrCreate")
@JvmStatic
fun getOrCreate(): WindowAreaController {
val windowAreaComponentExtensions = try {
this::class.java.classLoader?.let {
SafeWindowAreaComponentProvider(it).windowAreaComponent
}
} catch (t: Throwable) {
if (BuildConfig.verificationMode == VerificationMode.LOG) {
Log.d(TAG, "Failed to load WindowExtensions")
}
null
}
val deviceSupported = Build.VERSION.SDK_INT > Build.VERSION_CODES.Q &&
windowAreaComponentExtensions != null &&
(ExtensionsUtil.safeVendorApiLevel >= 3 || DeviceUtils.hasDeviceMetrics(
Build.MANUFACTURER,
Build.MODEL
))
val controller =
if (deviceSupported) {
WindowAreaControllerImpl(
windowAreaComponentExtensions!!,
ExtensionsUtil.safeVendorApiLevel
)
} else {
EmptyWindowAreaControllerImpl()
}
return decorator.decorate(controller)
}
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun overrideDecorator(overridingDecorator: WindowAreaControllerDecorator) {
decorator = overridingDecorator
}
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun reset() {
decorator = EmptyDecorator
}
}
}
/**
* Decorator that allows us to provide different functionality
* in our window-testing artifact.
*/
@ExperimentalWindowApi
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
interface WindowAreaControllerDecorator {
/**
* Returns an instance of [WindowAreaController] associated to the [Activity]
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun decorate(controller: WindowAreaController): WindowAreaController
}
@ExperimentalWindowApi
private object EmptyDecorator : WindowAreaControllerDecorator {
override fun decorate(controller: WindowAreaController): WindowAreaController {
return controller
}
}