/*
* 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.embedding
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.core.util.Consumer
import androidx.window.WindowProperties
import androidx.window.core.BuildConfig
import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.VerificationMode
import androidx.window.embedding.SplitController.Api31Impl.isSplitPropertyEnabled
import androidx.window.layout.WindowMetrics
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* A singleton controller class that gets information about the currently active activity
* splits and provides interaction points to customize the splits and form new
* splits.
*
* A split is a pair of containers that host activities in the same or different
* processes, combined under the same parent window of the hosting task.
*
* A pair of activities can be put into a split by providing a static or runtime
* split rule and then launching the activities in the same task using
* [Activity.startActivity()][android.app.Activity.startActivity].
*/
class SplitController private constructor(private val applicationContext: Context) {
private val embeddingBackend: EmbeddingBackend = ExtensionEmbeddingBackend
.getInstance(applicationContext)
// TODO(b/258356512): Make this method a flow API
/**
* Registers a listener for updates about the active split state(s) that this
* activity is part of. An activity can be in zero, one or more active splits.
* More than one active split is possible if an activity created multiple
* containers to side, stacked on top of each other. Or it can be in two
* different splits at the same time - in a secondary container for one (it was
* launched to the side) and in the primary for another (it launched another
* activity to the side). The reported splits in the list are ordered from
* bottom to top by their z-order, more recent splits appearing later.
* Guaranteed to be called at least once to report the most recent state.
*
* @param activity only split that this [Activity] is part of will be reported.
* @param executor when there is an update to the active split state(s), the [consumer] will be
* invoked on this [Executor].
* @param consumer [Consumer] that will be invoked on the [executor] when there is an update to
* the active split state(s).
*/
fun addSplitListener(
activity: Activity,
executor: Executor,
consumer: Consumer<List<SplitInfo>>
) {
embeddingBackend.addSplitListenerForActivity(activity, executor, consumer)
}
/**
* Unregisters a listener that was previously registered via [addSplitListener].
*
* @param consumer the previously registered [Consumer] to unregister.
*/
fun removeSplitListener(
consumer: Consumer<List<SplitInfo>>
) {
embeddingBackend.removeSplitListenerForActivity(consumer)
}
/**
* Indicates whether split functionality is supported on the device. Note
* that devices might not enable splits in all states or conditions. For
* example, a foldable device with multiple screens can choose to collapse
* splits when apps run on the device's small display, but enable splits
* when apps run on the device's large display. In cases like this,
* `isSplitSupported` always returns `true`, and if the split is collapsed,
* activities are launched on top, following the non-activity embedding
* model.
*
* Also the [androidx.window.WindowProperties.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED]
* must be enabled in AndroidManifest within <application> in order to get the correct
* state or `false` will be returned by default.
*/
@ExperimentalWindowApi
@Deprecated("Use splitSupportStatus instead",
replaceWith = ReplaceWith("splitSupportStatus")
)
fun isSplitSupported(): Boolean = splitSupportStatus == SplitSupportStatus.SPLIT_AVAILABLE
/**
* Indicates whether split functionality is supported on the device. Note
* that devices might not enable splits in all states or conditions. For
* example, a foldable device with multiple screens can choose to collapse
* splits when apps run on the device's small display, but enable splits
* when apps run on the device's large display. In cases like this,
* [splitSupportStatus] always returns [SplitSupportStatus.SPLIT_AVAILABLE], and if the
* split is collapsed, activities are launched on top, following the non-activity
* embedding model.
*
* Also the [androidx.window.WindowProperties.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED]
* must be enabled in AndroidManifest within <application> in order to get the correct
* state or [SplitSupportStatus.SPLIT_ERROR_PROPERTY_NOT_DECLARED] will be returned in some
* cases.
*
* @see SplitSupportStatus
*/
val splitSupportStatus: SplitSupportStatus by lazy {
if (!embeddingBackend.isSplitSupported()) {
SplitSupportStatus.SPLIT_UNAVAILABLE
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
isSplitPropertyEnabled(applicationContext)
} else {
// The PackageManager#getProperty API is not supported before S, assuming
// the property is enabled to keep the same behavior on earlier platforms.
SplitSupportStatus.SPLIT_AVAILABLE
}
}
/**
* Sets or replaces the previously registered [SplitAttributes] calculator.
*
* **Note** that it's callers' responsibility to check if this API is supported by calling
* [isSplitAttributesCalculatorSupported] before using the this API. It is suggested to always
* set meaningful [SplitRule.defaultSplitAttributes] in case this API is not supported on some
* devices.
*
* Also, replacing the calculator will only update existing split pairs after a change
* in the window or device state, such as orientation changes or folding state changes.
*
* The [SplitAttributes] calculator is a function to compute the current [SplitAttributes] for
* the given [SplitRule] with the current device and window state. Then The calculator will be
* invoked if either:
* - An activity is started and matches a registered [SplitRule].
* - A parent configuration is updated and there's an existing split pair.
*
* By default, [SplitRule.defaultSplitAttributes] are applied if the parent container's
* [WindowMetrics] satisfies the [SplitRule]'s dimensions requirements, which are
* [SplitRule.minWidthDp], [SplitRule.minHeightDp] and [SplitRule.minSmallestWidthDp].
* The [SplitRule.defaultSplitAttributes] can be set by
* - [SplitRule] Builder APIs, which are
* [SplitPairRule.Builder.setDefaultSplitAttributes] and
* [SplitPlaceholderRule.Builder.setDefaultSplitAttributes].
* - Specifying with `splitRatio` and `splitLayoutDirection` attributes in `<SplitPairRule>` or
* `<SplitPlaceHolderRule>` tags in XML files.
*
* Developers may want to apply different [SplitAttributes] for different device or window
* states. For example, on foldable devices, developers may want to split the screen vertically
* if the device is in landscape, fill the screen if the device is in portrait and split
* the screen horizontally if the device is in
* [tabletop posture](https://developer.android.com/guide/topics/ui/foldables#postures).
* In this case, the [SplitAttributes] can be customized by the [SplitAttributes] calculator,
* which takes effects after calling this API. Developers can also clear the calculator
* by [clearSplitAttributesCalculator].
* Then, developers could implement the [SplitAttributes] calculator as the sample linked below
* shows.
*
* @sample androidx.window.samples.embedding.splitAttributesCalculatorSample
* @param calculator the function to calculate [SplitAttributes] based on the
* [SplitAttributesCalculatorParams]. It will replace the previously set if it exists.
* @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
* `false`
*/
@ExperimentalWindowApi
fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
) {
embeddingBackend.setSplitAttributesCalculator(calculator)
}
/**
* Clears the callback previously set by [setSplitAttributesCalculator].
* The caller **must** make sure [isSplitAttributesCalculatorSupported] before invoking.
*
* @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
* `false`
*/
@ExperimentalWindowApi
fun clearSplitAttributesCalculator() {
embeddingBackend.clearSplitAttributesCalculator()
}
/** Returns whether [setSplitAttributesCalculator] is supported or not. */
@ExperimentalWindowApi
fun isSplitAttributesCalculatorSupported(): Boolean =
embeddingBackend.isSplitAttributesCalculatorSupported()
/**
* A class to determine if activity splits with Activity Embedding are currently available.
* "Depending on the split property declaration, device software version or user preferences
* the feature might not be available.
*/
class SplitSupportStatus private constructor(private val rawValue: Int) {
override fun toString(): String {
return when (rawValue) {
0 -> "SplitSupportStatus: AVAILABLE"
1 -> "SplitSupportStatus: UNAVAILABLE"
2 -> "SplitSupportStatus: ERROR_SPLIT_PROPERTY_NOT_DECLARED"
else -> "UNKNOWN"
}
}
companion object {
/**
* The activity splits API is available and split rules can take effect depending on
* the window state.
*/
@JvmField
val SPLIT_AVAILABLE = SplitSupportStatus(0)
/**
* The activity splits API is currently unavailable.
*/
@JvmField
val SPLIT_UNAVAILABLE = SplitSupportStatus(1)
/**
* Denotes that [WindowProperties.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED] has not
* been set. This property must be set and enabled in AndroidManifest.xml to use splits
* APIs.
*/
@JvmField
val SPLIT_ERROR_PROPERTY_NOT_DECLARED = SplitSupportStatus(2)
}
}
companion object {
@Volatile
private var globalInstance: SplitController? = null
private val globalLock = ReentrantLock()
private const val TAG = "SplitController"
internal const val sDebug = false
/**
* Obtains the singleton instance of [SplitController].
*
* @param context the [Context] to initialize the controller with
*/
@JvmStatic
fun getInstance(context: Context): SplitController {
if (globalInstance == null) {
globalLock.withLock {
if (globalInstance == null) {
globalInstance = SplitController(context.applicationContext)
}
}
}
return globalInstance!!
}
}
@RequiresApi(31)
private object Api31Impl {
@DoNotInline
fun isSplitPropertyEnabled(applicationContext: Context): SplitSupportStatus {
val property = try {
applicationContext.packageManager.getProperty(
WindowProperties.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED,
applicationContext.packageName
)
} catch (e: PackageManager.NameNotFoundException) {
if (BuildConfig.verificationMode == VerificationMode.LOG) {
Log.w(
TAG, WindowProperties.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED +
" must be set and enabled in AndroidManifest.xml to use splits APIs."
)
}
return SplitSupportStatus.SPLIT_ERROR_PROPERTY_NOT_DECLARED
} catch (e: Exception) {
if (BuildConfig.verificationMode == VerificationMode.LOG) {
// This can happen when it is a test environment that doesn't support
// getProperty.
Log.e(TAG, "PackageManager.getProperty is not supported", e)
}
return SplitSupportStatus.SPLIT_ERROR_PROPERTY_NOT_DECLARED
}
if (!property.isBoolean) {
if (BuildConfig.verificationMode == VerificationMode.LOG) {
Log.w(
TAG, WindowProperties.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED +
" must have a boolean value"
)
}
return SplitSupportStatus.SPLIT_ERROR_PROPERTY_NOT_DECLARED
}
return if (property.boolean) {
SplitSupportStatus.SPLIT_AVAILABLE
} else {
SplitSupportStatus.SPLIT_UNAVAILABLE
}
}
}
}