Updated project directories for unified 'systemUI' folder (#5034)

This commit is contained in:
John Andrew Camu
2024-12-04 08:03:15 +08:00
committed by GitHub
parent 46f8e92df9
commit f9dbf334aa
293 changed files with 44 additions and 27 deletions
@@ -0,0 +1,3 @@
package com.android.internal.util;
parcelable ScreenshotRequest;
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2023 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 com.android.systemui.dagger.qualifiers
import java.lang.annotation.Documented
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy.RUNTIME
import javax.inject.Qualifier
/** Annotates a class that is display specific. */
@Qualifier @Documented @Retention(RUNTIME) annotation class DisplaySpecific
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2019 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 com.android.systemui.dagger.qualifiers;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import javax.inject.Qualifier;
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Main {
}
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 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 com.android.systemui.dagger.qualifiers
import javax.inject.Qualifier
@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Tracing
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2019 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 com.android.systemui.dagger.qualifiers;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import javax.inject.Qualifier;
/**
* An annotation for injecting instances related to UI operations off the main-thread.
*/
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface UiBackground {
}
@@ -0,0 +1,356 @@
/*
* Copyright (C) 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 com.android.systemui.flags
import android.annotation.BoolRes
import android.annotation.IntegerRes
import android.annotation.StringRes
import android.os.Parcel
import android.os.Parcelable
/**
* Base interface for flags that can change value on a running device.
* @property teamfood Set to true to include this flag as part of the teamfood flag. This will
* be removed soon.
* @property name Used for server-side flagging where appropriate. Also used for display. No spaces.
* @property namespace The server-side namespace that this flag lives under.
*/
interface Flag<T> {
val teamfood: Boolean
val name: String
val namespace: String
}
interface ParcelableFlag<T> : Flag<T>, Parcelable {
val default: T
val overridden: Boolean
override fun describeContents() = 0
}
interface ResourceFlag<T> : Flag<T> {
val resourceId: Int
}
interface SysPropFlag<T> : Flag<T> {
val default: T
}
/**
* Base class for most common boolean flags.
*
* See [UnreleasedFlag] and [ReleasedFlag] for useful implementations.
*/
// Consider using the "parcelize" kotlin library.
abstract class BooleanFlag constructor(
override val name: String,
override val namespace: String,
override val default: Boolean = false,
override val teamfood: Boolean = false,
override val overridden: Boolean = false
) : ParcelableFlag<Boolean> {
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<BooleanFlag> {
override fun createFromParcel(parcel: Parcel) = object : BooleanFlag(parcel) {}
override fun newArray(size: Int) = arrayOfNulls<BooleanFlag>(size)
}
}
private constructor(
id: Int,
name: String,
namespace: String,
default: Boolean,
teamfood: Boolean,
overridden: Boolean,
) : this(name, namespace, default, teamfood, overridden)
private constructor(parcel: Parcel) : this(
parcel.readInt(),
name = parcel.readString() ?: "",
namespace = parcel.readString() ?: "",
default = parcel.readBoolean(),
teamfood = parcel.readBoolean(),
overridden = parcel.readBoolean()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(0)
parcel.writeString(name)
parcel.writeString(namespace)
parcel.writeBoolean(default)
parcel.writeBoolean(teamfood)
parcel.writeBoolean(overridden)
}
}
/**
* A Flag that is is false by default.
*
* It can be changed or overridden in debug builds but not in release builds.
*/
data class UnreleasedFlag constructor(
override val name: String,
override val namespace: String,
override val teamfood: Boolean = false,
override val overridden: Boolean = false
) : BooleanFlag(name, namespace, false, teamfood, overridden)
/**
* A Flag that is true by default.
*
* It can be changed or overridden in any build, meaning it can be turned off if needed.
*/
data class ReleasedFlag constructor(
override val name: String,
override val namespace: String,
override val overridden: Boolean = false
) : BooleanFlag(name, namespace, true, teamfood = false, overridden)
/**
* A Flag that reads its default values from a resource overlay instead of code.
*
* Prefer [UnreleasedFlag] and [ReleasedFlag].
*/
data class ResourceBooleanFlag constructor(
override val name: String,
override val namespace: String,
@BoolRes override val resourceId: Int,
) : ResourceFlag<Boolean> {
override val teamfood: Boolean = false
}
/**
* A Flag that can reads its overrides from System Properties.
*
* This is generally useful for flags that come from or are used _outside_ of SystemUI.
*
* Prefer [UnreleasedFlag] and [ReleasedFlag].
*/
data class SysPropBooleanFlag constructor(
override val name: String,
override val namespace: String,
override val default: Boolean = false,
) : SysPropFlag<Boolean> {
override val teamfood: Boolean = false
}
data class StringFlag constructor(
override val name: String,
override val namespace: String,
override val default: String = "",
override val teamfood: Boolean = false,
override val overridden: Boolean = false
) : ParcelableFlag<String> {
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<StringFlag> {
override fun createFromParcel(parcel: Parcel) = StringFlag(parcel)
override fun newArray(size: Int) = arrayOfNulls<StringFlag>(size)
}
}
private constructor(id: Int, name: String, namespace: String, default: String) : this(
name,
namespace,
default
)
private constructor(parcel: Parcel) : this(
parcel.readInt(),
name = parcel.readString() ?: "",
namespace = parcel.readString() ?: "",
default = parcel.readString() ?: ""
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(0)
parcel.writeString(name)
parcel.writeString(namespace)
parcel.writeString(default)
}
}
data class ResourceStringFlag constructor(
override val name: String,
override val namespace: String,
@StringRes override val resourceId: Int,
override val teamfood: Boolean = false
) : ResourceFlag<String>
data class IntFlag constructor(
override val name: String,
override val namespace: String,
override val default: Int = 0,
override val teamfood: Boolean = false,
override val overridden: Boolean = false
) : ParcelableFlag<Int> {
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<IntFlag> {
override fun createFromParcel(parcel: Parcel) = IntFlag(parcel)
override fun newArray(size: Int) = arrayOfNulls<IntFlag>(size)
}
}
private constructor(id: Int, name: String, namespace: String, default: Int) : this(
name,
namespace,
default
)
private constructor(parcel: Parcel) : this(
parcel.readInt(),
name = parcel.readString() ?: "",
namespace = parcel.readString() ?: "",
default = parcel.readInt()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(0)
parcel.writeString(name)
parcel.writeString(namespace)
parcel.writeInt(default)
}
}
data class ResourceIntFlag constructor(
override val name: String,
override val namespace: String,
@IntegerRes override val resourceId: Int,
override val teamfood: Boolean = false
) : ResourceFlag<Int>
data class LongFlag constructor(
override val name: String,
override val namespace: String,
override val default: Long = 0,
override val teamfood: Boolean = false,
override val overridden: Boolean = false
) : ParcelableFlag<Long> {
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<LongFlag> {
override fun createFromParcel(parcel: Parcel) = LongFlag(parcel)
override fun newArray(size: Int) = arrayOfNulls<LongFlag>(size)
}
}
private constructor(id: Int, name: String, namespace: String, default: Long) : this(
name,
namespace,
default
)
private constructor(parcel: Parcel) : this(
parcel.readInt(),
name = parcel.readString() ?: "",
namespace = parcel.readString() ?: "",
default = parcel.readLong()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(0)
parcel.writeString(name)
parcel.writeString(namespace)
parcel.writeLong(default)
}
}
data class FloatFlag constructor(
override val name: String,
override val namespace: String,
override val default: Float = 0f,
override val teamfood: Boolean = false,
override val overridden: Boolean = false
) : ParcelableFlag<Float> {
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<FloatFlag> {
override fun createFromParcel(parcel: Parcel) = FloatFlag(parcel)
override fun newArray(size: Int) = arrayOfNulls<FloatFlag>(size)
}
}
private constructor(id: Int, name: String, namespace: String, default: Float) : this(
name,
namespace,
default
)
private constructor(parcel: Parcel) : this(
parcel.readInt(),
name = parcel.readString() ?: "",
namespace = parcel.readString() ?: "",
default = parcel.readFloat()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(0)
parcel.writeString(name)
parcel.writeString(namespace)
parcel.writeFloat(default)
}
}
data class ResourceFloatFlag constructor(
override val name: String,
override val namespace: String,
override val resourceId: Int,
override val teamfood: Boolean = false,
) : ResourceFlag<Int>
data class DoubleFlag constructor(
override val name: String,
override val namespace: String,
override val default: Double = 0.0,
override val teamfood: Boolean = false,
override val overridden: Boolean = false
) : ParcelableFlag<Double> {
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<DoubleFlag> {
override fun createFromParcel(parcel: Parcel) = DoubleFlag(parcel)
override fun newArray(size: Int) = arrayOfNulls<DoubleFlag>(size)
}
}
private constructor(id: Int, name: String, namespace: String, default: Double) : this(
name,
namespace,
default
)
private constructor(parcel: Parcel) : this(
parcel.readInt(),
name = parcel.readString() ?: "",
namespace = parcel.readString() ?: "",
default = parcel.readDouble()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(0)
parcel.writeString(name)
parcel.writeString(namespace)
parcel.writeDouble(default)
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (C) 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 com.android.systemui.flags
/**
* Plugin for loading flag values
*/
interface FlagListenable {
/** Add a listener to be alerted when the given flag changes. */
fun addListener(flag: Flag<*>, listener: Listener)
/** Remove a listener to be alerted when any flag changes. */
fun removeListener(listener: Listener)
/** A simple listener to be alerted when a flag changes. */
fun interface Listener {
/** Called when the flag changes */
fun onFlagChanged(event: FlagEvent)
}
/** An event representing the change */
interface FlagEvent {
/** the id of the flag which changed */
val flagName: String
/** if all listeners alerted invoke this method, the restart will be skipped */
fun requestNoRestart()
}
}
@@ -0,0 +1,207 @@
/*
* Copyright (C) 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 com.android.systemui.flags
import android.app.Activity
import android.content.pm.PackageManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.database.ContentObserver
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import androidx.concurrent.futures.CallbackToFutureAdapter
import com.google.common.util.concurrent.ListenableFuture
import java.util.function.Consumer
class FlagManager constructor(
private val context: Context,
private val settings: FlagSettingsHelper,
private val handler: Handler
) : FlagListenable {
companion object {
const val RECEIVING_PACKAGE = "com.android.systemui"
const val RECEIVING_PACKAGE_WATCH = "com.google.android.apps.wearable.systemui"
const val ACTION_SET_FLAG = "com.android.systemui.action.SET_FLAG"
const val ACTION_GET_FLAGS = "com.android.systemui.action.GET_FLAGS"
const val FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS"
const val ACTION_SYSUI_STARTED = "com.android.systemui.STARTED"
const val EXTRA_NAME = "name"
const val EXTRA_VALUE = "value"
const val EXTRA_FLAGS = "flags"
private const val SETTINGS_PREFIX = "systemui/flags"
}
constructor(context: Context, handler: Handler) : this(
context,
FlagSettingsHelper(context.contentResolver),
handler
)
/**
* An action called on restart which takes as an argument whether the listeners requested
* that the restart be suppressed
*/
var onSettingsChangedAction: Consumer<Boolean>? = null
var clearCacheAction: Consumer<String>? = null
private val listeners: MutableSet<PerFlagListener> = mutableSetOf()
private val settingsObserver: ContentObserver = SettingsObserver()
fun getFlagsFuture(): ListenableFuture<Collection<Flag<*>>> {
val intent = Intent(ACTION_GET_FLAGS)
intent.setPackage(if (isWatch()) RECEIVING_PACKAGE_WATCH else RECEIVING_PACKAGE)
return CallbackToFutureAdapter.getFuture {
completer: CallbackToFutureAdapter.Completer<Collection<Flag<*>>> ->
context.sendOrderedBroadcast(
intent,
null,
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val extras: Bundle? = getResultExtras(false)
val listOfFlags: java.util.ArrayList<ParcelableFlag<*>>? =
extras?.getParcelableArrayList(
EXTRA_FLAGS, ParcelableFlag::class.java
)
if (listOfFlags != null) {
completer.set(listOfFlags)
} else {
completer.setException(NoFlagResultsException())
}
}
},
null,
Activity.RESULT_OK,
"extra data",
null
)
"QueryingFlags"
}
}
/**
* Returns the stored value or null if not set.
* This API is used by TheFlippinApp.
*/
fun isEnabled(name: String): Boolean? = readFlagValue(name, BooleanFlagSerializer)
/**
* Sets the value of a boolean flag.
* This API is used by TheFlippinApp.
*/
fun setFlagValue(name: String, enabled: Boolean) {
val intent = createIntent(name)
intent.putExtra(EXTRA_VALUE, enabled)
context.sendBroadcast(intent)
}
fun eraseFlag(name: String) {
val intent = createIntent(name)
context.sendBroadcast(intent)
}
/** Returns the stored value or null if not set. */
fun <T> readFlagValue(name: String, serializer: FlagSerializer<T>): T? {
val data = settings.getString(nameToSettingsKey(name))
return serializer.fromSettingsData(data)
}
override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {
synchronized(listeners) {
val registerNeeded = listeners.isEmpty()
listeners.add(PerFlagListener(flag.name, listener))
if (registerNeeded) {
settings.registerContentObserver(SETTINGS_PREFIX, true, settingsObserver)
}
}
}
override fun removeListener(listener: FlagListenable.Listener) {
synchronized(listeners) {
if (listeners.isEmpty()) {
return
}
listeners.removeIf { it.listener == listener }
if (listeners.isEmpty()) {
settings.unregisterContentObserver(settingsObserver)
}
}
}
private fun createIntent(name: String): Intent {
val intent = Intent(ACTION_SET_FLAG)
intent.setPackage(RECEIVING_PACKAGE)
intent.putExtra(EXTRA_NAME, name)
return intent
}
fun nameToSettingsKey(name: String): String {
return "$SETTINGS_PREFIX/$name"
}
inner class SettingsObserver : ContentObserver(handler) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
if (uri == null) {
return
}
val parts = uri.pathSegments
val name = parts[parts.size - 1]
clearCacheAction?.accept(name)
dispatchListenersAndMaybeRestart(name, onSettingsChangedAction)
}
}
fun dispatchListenersAndMaybeRestart(name: String, restartAction: Consumer<Boolean>?) {
val filteredListeners: List<FlagListenable.Listener> = synchronized(listeners) {
listeners.mapNotNull { if (it.name == name) it.listener else null }
}
// If there are no listeners, there's nothing to dispatch to, and nothing to suppress it.
if (filteredListeners.isEmpty()) {
restartAction?.accept(false)
return
}
// Dispatch to every listener and save whether each one called requestNoRestart.
val suppressRestartList: List<Boolean> = filteredListeners.map { listener ->
var didRequestNoRestart = false
val event = object : FlagListenable.FlagEvent {
override val flagName = name
override fun requestNoRestart() {
didRequestNoRestart = true
}
}
listener.onFlagChanged(event)
didRequestNoRestart
}
// Suppress restart only if ALL listeners request it.
val suppressRestart = suppressRestartList.all { it }
restartAction?.accept(suppressRestart)
}
private fun isWatch(): Boolean {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
}
private data class PerFlagListener(val name: String, val listener: FlagListenable.Listener)
}
class NoFlagResultsException : Exception(
"SystemUI failed to communicate its flags back successfully"
)
@@ -0,0 +1,87 @@
/*
* Copyright (C) 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 com.android.systemui.flags
import android.util.Log
import org.json.JSONException
import org.json.JSONObject
private const val FIELD_VALUE = "value"
private const val FIELD_TYPE = "type"
private const val TYPE_BOOLEAN = "boolean"
private const val TYPE_STRING = "string"
private const val TYPE_INT = "int"
private const val TAG = "FlagSerializer"
abstract class FlagSerializer<T>(
private val type: String,
private val setter: (JSONObject, String, T) -> Unit,
private val getter: (JSONObject, String) -> T
) {
fun toSettingsData(value: T): String? {
return try {
JSONObject()
.put(FIELD_TYPE, type)
.also { setter(it, FIELD_VALUE, value) }
.toString()
} catch (e: JSONException) {
Log.w(TAG, "write error", e)
null
}
}
/**
* @throws InvalidFlagStorageException
*/
fun fromSettingsData(data: String?): T? {
if (data == null || data.isEmpty()) {
return null
}
try {
val json = JSONObject(data)
return if (json.getString(FIELD_TYPE) == type) {
getter(json, FIELD_VALUE)
} else {
null
}
} catch (e: JSONException) {
Log.w(TAG, "read error", e)
throw InvalidFlagStorageException()
}
}
}
object BooleanFlagSerializer : FlagSerializer<Boolean>(
TYPE_BOOLEAN,
JSONObject::put,
JSONObject::getBoolean
)
object StringFlagSerializer : FlagSerializer<String>(
TYPE_STRING,
JSONObject::put,
JSONObject::getString
)
object IntFlagSerializer : FlagSerializer<Int>(
TYPE_INT,
JSONObject::put,
JSONObject::getInt
)
class InvalidFlagStorageException : Exception("Data found but is invalid")
@@ -0,0 +1,44 @@
/*
* Copyright (C) 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 com.android.systemui.flags
import android.content.ContentResolver
import android.database.ContentObserver
import android.provider.Settings
class FlagSettingsHelper(private val contentResolver: ContentResolver) {
fun getStringFromSecure(key: String): String? = Settings.Secure.getString(contentResolver, key)
fun getString(key: String): String? = Settings.Global.getString(contentResolver, key)
fun registerContentObserver(
name: String,
notifyForDescendants: Boolean,
observer: ContentObserver
) {
contentResolver.registerContentObserver(
Settings.Secure.getUriFor(name),
notifyForDescendants,
observer
)
}
fun unregisterContentObserver(observer: ContentObserver) {
contentResolver.unregisterContentObserver(observer)
}
}
@@ -0,0 +1,120 @@
/*
* Copyright (C) 2022 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 com.android.systemui.shared.animation
import android.view.View
import android.view.View.LAYOUT_DIRECTION_RTL
import android.view.ViewGroup
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import java.lang.ref.WeakReference
/**
* Translates items away/towards the hinge when the device is opened/closed, according to the
* direction specified in [ViewIdToTranslate.direction], for a maximum of [translationMax] when
* progresses are 0.
*/
class UnfoldConstantTranslateAnimator(
private val viewsIdToTranslate: Set<ViewIdToTranslate>,
private val progressProvider: UnfoldTransitionProgressProvider
) : TransitionProgressListener {
private var viewsToTranslate = listOf<ViewToTranslate>()
private lateinit var rootView: ViewGroup
private var translationMax = 0f
/**
* Initializes the animator, it is allowed to call this method multiple times, for example
* to update the rootView or maximum translation
*/
fun init(rootView: ViewGroup, translationMax: Float) {
if (!::rootView.isInitialized) {
progressProvider.addCallback(this)
}
this.rootView = rootView
this.translationMax = translationMax
}
override fun onTransitionStarted() {
registerViewsForAnimation(rootView, viewsIdToTranslate)
}
override fun onTransitionProgress(progress: Float) {
translateViews(progress)
}
override fun onTransitionFinished() {
translateViews(progress = 1f)
}
private fun translateViews(progress: Float) {
// progress == 0 -> -translationMax
// progress == 1 -> 0
val xTrans = (progress - 1f) * translationMax
val rtlMultiplier =
if (rootView.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
-1
} else {
1
}
viewsToTranslate.forEach { (view, direction, func) ->
view.get()?.let { func(it, xTrans * direction.multiplier * rtlMultiplier) }
}
}
/** Finds in [parent] all views specified by [ids] and register them for the animation. */
private fun registerViewsForAnimation(parent: ViewGroup, ids: Set<ViewIdToTranslate>) {
viewsToTranslate =
ids.asSequence()
.filter { it.shouldBeAnimated() }
.mapNotNull {
parent.findViewById<View>(it.viewId)?.let { view ->
ViewToTranslate(WeakReference(view), it.direction, it.translateFunc)
}
}
.toList()
}
/**
* Represents a view to animate. [rootView] should contain a view with [viewId] inside.
* [shouldBeAnimated] is only evaluated when the viewsToTranslate is registered in
* [registerViewsForAnimation].
*/
data class ViewIdToTranslate(
val viewId: Int,
val direction: Direction,
val shouldBeAnimated: () -> Boolean = { true },
val translateFunc: (View, Float) -> Unit = { view, value -> view.translationX = value },
)
/**
* Represents a view whose animation process is in-progress. It should be immutable because the
* started animation should be completed.
*/
private data class ViewToTranslate(
val view: WeakReference<View>,
val direction: Direction,
val translateFunc: (View, Float) -> Unit,
)
/** Direction of the animation. */
enum class Direction(val multiplier: Float) {
START(-1f),
END(1f),
}
}
@@ -0,0 +1,195 @@
/*
* Copyright (C) 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 com.android.systemui.shared.animation
import android.graphics.Point
import android.view.Surface
import android.view.Surface.Rotation
import android.view.View
import android.view.WindowManager
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import java.lang.ref.WeakReference
/**
* Creates an animation where all registered views are moved into their final location
* by moving from the center of the screen to the sides
*/
class UnfoldMoveFromCenterAnimator @JvmOverloads constructor(
private val windowManager: WindowManager,
/**
* Allows to set custom translation applier
* Could be useful when a view could be translated from
* several sources and we want to set the translation
* using custom methods instead of [View.setTranslationX] or
* [View.setTranslationY]
*/
private val translationApplier: TranslationApplier = object : TranslationApplier {},
/**
* Allows to set custom implementation for getting
* view location. Could be useful if logical view bounds
* are different than actual bounds (e.g. view container may
* have larger width than width of the items in the container)
*/
private val viewCenterProvider: ViewCenterProvider = object : ViewCenterProvider {},
/** Allows to set the alpha based on the progress. */
private val alphaProvider: AlphaProvider? = null
) : UnfoldTransitionProgressProvider.TransitionProgressListener {
private val screenSize = Point()
private var isVerticalFold = false
private val animatedViews: MutableList<AnimatedView> = arrayListOf()
private var lastAnimationProgress: Float = 1f
/**
* Updates display properties in order to calculate the initial position for the views
* Must be called before [registerViewForAnimation]
*/
@JvmOverloads
fun updateDisplayProperties(@Rotation rotation: Int = windowManager.defaultDisplay.rotation) {
windowManager.defaultDisplay.getSize(screenSize)
// Simple implementation to get current fold orientation,
// this might not be correct on all devices
// TODO: use JetPack WindowManager library to get the fold orientation
isVerticalFold = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
}
/**
* If target view positions have changed (e.g. because of layout changes) call this method
* to re-query view positions and update the translations
*/
fun updateViewPositions() {
animatedViews.forEach { animatedView ->
animatedView.view.get()?.let {
animatedView.updateAnimatedView(it)
}
}
onTransitionProgress(lastAnimationProgress)
}
/**
* Registers a view to be animated, the view should be measured and layouted
* After finishing the animation it is necessary to clear
* the views using [clearRegisteredViews]
*/
fun registerViewForAnimation(view: View) {
val animatedView = createAnimatedView(view)
animatedViews.add(animatedView)
}
/**
* Unregisters all registered views and resets their translation
*/
fun clearRegisteredViews() {
onTransitionProgress(1f)
animatedViews.clear()
}
override fun onTransitionProgress(progress: Float) {
animatedViews.forEach {
it.applyTransition(progress)
it.applyAlpha(progress)
}
lastAnimationProgress = progress
}
private fun AnimatedView.applyTransition(progress: Float) {
view.get()?.let { view ->
translationApplier.apply(
view = view,
x = startTranslationX * (1 - progress),
y = startTranslationY * (1 - progress)
)
}
}
private fun AnimatedView.applyAlpha(progress: Float) {
if (alphaProvider == null) return
view.get()?.alpha = alphaProvider.getAlpha(progress)
}
private fun createAnimatedView(view: View): AnimatedView =
AnimatedView(view = WeakReference(view)).updateAnimatedView(view)
private fun AnimatedView.updateAnimatedView(view: View): AnimatedView {
val viewCenter = Point()
viewCenterProvider.getViewCenter(view, viewCenter)
val viewCenterX = viewCenter.x
val viewCenterY = viewCenter.y
if (isVerticalFold) {
val distanceFromScreenCenterToViewCenter = screenSize.x / 2 - viewCenterX
startTranslationX = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE
startTranslationY = 0f
} else {
val distanceFromScreenCenterToViewCenter = screenSize.y / 2 - viewCenterY
startTranslationX = 0f
startTranslationY = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE
}
return this
}
/**
* Interface that allows to use custom logic to apply translation to view
*/
interface TranslationApplier {
/**
* Called when we need to apply [x] and [y] translation to [view]
*/
fun apply(view: View, x: Float, y: Float) {
view.translationX = x
view.translationY = y
}
}
/** Allows to set a custom alpha based on the progress. */
interface AlphaProvider {
/** Returns the alpha views should have at a given progress. */
fun getAlpha(progress: Float): Float
}
/**
* Interface that allows to use custom logic to get the center of the view
*/
interface ViewCenterProvider {
/**
* Called when we need to get the center of the view
*/
fun getViewCenter(view: View, outPoint: Point) {
val viewLocation = IntArray(2)
view.getLocationOnScreen(viewLocation)
val viewX = viewLocation[0]
val viewY = viewLocation[1]
outPoint.x = viewX + view.width / 2
outPoint.y = viewY + view.height / 2
}
}
private class AnimatedView(
val view: WeakReference<View>,
var startTranslationX: Float = 0f,
var startTranslationY: Float = 0f
)
}
private const val TRANSLATION_PERCENTAGE = 0.08f
@@ -0,0 +1,194 @@
/*
* Copyright (C) 2022 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 com.android.systemui.shared.condition
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
/**
* A higher order [Condition] which combines multiple conditions with a specified
* [Evaluator.ConditionOperand]. Conditions are executed lazily as-needed.
*
* @param scope The [CoroutineScope] to execute in.
* @param conditions The list of conditions to evaluate. Since conditions are executed lazily, the
* ordering is important here.
* @param operand The [Evaluator.ConditionOperand] to apply to the conditions.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class CombinedCondition
constructor(
private val scope: CoroutineScope,
private val conditions: Collection<Condition>,
@Evaluator.ConditionOperand private val operand: Int
) : Condition(scope, null, false) {
private var job: Job? = null
private val _startStrategy by lazy { calculateStartStrategy() }
override fun start() {
job =
scope.launch {
val groupedConditions = conditions.groupBy { it.isOverridingCondition }
lazilyEvaluate(
conditions = groupedConditions.getOrDefault(true, emptyList()),
filterUnknown = true
)
.distinctUntilChanged()
.flatMapLatest { overriddenValue ->
// If there are overriding conditions with values set, they take precedence.
if (overriddenValue == null) {
lazilyEvaluate(
conditions = groupedConditions.getOrDefault(false, emptyList()),
filterUnknown = false
)
} else {
flowOf(overriddenValue)
}
}
.collect { conditionMet ->
if (conditionMet == null) {
clearCondition()
} else {
updateCondition(conditionMet)
}
}
}
}
override fun stop() {
job?.cancel()
job = null
}
/**
* Evaluates a list of conditions lazily with support for short-circuiting. Conditions are
* executed serially in the order provided. At any point if the result can be determined, we
* short-circuit and return the result without executing all conditions.
*/
private fun lazilyEvaluate(
conditions: Collection<Condition>,
filterUnknown: Boolean,
): Flow<Boolean?> = callbackFlow {
val jobs = MutableList<Job?>(conditions.size) { null }
val values = MutableList<Boolean?>(conditions.size) { null }
val flows = conditions.map { it.toFlow() }
fun cancelAllExcept(indexToSkip: Int) {
for (index in 0 until jobs.size) {
if (index == indexToSkip) {
continue
}
if (
indexToSkip == -1 ||
conditions.elementAt(index).startStrategy == START_WHEN_NEEDED
) {
jobs[index]?.cancel()
jobs[index] = null
values[index] = null
}
}
}
fun collectFlow(index: Int) {
// Base case which is triggered once we have collected all the flows. In this case,
// we never short-circuited and therefore should return the fully evaluated
// conditions.
if (flows.isEmpty() || index == -1) {
val filteredValues =
if (filterUnknown) {
values.filterNotNull()
} else {
values
}
trySend(Evaluator.evaluate(filteredValues, operand))
return
}
jobs[index] =
scope.launch {
flows.elementAt(index).collect { value ->
values[index] = value
if (shouldEarlyReturn(value)) {
trySend(value)
// The overall result is contingent on this condition, so we don't need
// to monitor any other conditions.
cancelAllExcept(index)
} else {
collectFlow(jobs.indexOfFirst { it == null })
}
}
}
}
// Collect any eager conditions immediately.
var started = false
for ((index, condition) in conditions.withIndex()) {
if (condition.startStrategy == START_EAGERLY) {
collectFlow(index)
started = true
}
}
// If no eager conditions started, start the first condition to kick off evaluation.
if (!started) {
collectFlow(0)
}
awaitClose { cancelAllExcept(-1) }
}
private fun shouldEarlyReturn(conditionMet: Boolean?): Boolean {
return when (operand) {
Evaluator.OP_AND -> conditionMet == false
Evaluator.OP_OR -> conditionMet == true
else -> false
}
}
/**
* Calculate the start strategy for this condition. This depends on the strategies of the child
* conditions. If there are any eager conditions, we must also start this condition eagerly. In
* the absence of eager conditions, we check for lazy conditions. In the absence of either, we
* make the condition only start when needed.
*/
private fun calculateStartStrategy(): Int {
var startStrategy = START_WHEN_NEEDED
for (condition in conditions) {
when (condition.startStrategy) {
START_EAGERLY -> return START_EAGERLY
START_LAZILY -> {
startStrategy = START_LAZILY
}
START_WHEN_NEEDED -> {
// this is the default, so do nothing
}
}
}
return startStrategy
}
override fun getStartStrategy(): Int {
return _startStrategy
}
}
@@ -0,0 +1,306 @@
/*
* Copyright (C) 2022 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 com.android.systemui.shared.condition;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import kotlinx.coroutines.CoroutineScope;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
/**
* Base class for a condition that needs to be fulfilled in order for {@link Monitor} to inform
* its callbacks.
*/
public abstract class Condition {
private final String mTag = getClass().getSimpleName();
private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
private final boolean mOverriding;
private final CoroutineScope mScope;
private Boolean mIsConditionMet;
private boolean mStarted = false;
/**
* By default, conditions have an initial value of false and are not overriding.
*/
public Condition(CoroutineScope scope) {
this(scope, false, false);
}
/**
* Constructor for specifying initial state and overriding condition attribute.
*
* @param initialConditionMet Initial state of the condition.
* @param overriding Whether this condition overrides others.
*/
protected Condition(CoroutineScope scope, Boolean initialConditionMet, boolean overriding) {
mIsConditionMet = initialConditionMet;
mOverriding = overriding;
mScope = scope;
}
/**
* Starts monitoring the condition.
*/
protected abstract void start();
/**
* Stops monitoring the condition.
*/
protected abstract void stop();
/**
* Condition should be started as soon as there is an active subscription.
*/
public static final int START_EAGERLY = 0;
/**
* Condition should be started lazily only if needed. But once started, it will not be cancelled
* unless there are no more active subscriptions.
*/
public static final int START_LAZILY = 1;
/**
* Condition should be started lazily only if needed, and can be stopped when not needed. This
* should be used for conditions which are expensive to keep running.
*/
public static final int START_WHEN_NEEDED = 2;
@Retention(RetentionPolicy.SOURCE)
@IntDef({START_EAGERLY, START_LAZILY, START_WHEN_NEEDED})
@interface StartStrategy {
}
@StartStrategy
protected abstract int getStartStrategy();
/**
* Returns whether the current condition overrides
*/
public boolean isOverridingCondition() {
return mOverriding;
}
/**
* Registers a callback to receive updates once started. This should be called before
* {@link #start()}. Also triggers the callback immediately if already started.
*/
public void addCallback(@NonNull Callback callback) {
if (shouldLog()) Log.d(mTag, "adding callback");
mCallbacks.add(new WeakReference<>(callback));
if (mStarted) {
callback.onConditionChanged(this);
return;
}
start();
mStarted = true;
}
/**
* Removes the provided callback from further receiving updates.
*/
public void removeCallback(@NonNull Callback callback) {
if (shouldLog()) Log.d(mTag, "removing callback");
final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator();
while (iterator.hasNext()) {
final Callback cb = iterator.next().get();
if (cb == null || cb == callback) {
iterator.remove();
}
}
if (!mCallbacks.isEmpty() || !mStarted) {
return;
}
stop();
mStarted = false;
}
/**
* Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state
* and {@link #removeCallback(Callback)} when not resumed automatically.
*/
public Callback observe(LifecycleOwner owner, Callback listener) {
return observe(owner.getLifecycle(), listener);
}
/**
* Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state
* and {@link #removeCallback(Condition.Callback)} when not resumed automatically.
*/
public Callback observe(Lifecycle lifecycle, Callback listener) {
lifecycle.addObserver((LifecycleEventObserver) (lifecycleOwner, event) -> {
if (event == Lifecycle.Event.ON_RESUME) {
addCallback(listener);
} else if (event == Lifecycle.Event.ON_PAUSE) {
removeCallback(listener);
}
});
return listener;
}
/**
* Updates the value for whether the condition has been fulfilled, and sends an update if the
* value changes and any callback is registered.
*
* @param isConditionMet True if the condition has been fulfilled. False otherwise.
*/
protected void updateCondition(boolean isConditionMet) {
if (mIsConditionMet != null && mIsConditionMet == isConditionMet) {
return;
}
if (shouldLog()) Log.d(mTag, "updating condition to " + isConditionMet);
mIsConditionMet = isConditionMet;
sendUpdate();
}
/**
* Clears the set condition value. This is purposefully separate from
* {@link #updateCondition(boolean)} to avoid confusion around {@code null} values.
*/
protected void clearCondition() {
if (mIsConditionMet == null) {
return;
}
if (shouldLog()) Log.d(mTag, "clearing condition");
mIsConditionMet = null;
sendUpdate();
}
private void sendUpdate() {
final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator();
while (iterator.hasNext()) {
final Callback cb = iterator.next().get();
if (cb == null) {
iterator.remove();
} else {
cb.onConditionChanged(this);
}
}
}
/**
* Returns whether the condition is set. This method should be consulted to understand the
* value of {@link #isConditionMet()}.
*
* @return {@code true} if value is present, {@code false} otherwise.
*/
public boolean isConditionSet() {
return mIsConditionMet != null;
}
/**
* Returns whether the condition has been met. Note that this method will return {@code false}
* if the condition is not set as well.
*/
public boolean isConditionMet() {
return Boolean.TRUE.equals(mIsConditionMet);
}
protected final boolean shouldLog() {
return Log.isLoggable(mTag, Log.DEBUG);
}
protected final String getTag() {
if (isOverridingCondition()) {
return mTag + "[OVRD]";
}
return mTag;
}
/**
* Returns the state of the condition.
* - "Invalid", condition hasn't been set / not monitored
* - "True", condition has been met
* - "False", condition has not been met
*/
protected final String getState() {
if (!isConditionSet()) {
return "Invalid";
}
return isConditionMet() ? "True" : "False";
}
/**
* Creates a new condition which will only be true when both this condition and all the provided
* conditions are true.
*/
public Condition and(@NonNull Collection<Condition> others) {
final List<Condition> conditions = new ArrayList<>();
conditions.add(this);
conditions.addAll(others);
return new CombinedCondition(mScope, conditions, Evaluator.OP_AND);
}
/**
* Creates a new condition which will only be true when both this condition and the provided
* condition is true.
*/
public Condition and(@NonNull Condition... others) {
return and(Arrays.asList(others));
}
/**
* Creates a new condition which will only be true when either this condition or any of the
* provided conditions are true.
*/
public Condition or(@NonNull Collection<Condition> others) {
final List<Condition> conditions = new ArrayList<>();
conditions.add(this);
conditions.addAll(others);
return new CombinedCondition(mScope, conditions, Evaluator.OP_OR);
}
/**
* Creates a new condition which will only be true when either this condition or the provided
* condition is true.
*/
public Condition or(@NonNull Condition... others) {
return or(Arrays.asList(others));
}
/**
* Callback that receives updates about whether the condition has been fulfilled.
*/
public interface Callback {
/**
* Called when the fulfillment of the condition changes.
*
* @param condition The condition in question.
*/
void onConditionChanged(Condition condition);
}
}
@@ -0,0 +1,51 @@
package com.android.systemui.shared.condition
import com.android.systemui.shared.condition.Condition.StartStrategy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
/** Converts a boolean flow to a [Condition] object which can be used with a [Monitor] */
@JvmOverloads
fun Flow<Boolean>.toCondition(
scope: CoroutineScope,
@StartStrategy strategy: Int,
initialValue: Boolean? = null
): Condition {
return object : Condition(scope, initialValue, false) {
var job: Job? = null
override fun start() {
job = scope.launch { collect { updateCondition(it) } }
}
override fun stop() {
job?.cancel()
job = null
}
override fun getStartStrategy() = strategy
}
}
/** Converts a [Condition] to a boolean flow */
fun Condition.toFlow(): Flow<Boolean?> {
return callbackFlow {
val callback =
Condition.Callback { condition ->
if (condition.isConditionSet) {
trySend(condition.isConditionMet)
} else {
trySend(null)
}
}
addCallback(callback)
callback.onConditionChanged(this@toFlow)
awaitClose { removeCallback(callback) }
}
.distinctUntilChanged()
}
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2022 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 com.android.systemui.shared.condition
import android.annotation.IntDef
/**
* Helper for evaluating a collection of [Condition] objects with a given
* [Evaluator.ConditionOperand]
*/
object Evaluator {
/** Operands for combining multiple conditions together */
@Retention(AnnotationRetention.SOURCE)
@IntDef(value = [OP_AND, OP_OR])
annotation class ConditionOperand
/**
* 3-valued logical AND operand, with handling for unknown values (represented as null)
*
* ```
* +-----+----+---+---+
* | AND | T | F | U |
* +-----+----+---+---+
* | T | T | F | U |
* | F | F | F | F |
* | U | U | F | U |
* +-----+----+---+---+
* ```
*/
const val OP_AND = 0
/**
* 3-valued logical OR operand, with handling for unknown values (represented as null)
*
* ```
* +-----+----+---+---+
* | OR | T | F | U |
* +-----+----+---+---+
* | T | T | T | T |
* | F | T | F | U |
* | U | T | U | U |
* +-----+----+---+---+
* ```
*/
const val OP_OR = 1
/**
* Evaluates a set of conditions with a given operand
*
* If overriding conditions are present, they take precedence over normal conditions if set.
*
* @param conditions The collection of conditions to evaluate. If empty, null is returned.
* @param operand The operand to use when evaluating.
* @return Either true or false if the value is known, or null if value is unknown
*/
fun evaluate(conditions: Collection<Condition>, @ConditionOperand operand: Int): Boolean? {
if (conditions.isEmpty()) return null
// If there are overriding conditions with values set, they take precedence.
val values: Collection<Boolean?> =
conditions
.filter { it.isConditionSet && it.isOverridingCondition }
.ifEmpty { conditions }
.map { condition ->
if (condition.isConditionSet) {
condition.isConditionMet
} else {
null
}
}
return evaluate(values = values, operand = operand)
}
/**
* Evaluates a set of booleans with a given operand
*
* @param operand The operand to use when evaluating.
* @return Either true or false if the value is known, or null if value is unknown
*/
internal fun evaluate(values: Collection<Boolean?>, @ConditionOperand operand: Int): Boolean? {
if (values.isEmpty()) return null
return when (operand) {
OP_AND -> threeValuedAndOrOr(values = values, returnValueIfAnyMatches = false)
OP_OR -> threeValuedAndOrOr(values = values, returnValueIfAnyMatches = true)
else -> null
}
}
/**
* Helper for evaluating 3-valued logical AND/OR.
*
* @param returnValueIfAnyMatches AND returns false if any value is false. OR returns true if
* any value is true.
*/
private fun threeValuedAndOrOr(
values: Collection<Boolean?>,
returnValueIfAnyMatches: Boolean
): Boolean? {
var hasUnknown = false
for (value in values) {
if (value == null) {
hasUnknown = true
continue
}
if (value == returnValueIfAnyMatches) {
return returnValueIfAnyMatches
}
}
return if (hasUnknown) null else !returnValueIfAnyMatches
}
}
@@ -0,0 +1,404 @@
/*
* Copyright (C) 2022 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 com.android.systemui.shared.condition;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.NonNull;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.log.TableLogBufferBase;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.Executor;
import javax.inject.Inject;
/**
* {@link Monitor} allows {@link Subscription}s to a set of conditions and monitors whether all of
* them have been fulfilled.
* <p>
* This class should be used as a singleton, to prevent duplicate monitoring of the same conditions.
*/
public class Monitor {
private final String mTag = getClass().getSimpleName();
private final Executor mExecutor;
private final Set<Condition> mPreconditions;
private final TableLogBufferBase mLogBuffer;
private final HashMap<Condition, ArraySet<Subscription.Token>> mConditions = new HashMap<>();
private final HashMap<Subscription.Token, SubscriptionState> mSubscriptions = new HashMap<>();
private static class SubscriptionState {
private final Subscription mSubscription;
// A subscription must maintain a reference to any active nested subscription so that it may
// be later removed when the current subscription becomes invalid.
private Subscription.Token mNestedSubscriptionToken;
private Boolean mAllConditionsMet;
private boolean mActive;
SubscriptionState(Subscription subscription) {
mSubscription = subscription;
}
public Set<Condition> getConditions() {
return mSubscription.mConditions;
}
/**
* Signals that the {@link Subscription} is now being monitored and will receive updates
* based on its conditions.
*/
private void setActive(boolean active) {
if (mActive == active) {
return;
}
mActive = active;
final Callback callback = mSubscription.getCallback();
if (callback == null) {
return;
}
callback.onActiveChanged(active);
}
public void update(Monitor monitor) {
final Boolean result = Evaluator.INSTANCE.evaluate(mSubscription.mConditions,
Evaluator.OP_AND);
// Consider unknown (null) as true
final boolean newAllConditionsMet = result == null || result;
if (mAllConditionsMet != null && newAllConditionsMet == mAllConditionsMet) {
return;
}
mAllConditionsMet = newAllConditionsMet;
final Subscription nestedSubscription = mSubscription.getNestedSubscription();
if (nestedSubscription != null) {
if (mAllConditionsMet && mNestedSubscriptionToken == null) {
// When all conditions are met for a subscription with a nested subscription
// that is not currently being monitored, add the nested subscription for
// monitor.
mNestedSubscriptionToken =
monitor.addSubscription(nestedSubscription, null);
} else if (!mAllConditionsMet && mNestedSubscriptionToken != null) {
// When conditions are not met and there is an active nested condition, remove
// the nested condition from monitoring.
removeNestedSubscription(monitor);
}
return;
}
mSubscription.getCallback().onConditionsChanged(mAllConditionsMet);
}
/**
* Invoked when the {@link Subscription} has been added to the {@link Monitor}.
*/
public void onAdded() {
setActive(true);
}
/**
* Invoked when the {@link Subscription} has been removed from the {@link Monitor},
* allowing cleanup code to run.
*/
public void onRemoved(Monitor monitor) {
setActive(false);
removeNestedSubscription(monitor);
}
private void removeNestedSubscription(Monitor monitor) {
if (mNestedSubscriptionToken == null) {
return;
}
monitor.removeSubscription(mNestedSubscriptionToken);
mNestedSubscriptionToken = null;
}
}
// Callback for when each condition has been updated.
private final Condition.Callback mConditionCallback = new Condition.Callback() {
@Override
public void onConditionChanged(Condition condition) {
mExecutor.execute(() -> updateConditionMetState(condition));
}
};
/**
* Constructor for injected use-cases. By default, no preconditions are present.
*/
@Inject
public Monitor(@Main Executor executor) {
this(executor, Collections.emptySet());
}
/**
* Main constructor, allowing specifying preconditions.
*/
public Monitor(Executor executor, Set<Condition> preconditions) {
this(executor, preconditions, null);
}
/**
* Main constructor, allowing specifying preconditions and a log buffer for logging.
*/
public Monitor(Executor executor, Set<Condition> preconditions, TableLogBufferBase logBuffer) {
mExecutor = executor;
mPreconditions = preconditions;
mLogBuffer = logBuffer;
}
private void updateConditionMetState(Condition condition) {
if (mLogBuffer != null) {
mLogBuffer.logChange(/* prefix= */ "", condition.getTag(), condition.getState());
}
final ArraySet<Subscription.Token> subscriptions = mConditions.get(condition);
// It's possible the condition was removed between the time the callback occurred and
// update was executed on the main thread.
if (subscriptions == null) {
return;
}
subscriptions.stream().forEach(token -> mSubscriptions.get(token).update(this));
}
/**
* Registers a callback and the set of conditions to trigger it.
*
* @param subscription A {@link Subscription} detailing the desired conditions and callback.
* @return A {@link Subscription.Token} that can be used to remove the subscription.
*/
public Subscription.Token addSubscription(@NonNull Subscription subscription) {
return addSubscription(subscription, mPreconditions);
}
private Subscription.Token addSubscription(@NonNull Subscription subscription,
Set<Condition> preconditions) {
// If preconditions are set on the monitor, set up as a nested condition.
final Subscription normalizedCondition = preconditions != null
? new Subscription.Builder(subscription).addConditions(preconditions).build()
: subscription;
final Subscription.Token token = new Subscription.Token();
final SubscriptionState state = new SubscriptionState(normalizedCondition);
mExecutor.execute(() -> {
if (shouldLog()) Log.d(mTag, "adding subscription");
mSubscriptions.put(token, state);
// Add and associate conditions.
normalizedCondition.getConditions().forEach(condition -> {
if (!mConditions.containsKey(condition)) {
mConditions.put(condition, new ArraySet<>());
condition.addCallback(mConditionCallback);
}
mConditions.get(condition).add(token);
});
state.onAdded();
// Update subscription state.
state.update(this);
});
return token;
}
/**
* Removes a subscription from participating in future callbacks.
*
* @param token The {@link Subscription.Token} returned when the {@link Subscription} was
* originally added.
*/
public void removeSubscription(@NonNull Subscription.Token token) {
mExecutor.execute(() -> {
if (shouldLog()) Log.d(mTag, "removing subscription");
if (!mSubscriptions.containsKey(token)) {
Log.e(mTag, "subscription not present:" + token);
return;
}
final SubscriptionState removedSubscription = mSubscriptions.remove(token);
removedSubscription.getConditions().forEach(condition -> {
if (!mConditions.containsKey(condition)) {
Log.e(mTag, "condition not present:" + condition);
return;
}
final Set<Subscription.Token> conditionSubscriptions = mConditions.get(condition);
conditionSubscriptions.remove(token);
if (conditionSubscriptions.isEmpty()) {
condition.removeCallback(mConditionCallback);
mConditions.remove(condition);
}
});
removedSubscription.onRemoved(this);
});
}
private boolean shouldLog() {
return Log.isLoggable(mTag, Log.DEBUG);
}
/**
* A {@link Subscription} represents a set of conditions and a callback that is informed when
* these conditions change.
*/
public static class Subscription {
private final Set<Condition> mConditions;
private final Callback mCallback;
// A nested {@link Subscription} is a special callback where the specified condition's
// active state is dependent on the conditions of the parent {@link Subscription} being met.
// Once active, the nested subscription's conditions are registered as normal with the
// monitor and its callback (which could also be a nested condition) is triggered based on
// those conditions. The nested condition will be removed from monitor if the outer
// subscription's conditions ever become invalid.
private final Subscription mNestedSubscription;
private Subscription(Set<Condition> conditions, Callback callback,
Subscription nestedSubscription) {
this.mConditions = Collections.unmodifiableSet(conditions);
this.mCallback = callback;
this.mNestedSubscription = nestedSubscription;
}
public Set<Condition> getConditions() {
return mConditions;
}
public Callback getCallback() {
return mCallback;
}
public Subscription getNestedSubscription() {
return mNestedSubscription;
}
/**
* A {@link Token} is an identifier that is associated with a {@link Subscription} which is
* registered with a {@link Monitor}.
*/
public static class Token {
}
/**
* {@link Builder} is a helper class for constructing a {@link Subscription}.
*/
public static class Builder {
private final Callback mCallback;
private final Subscription mNestedSubscription;
private final ArraySet<Condition> mConditions;
/**
* Default constructor specifying the {@link Callback} for the {@link Subscription}.
*/
public Builder(Callback callback) {
this(null, callback);
}
public Builder(Subscription nestedSubscription) {
this(nestedSubscription, null);
}
private Builder(Subscription nestedSubscription, Callback callback) {
mNestedSubscription = nestedSubscription;
mCallback = callback;
mConditions = new ArraySet<>();
}
/**
* Adds a {@link Condition} to be associated with the {@link Subscription}.
*
* @return The updated {@link Builder}.
*/
public Builder addCondition(Condition condition) {
mConditions.add(condition);
return this;
}
/**
* Adds a set of {@link Condition} to be associated with the {@link Subscription}.
*
* @return The updated {@link Builder}.
*/
public Builder addConditions(Set<Condition> condition) {
if (condition == null) {
return this;
}
mConditions.addAll(condition);
return this;
}
/**
* Builds the {@link Subscription}.
*
* @return The resulting {@link Subscription}.
*/
public Subscription build() {
return new Subscription(mConditions, mCallback, mNestedSubscription);
}
}
}
/**
* Callback that receives updates of whether all conditions have been fulfilled.
*/
public interface Callback {
/**
* Returns the conditions associated with this callback.
*/
default ArrayList<Condition> getConditions() {
return new ArrayList<>();
}
/**
* Triggered when the fulfillment of all conditions have been met.
*
* @param allConditionsMet True if all conditions have been fulfilled. False if none or
* only partial conditions have been fulfilled.
*/
void onConditionsChanged(boolean allConditionsMet);
/**
* Called when the active state of the {@link Subscription} changes.
* @param active {@code true} when changes to the conditions will affect the
* {@link Subscription}, {@code false} otherwise.
*/
default void onActiveChanged(boolean active) {
}
}
}
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2023 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 com.android.systemui.shared.hardware
import android.view.InputDevice
/**
* Returns true if [InputDevice] is electronic components to allow a user to use an active stylus in
* the host device or a passive stylus is detected by the host device.
*/
val InputDevice.isInternalStylusSource: Boolean
get() = isAnyStylusSource && !isExternal
/** Returns true if [InputDevice] is an active stylus. */
val InputDevice.isExternalStylusSource: Boolean
get() = isAnyStylusSource && isExternal
/**
* Returns true if [InputDevice] supports any stylus source.
*
* @see InputDevice.isInternalStylusSource
* @see InputDevice.isExternalStylusSource
*/
val InputDevice.isAnyStylusSource: Boolean
get() = supportsSource(InputDevice.SOURCE_STYLUS)
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2023 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 com.android.systemui.shared.hardware
import android.hardware.input.InputManager
import android.view.InputDevice
/**
* Gets information about all input devices in the system and returns as a lazy [Sequence].
*
* For performance reasons, it is preferred to operate atop the returned [Sequence] to ensure each
* operation is executed on an element-per-element basis yet customizable.
*
* For example:
* ```kotlin
* val stylusDevices = inputManager.getInputDeviceSequence().filter {
* it.supportsSource(InputDevice.SOURCE_STYLUS)
* }
*
* val hasInternalStylus = stylusDevices.any { it.isInternal }
* val hasExternalStylus = stylusDevices.any { !it.isInternal }
* ```
*
* @return a [Sequence] of [InputDevice].
*/
fun InputManager.getInputDeviceSequence(): Sequence<InputDevice> =
inputDeviceIds.asSequence().mapNotNull { getInputDevice(it) }
/**
* Returns the first [InputDevice] matching the given predicate, or null if no such [InputDevice]
* was found.
*/
fun InputManager.findInputDevice(predicate: (InputDevice) -> Boolean): InputDevice? =
getInputDeviceSequence().find { predicate(it) }
/**
* Returns true if [any] [InputDevice] matches with [predicate].
*
* For example:
* ```kotlin
* val hasStylusSupport = inputManager.hasInputDevice { it.isStylusSupport() }
* val hasStylusPen = inputManager.hasInputDevice { it.isStylusPen() }
* ```
*/
fun InputManager.hasInputDevice(predicate: (InputDevice) -> Boolean): Boolean =
getInputDeviceSequence().any { predicate(it) }
/** Returns true if host device has any [InputDevice] where [InputDevice.isInternalStylusSource]. */
fun InputManager.hasInternalStylusSource(): Boolean = hasInputDevice { it.isInternalStylusSource }
/** Returns true if host device has any [InputDevice] where [InputDevice.isExternalStylusSource]. */
fun InputManager.hasExternalStylusSource(): Boolean = hasInputDevice { it.isExternalStylusSource }
/** Returns true if host device has any [InputDevice] where [InputDevice.isAnyStylusSource]. */
fun InputManager.hasAnyStylusSource(): Boolean = hasInputDevice { it.isAnyStylusSource }
@@ -0,0 +1,564 @@
/*
* Copyright (C) 2024 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 com.android.systemui.shared.navigationbar;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.CanvasProperty;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.RecordingCanvas;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.os.Trace;
import android.view.RenderNodeAnimator;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import androidx.annotation.DimenRes;
import androidx.annotation.Keep;
import java.util.ArrayList;
import java.util.HashSet;
public class KeyButtonRipple extends Drawable {
private static final float GLOW_MAX_SCALE_FACTOR = 1.35f;
private static final float GLOW_MAX_ALPHA = 0.2f;
private static final float GLOW_MAX_ALPHA_DARK = 0.1f;
private static final int ANIMATION_DURATION_SCALE = 350;
private static final int ANIMATION_DURATION_FADE = 450;
private static final int ANIMATION_DURATION_FADE_FAST = 80;
private static final Interpolator ALPHA_OUT_INTERPOLATOR =
new PathInterpolator(0f, 0f, 0.8f, 1f);
@DimenRes
private final int mMaxWidthResource;
private Paint mRipplePaint;
private CanvasProperty<Float> mLeftProp;
private CanvasProperty<Float> mTopProp;
private CanvasProperty<Float> mRightProp;
private CanvasProperty<Float> mBottomProp;
private CanvasProperty<Float> mRxProp;
private CanvasProperty<Float> mRyProp;
private CanvasProperty<Paint> mPaintProp;
private float mGlowAlpha = 0f;
private float mGlowScale = 1f;
private boolean mPressed;
private boolean mVisible;
private boolean mDrawingHardwareGlow;
private int mMaxWidth;
private boolean mLastDark;
private boolean mDark;
private boolean mDelayTouchFeedback;
private boolean mSpeedUpNextFade;
// When non-null, this runs the next time this ripple is drawn invisibly.
private Runnable mOnInvisibleRunnable;
private final Interpolator mInterpolator = new LogInterpolator();
private boolean mSupportHardware;
private final View mTargetView;
private final Handler mHandler = new Handler();
private final HashSet<Animator> mRunningAnimations = new HashSet<>();
private final ArrayList<Animator> mTmpArray = new ArrayList<>();
private final TraceAnimatorListener mExitHwTraceAnimator =
new TraceAnimatorListener("exitHardware");
private final TraceAnimatorListener mEnterHwTraceAnimator =
new TraceAnimatorListener("enterHardware");
public enum Type {
OVAL,
ROUNDED_RECT
}
private Type mType = Type.ROUNDED_RECT;
public KeyButtonRipple(Context ctx, View targetView, @DimenRes int maxWidthResource) {
mMaxWidthResource = maxWidthResource;
mMaxWidth = ctx.getResources().getDimensionPixelSize(maxWidthResource);
mTargetView = targetView;
}
public void updateResources() {
mMaxWidth = mTargetView.getContext().getResources()
.getDimensionPixelSize(mMaxWidthResource);
invalidateSelf();
}
public void setDarkIntensity(float darkIntensity) {
mDark = darkIntensity >= 0.5f;
}
public void setDelayTouchFeedback(boolean delay) {
mDelayTouchFeedback = delay;
}
/** Next time we fade out (pressed==false), use a shorter duration than the standard. */
public void speedUpNextFade() {
mSpeedUpNextFade = true;
}
/**
* @param onInvisibleRunnable run after we are next drawn invisibly. Only used once.
*/
public void setOnInvisibleRunnable(Runnable onInvisibleRunnable) {
mOnInvisibleRunnable = onInvisibleRunnable;
}
public void setType(Type type) {
mType = type;
}
private Paint getRipplePaint() {
if (mRipplePaint == null) {
mRipplePaint = new Paint();
mRipplePaint.setAntiAlias(true);
mRipplePaint.setColor(mLastDark ? 0xff000000 : 0xffffffff);
}
return mRipplePaint;
}
private void drawSoftware(Canvas canvas) {
if (mGlowAlpha > 0f) {
final Paint p = getRipplePaint();
p.setAlpha((int)(mGlowAlpha * 255f));
final float w = getBounds().width();
final float h = getBounds().height();
final boolean horizontal = w > h;
final float diameter = getRippleSize() * mGlowScale;
final float radius = diameter * .5f;
final float cx = w * .5f;
final float cy = h * .5f;
final float rx = horizontal ? radius : cx;
final float ry = horizontal ? cy : radius;
final float corner = horizontal ? cy : cx;
if (mType == Type.ROUNDED_RECT) {
canvas.drawRoundRect(cx - rx, cy - ry, cx + rx, cy + ry, corner, corner, p);
} else {
canvas.save();
canvas.translate(cx, cy);
float r = Math.min(rx, ry);
canvas.drawOval(-r, -r, r, r, p);
canvas.restore();
}
}
}
@Override
public void draw(Canvas canvas) {
mSupportHardware = canvas.isHardwareAccelerated();
if (mSupportHardware) {
drawHardware((RecordingCanvas) canvas);
} else {
drawSoftware(canvas);
}
if (!mPressed && !mVisible && mOnInvisibleRunnable != null) {
new Handler(Looper.getMainLooper()).post(mOnInvisibleRunnable);
mOnInvisibleRunnable = null;
}
}
@Override
public void setAlpha(int alpha) {
// Not supported.
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
// Not supported.
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
private boolean isHorizontal() {
return getBounds().width() > getBounds().height();
}
private void drawHardware(RecordingCanvas c) {
if (mDrawingHardwareGlow) {
if (mType == Type.ROUNDED_RECT) {
c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp,
mPaintProp);
} else {
CanvasProperty<Float> cx = CanvasProperty.createFloat(getBounds().width() / 2);
CanvasProperty<Float> cy = CanvasProperty.createFloat(getBounds().height() / 2);
int d = Math.min(getBounds().width(), getBounds().height());
CanvasProperty<Float> r = CanvasProperty.createFloat(1.0f * d / 2);
c.drawCircle(cx, cy, r, mPaintProp);
}
}
}
/** Gets the glow alpha, used by {@link android.animation.ObjectAnimator} via reflection. */
@Keep
public float getGlowAlpha() {
return mGlowAlpha;
}
/** Sets the glow alpha, used by {@link android.animation.ObjectAnimator} via reflection. */
@Keep
public void setGlowAlpha(float x) {
mGlowAlpha = x;
invalidateSelf();
}
/** Gets the glow scale, used by {@link android.animation.ObjectAnimator} via reflection. */
@Keep
public float getGlowScale() {
return mGlowScale;
}
/** Sets the glow scale, used by {@link android.animation.ObjectAnimator} via reflection. */
@Keep
public void setGlowScale(float x) {
mGlowScale = x;
invalidateSelf();
}
private float getMaxGlowAlpha() {
return mLastDark ? GLOW_MAX_ALPHA_DARK : GLOW_MAX_ALPHA;
}
@Override
protected boolean onStateChange(int[] state) {
boolean pressed = false;
for (int i = 0; i < state.length; i++) {
if (state[i] == android.R.attr.state_pressed) {
pressed = true;
break;
}
}
if (pressed != mPressed) {
setPressed(pressed);
mPressed = pressed;
return true;
} else {
return false;
}
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
boolean changed = super.setVisible(visible, restart);
if (changed) {
// End any existing animations when the visibility changes
jumpToCurrentState();
}
return changed;
}
@Override
public void jumpToCurrentState() {
endAnimations("jumpToCurrentState", false /* cancel */);
}
@Override
public boolean isStateful() {
return true;
}
@Override
public boolean hasFocusStateSpecified() {
return true;
}
private void setPressed(boolean pressed) {
if (mDark != mLastDark && pressed) {
mRipplePaint = null;
mLastDark = mDark;
}
if (mSupportHardware) {
setPressedHardware(pressed);
} else {
setPressedSoftware(pressed);
}
}
/**
* Abort the ripple while it is delayed and before shown used only when setShouldDelayStartTouch
* is enabled.
*/
public void abortDelayedRipple() {
mHandler.removeCallbacksAndMessages(null);
}
private void endAnimations(String reason, boolean cancel) {
if (Trace.isEnabled()) {
Trace.instant(Trace.TRACE_TAG_APP,
"KeyButtonRipple.endAnim: reason=" + reason + " cancel=" + cancel);
}
mVisible = false;
mTmpArray.addAll(mRunningAnimations);
int size = mTmpArray.size();
for (int i = 0; i < size; i++) {
Animator a = mTmpArray.get(i);
if (cancel) {
a.cancel();
} else {
a.end();
}
}
mTmpArray.clear();
mRunningAnimations.clear();
mHandler.removeCallbacksAndMessages(null);
}
private void setPressedSoftware(boolean pressed) {
if (pressed) {
if (mDelayTouchFeedback) {
if (mRunningAnimations.isEmpty()) {
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(this::enterSoftware, ViewConfiguration.getTapTimeout());
} else if (mVisible) {
enterSoftware();
}
} else {
enterSoftware();
}
} else {
exitSoftware();
}
}
private void enterSoftware() {
endAnimations("enterSoftware", true /* cancel */);
mVisible = true;
mGlowAlpha = getMaxGlowAlpha();
ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale",
0f, GLOW_MAX_SCALE_FACTOR);
scaleAnimator.setInterpolator(mInterpolator);
scaleAnimator.setDuration(ANIMATION_DURATION_SCALE);
scaleAnimator.addListener(mAnimatorListener);
scaleAnimator.start();
mRunningAnimations.add(scaleAnimator);
// With the delay, it could eventually animate the enter animation with no pressed state,
// then immediately show the exit animation. If this is skipped there will be no ripple.
if (mDelayTouchFeedback && !mPressed) {
exitSoftware();
}
}
private void exitSoftware() {
ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f);
alphaAnimator.setInterpolator(ALPHA_OUT_INTERPOLATOR);
alphaAnimator.setDuration(getFadeDuration());
alphaAnimator.addListener(mAnimatorListener);
alphaAnimator.start();
mRunningAnimations.add(alphaAnimator);
}
private void setPressedHardware(boolean pressed) {
if (pressed) {
if (mDelayTouchFeedback) {
if (mRunningAnimations.isEmpty()) {
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(this::enterHardware, ViewConfiguration.getTapTimeout());
} else if (mVisible) {
enterHardware();
}
} else {
enterHardware();
}
} else {
exitHardware();
}
}
/**
* Sets the left/top property for the round rect to {@code prop} depending on whether we are
* horizontal or vertical mode.
*/
private void setExtendStart(CanvasProperty<Float> prop) {
if (isHorizontal()) {
mLeftProp = prop;
} else {
mTopProp = prop;
}
}
private CanvasProperty<Float> getExtendStart() {
return isHorizontal() ? mLeftProp : mTopProp;
}
/**
* Sets the right/bottom property for the round rect to {@code prop} depending on whether we are
* horizontal or vertical mode.
*/
private void setExtendEnd(CanvasProperty<Float> prop) {
if (isHorizontal()) {
mRightProp = prop;
} else {
mBottomProp = prop;
}
}
private CanvasProperty<Float> getExtendEnd() {
return isHorizontal() ? mRightProp : mBottomProp;
}
private int getExtendSize() {
return isHorizontal() ? getBounds().width() : getBounds().height();
}
private int getRippleSize() {
int size = isHorizontal() ? getBounds().width() : getBounds().height();
return Math.min(size, mMaxWidth);
}
private int getFadeDuration() {
int duration = mSpeedUpNextFade ? ANIMATION_DURATION_FADE_FAST : ANIMATION_DURATION_FADE;
mSpeedUpNextFade = false;
return duration;
}
private void enterHardware() {
endAnimations("enterHardware", true /* cancel */);
mVisible = true;
mDrawingHardwareGlow = true;
setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2));
final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(),
getExtendSize()/2 - GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
startAnim.setDuration(ANIMATION_DURATION_SCALE);
startAnim.setInterpolator(mInterpolator);
startAnim.addListener(mAnimatorListener);
startAnim.setTarget(mTargetView);
setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2));
final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(),
getExtendSize()/2 + GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
endAnim.setDuration(ANIMATION_DURATION_SCALE);
endAnim.setInterpolator(mInterpolator);
endAnim.addListener(mAnimatorListener);
endAnim.addListener(mEnterHwTraceAnimator);
endAnim.setTarget(mTargetView);
if (isHorizontal()) {
mTopProp = CanvasProperty.createFloat(0f);
mBottomProp = CanvasProperty.createFloat(getBounds().height());
mRxProp = CanvasProperty.createFloat(getBounds().height()/2);
mRyProp = CanvasProperty.createFloat(getBounds().height()/2);
} else {
mLeftProp = CanvasProperty.createFloat(0f);
mRightProp = CanvasProperty.createFloat(getBounds().width());
mRxProp = CanvasProperty.createFloat(getBounds().width()/2);
mRyProp = CanvasProperty.createFloat(getBounds().width()/2);
}
mGlowScale = GLOW_MAX_SCALE_FACTOR;
mGlowAlpha = getMaxGlowAlpha();
mRipplePaint = getRipplePaint();
mRipplePaint.setAlpha((int) (mGlowAlpha * 255));
mPaintProp = CanvasProperty.createPaint(mRipplePaint);
startAnim.start();
endAnim.start();
mRunningAnimations.add(startAnim);
mRunningAnimations.add(endAnim);
invalidateSelf();
// With the delay, it could eventually animate the enter animation with no pressed state,
// then immediately show the exit animation. If this is skipped there will be no ripple.
if (mDelayTouchFeedback && !mPressed) {
exitHardware();
}
}
private void exitHardware() {
mPaintProp = CanvasProperty.createPaint(getRipplePaint());
final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp,
RenderNodeAnimator.PAINT_ALPHA, 0);
opacityAnim.setDuration(getFadeDuration());
opacityAnim.setInterpolator(ALPHA_OUT_INTERPOLATOR);
opacityAnim.addListener(mAnimatorListener);
opacityAnim.addListener(mExitHwTraceAnimator);
opacityAnim.setTarget(mTargetView);
opacityAnim.start();
mRunningAnimations.add(opacityAnim);
invalidateSelf();
}
private final AnimatorListenerAdapter mAnimatorListener =
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mRunningAnimations.remove(animation);
if (mRunningAnimations.isEmpty() && !mPressed) {
mVisible = false;
mDrawingHardwareGlow = false;
invalidateSelf();
}
}
};
private static final class TraceAnimatorListener extends AnimatorListenerAdapter {
private final String mName;
TraceAnimatorListener(String name) {
mName = name;
}
@Override
public void onAnimationStart(Animator animation) {
if (Trace.isEnabled()) {
Trace.instant(Trace.TRACE_TAG_APP, "KeyButtonRipple.start." + mName);
}
}
@Override
public void onAnimationCancel(Animator animation) {
if (Trace.isEnabled()) {
Trace.instant(Trace.TRACE_TAG_APP, "KeyButtonRipple.cancel." + mName);
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (Trace.isEnabled()) {
Trace.instant(Trace.TRACE_TAG_APP, "KeyButtonRipple.end." + mName);
}
}
}
/**
* Interpolator with a smooth log deceleration
*/
private static final class LogInterpolator implements Interpolator {
@Override
public float getInterpolation(float input) {
return 1 - (float) Math.pow(400, -input * 1.4);
}
}
}
@@ -0,0 +1,359 @@
/*
* Copyright (C) 2020 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 com.android.systemui.shared.navigationbar;
import static android.view.Display.DEFAULT_DISPLAY;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.view.CompositionSamplingListener;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewRootImpl;
import android.view.ViewTreeObserver;
import androidx.annotation.VisibleForTesting;
import java.io.PrintWriter;
import java.util.concurrent.Executor;
/**
* A helper class to sample regions on the screen and inspect its luminosity.
*/
@TargetApi(Build.VERSION_CODES.Q)
public class RegionSamplingHelper implements View.OnAttachStateChangeListener,
View.OnLayoutChangeListener {
// Luminance threshold to determine black/white contrast for the navigation affordances.
// Passing the threshold of this luminance value will make the button black otherwise white
private static final float NAVIGATION_LUMINANCE_THRESHOLD = 0.5f;
// Luminance change threshold that allows applying new value if difference was exceeded
private static final float NAVIGATION_LUMINANCE_CHANGE_THRESHOLD = 0.05f;
private final Handler mHandler = new Handler();
private final View mSampledView;
private final CompositionSamplingListener mSamplingListener;
/**
* The requested sampling bounds that we want to sample from
*/
private final Rect mSamplingRequestBounds = new Rect();
/**
* The sampling bounds that are currently registered.
*/
private final Rect mRegisteredSamplingBounds = new Rect();
private final SamplingCallback mCallback;
private final Executor mBackgroundExecutor;
private final SysuiCompositionSamplingListener mCompositionSamplingListener;
private boolean mSamplingEnabled = false;
private boolean mSamplingListenerRegistered = false;
private float mLastMedianLuma;
private float mCurrentMedianLuma;
private boolean mWaitingOnDraw;
private boolean mIsDestroyed;
private boolean mFirstSamplingAfterStart;
private boolean mWindowVisible;
private boolean mWindowHasBlurs;
private SurfaceControl mRegisteredStopLayer = null;
// A copy of mRegisteredStopLayer where we own the life cycle and can access from a bg thread.
private SurfaceControl mWrappedStopLayer = null;
private ViewTreeObserver.OnDrawListener mUpdateOnDraw = new ViewTreeObserver.OnDrawListener() {
@Override
public void onDraw() {
// We need to post the remove runnable, since it's not allowed to remove in onDraw
mHandler.post(mRemoveDrawRunnable);
RegionSamplingHelper.this.onDraw();
}
};
private Runnable mRemoveDrawRunnable = new Runnable() {
@Override
public void run() {
mSampledView.getViewTreeObserver().removeOnDrawListener(mUpdateOnDraw);
}
};
/**
* @deprecated Pass a main executor.
*/
public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback,
Executor backgroundExecutor) {
this(sampledView, samplingCallback, sampledView.getContext().getMainExecutor(),
backgroundExecutor);
}
public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback,
Executor mainExecutor, Executor backgroundExecutor) {
this(sampledView, samplingCallback, mainExecutor,
backgroundExecutor, new SysuiCompositionSamplingListener());
}
@VisibleForTesting
RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback,
Executor mainExecutor, Executor backgroundExecutor,
SysuiCompositionSamplingListener compositionSamplingListener) {
mBackgroundExecutor = backgroundExecutor;
mCompositionSamplingListener = compositionSamplingListener;
mSamplingListener = new CompositionSamplingListener(mainExecutor) {
@Override
public void onSampleCollected(float medianLuma) {
if (mSamplingEnabled) {
updateMedianLuma(medianLuma);
}
}
};
mSampledView = sampledView;
mSampledView.addOnAttachStateChangeListener(this);
mSampledView.addOnLayoutChangeListener(this);
mCallback = samplingCallback;
}
/**
* Make callback accessible
*/
@VisibleForTesting
public SamplingCallback getCallback() {
return mCallback;
}
private void onDraw() {
if (mWaitingOnDraw) {
mWaitingOnDraw = false;
updateSamplingListener();
}
}
public void start(Rect initialSamplingBounds) {
if (!mCallback.isSamplingEnabled()) {
return;
}
if (initialSamplingBounds != null) {
mSamplingRequestBounds.set(initialSamplingBounds);
}
mSamplingEnabled = true;
// make sure we notify once
mLastMedianLuma = -1;
mFirstSamplingAfterStart = true;
updateSamplingListener();
}
public void stop() {
mSamplingEnabled = false;
updateSamplingListener();
}
public void stopAndDestroy() {
stop();
mBackgroundExecutor.execute(mSamplingListener::destroy);
mIsDestroyed = true;
}
@Override
public void onViewAttachedToWindow(View view) {
updateSamplingListener();
}
@Override
public void onViewDetachedFromWindow(View view) {
stopAndDestroy();
}
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
updateSamplingRect();
}
private void updateSamplingListener() {
boolean isSamplingEnabled = mSamplingEnabled
&& !mSamplingRequestBounds.isEmpty()
&& mWindowVisible
&& !mWindowHasBlurs
&& (mSampledView.isAttachedToWindow() || mFirstSamplingAfterStart);
if (isSamplingEnabled) {
ViewRootImpl viewRootImpl = mSampledView.getViewRootImpl();
SurfaceControl stopLayerControl = null;
if (viewRootImpl != null) {
stopLayerControl = viewRootImpl.getSurfaceControl();
}
if (stopLayerControl == null || !stopLayerControl.isValid()) {
if (!mWaitingOnDraw) {
mWaitingOnDraw = true;
// The view might be attached but we haven't drawn yet, so wait until the
// next draw to update the listener again with the stop layer, such that our
// own drawing doesn't affect the sampling.
if (mHandler.hasCallbacks(mRemoveDrawRunnable)) {
mHandler.removeCallbacks(mRemoveDrawRunnable);
} else {
mSampledView.getViewTreeObserver().addOnDrawListener(mUpdateOnDraw);
}
}
// If there's no valid surface, let's just sample without a stop layer, so we
// don't have to delay
stopLayerControl = null;
}
if (!mSamplingRequestBounds.equals(mRegisteredSamplingBounds)
|| mRegisteredStopLayer != stopLayerControl) {
// We only want to re-register if something actually changed
unregisterSamplingListener();
mSamplingListenerRegistered = true;
SurfaceControl wrappedStopLayer = wrap(stopLayerControl);
// pass this to background thread to avoid empty Rect race condition
final Rect boundsCopy = new Rect(mSamplingRequestBounds);
mBackgroundExecutor.execute(() -> {
if (wrappedStopLayer != null && !wrappedStopLayer.isValid()) {
return;
}
mCompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY,
wrappedStopLayer, boundsCopy);
});
mRegisteredSamplingBounds.set(mSamplingRequestBounds);
mRegisteredStopLayer = stopLayerControl;
mWrappedStopLayer = wrappedStopLayer;
}
mFirstSamplingAfterStart = false;
} else {
unregisterSamplingListener();
}
}
@VisibleForTesting
protected SurfaceControl wrap(SurfaceControl stopLayerControl) {
return stopLayerControl == null ? null : new SurfaceControl(stopLayerControl,
"regionSampling");
}
private void unregisterSamplingListener() {
if (mSamplingListenerRegistered) {
mSamplingListenerRegistered = false;
SurfaceControl wrappedStopLayer = mWrappedStopLayer;
mRegisteredStopLayer = null;
mWrappedStopLayer = null;
mRegisteredSamplingBounds.setEmpty();
mBackgroundExecutor.execute(() -> {
mCompositionSamplingListener.unregister(mSamplingListener);
if (wrappedStopLayer != null && wrappedStopLayer.isValid()) {
wrappedStopLayer.release();
}
});
}
}
private void updateMedianLuma(float medianLuma) {
mCurrentMedianLuma = medianLuma;
// If the difference between the new luma and the current luma is larger than threshold
// then apply the current luma, this is to prevent small changes causing colors to flicker
if (Math.abs(mCurrentMedianLuma - mLastMedianLuma)
> NAVIGATION_LUMINANCE_CHANGE_THRESHOLD) {
mCallback.onRegionDarknessChanged(
medianLuma < NAVIGATION_LUMINANCE_THRESHOLD /* isRegionDark */);
mLastMedianLuma = medianLuma;
}
}
public void updateSamplingRect() {
Rect sampledRegion = mCallback.getSampledRegion(mSampledView);
if (!mSamplingRequestBounds.equals(sampledRegion)) {
mSamplingRequestBounds.set(sampledRegion);
updateSamplingListener();
}
}
public void setWindowVisible(boolean visible) {
mWindowVisible = visible;
updateSamplingListener();
}
/**
* If we're blurring the shade window.
*/
public void setWindowHasBlurs(boolean hasBlurs) {
mWindowHasBlurs = hasBlurs;
updateSamplingListener();
}
public void dump(PrintWriter pw) {
dump("", pw);
}
public void dump(String prefix, PrintWriter pw) {
pw.println(prefix + "RegionSamplingHelper:");
pw.println(prefix + "\tsampleView isAttached: " + mSampledView.isAttachedToWindow());
pw.println(prefix + "\tsampleView isScValid: " + (mSampledView.isAttachedToWindow()
? mSampledView.getViewRootImpl().getSurfaceControl().isValid()
: "notAttached"));
pw.println(prefix + "\tmSamplingEnabled: " + mSamplingEnabled);
pw.println(prefix + "\tmSamplingListenerRegistered: " + mSamplingListenerRegistered);
pw.println(prefix + "\tmSamplingRequestBounds: " + mSamplingRequestBounds);
pw.println(prefix + "\tmRegisteredSamplingBounds: " + mRegisteredSamplingBounds);
pw.println(prefix + "\tmLastMedianLuma: " + mLastMedianLuma);
pw.println(prefix + "\tmCurrentMedianLuma: " + mCurrentMedianLuma);
pw.println(prefix + "\tmWindowVisible: " + mWindowVisible);
pw.println(prefix + "\tmWindowHasBlurs: " + mWindowHasBlurs);
pw.println(prefix + "\tmWaitingOnDraw: " + mWaitingOnDraw);
pw.println(prefix + "\tmRegisteredStopLayer: " + mRegisteredStopLayer);
pw.println(prefix + "\tmWrappedStopLayer: " + mWrappedStopLayer);
pw.println(prefix + "\tmIsDestroyed: " + mIsDestroyed);
}
public interface SamplingCallback {
/**
* Called when the darkness of the sampled region changes
* @param isRegionDark true if the sampled luminance is below the luminance threshold
*/
void onRegionDarknessChanged(boolean isRegionDark);
/**
* Get the sampled region of interest from the sampled view
* @param sampledView The view that this helper is attached to for convenience
* @return the region to be sampled in sceen coordinates. Return {@code null} to avoid
* sampling in this frame
*/
Rect getSampledRegion(View sampledView);
/**
* @return if sampling should be enabled in the current configuration
*/
default boolean isSamplingEnabled() {
return true;
}
}
@VisibleForTesting
public static class SysuiCompositionSamplingListener {
public void register(CompositionSamplingListener listener,
int displayId, SurfaceControl stopLayer, Rect samplingArea) {
CompositionSamplingListener.register(listener, displayId, stopLayer, samplingArea);
}
/**
* Unregisters a sampling listener.
*/
public void unregister(CompositionSamplingListener listener) {
CompositionSamplingListener.unregister(listener);
}
}
}
@@ -0,0 +1,180 @@
/*
* Copyright (C) 2020 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 com.android.systemui.shared.pip;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.view.Choreographer;
import android.view.SurfaceControl;
import android.window.PictureInPictureSurfaceTransaction;
/**
* TODO(b/171721389): unify this class with
* {@link com.android.wm.shell.pip.PipSurfaceTransactionHelper}, for instance, there should be one
* source of truth on enabling/disabling and the actual value of corner radius.
*/
public class PipSurfaceTransactionHelper {
private final int mCornerRadius;
private final int mShadowRadius;
private final Matrix mTmpTransform = new Matrix();
private final float[] mTmpFloat9 = new float[9];
private final RectF mTmpSourceRectF = new RectF();
private final RectF mTmpDestinationRectF = new RectF();
private final Rect mTmpDestinationRect = new Rect();
public PipSurfaceTransactionHelper(int cornerRadius, int shadowRadius) {
mCornerRadius = cornerRadius;
mShadowRadius = shadowRadius;
}
public PictureInPictureSurfaceTransaction scale(
SurfaceControl.Transaction tx, SurfaceControl leash,
Rect sourceBounds, Rect destinationBounds) {
float positionX = destinationBounds.left;
float positionY = destinationBounds.top;
mTmpSourceRectF.set(sourceBounds);
mTmpDestinationRectF.set(destinationBounds);
mTmpDestinationRectF.offsetTo(0, 0);
mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
final float cornerRadius = getScaledCornerRadius(sourceBounds, destinationBounds);
tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
.setPosition(leash, positionX, positionY)
.setCornerRadius(leash, cornerRadius)
.setShadowRadius(leash, mShadowRadius);
return newPipSurfaceTransaction(positionX, positionY,
mTmpFloat9, 0 /* rotation */, cornerRadius, mShadowRadius, sourceBounds);
}
public PictureInPictureSurfaceTransaction scale(
SurfaceControl.Transaction tx, SurfaceControl leash,
Rect sourceBounds, Rect destinationBounds,
float degree, float positionX, float positionY) {
mTmpSourceRectF.set(sourceBounds);
mTmpDestinationRectF.set(destinationBounds);
mTmpDestinationRectF.offsetTo(0, 0);
mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
mTmpTransform.postRotate(degree, 0, 0);
final float cornerRadius = getScaledCornerRadius(sourceBounds, destinationBounds);
tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
.setPosition(leash, positionX, positionY)
.setCornerRadius(leash, cornerRadius)
.setShadowRadius(leash, mShadowRadius);
return newPipSurfaceTransaction(positionX, positionY,
mTmpFloat9, degree, cornerRadius, mShadowRadius, sourceBounds);
}
public PictureInPictureSurfaceTransaction scaleAndCrop(
SurfaceControl.Transaction tx, SurfaceControl leash,
Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets,
float progress) {
mTmpSourceRectF.set(sourceBounds);
mTmpDestinationRect.set(sourceBounds);
mTmpDestinationRect.inset(insets);
// Scale to the bounds no smaller than the destination and offset such that the top/left
// of the scaled inset source rect aligns with the top/left of the destination bounds
final float scale, left, top;
if (sourceRectHint.isEmpty() || sourceRectHint.width() == sourceBounds.width()) {
scale = Math.max((float) destinationBounds.width() / sourceBounds.width(),
(float) destinationBounds.height() / sourceBounds.height());
// Work around the rounding error by fix the position at very beginning.
left = scale == 1
? 0 : destinationBounds.left - (insets.left + sourceBounds.left) * scale;
top = scale == 1
? 0 : destinationBounds.top - (insets.top + sourceBounds.top) * scale;
} else {
// scale by sourceRectHint if it's not edge-to-edge
final float endScale = sourceRectHint.width() <= sourceRectHint.height()
? (float) destinationBounds.width() / sourceRectHint.width()
: (float) destinationBounds.height() / sourceRectHint.height();
final float startScale = sourceRectHint.width() <= sourceRectHint.height()
? (float) destinationBounds.width() / sourceBounds.width()
: (float) destinationBounds.height() / sourceBounds.height();
scale = Math.min((1 - progress) * startScale + progress * endScale, 1.0f);
left = destinationBounds.left - (insets.left + sourceBounds.left) * scale;
top = destinationBounds.top - (insets.top + sourceBounds.top) * scale;
}
mTmpTransform.setScale(scale, scale);
final float cornerRadius = getScaledCornerRadius(mTmpDestinationRect, destinationBounds);
tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
.setCrop(leash, mTmpDestinationRect)
.setPosition(leash, left, top)
.setCornerRadius(leash, cornerRadius)
.setShadowRadius(leash, mShadowRadius);
return newPipSurfaceTransaction(left, top,
mTmpFloat9, 0 /* rotation */, cornerRadius, mShadowRadius, mTmpDestinationRect);
}
public PictureInPictureSurfaceTransaction scaleAndRotate(
SurfaceControl.Transaction tx, SurfaceControl leash,
Rect sourceBounds, Rect destinationBounds, Rect insets,
float degree, float positionX, float positionY) {
mTmpSourceRectF.set(sourceBounds);
mTmpDestinationRect.set(sourceBounds);
mTmpDestinationRect.inset(insets);
// Scale by the shortest edge and offset such that the top/left of the scaled inset
// source rect aligns with the top/left of the destination bounds
final float scale = sourceBounds.width() <= sourceBounds.height()
? (float) destinationBounds.width() / sourceBounds.width()
: (float) destinationBounds.height() / sourceBounds.height();
mTmpTransform.setRotate(degree, 0, 0);
mTmpTransform.postScale(scale, scale);
final float cornerRadius = getScaledCornerRadius(mTmpDestinationRect, destinationBounds);
// adjust the positions, take account also the insets
final float adjustedPositionX, adjustedPositionY;
if (degree < 0) {
adjustedPositionX = positionX + insets.top * scale;
adjustedPositionY = positionY + insets.left * scale;
} else {
adjustedPositionX = positionX - insets.top * scale;
adjustedPositionY = positionY - insets.left * scale;
}
tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
.setCrop(leash, mTmpDestinationRect)
.setPosition(leash, adjustedPositionX, adjustedPositionY)
.setCornerRadius(leash, cornerRadius)
.setShadowRadius(leash, mShadowRadius);
return newPipSurfaceTransaction(adjustedPositionX, adjustedPositionY,
mTmpFloat9, degree, cornerRadius, mShadowRadius, mTmpDestinationRect);
}
/** @return the round corner radius scaled by given from and to bounds */
private float getScaledCornerRadius(Rect fromBounds, Rect toBounds) {
final float scale = (float) (Math.hypot(fromBounds.width(), fromBounds.height())
/ Math.hypot(toBounds.width(), toBounds.height()));
return mCornerRadius * scale;
}
private static PictureInPictureSurfaceTransaction newPipSurfaceTransaction(
float posX, float posY, float[] float9, float rotation,
float cornerRadius, float shadowRadius, Rect windowCrop) {
return new PictureInPictureSurfaceTransaction.Builder()
.setPosition(posX, posY)
.setTransform(float9, rotation)
.setCornerRadius(cornerRadius)
.setShadowRadius(shadowRadius)
.setWindowCrop(windowCrop)
.build();
}
/** @return {@link SurfaceControl.Transaction} instance with vsync-id */
public static SurfaceControl.Transaction newSurfaceControlTransaction() {
final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
tx.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
return tx;
}
}
@@ -0,0 +1,433 @@
/*
* Copyright (C) 2016 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 com.android.systemui.shared.plugins;
import android.app.Notification;
import android.app.Notification.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.net.Uri;
import android.util.ArraySet;
import android.util.Log;
import android.view.LayoutInflater;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Coordinates all the available plugins for a given action.
*
* The available plugins are queried from the {@link PackageManager} via an an {@link Intent}
* action.
*
* @param <T> The type of plugin that this contains.
*/
public class PluginActionManager<T extends Plugin> {
private static final boolean DEBUG = false;
private static final String TAG = "PluginActionManager";
public static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
private final Context mContext;
private final PluginListener<T> mListener;
private final String mAction;
private final boolean mAllowMultiple;
private final NotificationManager mNotificationManager;
private final PluginEnabler mPluginEnabler;
private final PluginInstance.Factory mPluginInstanceFactory;
private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>();
@VisibleForTesting
private final ArrayList<PluginInstance<T>> mPluginInstances = new ArrayList<>();
private final boolean mIsDebuggable;
private final PackageManager mPm;
private final Class<T> mPluginClass;
private final Executor mMainExecutor;
private final Executor mBgExecutor;
private PluginActionManager(
Context context,
PackageManager pm,
String action,
PluginListener<T> listener,
Class<T> pluginClass,
boolean allowMultiple,
Executor mainExecutor,
Executor bgExecutor,
boolean debuggable,
NotificationManager notificationManager,
PluginEnabler pluginEnabler,
List<String> privilegedPlugins,
PluginInstance.Factory pluginInstanceFactory) {
mPluginClass = pluginClass;
mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor;
mContext = context;
mPm = pm;
mAction = action;
mListener = listener;
mAllowMultiple = allowMultiple;
mNotificationManager = notificationManager;
mPluginEnabler = pluginEnabler;
mPluginInstanceFactory = pluginInstanceFactory;
mPrivilegedPlugins.addAll(privilegedPlugins);
mIsDebuggable = debuggable;
}
/** Load all plugins matching this instance's action. */
public void loadAll() {
if (DEBUG) Log.d(TAG, "startListening");
mBgExecutor.execute(() -> queryAll());
}
/** Unload all plugins managed by this instance. */
public void destroy() {
if (DEBUG) Log.d(TAG, "stopListening");
ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
for (PluginInstance<T> plugInstance : plugins) {
mMainExecutor.execute(() -> onPluginDisconnected(plugInstance));
}
}
/** Unload all matching plugins managed by this instance. */
public void onPackageRemoved(String pkg) {
mBgExecutor.execute(() -> removePkg(pkg));
}
/** Unload and then reload all matching plugins managed by this instance. */
public void reloadPackage(String pkg) {
mBgExecutor.execute(() -> {
removePkg(pkg);
queryPkg(pkg);
});
}
/** Disable a specific plugin managed by this instance. */
public boolean checkAndDisable(String className) {
boolean disableAny = false;
ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
for (PluginInstance<T> info : plugins) {
if (className.startsWith(info.getPackage())) {
disableAny |= disable(info, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
}
}
return disableAny;
}
/** Disable all plugins managed by this instance. */
public boolean disableAll() {
ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
boolean disabledAny = false;
for (int i = 0; i < plugins.size(); i++) {
disabledAny |= disable(plugins.get(i), PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
}
return disabledAny;
}
boolean isPluginPrivileged(ComponentName pluginName) {
for (String componentNameOrPackage : mPrivilegedPlugins) {
ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
if (componentName == null) {
if (componentNameOrPackage.equals(pluginName.getPackageName())) {
return true;
}
} else {
if (componentName.equals(pluginName)) {
return true;
}
}
}
return false;
}
private boolean disable(
PluginInstance<T> pluginInstance, @PluginEnabler.DisableReason int reason) {
// Live by the sword, die by the sword.
// Misbehaving plugins get disabled and won't come back until uninstall/reinstall.
ComponentName pluginComponent = pluginInstance.getComponentName();
// If a plugin is detected in the stack of a crash then this will be called for that
// plugin, if the plugin causing a crash cannot be identified, they are all disabled
// assuming one of them must be bad.
if (isPluginPrivileged(pluginComponent)) {
// Don't disable privileged plugins as they are a part of the OS.
return false;
}
Log.w(TAG, "Disabling plugin " + pluginComponent.flattenToShortString());
mPluginEnabler.setDisabled(pluginComponent, reason);
return true;
}
<C> boolean dependsOn(Plugin p, Class<C> cls) {
ArrayList<PluginInstance<T>> instances = new ArrayList<>(mPluginInstances);
for (PluginInstance<T> instance : instances) {
if (instance.containsPluginClass(p.getClass())) {
return instance.getVersionInfo() != null && instance.getVersionInfo().hasClass(cls);
}
}
return false;
}
@Override
public String toString() {
return String.format("%s@%s (action=%s)",
getClass().getSimpleName(), hashCode(), mAction);
}
private void onPluginConnected(PluginInstance<T> pluginInstance) {
if (DEBUG) Log.d(TAG, "onPluginConnected");
PluginPrefs.setHasPlugins(mContext);
pluginInstance.onCreate();
}
private void onPluginDisconnected(PluginInstance<T> pluginInstance) {
if (DEBUG) Log.d(TAG, "onPluginDisconnected");
pluginInstance.onDestroy();
}
private void queryAll() {
if (DEBUG) Log.d(TAG, "queryAll " + mAction);
for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
PluginInstance<T> pluginInstance = mPluginInstances.get(i);
mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
}
mPluginInstances.clear();
handleQueryPlugins(null);
}
private void removePkg(String pkg) {
for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
final PluginInstance<T> pluginInstance = mPluginInstances.get(i);
if (pluginInstance.getPackage().equals(pkg)) {
mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
mPluginInstances.remove(i);
}
}
}
private void queryPkg(String pkg) {
if (DEBUG) Log.d(TAG, "queryPkg " + pkg + " " + mAction);
if (mAllowMultiple || (mPluginInstances.size() == 0)) {
handleQueryPlugins(pkg);
} else {
if (DEBUG) Log.d(TAG, "Too many of " + mAction);
}
}
private void handleQueryPlugins(String pkgName) {
// This isn't actually a service and shouldn't ever be started, but is
// a convenient PM based way to manage our plugins.
Intent intent = new Intent(mAction);
if (pkgName != null) {
intent.setPackage(pkgName);
}
List<ResolveInfo> result = mPm.queryIntentServices(intent, 0);
if (DEBUG) {
Log.d(TAG, "Found " + result.size() + " plugins");
for (ResolveInfo info : result) {
ComponentName name = new ComponentName(info.serviceInfo.packageName,
info.serviceInfo.name);
Log.d(TAG, " " + name);
}
}
if (result.size() > 1 && !mAllowMultiple) {
// TODO: Show warning.
Log.w(TAG, "Multiple plugins found for " + mAction);
return;
}
for (ResolveInfo info : result) {
ComponentName name = new ComponentName(info.serviceInfo.packageName,
info.serviceInfo.name);
PluginInstance<T> pluginInstance = loadPluginComponent(name);
if (pluginInstance != null) {
// add plugin before sending PLUGIN_CONNECTED message
mPluginInstances.add(pluginInstance);
mMainExecutor.execute(() -> onPluginConnected(pluginInstance));
}
}
}
private PluginInstance<T> loadPluginComponent(ComponentName component) {
// This was already checked, but do it again here to make extra extra sure, we don't
// use these on production builds.
if (!mIsDebuggable && !isPluginPrivileged(component)) {
// Never ever ever allow these on production builds, they are only for prototyping.
Log.w(TAG, "Plugin cannot be loaded on production build: " + component);
return null;
}
if (!mPluginEnabler.isEnabled(component)) {
if (DEBUG) {
Log.d(TAG, "Plugin is not enabled, aborting load: " + component);
}
return null;
}
String packageName = component.getPackageName();
try {
// TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
if (mPm.checkPermission(PLUGIN_PERMISSION, packageName)
!= PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Plugin doesn't have permission: " + packageName);
return null;
}
ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
// TODO: Only create the plugin before version check if we need it for
// legacy version check.
if (DEBUG) {
Log.d(TAG, "createPlugin: " + component);
}
try {
return mPluginInstanceFactory.create(
mContext, appInfo, component,
mPluginClass, mListener);
} catch (InvalidVersionException e) {
reportInvalidVersion(component, component.getClassName(), e);
}
} catch (Throwable e) {
Log.w(TAG, "Couldn't load plugin: " + component, e);
return null;
}
return null;
}
private void reportInvalidVersion(
ComponentName component, String className, InvalidVersionException e) {
final int icon = Resources.getSystem().getIdentifier(
"stat_sys_warning", "drawable", "android");
final int color = Resources.getSystem().getIdentifier(
"system_notification_accent_color", "color", "android");
final Notification.Builder nb = new Notification.Builder(mContext,
PluginManager.NOTIFICATION_CHANNEL_ID)
.setStyle(new Notification.BigTextStyle())
.setSmallIcon(icon)
.setWhen(0)
.setShowWhen(false)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setColor(mContext.getColor(color));
String label = className;
try {
label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
} catch (NameNotFoundException e2) {
// no-op
}
if (!e.isTooNew()) {
// Localization not required as this will never ever appear in a user build.
nb.setContentTitle("Plugin \"" + label + "\" is too old")
.setContentText("Contact plugin developer to get an updated"
+ " version.\n" + e.getMessage());
} else {
// Localization not required as this will never ever appear in a user build.
nb.setContentTitle("Plugin \"" + label + "\" is too new")
.setContentText("Check to see if an OTA is available.\n"
+ e.getMessage());
}
Intent i = new Intent(PluginManagerImpl.DISABLE_PLUGIN).setData(
Uri.parse("package://" + component.flattenToString()));
PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i,
PendingIntent.FLAG_IMMUTABLE);
nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
mNotificationManager.notify(SystemMessage.NOTE_PLUGIN, nb.build());
// TODO: Warn user.
Log.w(TAG, "Error loading plugin; " + e.getMessage());
}
/**
* Construct a {@link PluginActionManager}
*/
public static class Factory {
private final Context mContext;
private final PackageManager mPackageManager;
private final Executor mMainExecutor;
private final Executor mBgExecutor;
private final NotificationManager mNotificationManager;
private final PluginEnabler mPluginEnabler;
private final List<String> mPrivilegedPlugins;
private final PluginInstance.Factory mPluginInstanceFactory;
public Factory(Context context, PackageManager packageManager,
Executor mainExecutor, Executor bgExecutor,
NotificationManager notificationManager, PluginEnabler pluginEnabler,
List<String> privilegedPlugins, PluginInstance.Factory pluginInstanceFactory) {
mContext = context;
mPackageManager = packageManager;
mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor;
mNotificationManager = notificationManager;
mPluginEnabler = pluginEnabler;
mPrivilegedPlugins = privilegedPlugins;
mPluginInstanceFactory = pluginInstanceFactory;
}
<T extends Plugin> PluginActionManager<T> create(
String action, PluginListener<T> listener, Class<T> pluginClass,
boolean allowMultiple, boolean debuggable) {
return new PluginActionManager<>(mContext, mPackageManager, action, listener,
pluginClass, allowMultiple, mMainExecutor, mBgExecutor,
debuggable, mNotificationManager, mPluginEnabler,
mPrivilegedPlugins, mPluginInstanceFactory);
}
}
/** */
public static class PluginContextWrapper extends ContextWrapper {
private final ClassLoader mClassLoader;
private LayoutInflater mInflater;
public PluginContextWrapper(Context base, ClassLoader classLoader) {
super(base);
mClassLoader = classLoader;
}
@Override
public ClassLoader getClassLoader() {
return mClassLoader;
}
@Override
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2018 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 com.android.systemui.shared.plugins;
import android.annotation.IntDef;
import android.content.ComponentName;
/**
* Enables and disables plugins.
*/
public interface PluginEnabler {
int ENABLED = 0;
int DISABLED_MANUALLY = 1;
int DISABLED_INVALID_VERSION = 2;
int DISABLED_FROM_EXPLICIT_CRASH = 3;
int DISABLED_FROM_SYSTEM_CRASH = 4;
@IntDef({ENABLED, DISABLED_MANUALLY, DISABLED_INVALID_VERSION, DISABLED_FROM_EXPLICIT_CRASH,
DISABLED_FROM_SYSTEM_CRASH})
@interface DisableReason {
}
/** Enables plugin via the PackageManager. */
void setEnabled(ComponentName component);
/** Disables a plugin via the PackageManager and records the reason for disabling. */
void setDisabled(ComponentName component, @DisableReason int reason);
/** Returns true if the plugin is enabled in the PackageManager. */
boolean isEnabled(ComponentName component);
/**
* Returns the reason that a plugin is disabled, (if it is).
*
* It should return {@link #ENABLED} if the plugin is turned on.
* It should return {@link #DISABLED_MANUALLY} if the plugin is off but the reason is unknown.
*/
@DisableReason
int getDisableReason(ComponentName componentName);
}
@@ -0,0 +1,406 @@
/*
* Copyright (C) 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 com.android.systemui.shared.plugins;
import android.app.LoadedApk;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginFragment;
import com.android.systemui.plugins.PluginLifecycleManager;
import com.android.systemui.plugins.PluginListener;
import dalvik.system.PathClassLoader;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
/**
* Contains a single instantiation of a Plugin.
*
* This class and its related Factory are in charge of actually instantiating a plugin and
* managing any state related to it.
*
* @param <T> The type of plugin that this contains.
*/
public class PluginInstance<T extends Plugin> implements PluginLifecycleManager {
private static final String TAG = "PluginInstance";
private final Context mAppContext;
private final PluginListener<T> mListener;
private final ComponentName mComponentName;
private final PluginFactory<T> mPluginFactory;
private final String mTag;
private BiConsumer<String, String> mLogConsumer = null;
private Context mPluginContext;
private T mPlugin;
/** */
public PluginInstance(
Context appContext,
PluginListener<T> listener,
ComponentName componentName,
PluginFactory<T> pluginFactory,
@Nullable T plugin) {
mAppContext = appContext;
mListener = listener;
mComponentName = componentName;
mPluginFactory = pluginFactory;
mPlugin = plugin;
mTag = TAG + "[" + mComponentName.getShortClassName() + "]"
+ '@' + Integer.toHexString(hashCode());
if (mPlugin != null) {
mPluginContext = mPluginFactory.createPluginContext();
}
}
@Override
public String toString() {
return mTag;
}
public void setLogFunc(BiConsumer logConsumer) {
mLogConsumer = logConsumer;
}
private void log(String message) {
if (mLogConsumer != null) {
mLogConsumer.accept(mTag, message);
}
}
/** Alerts listener and plugin that the plugin has been created. */
public synchronized void onCreate() {
boolean loadPlugin = mListener.onPluginAttached(this);
if (!loadPlugin) {
if (mPlugin != null) {
log("onCreate: auto-unload");
unloadPlugin();
}
return;
}
if (mPlugin == null) {
log("onCreate auto-load");
loadPlugin();
return;
}
log("onCreate: load callbacks");
mPluginFactory.checkVersion(mPlugin);
if (!(mPlugin instanceof PluginFragment)) {
// Only call onCreate for plugins that aren't fragments, as fragments
// will get the onCreate as part of the fragment lifecycle.
mPlugin.onCreate(mAppContext, mPluginContext);
}
mListener.onPluginLoaded(mPlugin, mPluginContext, this);
}
/** Alerts listener and plugin that the plugin is being shutdown. */
public synchronized void onDestroy() {
log("onDestroy");
unloadPlugin();
mListener.onPluginDetached(this);
}
/** Returns the current plugin instance (if it is loaded). */
@Nullable
public T getPlugin() {
return mPlugin;
}
/**
* Loads and creates the plugin if it does not exist.
*/
public synchronized void loadPlugin() {
if (mPlugin != null) {
log("Load request when already loaded");
return;
}
// Both of these calls take about 1 - 1.5 seconds in test runs
mPlugin = mPluginFactory.createPlugin();
mPluginContext = mPluginFactory.createPluginContext();
if (mPlugin == null || mPluginContext == null) {
Log.e(mTag, "Requested load, but failed");
return;
}
log("Loaded plugin; running callbacks");
mPluginFactory.checkVersion(mPlugin);
if (!(mPlugin instanceof PluginFragment)) {
// Only call onCreate for plugins that aren't fragments, as fragments
// will get the onCreate as part of the fragment lifecycle.
mPlugin.onCreate(mAppContext, mPluginContext);
}
mListener.onPluginLoaded(mPlugin, mPluginContext, this);
}
/**
* Unloads and destroys the current plugin instance if it exists.
*
* This will free the associated memory if there are not other references.
*/
public synchronized void unloadPlugin() {
if (mPlugin == null) {
log("Unload request when already unloaded");
return;
}
log("Unloading plugin, running callbacks");
mListener.onPluginUnloaded(mPlugin, this);
if (!(mPlugin instanceof PluginFragment)) {
// Only call onDestroy for plugins that aren't fragments, as fragments
// will get the onDestroy as part of the fragment lifecycle.
mPlugin.onDestroy();
}
mPlugin = null;
mPluginContext = null;
}
/**
* Returns if the contained plugin matches the passed in class name.
*
* It does this by string comparison of the class names.
**/
public boolean containsPluginClass(Class pluginClass) {
return mComponentName.getClassName().equals(pluginClass.getName());
}
public ComponentName getComponentName() {
return mComponentName;
}
public String getPackage() {
return mComponentName.getPackageName();
}
public VersionInfo getVersionInfo() {
return mPluginFactory.checkVersion(mPlugin);
}
@VisibleForTesting
Context getPluginContext() {
return mPluginContext;
}
/** Used to create new {@link PluginInstance}s. */
public static class Factory {
private final ClassLoader mBaseClassLoader;
private final InstanceFactory<?> mInstanceFactory;
private final VersionChecker mVersionChecker;
private final boolean mIsDebug;
private final List<String> mPrivilegedPlugins;
/** Factory used to construct {@link PluginInstance}s. */
public Factory(ClassLoader classLoader, InstanceFactory<?> instanceFactory,
VersionChecker versionChecker,
List<String> privilegedPlugins,
boolean isDebug) {
mPrivilegedPlugins = privilegedPlugins;
mBaseClassLoader = classLoader;
mInstanceFactory = instanceFactory;
mVersionChecker = versionChecker;
mIsDebug = isDebug;
}
/** Construct a new PluginInstance. */
public <T extends Plugin> PluginInstance<T> create(
Context context,
ApplicationInfo appInfo,
ComponentName componentName,
Class<T> pluginClass,
PluginListener<T> listener)
throws PackageManager.NameNotFoundException, ClassNotFoundException,
InstantiationException, IllegalAccessException {
PluginFactory<T> pluginFactory = new PluginFactory<T>(
context, mInstanceFactory, appInfo, componentName, mVersionChecker, pluginClass,
() -> getClassLoader(appInfo, mBaseClassLoader));
return new PluginInstance<T>(
context, listener, componentName, pluginFactory, null);
}
private boolean isPluginPackagePrivileged(String packageName) {
for (String componentNameOrPackage : mPrivilegedPlugins) {
ComponentName componentName = ComponentName.unflattenFromString(
componentNameOrPackage);
if (componentName != null) {
if (componentName.getPackageName().equals(packageName)) {
return true;
}
} else if (componentNameOrPackage.equals(packageName)) {
return true;
}
}
return false;
}
private ClassLoader getParentClassLoader(ClassLoader baseClassLoader) {
return new PluginManagerImpl.ClassLoaderFilter(
baseClassLoader,
"androidx.constraintlayout.widget",
"com.android.systemui.common",
"com.android.systemui.log",
"com.android.systemui.plugin");
}
/** Returns class loader specific for the given plugin. */
private ClassLoader getClassLoader(ApplicationInfo appInfo,
ClassLoader baseClassLoader) {
if (!mIsDebug && !isPluginPackagePrivileged(appInfo.packageName)) {
Log.w(TAG, "Cannot get class loader for non-privileged plugin. Src:"
+ appInfo.sourceDir + ", pkg: " + appInfo.packageName);
return null;
}
List<String> zipPaths = new ArrayList<>();
List<String> libPaths = new ArrayList<>();
LoadedApk.makePaths(null, true, appInfo, zipPaths, libPaths);
ClassLoader classLoader = new PathClassLoader(
TextUtils.join(File.pathSeparator, zipPaths),
TextUtils.join(File.pathSeparator, libPaths),
getParentClassLoader(baseClassLoader));
return classLoader;
}
}
/** Class that compares a plugin class against an implementation for version matching. */
public interface VersionChecker {
/** Compares two plugin classes. */
<T extends Plugin> VersionInfo checkVersion(
Class<T> instanceClass, Class<T> pluginClass, Plugin plugin);
}
/** Class that compares a plugin class against an implementation for version matching. */
public static class VersionCheckerImpl implements VersionChecker {
@Override
/** Compares two plugin classes. */
public <T extends Plugin> VersionInfo checkVersion(
Class<T> instanceClass, Class<T> pluginClass, Plugin plugin) {
VersionInfo pluginVersion = new VersionInfo().addClass(pluginClass);
VersionInfo instanceVersion = new VersionInfo().addClass(instanceClass);
if (instanceVersion.hasVersionInfo()) {
pluginVersion.checkVersion(instanceVersion);
} else if (plugin != null) {
int fallbackVersion = plugin.getVersion();
if (fallbackVersion != pluginVersion.getDefaultVersion()) {
throw new VersionInfo.InvalidVersionException("Invalid legacy version", false);
}
return null;
}
return instanceVersion;
}
}
/**
* Simple class to create a new instance. Useful for testing.
*
* @param <T> The type of plugin this create.
**/
public static class InstanceFactory<T extends Plugin> {
T create(Class cls) throws IllegalAccessException, InstantiationException {
return (T) cls.newInstance();
}
}
/**
* Instanced wrapper of InstanceFactory
*
* @param <T> is the type of the plugin object to be built
**/
public static class PluginFactory<T extends Plugin> {
private final Context mContext;
private final InstanceFactory<?> mInstanceFactory;
private final ApplicationInfo mAppInfo;
private final ComponentName mComponentName;
private final VersionChecker mVersionChecker;
private final Class<T> mPluginClass;
private final Supplier<ClassLoader> mClassLoaderFactory;
public PluginFactory(
Context context,
InstanceFactory<?> instanceFactory,
ApplicationInfo appInfo,
ComponentName componentName,
VersionChecker versionChecker,
Class<T> pluginClass,
Supplier<ClassLoader> classLoaderFactory) {
mContext = context;
mInstanceFactory = instanceFactory;
mAppInfo = appInfo;
mComponentName = componentName;
mVersionChecker = versionChecker;
mPluginClass = pluginClass;
mClassLoaderFactory = classLoaderFactory;
}
/** Creates the related plugin object from the factory */
public T createPlugin() {
try {
ClassLoader loader = mClassLoaderFactory.get();
Class<T> instanceClass = (Class<T>) Class.forName(
mComponentName.getClassName(), true, loader);
T result = (T) mInstanceFactory.create(instanceClass);
Log.v(TAG, "Created plugin: " + result);
return result;
} catch (ClassNotFoundException ex) {
Log.e(TAG, "Failed to load plugin", ex);
} catch (IllegalAccessException ex) {
Log.e(TAG, "Failed to load plugin", ex);
} catch (InstantiationException ex) {
Log.e(TAG, "Failed to load plugin", ex);
}
return null;
}
/** Creates a context wrapper for the plugin */
public Context createPluginContext() {
try {
ClassLoader loader = mClassLoaderFactory.get();
return new PluginActionManager.PluginContextWrapper(
mContext.createApplicationContext(mAppInfo, 0), loader);
} catch (NameNotFoundException ex) {
Log.e(TAG, "Failed to create plugin context", ex);
}
return null;
}
/** Check Version and create VersionInfo for instance */
public VersionInfo checkVersion(T instance) {
if (instance == null) {
instance = createPlugin();
}
return mVersionChecker.checkVersion(
(Class<T>) instance.getClass(), mPluginClass, instance);
}
}
}
@@ -0,0 +1,316 @@
/*
* Copyright (C) 2016 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 com.android.systemui.shared.plugins;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Build;
import android.os.SystemProperties;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.widget.Toast;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.List;
import java.util.Map;
/**
* @see Plugin
*/
public class PluginManagerImpl extends BroadcastReceiver implements PluginManager {
private static final String TAG = PluginManagerImpl.class.getSimpleName();
static final String DISABLE_PLUGIN = "com.android.systemui.action.DISABLE_PLUGIN";
private final ArrayMap<PluginListener<?>, PluginActionManager<?>> mPluginMap
= new ArrayMap<>();
private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>();
private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>();
private final Context mContext;
private final PluginActionManager.Factory mActionManagerFactory;
private final boolean mIsDebuggable;
private final PluginPrefs mPluginPrefs;
private final PluginEnabler mPluginEnabler;
private boolean mListening;
public PluginManagerImpl(Context context,
PluginActionManager.Factory actionManagerFactory,
boolean debuggable,
UncaughtExceptionPreHandlerManager preHandlerManager,
PluginEnabler pluginEnabler,
PluginPrefs pluginPrefs,
List<String> privilegedPlugins) {
mContext = context;
mActionManagerFactory = actionManagerFactory;
mIsDebuggable = debuggable;
mPrivilegedPlugins.addAll(privilegedPlugins);
mPluginPrefs = pluginPrefs;
mPluginEnabler = pluginEnabler;
preHandlerManager.registerHandler(new PluginExceptionHandler());
}
public boolean isDebuggable() {
return mIsDebuggable;
}
public String[] getPrivilegedPlugins() {
return mPrivilegedPlugins.toArray(new String[0]);
}
/** */
public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls) {
addPluginListener(listener, cls, false);
}
/** */
public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls,
boolean allowMultiple) {
addPluginListener(PluginManager.Helper.getAction(cls), listener, cls, allowMultiple);
}
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
Class<T> cls) {
addPluginListener(action, listener, cls, false);
}
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
Class<T> cls, boolean allowMultiple) {
mPluginPrefs.addAction(action);
PluginActionManager<T> p = mActionManagerFactory.create(action, listener, cls,
allowMultiple, isDebuggable());
p.loadAll();
synchronized (this) {
mPluginMap.put(listener, p);
}
startListening();
}
public void removePluginListener(PluginListener<?> listener) {
synchronized (this) {
if (!mPluginMap.containsKey(listener)) {
return;
}
mPluginMap.remove(listener).destroy();
if (mPluginMap.size() == 0) {
stopListening();
}
}
}
private void startListening() {
if (mListening) return;
mListening = true;
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addDataScheme("package");
mContext.registerReceiver(this, filter);
filter.addAction(PLUGIN_CHANGED);
filter.addAction(DISABLE_PLUGIN);
filter.addDataScheme("package");
mContext.registerReceiver(this, filter, PluginActionManager.PLUGIN_PERMISSION, null,
Context.RECEIVER_EXPORTED_UNAUDITED);
filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
mContext.registerReceiver(this, filter);
}
private void stopListening() {
if (!mListening) return;
mListening = false;
mContext.unregisterReceiver(this);
}
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
synchronized (this) {
for (PluginActionManager<?> manager : mPluginMap.values()) {
manager.loadAll();
}
}
} else if (DISABLE_PLUGIN.equals(intent.getAction())) {
Uri uri = intent.getData();
ComponentName component = ComponentName.unflattenFromString(
uri.toString().substring(10));
if (isPluginPrivileged(component)) {
// Don't disable privileged plugins as they are a part of the OS.
return;
}
mPluginEnabler.setDisabled(component, PluginEnabler.DISABLED_INVALID_VERSION);
mContext.getSystemService(NotificationManager.class).cancel(component.getClassName(),
SystemMessage.NOTE_PLUGIN);
} else {
Uri data = intent.getData();
String pkg = data.getEncodedSchemeSpecificPart();
ComponentName componentName = ComponentName.unflattenFromString(pkg);
if (clearClassLoader(pkg)) {
if (Build.IS_ENG) {
Toast.makeText(mContext, "Reloading " + pkg, Toast.LENGTH_LONG).show();
} else {
Log.v(TAG, "Reloading " + pkg);
}
}
if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())
&& componentName != null) {
@PluginEnabler.DisableReason int disableReason =
mPluginEnabler.getDisableReason(componentName);
if (disableReason == PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH
|| disableReason == PluginEnabler.DISABLED_FROM_SYSTEM_CRASH
|| disableReason == PluginEnabler.DISABLED_INVALID_VERSION) {
Log.i(TAG, "Re-enabling previously disabled plugin that has been "
+ "updated: " + componentName.flattenToShortString());
mPluginEnabler.setEnabled(componentName);
}
}
synchronized (this) {
if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())
|| Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())
|| Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
for (PluginActionManager<?> actionManager : mPluginMap.values()) {
actionManager.reloadPackage(pkg);
}
} else {
for (PluginActionManager<?> manager : mPluginMap.values()) {
manager.onPackageRemoved(pkg);
}
}
}
}
}
private boolean clearClassLoader(String pkg) {
return mClassLoaders.remove(pkg) != null;
}
public <T> boolean dependsOn(Plugin p, Class<T> cls) {
synchronized (this) {
for (int i = 0; i < mPluginMap.size(); i++) {
if (mPluginMap.valueAt(i).dependsOn(p, cls)) {
return true;
}
}
}
return false;
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
synchronized (this) {
pw.println(String.format(" plugin map (%d):", mPluginMap.size()));
for (PluginListener<?> listener : mPluginMap.keySet()) {
pw.println(String.format(" %s -> %s",
listener, mPluginMap.get(listener)));
}
}
}
private boolean isPluginPrivileged(ComponentName pluginName) {
for (String componentNameOrPackage : mPrivilegedPlugins) {
ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
if (componentName != null) {
if (componentName.equals(pluginName)) {
return true;
}
} else if (componentNameOrPackage.equals(pluginName.getPackageName())) {
return true;
}
}
return false;
}
// This allows plugins to include any libraries or copied code they want by only including
// classes from the plugin library.
static class ClassLoaderFilter extends ClassLoader {
private final String[] mPackages;
private final ClassLoader mBase;
ClassLoaderFilter(ClassLoader base, String... pkgs) {
super(ClassLoader.getSystemClassLoader());
mBase = base;
mPackages = pkgs;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
for (String pkg : mPackages) {
if (name.startsWith(pkg)) {
return mBase.loadClass(name);
}
}
return super.loadClass(name, resolve);
}
}
private class PluginExceptionHandler implements UncaughtExceptionHandler {
private PluginExceptionHandler() {}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
if (SystemProperties.getBoolean("plugin.debugging", false)) {
return;
}
// Search for and disable plugins that may have been involved in this crash.
boolean disabledAny = checkStack(throwable);
if (!disabledAny) {
// We couldn't find any plugins involved in this crash, just to be safe
// disable all the plugins, so we can be sure that SysUI is running as
// best as possible.
synchronized (this) {
for (PluginActionManager<?> manager : mPluginMap.values()) {
disabledAny |= manager.disableAll();
}
}
}
if (disabledAny) {
throwable = new CrashWhilePluginActiveException(throwable);
}
}
private boolean checkStack(Throwable throwable) {
if (throwable == null) return false;
boolean disabledAny = false;
synchronized (this) {
for (StackTraceElement element : throwable.getStackTrace()) {
for (PluginActionManager<?> manager : mPluginMap.values()) {
disabledAny |= manager.checkAndDisable(element.getClassName());
}
}
}
return disabledAny | checkStack(throwable.getCause());
}
}
public static class CrashWhilePluginActiveException extends RuntimeException {
public CrashWhilePluginActiveException(Throwable throwable) {
super(throwable);
}
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 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 com.android.systemui.shared.plugins;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.ArraySet;
import java.util.Set;
/**
* Storage for all plugin actions in SharedPreferences.
*
* This allows the list of actions that the Tuner needs to search for to be generated
* instead of hard coded.
*/
public class PluginPrefs {
private static final String PREFS = "plugin_prefs";
private static final String PLUGIN_ACTIONS = "actions";
private static final String HAS_PLUGINS = "plugins";
private final Set<String> mPluginActions;
private final SharedPreferences mSharedPrefs;
public PluginPrefs(Context context) {
mSharedPrefs = context.getSharedPreferences(PREFS, 0);
mPluginActions = new ArraySet<>(mSharedPrefs.getStringSet(PLUGIN_ACTIONS, null));
}
public Set<String> getPluginList() {
return new ArraySet<>(mPluginActions);
}
public synchronized void addAction(String action) {
if (mPluginActions.add(action)){
mSharedPrefs.edit().putStringSet(PLUGIN_ACTIONS, mPluginActions).apply();
}
}
public static boolean hasPlugins(Context context) {
return context.getSharedPreferences(PREFS, 0).getBoolean(HAS_PLUGINS, false);
}
public static void setHasPlugins(Context context) {
context.getSharedPreferences(PREFS, 0).edit().putBoolean(HAS_PLUGINS, true).apply();
}
}
@@ -0,0 +1,160 @@
/*
* Copyright (C) 2017 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 com.android.systemui.shared.plugins;
import android.util.ArrayMap;
import com.android.systemui.plugins.annotations.Dependencies;
import com.android.systemui.plugins.annotations.DependsOn;
import com.android.systemui.plugins.annotations.ProvidesInterface;
import com.android.systemui.plugins.annotations.Requirements;
import com.android.systemui.plugins.annotations.Requires;
import java.util.function.BiConsumer;
public class VersionInfo {
private final ArrayMap<Class<?>, Version> mVersions = new ArrayMap<>();
private Class<?> mDefault;
public boolean hasVersionInfo() {
return !mVersions.isEmpty();
}
public int getDefaultVersion() {
return mVersions.get(mDefault).mVersion;
}
public VersionInfo addClass(Class<?> cls) {
if (mDefault == null) {
// The legacy default version is from the first class we add.
mDefault = cls;
}
addClass(cls, false);
return this;
}
private void addClass(Class<?> cls, boolean required) {
if (mVersions.containsKey(cls)) return;
ProvidesInterface provider = cls.getDeclaredAnnotation(ProvidesInterface.class);
if (provider != null) {
mVersions.put(cls, new Version(provider.version(), true));
}
Requires requires = cls.getDeclaredAnnotation(Requires.class);
if (requires != null) {
mVersions.put(requires.target(), new Version(requires.version(), required));
}
Requirements requirements = cls.getDeclaredAnnotation(Requirements.class);
if (requirements != null) {
for (Requires r : requirements.value()) {
mVersions.put(r.target(), new Version(r.version(), required));
}
}
DependsOn depends = cls.getDeclaredAnnotation(DependsOn.class);
if (depends != null) {
addClass(depends.target(), true);
}
Dependencies dependencies = cls.getDeclaredAnnotation(Dependencies.class);
if (dependencies != null) {
for (DependsOn d : dependencies.value()) {
addClass(d.target(), true);
}
}
}
public void checkVersion(VersionInfo plugin) throws InvalidVersionException {
final ArrayMap<Class<?>, Version> versions = new ArrayMap<>(mVersions);
plugin.mVersions.forEach(new BiConsumer<Class<?>, Version>() {
@Override
public void accept(Class<?> aClass, Version version) {
Version v = versions.remove(aClass);
if (v == null) {
v = VersionInfo.this.createVersion(aClass);
}
if (v == null) {
throw new InvalidVersionException(aClass.getSimpleName()
+ " does not provide an interface", false);
}
if (v.mVersion != version.mVersion) {
throw new InvalidVersionException(aClass, v.mVersion < version.mVersion,
v.mVersion,
version.mVersion);
}
}
});
versions.forEach(new BiConsumer<Class<?>, Version>() {
@Override
public void accept(Class<?> aClass, Version version) {
if (version.mRequired) {
throw new InvalidVersionException("Missing required dependency "
+ aClass.getSimpleName(), false);
}
}
});
}
private Version createVersion(Class<?> cls) {
ProvidesInterface provider = cls.getDeclaredAnnotation(ProvidesInterface.class);
if (provider != null) {
return new Version(provider.version(), false);
}
return null;
}
public <T> boolean hasClass(Class<T> cls) {
return mVersions.containsKey(cls);
}
public static class InvalidVersionException extends RuntimeException {
private final boolean mTooNew;
private int mExpected;
private int mActual;
public InvalidVersionException(String str, boolean tooNew) {
super(str);
mTooNew = tooNew;
}
public InvalidVersionException(Class<?> cls, boolean tooNew, int expected, int actual) {
super(cls.getSimpleName() + " expected version " + expected + " but had " + actual);
mTooNew = tooNew;
mExpected = expected;
mActual = actual;
}
public boolean isTooNew() {
return mTooNew;
}
public int getExpectedVersion() {
return mExpected;
}
public int getActualVersion() {
return mActual;
}
}
private static class Version {
private final int mVersion;
private final boolean mRequired;
public Version(int version, boolean required) {
mVersion = version;
mRequired = required;
}
}
}
@@ -0,0 +1,104 @@
/*
* Copyright (C) 2017 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 com.android.systemui.shared.recents;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Bundle;
import android.view.MotionEvent;
import com.android.systemui.shared.recents.ISystemUiProxy;
// Next ID: 29
oneway interface IOverviewProxy {
void onActiveNavBarRegionChanges(in Region activeRegion) = 11;
void onInitialize(in Bundle params) = 12;
/**
* Sent when overview button is pressed to toggle show/hide of overview.
*/
void onOverviewToggle() = 6;
/**
* Sent when overview is to be shown.
*/
void onOverviewShown(boolean triggeredFromAltTab) = 7;
/**
* Sent when overview is to be hidden.
*/
void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) = 8;
/**
* Sent when device assistant changes its default assistant whether it is available or not.
* @param longPressHomeEnabled if 3-button nav assistant can be invoked or not
*/
void onAssistantAvailable(boolean available, boolean longPressHomeEnabled) = 13;
/**
* Sent when the assistant changes how visible it is to the user.
*/
void onAssistantVisibilityChanged(float visibility) = 14;
/**
* Sent when the assistant has been invoked with the given type (defined in AssistManager) and
* should be shown. This method should be used if SystemUiProxy#setAssistantOverridesRequested
* was previously called including this invocation type.
*/
void onAssistantOverrideInvoked(int invocationType) = 28;
/**
* Sent when some system ui state changes.
*/
void onSystemUiStateChanged(long stateFlags) = 16;
/**
* Sent when suggested rotation button could be shown
*/
void onRotationProposal(int rotation, boolean isValid) = 18;
/**
* Sent when disable flags change
*/
void disable(int displayId, int state1, int state2, boolean animate) = 19;
/**
* Sent when behavior changes. See WindowInsetsController#@Behavior
*/
void onSystemBarAttributesChanged(int displayId, int behavior) = 20;
/**
* Sent when the desired dark intensity of the nav buttons has changed
*/
void onNavButtonsDarkIntensityChanged(float darkIntensity) = 22;
/**
* Sent when when navigation bar luma sampling is enabled or disabled.
*/
void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) = 23;
/**
* Sent when split keyboard shortcut is triggered to enter stage split.
*/
void enterStageSplitFromRunningApp(boolean leftOrTop) = 25;
/**
* Sent when the task bar stash state is toggled.
*/
void onTaskbarToggled() = 27;
}
@@ -0,0 +1,171 @@
/*
* Copyright (C) 2017 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 com.android.systemui.shared.recents;
import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.UserHandle;
import android.view.MotionEvent;
import com.android.internal.util.ScreenshotRequest;
import com.android.systemui.shared.recents.model.Task;
/**
* Temporary callbacks into SystemUI.
*/
interface ISystemUiProxy {
/**
* Begins screen pinning on the provided {@param taskId}.
*/
oneway void startScreenPinning(int taskId) = 1;
/**
* Notifies SystemUI that Overview is shown.
*/
oneway void onOverviewShown(boolean fromHome) = 6;
/**
* Proxies motion events from the homescreen UI to the status bar. Only called when
* swipe down is detected on WORKSPACE. The sender guarantees the following order of events on
* the tracking pointer.
*
* Normal gesture: DOWN, MOVE/POINTER_DOWN/POINTER_UP)*, UP or CANCLE
*/
oneway void onStatusBarTouchEvent(in MotionEvent event) = 9;
/**
* Proxies the assistant gesture's progress started from navigation bar.
*/
oneway void onAssistantProgress(float progress) = 12;
/**
* Proxies the assistant gesture fling velocity (in pixels per millisecond) upon completion.
* Velocity is 0 for drag gestures.
*/
oneway void onAssistantGestureCompletion(float velocity) = 18;
/**
* Start the assistant.
*/
oneway void startAssistant(in Bundle bundle) = 13;
/**
* Indicates that the given Assist invocation types should be handled by Launcher via
* OverviewProxy#onAssistantOverrideInvoked and should not be invoked by SystemUI.
*
* @param invocationTypes The invocation types that will henceforth be handled via
* OverviewProxy (Launcher); other invocation types should be handled by SysUI.
*/
oneway void setAssistantOverridesRequested(in int[] invocationTypes) = 53;
/**
* Notifies that the accessibility button in the system's navigation area has been clicked
*/
oneway void notifyAccessibilityButtonClicked(int displayId) = 15;
/**
* Notifies that the accessibility button in the system's navigation area has been long clicked
*/
oneway void notifyAccessibilityButtonLongClicked() = 16;
/**
* Ends the system screen pinning.
*/
oneway void stopScreenPinning() = 17;
/**
* Notifies that quickstep will switch to a new task
* @param rotation indicates which Surface.Rotation the gesture was started in
*/
oneway void notifyPrioritizedRotation(int rotation) = 25;
/**
* Notifies to expand notification panel.
*/
oneway void expandNotificationPanel() = 29;
/**
* Notifies SystemUI to invoke Back.
*/
oneway void onBackPressed() = 44;
/** Sets home rotation enabled. */
oneway void setHomeRotationEnabled(boolean enabled) = 45;
/** Notifies when taskbar status updated */
oneway void notifyTaskbarStatus(boolean visible, boolean stashed) = 47;
/**
* Notifies sysui when taskbar requests autoHide to stop auto-hiding
* If called to suspend, caller is also responsible for calling this method to un-suspend
* @param suspend should be true to stop auto-hide, false to resume normal behavior
*/
oneway void notifyTaskbarAutohideSuspend(boolean suspend) = 48;
/**
* Notifies SystemUI to invoke IME Switcher.
*/
oneway void onImeSwitcherPressed() = 49;
/**
* Notifies to toggle notification panel.
*/
oneway void toggleNotificationPanel() = 50;
/**
* Handle the screenshot request.
*/
oneway void takeScreenshot(in ScreenshotRequest request) = 51;
/**
* Dispatches trackpad status bar motion event to the notification shade. Currently these events
* are from the input monitor in {@link TouchInteractionService}. This is different from
* {@link #onStatusBarTouchEvent} above in that, this directly dispatches motion events to the
* notification shade, while {@link #onStatusBarTouchEvent} relies on setting the launcher
* window slippery to allow the frameworks to route those events after passing the initial
* threshold.
*/
oneway void onStatusBarTrackpadEvent(in MotionEvent event) = 52;
/**
* Animate the nav bar being long-pressed.
*
* @param isTouchDown {@code true} if the button is starting to be pressed ({@code false} if
* released or canceled)
* @param shrink {@code true} if the handle should shrink, {@code false} if it should grow
* @param durationMs how long the animation should take (for the {@code isTouchDown} case, this
* should be the same as the amount of time to trigger a long-press)
*/
oneway void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) = 54;
/**
* Set the override value for home button long press duration in ms and slop multiplier and
* haptic.
*/
oneway void setOverrideHomeButtonLongPress(long duration, float slopMultiplier, boolean haptic)
= 55;
/**
* Notifies to toggle quick settings panel.
*/
oneway void toggleQuickSettingsPanel() = 56;
// Next id = 57
}
@@ -0,0 +1,19 @@
/*
* Copyright (C) 2020 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 com.android.systemui.shared.recents.model;
parcelable Task.TaskKey;
@@ -0,0 +1,367 @@
/*
* Copyright (C) 2014 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 com.android.systemui.shared.recents.model;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE;
import android.app.ActivityManager;
import android.app.ActivityManager.TaskDescription;
import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.ViewDebug;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.util.ArrayUtils;
import java.io.PrintWriter;
import java.util.Objects;
/**
* A task in the recent tasks list.
* TODO: Move this into Launcher or see if we can remove now
*/
public class Task {
public static final String TAG = "Task";
/**
* The Task Key represents the unique primary key for the task
*/
public static class TaskKey implements Parcelable {
@ViewDebug.ExportedProperty(category="recents")
public final int id;
@ViewDebug.ExportedProperty(category="recents")
public int windowingMode;
@ViewDebug.ExportedProperty(category="recents")
@NonNull
public final Intent baseIntent;
@ViewDebug.ExportedProperty(category="recents")
public final int userId;
@ViewDebug.ExportedProperty(category="recents")
public long lastActiveTime;
/**
* The id of the task was running from which display.
*/
@ViewDebug.ExportedProperty(category = "recents")
public final int displayId;
// The source component name which started this task
public final ComponentName sourceComponent;
private int mHashCode;
public TaskKey(TaskInfo t) {
ComponentName sourceComponent = t.origActivity != null
// Activity alias if there is one
? t.origActivity
// The real activity if there is no alias (or the target if there is one)
: t.realActivity;
this.id = t.taskId;
this.windowingMode = t.configuration.windowConfiguration.getWindowingMode();
this.baseIntent = t.baseIntent;
this.sourceComponent = sourceComponent;
this.userId = t.userId;
this.lastActiveTime = t.lastActiveTime;
this.displayId = t.displayId;
updateHashCode();
}
public TaskKey(int id, int windowingMode, @NonNull Intent intent,
ComponentName sourceComponent, int userId, long lastActiveTime) {
this.id = id;
this.windowingMode = windowingMode;
this.baseIntent = intent;
this.sourceComponent = sourceComponent;
this.userId = userId;
this.lastActiveTime = lastActiveTime;
this.displayId = DEFAULT_DISPLAY;
updateHashCode();
}
public TaskKey(int id, int windowingMode, @NonNull Intent intent,
ComponentName sourceComponent, int userId, long lastActiveTime, int displayId) {
this.id = id;
this.windowingMode = windowingMode;
this.baseIntent = intent;
this.sourceComponent = sourceComponent;
this.userId = userId;
this.lastActiveTime = lastActiveTime;
this.displayId = displayId;
updateHashCode();
}
public void setWindowingMode(int windowingMode) {
this.windowingMode = windowingMode;
updateHashCode();
}
public ComponentName getComponent() {
return this.baseIntent.getComponent();
}
public String getPackageName() {
if (this.baseIntent.getComponent() != null) {
return this.baseIntent.getComponent().getPackageName();
}
return this.baseIntent.getPackage();
}
public int getId() {
return id;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof TaskKey)) {
return false;
}
TaskKey otherKey = (TaskKey) o;
return id == otherKey.id
&& windowingMode == otherKey.windowingMode
&& userId == otherKey.userId;
}
@Override
public int hashCode() {
return mHashCode;
}
@Override
public String toString() {
return "id=" + id + " windowingMode=" + windowingMode + " user=" + userId
+ " lastActiveTime=" + lastActiveTime;
}
private void updateHashCode() {
mHashCode = Objects.hash(id, windowingMode, userId);
}
public static final Parcelable.Creator<TaskKey> CREATOR =
new Parcelable.Creator<TaskKey>() {
@Override
public TaskKey createFromParcel(Parcel source) {
return TaskKey.readFromParcel(source);
}
@Override
public TaskKey[] newArray(int size) {
return new TaskKey[size];
}
};
@Override
public final void writeToParcel(Parcel parcel, int flags) {
parcel.writeInt(id);
parcel.writeInt(windowingMode);
parcel.writeTypedObject(baseIntent, flags);
parcel.writeInt(userId);
parcel.writeLong(lastActiveTime);
parcel.writeInt(displayId);
parcel.writeTypedObject(sourceComponent, flags);
}
private static TaskKey readFromParcel(Parcel parcel) {
int id = parcel.readInt();
int windowingMode = parcel.readInt();
Intent baseIntent = parcel.readTypedObject(Intent.CREATOR);
int userId = parcel.readInt();
long lastActiveTime = parcel.readLong();
int displayId = parcel.readInt();
ComponentName sourceComponent = parcel.readTypedObject(ComponentName.CREATOR);
return new TaskKey(id, windowingMode, baseIntent, sourceComponent, userId,
lastActiveTime, displayId);
}
@Override
public int describeContents() {
return 0;
}
}
@ViewDebug.ExportedProperty(deepExport=true, prefix="key_")
public TaskKey key;
/**
* The icon is the task description icon (if provided), which falls back to the activity icon,
* which can then fall back to the application icon.
*/
@Nullable public Drawable icon;
@Nullable public ThumbnailData thumbnail;
@ViewDebug.ExportedProperty(category="recents")
public String title;
@ViewDebug.ExportedProperty(category="recents")
public String titleDescription;
@ViewDebug.ExportedProperty(category="recents")
public int colorPrimary;
@ViewDebug.ExportedProperty(category="recents")
public int colorBackground;
/**
* The task description for this task, only used to reload task icons.
*/
public TaskDescription taskDescription;
@ViewDebug.ExportedProperty(category="recents")
public boolean isDockable;
@ViewDebug.ExportedProperty(category="recents")
public ComponentName topActivity;
@ViewDebug.ExportedProperty(category="recents")
public boolean isLocked;
public Point positionInParent;
public Rect appBounds;
// Last snapshot data, only used for recent tasks
public ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData =
new ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData();
public Task() {
// Do nothing
}
/**
* Creates a task object from the provided task info
*/
public static Task from(TaskKey taskKey, TaskInfo taskInfo, boolean isLocked) {
ActivityManager.TaskDescription td = taskInfo.taskDescription;
// Also consider undefined activity type to include tasks in overview right after rebooting
// the device.
final boolean isDockable = taskInfo.supportsMultiWindow
&& ArrayUtils.contains(
CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, taskInfo.getWindowingMode())
&& (taskInfo.getActivityType() == ACTIVITY_TYPE_UNDEFINED
|| ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType()));
return new Task(taskKey,
td != null ? td.getPrimaryColor() : 0,
td != null ? td.getBackgroundColor() : 0, isDockable , isLocked, td,
taskInfo.topActivity);
}
public Task(TaskKey key) {
this.key = key;
this.taskDescription = new TaskDescription();
}
public Task(Task other) {
this(other.key, other.colorPrimary, other.colorBackground, other.isDockable,
other.isLocked, other.taskDescription, other.topActivity);
lastSnapshotData.set(other.lastSnapshotData);
positionInParent = other.positionInParent;
appBounds = other.appBounds;
}
/**
* Use {@link Task#Task(Task)}.
*/
@Deprecated
public Task(TaskKey key, int colorPrimary, int colorBackground,
boolean isDockable, boolean isLocked, TaskDescription taskDescription,
ComponentName topActivity) {
this.key = key;
this.colorPrimary = colorPrimary;
this.colorBackground = colorBackground;
this.taskDescription = taskDescription;
this.isDockable = isDockable;
this.isLocked = isLocked;
this.topActivity = topActivity;
}
/**
* Returns the top activity component.
*/
public ComponentName getTopComponent() {
return topActivity != null
? topActivity
: key.baseIntent.getComponent();
}
public void setLastSnapshotData(ActivityManager.RecentTaskInfo rawTask) {
lastSnapshotData.set(rawTask.lastSnapshotData);
}
public TaskKey getKey() {
return key;
}
/**
* Returns the visible width to height ratio. Returns 0f if snapshot data is not available.
*/
public float getVisibleThumbnailRatio(boolean clipInsets) {
if (lastSnapshotData.taskSize == null || lastSnapshotData.contentInsets == null) {
return 0f;
}
float availableWidth = lastSnapshotData.taskSize.x;
float availableHeight = lastSnapshotData.taskSize.y;
if (clipInsets) {
availableWidth -=
(lastSnapshotData.contentInsets.left + lastSnapshotData.contentInsets.right);
availableHeight -=
(lastSnapshotData.contentInsets.top + lastSnapshotData.contentInsets.bottom);
}
return availableWidth / availableHeight;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Task)) {
return false;
}
// Check that the id matches
Task t = (Task) o;
return key.equals(t.key);
}
@Override
public String toString() {
return "[" + key.toString() + "] " + title;
}
public void dump(String prefix, PrintWriter writer) {
writer.print(prefix); writer.print(key);
if (!isDockable) {
writer.print(" dockable=N");
}
if (isLocked) {
writer.print(" locked=Y");
}
writer.print(" "); writer.print(title);
writer.println();
}
}
@@ -0,0 +1,101 @@
/*
* Copyright (C) 2016 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 com.android.systemui.shared.recents.model
import android.app.WindowConfiguration
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Bitmap.Config.ARGB_8888
import android.graphics.Color
import android.graphics.Rect
import android.util.Log
import android.view.WindowInsetsController.Appearance
import android.window.TaskSnapshot
/** Data for a single thumbnail. */
data class ThumbnailData(
val thumbnail: Bitmap? = null,
var orientation: Int = Configuration.ORIENTATION_UNDEFINED,
@JvmField var rotation: Int = WindowConfiguration.ROTATION_UNDEFINED,
@JvmField var insets: Rect = Rect(),
@JvmField var letterboxInsets: Rect = Rect(),
@JvmField var reducedResolution: Boolean = false,
@JvmField var isRealSnapshot: Boolean = true,
var isTranslucent: Boolean = false,
@JvmField var windowingMode: Int = WindowConfiguration.WINDOWING_MODE_UNDEFINED,
@JvmField @Appearance var appearance: Int = 0,
@JvmField var scale: Float = 1f,
var snapshotId: Long = 0,
) {
fun recycleBitmap() {
thumbnail?.recycle()
}
companion object {
private fun makeThumbnail(snapshot: TaskSnapshot): Bitmap {
var thumbnail: Bitmap? = null
try {
snapshot.hardwareBuffer?.use { buffer ->
thumbnail = Bitmap.wrapHardwareBuffer(buffer, snapshot.colorSpace)
}
} catch (ex: IllegalArgumentException) {
// TODO(b/157562905): Workaround for a crash when we get a snapshot without this
// state
Log.e(
"ThumbnailData",
"Unexpected snapshot without USAGE_GPU_SAMPLED_IMAGE: " +
"${snapshot.hardwareBuffer}",
ex
)
}
return thumbnail
?: Bitmap.createBitmap(snapshot.taskSize.x, snapshot.taskSize.y, ARGB_8888).apply {
eraseColor(Color.BLACK)
}
}
@JvmStatic
fun wrap(taskIds: IntArray?, snapshots: Array<TaskSnapshot>?): HashMap<Int, ThumbnailData> {
return hashMapOf<Int, ThumbnailData>().apply {
if (taskIds != null && snapshots != null && taskIds.size == snapshots.size) {
repeat(snapshots.size) { put(taskIds[it], fromSnapshot(snapshots[it])) }
}
}
}
@JvmStatic
fun fromSnapshot(snapshot: TaskSnapshot): ThumbnailData {
val thumbnail = makeThumbnail(snapshot)
return ThumbnailData(
thumbnail = thumbnail,
insets = Rect(snapshot.contentInsets),
letterboxInsets = Rect(snapshot.letterboxInsets),
orientation = snapshot.orientation,
rotation = snapshot.rotation,
reducedResolution = snapshot.isLowResolution,
// TODO(b/149579527): Pass task size instead of computing scale.
// Assume width and height were scaled the same; compute scale only for width
scale = thumbnail.width.toFloat() / snapshot.taskSize.x,
isRealSnapshot = snapshot.isRealSnapshot,
isTranslucent = snapshot.isTranslucent,
windowingMode = snapshot.windowingMode,
appearance = snapshot.appearance,
snapshotId = snapshot.id,
)
}
}
}
@@ -0,0 +1,219 @@
package com.android.systemui.shared.recents.utilities;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.wm.shell.util.SplitBounds;
/**
* Utility class to position the thumbnail in the TaskView
*/
public class PreviewPositionHelper {
public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f;
/**
* Specifies that a stage is positioned at the top half of the screen if
* in portrait mode or at the left half of the screen if in landscape mode.
* TODO(b/254378592): Remove after consolidation
*/
public static final int STAGE_POSITION_TOP_OR_LEFT = 0;
/**
* Specifies that a stage is positioned at the bottom half of the screen if
* in portrait mode or at the right half of the screen if in landscape mode.
* TODO(b/254378592): Remove after consolidation
*/
public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1;
private final Matrix mMatrix = new Matrix();
private boolean mIsOrientationChanged;
private SplitBounds mSplitBounds;
private int mDesiredStagePosition;
public Matrix getMatrix() {
return mMatrix;
}
public void setOrientationChanged(boolean orientationChanged) {
mIsOrientationChanged = orientationChanged;
}
public boolean isOrientationChanged() {
return mIsOrientationChanged;
}
public void setSplitBounds(SplitBounds splitBounds, int desiredStagePosition) {
mSplitBounds = splitBounds;
mDesiredStagePosition = desiredStagePosition;
}
/**
* Updates the matrix based on the provided parameters
*/
public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData,
int canvasWidth, int canvasHeight, boolean isLargeScreen, int currentRotation,
boolean isRtl) {
boolean isRotated = false;
boolean isOrientationDifferent;
int thumbnailRotation = thumbnailData.rotation;
int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
RectF thumbnailClipHint = new RectF();
float scale = thumbnailData.scale;
final float thumbnailScale;
// Landscape vs portrait change.
// Note: Disable rotation in grid layout.
boolean windowingModeSupportsRotation =
thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN && !isLargeScreen;
isOrientationDifferent = isOrientationChange(deltaRotate)
&& windowingModeSupportsRotation;
if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) {
// If we haven't measured , skip the thumbnail drawing and only draw the background
// color
thumbnailScale = 0f;
} else {
// Rotate the screenshot if not in multi-window mode
isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
float surfaceWidth = thumbnailBounds.width() / scale;
float surfaceHeight = thumbnailBounds.height() / scale;
float availableWidth = surfaceWidth;
float availableHeight = surfaceHeight;
float canvasAspect = canvasWidth / (float) canvasHeight;
float availableAspect = isRotated
? availableHeight / availableWidth
: availableWidth / availableHeight;
boolean isAspectLargelyDifferent =
Utilities.isRelativePercentDifferenceGreaterThan(canvasAspect,
availableAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT);
if (isRotated && isAspectLargelyDifferent) {
// Do not rotate thumbnail if it would not improve fit
isRotated = false;
isOrientationDifferent = false;
}
if (isAspectLargelyDifferent) {
// Crop letterbox insets if insets isn't already clipped
thumbnailClipHint.left = thumbnailData.letterboxInsets.left;
thumbnailClipHint.right = thumbnailData.letterboxInsets.right;
thumbnailClipHint.top = thumbnailData.letterboxInsets.top;
thumbnailClipHint.bottom = thumbnailData.letterboxInsets.bottom;
availableWidth = surfaceWidth
- (thumbnailClipHint.left + thumbnailClipHint.right);
availableHeight = surfaceHeight
- (thumbnailClipHint.top + thumbnailClipHint.bottom);
}
final float targetW, targetH;
if (isOrientationDifferent) {
targetW = canvasHeight;
targetH = canvasWidth;
} else {
targetW = canvasWidth;
targetH = canvasHeight;
}
float targetAspect = targetW / targetH;
// Update the clipHint such that
// > the final clipped position has same aspect ratio as requested by canvas
// > first fit the width and crop the extra height
// > if that will leave empty space, fit the height and crop the width instead
float croppedWidth = availableWidth;
float croppedHeight = croppedWidth / targetAspect;
if (croppedHeight > availableHeight) {
croppedHeight = availableHeight;
if (croppedHeight < targetH) {
croppedHeight = Math.min(targetH, surfaceHeight);
}
croppedWidth = croppedHeight * targetAspect;
// One last check in case the task aspect radio messed up something
if (croppedWidth > surfaceWidth) {
croppedWidth = surfaceWidth;
croppedHeight = croppedWidth / targetAspect;
}
}
// Update the clip hints. Align to 0,0, crop the remaining.
if (isRtl) {
thumbnailClipHint.left += availableWidth - croppedWidth;
if (thumbnailClipHint.right < 0) {
thumbnailClipHint.left += thumbnailClipHint.right;
thumbnailClipHint.right = 0;
}
} else {
thumbnailClipHint.right += availableWidth - croppedWidth;
if (thumbnailClipHint.left < 0) {
thumbnailClipHint.right += thumbnailClipHint.left;
thumbnailClipHint.left = 0;
}
}
thumbnailClipHint.bottom += availableHeight - croppedHeight;
if (thumbnailClipHint.top < 0) {
thumbnailClipHint.bottom += thumbnailClipHint.top;
thumbnailClipHint.top = 0;
} else if (thumbnailClipHint.bottom < 0) {
thumbnailClipHint.top += thumbnailClipHint.bottom;
thumbnailClipHint.bottom = 0;
}
thumbnailScale = targetW / (croppedWidth * scale);
}
if (!isRotated) {
mMatrix.setTranslate(
-thumbnailClipHint.left * scale,
-thumbnailClipHint.top * scale);
} else {
setThumbnailRotation(deltaRotate, thumbnailBounds);
}
mMatrix.postScale(thumbnailScale, thumbnailScale);
mIsOrientationChanged = isOrientationDifferent;
}
private int getRotationDelta(int oldRotation, int newRotation) {
int delta = newRotation - oldRotation;
if (delta < 0) delta += 4;
return delta;
}
/**
* @param deltaRotation the number of 90 degree turns from the current orientation
* @return {@code true} if the change in rotation results in a shift from landscape to
* portrait or vice versa, {@code false} otherwise
*/
private boolean isOrientationChange(int deltaRotation) {
return deltaRotation == ROTATION_90 || deltaRotation == ROTATION_270;
}
private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) {
float translateX = 0;
float translateY = 0;
mMatrix.setRotate(90 * deltaRotate);
switch (deltaRotate) { /* Counter-clockwise */
case ROTATION_90:
translateX = thumbnailPosition.height();
break;
case ROTATION_270:
translateY = thumbnailPosition.width();
break;
case ROTATION_180:
translateX = thumbnailPosition.width();
translateY = thumbnailPosition.height();
break;
}
mMatrix.postTranslate(translateX, translateY);
}
}
@@ -0,0 +1,158 @@
/*
* Copyright (C) 2018 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 com.android.systemui.shared.recents.utilities;
import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Rect;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.DisplayMetrics;
import android.view.Surface;
import android.view.WindowManager;
/* Common code */
public class Utilities {
private static final float TABLET_MIN_DPS = 600;
/**
* Posts a runnable on a handler at the front of the queue ignoring any sync barriers.
*/
public static void postAtFrontOfQueueAsynchronously(Handler h, Runnable r) {
Message msg = h.obtainMessage().setCallback(r);
h.sendMessageAtFrontOfQueue(msg);
}
public static boolean isRotationAnimationCCW(int from, int to) {
// All 180deg WM rotation animations are CCW, match that
if (from == Surface.ROTATION_0 && to == Surface.ROTATION_90) return false;
if (from == Surface.ROTATION_0 && to == Surface.ROTATION_180) return true; //180d so CCW
if (from == Surface.ROTATION_0 && to == Surface.ROTATION_270) return true;
if (from == Surface.ROTATION_90 && to == Surface.ROTATION_0) return true;
if (from == Surface.ROTATION_90 && to == Surface.ROTATION_180) return false;
if (from == Surface.ROTATION_90 && to == Surface.ROTATION_270) return true; //180d so CCW
if (from == Surface.ROTATION_180 && to == Surface.ROTATION_0) return true; //180d so CCW
if (from == Surface.ROTATION_180 && to == Surface.ROTATION_90) return true;
if (from == Surface.ROTATION_180 && to == Surface.ROTATION_270) return false;
if (from == Surface.ROTATION_270 && to == Surface.ROTATION_0) return false;
if (from == Surface.ROTATION_270 && to == Surface.ROTATION_90) return true; //180d so CCW
if (from == Surface.ROTATION_270 && to == Surface.ROTATION_180) return true;
return false; // Default
}
/**
* Compares the ratio of two quantities and returns whether that ratio is greater than the
* provided bound. Order of quantities does not matter. Bound should be a decimal representation
* of a percentage.
*/
public static boolean isRelativePercentDifferenceGreaterThan(float first, float second,
float bound) {
return (Math.abs(first - second) / Math.abs((first + second) / 2.0f)) > bound;
}
/** Calculates the constrast between two colors, using the algorithm provided by the WCAG v2. */
public static float computeContrastBetweenColors(int bg, int fg) {
float bgR = Color.red(bg) / 255f;
float bgG = Color.green(bg) / 255f;
float bgB = Color.blue(bg) / 255f;
bgR = (bgR < 0.03928f) ? bgR / 12.92f : (float) Math.pow((bgR + 0.055f) / 1.055f, 2.4f);
bgG = (bgG < 0.03928f) ? bgG / 12.92f : (float) Math.pow((bgG + 0.055f) / 1.055f, 2.4f);
bgB = (bgB < 0.03928f) ? bgB / 12.92f : (float) Math.pow((bgB + 0.055f) / 1.055f, 2.4f);
float bgL = 0.2126f * bgR + 0.7152f * bgG + 0.0722f * bgB;
float fgR = Color.red(fg) / 255f;
float fgG = Color.green(fg) / 255f;
float fgB = Color.blue(fg) / 255f;
fgR = (fgR < 0.03928f) ? fgR / 12.92f : (float) Math.pow((fgR + 0.055f) / 1.055f, 2.4f);
fgG = (fgG < 0.03928f) ? fgG / 12.92f : (float) Math.pow((fgG + 0.055f) / 1.055f, 2.4f);
fgB = (fgB < 0.03928f) ? fgB / 12.92f : (float) Math.pow((fgB + 0.055f) / 1.055f, 2.4f);
float fgL = 0.2126f * fgR + 0.7152f * fgG + 0.0722f * fgB;
return Math.abs((fgL + 0.05f) / (bgL + 0.05f));
}
/**
* @return the clamped {@param value} between the provided {@param min} and {@param max}.
*/
public static float clamp(float value, float min, float max) {
return Math.max(min, Math.min(max, value));
}
/**
* @return updated set of flags from InputMethodService based off {@param oldHints}
* Leaves original hints unmodified
*/
public static int calculateBackDispositionHints(int oldHints, int backDisposition,
boolean imeShown, boolean showImeSwitcher) {
int hints = oldHints;
switch (backDisposition) {
case InputMethodService.BACK_DISPOSITION_DEFAULT:
case InputMethodService.BACK_DISPOSITION_WILL_NOT_DISMISS:
case InputMethodService.BACK_DISPOSITION_WILL_DISMISS:
if (imeShown) {
hints |= NAVIGATION_HINT_BACK_ALT;
} else {
hints &= ~NAVIGATION_HINT_BACK_ALT;
}
break;
case InputMethodService.BACK_DISPOSITION_ADJUST_NOTHING:
hints &= ~NAVIGATION_HINT_BACK_ALT;
break;
}
if (imeShown) {
hints |= NAVIGATION_HINT_IME_SHOWN;
} else {
hints &= ~NAVIGATION_HINT_IME_SHOWN;
}
if (showImeSwitcher) {
hints |= NAVIGATION_HINT_IME_SWITCHER_SHOWN;
} else {
hints &= ~NAVIGATION_HINT_IME_SWITCHER_SHOWN;
}
return hints;
}
/** @return whether or not {@param context} represents that of a large screen device or not */
@TargetApi(Build.VERSION_CODES.R)
public static boolean isLargeScreen(Context context) {
return isLargeScreen(context.getSystemService(WindowManager.class), context.getResources());
}
/** @return whether or not {@param context} represents that of a large screen device or not */
public static boolean isLargeScreen(WindowManager windowManager, Resources resources) {
final Rect bounds = windowManager.getCurrentWindowMetrics().getBounds();
float smallestWidth = dpiFromPx(Math.min(bounds.width(), bounds.height()),
resources.getConfiguration().densityDpi);
return smallestWidth >= TABLET_MIN_DPS;
}
public static float dpiFromPx(float size, int densityDpi) {
float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
return (size / densityRatio);
}
}
@@ -0,0 +1,55 @@
/*
* 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 com.android.systemui.shared.recents.utilities;
import android.view.View;
/**
* Shows view ripples by toggling the provided Views "pressed" state.
* Ripples 4 times.
*/
public class ViewRippler {
private static final int RIPPLE_OFFSET_MS = 50;
private static final int RIPPLE_INTERVAL_MS = 2000;
private View mRoot;
public void start(View root) {
stop(); // Stop any pending ripple animations
mRoot = root;
// Schedule pending ripples, offset the 1st to avoid problems with visibility change
mRoot.postOnAnimationDelayed(mRipple, RIPPLE_OFFSET_MS);
mRoot.postOnAnimationDelayed(mRipple, RIPPLE_INTERVAL_MS);
mRoot.postOnAnimationDelayed(mRipple, 2 * RIPPLE_INTERVAL_MS);
mRoot.postOnAnimationDelayed(mRipple, 3 * RIPPLE_INTERVAL_MS);
mRoot.postOnAnimationDelayed(mRipple, 4 * RIPPLE_INTERVAL_MS);
}
public void stop() {
if (mRoot != null) mRoot.removeCallbacks(mRipple);
}
private final Runnable mRipple = new Runnable() {
@Override
public void run() { // Cause the ripple to fire via false presses
if (!mRoot.isAttachedToWindow()) return;
mRoot.setPressed(true /* pressed */);
mRoot.setPressed(false /* pressed */);
}
};
}
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2017 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 com.android.systemui.shared.recents.view;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.view.AppTransitionAnimationSpec;
/**
* Wraps the internal app transition animation spec.
*/
public class AppTransitionAnimationSpecCompat {
private int mTaskId;
private Bitmap mBuffer;
private Rect mRect;
public AppTransitionAnimationSpecCompat(int taskId, Bitmap buffer, Rect rect) {
mTaskId = taskId;
mBuffer = buffer;
mRect = rect;
}
public AppTransitionAnimationSpec toAppTransitionAnimationSpec() {
return new AppTransitionAnimationSpec(mTaskId,
mBuffer != null ? mBuffer.getHardwareBuffer() : null, mRect);
}
}
@@ -0,0 +1,93 @@
/*
* Copyright (C) 2017 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 com.android.systemui.shared.recents.view;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.view.AppTransitionAnimationSpec;
import android.view.IAppTransitionAnimationSpecsFuture;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* To be implemented by a particular animation to asynchronously provide the animation specs for a
* particular transition.
*/
public abstract class AppTransitionAnimationSpecsFuture {
private final Handler mHandler;
private FutureTask<List<AppTransitionAnimationSpecCompat>> mComposeTask = new FutureTask<>(
new Callable<List<AppTransitionAnimationSpecCompat>>() {
@Override
public List<AppTransitionAnimationSpecCompat> call() throws Exception {
return composeSpecs();
}
});
private final IAppTransitionAnimationSpecsFuture mFuture =
new IAppTransitionAnimationSpecsFuture.Stub() {
@Override
public AppTransitionAnimationSpec[] get() throws RemoteException {
try {
if (!mComposeTask.isDone()) {
mHandler.post(mComposeTask);
}
List<AppTransitionAnimationSpecCompat> specs = mComposeTask.get();
// Clear reference to the compose task this future holds onto the reference to it's
// implementation (which can leak references to the bitmap it creates for the
// transition)
mComposeTask = null;
if (specs == null) {
return null;
}
AppTransitionAnimationSpec[] arr = new AppTransitionAnimationSpec[specs.size()];
for (int i = 0; i < specs.size(); i++) {
arr[i] = specs.get(i).toAppTransitionAnimationSpec();
}
return arr;
} catch (Exception e) {
return null;
}
}
};
public AppTransitionAnimationSpecsFuture(Handler handler) {
mHandler = handler;
}
/**
* Returns the future to handle the call from window manager.
*/
public final IAppTransitionAnimationSpecsFuture getFuture() {
return mFuture;
}
/**
* Called ahead of the future callback to compose the specs to be returned in the future.
*/
public final void composeSpecsSynchronous() {
if (Looper.myLooper() != mHandler.getLooper()) {
throw new RuntimeException("composeSpecsSynchronous() called from wrong looper");
}
mComposeTask.run();
}
public abstract List<AppTransitionAnimationSpecCompat> composeSpecs();
}
@@ -0,0 +1,115 @@
/*
* Copyright (C) 2017 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 com.android.systemui.shared.recents.view;
import android.app.ActivityOptions;
import android.app.ActivityOptions.OnAnimationStartedListener;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.GraphicBuffer;
import android.graphics.Picture;
import android.os.Bundle;
import android.os.Handler;
import android.os.IRemoteCallback;
import android.os.RemoteException;
import android.view.View;
import java.util.function.Consumer;
/**
* A helper class to create transitions to/from an App to Recents.
*/
public class RecentsTransition {
/**
* Creates a new transition aspect scaled transition activity options.
*/
public static ActivityOptions createAspectScaleAnimation(Context context, Handler handler,
boolean scaleUp, AppTransitionAnimationSpecsFuture animationSpecsFuture,
final Runnable animationStartCallback) {
final OnAnimationStartedListener animStartedListener = new OnAnimationStartedListener() {
private boolean mHandled;
@Override
public void onAnimationStarted(long elapsedRealTime) {
// OnAnimationStartedListener can be called numerous times, so debounce here to
// prevent multiple callbacks
if (mHandled) {
return;
}
mHandled = true;
if (animationStartCallback != null) {
animationStartCallback.run();
}
}
};
final ActivityOptions opts = ActivityOptions.makeMultiThumbFutureAspectScaleAnimation(
context, handler,
animationSpecsFuture != null ? animationSpecsFuture.getFuture() : null,
animStartedListener, scaleUp);
return opts;
}
/**
* Wraps a animation-start callback in a binder that can be called from window manager.
*/
public static IRemoteCallback wrapStartedListener(final Handler handler,
final Runnable animationStartCallback) {
if (animationStartCallback == null) {
return null;
}
return new IRemoteCallback.Stub() {
@Override
public void sendResult(Bundle data) throws RemoteException {
handler.post(animationStartCallback);
}
};
}
/**
* @return a {@link GraphicBuffer} with the {@param view} drawn into it. Result can be null if
* we were unable to allocate a hardware bitmap.
*/
public static Bitmap drawViewIntoHardwareBitmap(int width, int height, final View view,
final float scale, final int eraseColor) {
return createHardwareBitmap(width, height, new Consumer<Canvas>() {
@Override
public void accept(Canvas c) {
c.scale(scale, scale);
if (eraseColor != 0) {
c.drawColor(eraseColor);
}
if (view != null) {
view.draw(c);
}
}
});
}
/**
* @return a hardware {@link Bitmap} after being drawn with the {@param consumer}. Result can be
* null if we were unable to allocate a hardware bitmap.
*/
public static Bitmap createHardwareBitmap(int width, int height, Consumer<Canvas> consumer) {
final Picture picture = new Picture();
final Canvas canvas = picture.beginRecording(width, height);
consumer.accept(canvas);
picture.endRecording();
return Bitmap.createBitmap(picture);
}
}
@@ -0,0 +1,10 @@
package com.android.systemui.shared.regionsampling
/**
* Enum for whether clock region is dark or light.
*/
enum class RegionDarkness(val isDark: Boolean) {
DEFAULT(false),
DARK(true),
LIGHT(false)
}
@@ -0,0 +1,279 @@
/*
* Copyright (C) 2022 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 com.android.systemui.shared.regionsampling
import android.app.WallpaperColors
import android.app.WallpaperManager
import android.graphics.Color
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.util.Log
import android.view.View
import androidx.annotation.VisibleForTesting
import java.io.PrintWriter
import java.util.concurrent.Executor
/** Class for instance of RegionSamplingHelper */
open class RegionSampler
@JvmOverloads
constructor(
val sampledView: View,
val mainExecutor: Executor?,
val bgExecutor: Executor?,
val regionSamplingEnabled: Boolean,
val isLockscreen: Boolean = false,
val wallpaperManager: WallpaperManager? = WallpaperManager.getInstance(sampledView.context),
val updateForegroundColor: UpdateColorCallback,
) : WallpaperManager.LocalWallpaperColorConsumer {
private var regionDarkness = RegionDarkness.DEFAULT
private var samplingBounds = Rect()
private val tmpScreenLocation = IntArray(2)
private var lightForegroundColor = Color.WHITE
private var darkForegroundColor = Color.BLACK
@VisibleForTesting val displaySize = Point()
private var initialSampling: WallpaperColors? = null
/**
* Sets the colors to be used for Dark and Light Foreground.
*
* @param lightColor The color used for Light Foreground.
* @param darkColor The color used for Dark Foreground.
*/
fun setForegroundColors(lightColor: Int, darkColor: Int) {
lightForegroundColor = lightColor
darkForegroundColor = darkColor
}
private val layoutChangedListener =
object : View.OnLayoutChangeListener {
override fun onLayoutChange(
view: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
// don't pass in negative bounds when region is in transition state
if (sampledView.locationOnScreen[0] < 0 || sampledView.locationOnScreen[1] < 0) {
return
}
val currentViewRect = Rect(left, top, right, bottom)
val oldViewRect = Rect(oldLeft, oldTop, oldRight, oldBottom)
if (currentViewRect != oldViewRect) {
stopRegionSampler()
startRegionSampler()
}
}
}
/**
* Determines which foreground color to use based on region darkness.
*
* @return the determined foreground color
*/
fun currentForegroundColor(): Int {
return if (regionDarkness.isDark) {
lightForegroundColor
} else {
darkForegroundColor
}
}
private fun getRegionDarkness(isRegionDark: Boolean): RegionDarkness {
return if (isRegionDark) {
RegionDarkness.DARK
} else {
RegionDarkness.LIGHT
}
}
fun currentRegionDarkness(): RegionDarkness {
return regionDarkness
}
/** Start region sampler */
fun startRegionSampler() {
if (!regionSamplingEnabled) {
if (DEBUG) Log.d(TAG, "startRegionSampler() | RegionSampling flag not enabled")
return
}
sampledView.addOnLayoutChangeListener(layoutChangedListener)
val screenLocationBounds = calculateScreenLocation(sampledView)
if (screenLocationBounds == null) {
if (DEBUG) Log.d(TAG, "startRegionSampler() | passed in null region")
return
}
if (screenLocationBounds.isEmpty) {
if (DEBUG) Log.d(TAG, "startRegionSampler() | passed in empty region")
return
}
val sampledRegionWithOffset = convertBounds(screenLocationBounds)
if (
sampledRegionWithOffset.left < 0.0 ||
sampledRegionWithOffset.right > 1.0 ||
sampledRegionWithOffset.top < 0.0 ||
sampledRegionWithOffset.bottom > 1.0
) {
if (DEBUG)
Log.d(
TAG,
"startRegionSampler() | view out of bounds: $screenLocationBounds | " +
"screen width: ${displaySize.x}, screen height: ${displaySize.y}",
Exception()
)
return
}
val regions = ArrayList<RectF>()
regions.add(sampledRegionWithOffset)
wallpaperManager?.addOnColorsChangedListener(
this,
regions,
if (isLockscreen) WallpaperManager.FLAG_LOCK else WallpaperManager.FLAG_SYSTEM
)
bgExecutor?.execute(
Runnable {
initialSampling =
wallpaperManager?.getWallpaperColors(
if (isLockscreen) WallpaperManager.FLAG_LOCK
else WallpaperManager.FLAG_SYSTEM
)
mainExecutor?.execute { onColorsChanged(sampledRegionWithOffset, initialSampling) }
}
)
}
/** Stop region sampler */
fun stopRegionSampler() {
wallpaperManager?.removeOnColorsChangedListener(this)
sampledView.removeOnLayoutChangeListener(layoutChangedListener)
}
/** Dump region sampler */
fun dump(pw: PrintWriter) {
pw.println("[RegionSampler]")
pw.println("regionSamplingEnabled: $regionSamplingEnabled")
pw.println("regionDarkness: $regionDarkness")
pw.println("lightForegroundColor: ${Integer.toHexString(lightForegroundColor)}")
pw.println("darkForegroundColor: ${Integer.toHexString(darkForegroundColor)}")
pw.println("passed-in sampledView: $sampledView")
pw.println("calculated samplingBounds: $samplingBounds")
pw.println(
"sampledView width: ${sampledView.width}, sampledView height: ${sampledView.height}"
)
pw.println("screen width: ${displaySize.x}, screen height: ${displaySize.y}")
pw.println(
"sampledRegionWithOffset: ${convertBounds(
calculateScreenLocation(sampledView) ?: RectF())}"
)
pw.println(
"initialSampling for ${if (isLockscreen) "lockscreen" else "homescreen" }" +
": $initialSampling"
)
}
fun calculateScreenLocation(sampledView: View): RectF? {
val screenLocation = tmpScreenLocation
/**
* The method getLocationOnScreen is used to obtain the view coordinates relative to its
* left and top edges on the device screen. Directly accessing the X and Y coordinates of
* the view returns the location relative to its parent view instead.
*/
sampledView.getLocationOnScreen(screenLocation)
val left = screenLocation[0]
val top = screenLocation[1]
samplingBounds.left = left
samplingBounds.top = top
samplingBounds.right = left + sampledView.width
samplingBounds.bottom = top + sampledView.height
// ensure never go out of bounds
if (samplingBounds.right > displaySize.x) samplingBounds.right = displaySize.x
if (samplingBounds.bottom > displaySize.y) samplingBounds.bottom = displaySize.y
return RectF(samplingBounds)
}
/**
* Convert the bounds of the region we want to sample from to fractional offsets because
* WallpaperManager requires the bounds to be between [0,1]. The wallpaper is treated as one
* continuous image, so if there are multiple screens, then each screen falls into a fractional
* range. For instance, 4 screens have the ranges [0, 0.25], [0,25, 0.5], [0.5, 0.75], [0.75,
* 1].
*/
fun convertBounds(originalBounds: RectF): RectF {
// TODO(b/265969235): GRAB # PAGES + CURRENT WALLPAPER PAGE # FROM LAUNCHER (--> HS
// Smartspace always on 1st page)
// TODO(b/265968912): remove hard-coded value once LS wallpaper supported
val wallpaperPageNum = 0
val numScreens = 1
val screenWidth = displaySize.x
// TODO: investigate small difference between this and the height reported in go/web-hv
val screenHeight = displaySize.y
val newBounds = RectF()
// horizontal
newBounds.left = ((originalBounds.left / screenWidth) + wallpaperPageNum) / numScreens
newBounds.right = ((originalBounds.right / screenWidth) + wallpaperPageNum) / numScreens
// vertical
newBounds.top = originalBounds.top / screenHeight
newBounds.bottom = originalBounds.bottom / screenHeight
return newBounds
}
init {
sampledView?.context?.display?.getSize(displaySize)
}
override fun onColorsChanged(area: RectF?, colors: WallpaperColors?) {
// update text color when wallpaper color changes
regionDarkness =
getRegionDarkness(
(colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) !=
WallpaperColors.HINT_SUPPORTS_DARK_TEXT
)
if (DEBUG)
Log.d(TAG, "onColorsChanged() | region darkness = $regionDarkness for region $area")
updateForegroundColor()
}
companion object {
private const val TAG = "RegionSampler"
private const val DEBUG = false
}
}
typealias UpdateColorCallback = () -> Unit
@@ -0,0 +1,308 @@
/*
* Copyright (C) 2020 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 com.android.systemui.shared.rotation;
import android.annotation.DimenRes;
import android.annotation.IdRes;
import android.annotation.LayoutRes;
import android.annotation.StringRes;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ActivityInfo.Config;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
import androidx.annotation.BoolRes;
import androidx.core.view.OneShotPreDrawListener;
import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position;
/**
* Containing logic for the rotation button on the physical left bottom corner of the screen.
*/
public class FloatingRotationButton implements RotationButton {
private static final int MARGIN_ANIMATION_DURATION_MILLIS = 300;
private final WindowManager mWindowManager;
private final ViewGroup mKeyButtonContainer;
private final FloatingRotationButtonView mKeyButtonView;
private int mContainerSize;
private final Context mContext;
@StringRes
private final int mContentDescriptionResource;
@DimenRes
private final int mMinMarginResource;
@DimenRes
private final int mRoundedContentPaddingResource;
@DimenRes
private final int mTaskbarLeftMarginResource;
@DimenRes
private final int mTaskbarBottomMarginResource;
@DimenRes
private final int mButtonDiameterResource;
@BoolRes
private final int mFloatingRotationBtnPositionLeftResource;
private AnimatedVectorDrawable mAnimatedDrawable;
private boolean mIsShowing;
private int mDisplayRotation;
private boolean mIsTaskbarVisible = false;
private boolean mIsTaskbarStashed = false;
private FloatingRotationButtonPositionCalculator mPositionCalculator;
private RotationButtonController mRotationButtonController;
private RotationButtonUpdatesCallback mUpdatesCallback;
private Position mPosition;
public FloatingRotationButton(Context context, @StringRes int contentDescriptionResource,
@LayoutRes int layout, @IdRes int keyButtonId, @DimenRes int minMargin,
@DimenRes int roundedContentPadding, @DimenRes int taskbarLeftMargin,
@DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter,
@DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) {
mContext = context;
mWindowManager = mContext.getSystemService(WindowManager.class);
mKeyButtonContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(layout, null);
mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId);
mKeyButtonView.setVisibility(View.VISIBLE);
mKeyButtonView.setContentDescription(mContext.getString(contentDescriptionResource));
mKeyButtonView.setRipple(rippleMaxWidth);
mContentDescriptionResource = contentDescriptionResource;
mMinMarginResource = minMargin;
mRoundedContentPaddingResource = roundedContentPadding;
mTaskbarLeftMarginResource = taskbarLeftMargin;
mTaskbarBottomMarginResource = taskbarBottomMargin;
mButtonDiameterResource = buttonDiameter;
mFloatingRotationBtnPositionLeftResource = floatingRotationBtnPositionLeftResource;
updateDimensionResources();
}
private void updateDimensionResources() {
Resources res = mContext.getResources();
int defaultMargin = Math.max(
res.getDimensionPixelSize(mMinMarginResource),
res.getDimensionPixelSize(mRoundedContentPaddingResource));
int taskbarMarginLeft =
res.getDimensionPixelSize(mTaskbarLeftMarginResource);
int taskbarMarginBottom =
res.getDimensionPixelSize(mTaskbarBottomMarginResource);
boolean floatingRotationButtonPositionLeft =
res.getBoolean(mFloatingRotationBtnPositionLeftResource);
mPositionCalculator = new FloatingRotationButtonPositionCalculator(defaultMargin,
taskbarMarginLeft, taskbarMarginBottom, floatingRotationButtonPositionLeft);
final int diameter = res.getDimensionPixelSize(mButtonDiameterResource);
mContainerSize = diameter + Math.max(defaultMargin, Math.max(taskbarMarginLeft,
taskbarMarginBottom));
}
@Override
public void setRotationButtonController(RotationButtonController rotationButtonController) {
mRotationButtonController = rotationButtonController;
updateIcon(mRotationButtonController.getLightIconColor(),
mRotationButtonController.getDarkIconColor());
}
@Override
public void setUpdatesCallback(RotationButtonUpdatesCallback updatesCallback) {
mUpdatesCallback = updatesCallback;
}
@Override
public View getCurrentView() {
return mKeyButtonView;
}
@Override
public boolean show() {
if (mIsShowing) {
return false;
}
mIsShowing = true;
final LayoutParams layoutParams = adjustViewPositionAndCreateLayoutParams();
mWindowManager.addView(mKeyButtonContainer, layoutParams);
if (mAnimatedDrawable != null) {
mAnimatedDrawable.reset();
mAnimatedDrawable.start();
}
// Notify about visibility only after first traversal so we can properly calculate
// the touch region for the button
OneShotPreDrawListener.add(mKeyButtonView, () -> {
if (mIsShowing && mUpdatesCallback != null) {
mUpdatesCallback.onVisibilityChanged(true);
}
});
return true;
}
@Override
public boolean hide() {
if (!mIsShowing) {
return false;
}
mWindowManager.removeViewImmediate(mKeyButtonContainer);
mIsShowing = false;
if (mUpdatesCallback != null) {
mUpdatesCallback.onVisibilityChanged(false);
}
return true;
}
@Override
public boolean isVisible() {
return mIsShowing;
}
@Override
public void updateIcon(int lightIconColor, int darkIconColor) {
mAnimatedDrawable = (AnimatedVectorDrawable) mKeyButtonView.getContext()
.getDrawable(mRotationButtonController.getIconResId());
mKeyButtonView.setImageDrawable(mAnimatedDrawable);
mKeyButtonView.setColors(lightIconColor, darkIconColor);
}
@Override
public void setOnClickListener(View.OnClickListener onClickListener) {
mKeyButtonView.setOnClickListener(onClickListener);
}
@Override
public void setOnHoverListener(View.OnHoverListener onHoverListener) {
mKeyButtonView.setOnHoverListener(onHoverListener);
}
@Override
public Drawable getImageDrawable() {
return mAnimatedDrawable;
}
@Override
public void setDarkIntensity(float darkIntensity) {
mKeyButtonView.setDarkIntensity(darkIntensity);
}
@Override
public void onTaskbarStateChanged(boolean taskbarVisible, boolean taskbarStashed) {
mIsTaskbarVisible = taskbarVisible;
mIsTaskbarStashed = taskbarStashed;
if (!mIsShowing) return;
final Position newPosition = mPositionCalculator
.calculatePosition(mDisplayRotation, mIsTaskbarVisible, mIsTaskbarStashed);
if (newPosition.getTranslationX() != mPosition.getTranslationX()
|| newPosition.getTranslationY() != mPosition.getTranslationY()) {
updateTranslation(newPosition, /* animate */ true);
mPosition = newPosition;
}
}
/**
* Updates resources that could be changed in runtime, should be called on configuration
* change with changes diff integer mask
* @param configurationChanges - configuration changes with flags from ActivityInfo e.g.
* {@link android.content.pm.ActivityInfo#CONFIG_DENSITY}
*/
public void onConfigurationChanged(@Config int configurationChanges) {
if ((configurationChanges & ActivityInfo.CONFIG_DENSITY) != 0
|| (configurationChanges & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) {
updateDimensionResources();
if (mIsShowing) {
final LayoutParams layoutParams = adjustViewPositionAndCreateLayoutParams();
mWindowManager.updateViewLayout(mKeyButtonContainer, layoutParams);
}
}
if ((configurationChanges & ActivityInfo.CONFIG_LOCALE) != 0) {
mKeyButtonView.setContentDescription(mContext.getString(mContentDescriptionResource));
}
}
private LayoutParams adjustViewPositionAndCreateLayoutParams() {
final LayoutParams lp = new LayoutParams(
mContainerSize,
mContainerSize,
/* xpos */ 0, /* ypos */ 0, LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
lp.privateFlags |= LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
lp.setTitle("FloatingRotationButton");
lp.setFitInsetsTypes(/* types */ 0);
mDisplayRotation = mWindowManager.getDefaultDisplay().getRotation();
mPosition = mPositionCalculator
.calculatePosition(mDisplayRotation, mIsTaskbarVisible, mIsTaskbarStashed);
lp.gravity = mPosition.getGravity();
((FrameLayout.LayoutParams) mKeyButtonView.getLayoutParams()).gravity =
mPosition.getGravity();
updateTranslation(mPosition, /* animate */ false);
return lp;
}
private void updateTranslation(Position position, boolean animate) {
final int translationX = position.getTranslationX();
final int translationY = position.getTranslationY();
if (animate) {
mKeyButtonView
.animate()
.translationX(translationX)
.translationY(translationY)
.setDuration(MARGIN_ANIMATION_DURATION_MILLIS)
.setInterpolator(new AccelerateDecelerateInterpolator())
.withEndAction(() -> {
if (mUpdatesCallback != null && mIsShowing) {
mUpdatesCallback.onPositionChanged();
}
})
.start();
} else {
mKeyButtonView.setTranslationX(translationX);
mKeyButtonView.setTranslationY(translationY);
}
}
}
@@ -0,0 +1,75 @@
package com.android.systemui.shared.rotation
import android.view.Gravity
import android.view.Surface
/**
* Calculates gravity and translation that is necessary to display
* the button in the correct position based on the current state
*/
class FloatingRotationButtonPositionCalculator(
private val defaultMargin: Int,
private val taskbarMarginLeft: Int,
private val taskbarMarginBottom: Int,
private val floatingRotationButtonPositionLeft: Boolean
) {
fun calculatePosition(
currentRotation: Int,
taskbarVisible: Boolean,
taskbarStashed: Boolean
): Position {
val isTaskbarSide = currentRotation == Surface.ROTATION_0
|| currentRotation == Surface.ROTATION_90
val useTaskbarMargin = isTaskbarSide && taskbarVisible && !taskbarStashed
val gravity = resolveGravity(currentRotation)
val marginLeft = if (useTaskbarMargin) taskbarMarginLeft else defaultMargin
val marginBottom = if (useTaskbarMargin) taskbarMarginBottom else defaultMargin
val translationX =
if (gravity and Gravity.RIGHT == Gravity.RIGHT) {
-marginLeft
} else {
marginLeft
}
val translationY =
if (gravity and Gravity.BOTTOM == Gravity.BOTTOM) {
-marginBottom
} else {
marginBottom
}
return Position(
gravity = gravity,
translationX = translationX,
translationY = translationY
)
}
data class Position(
val gravity: Int,
val translationX: Int,
val translationY: Int
)
private fun resolveGravity(rotation: Int): Int =
if (floatingRotationButtonPositionLeft) {
when (rotation) {
Surface.ROTATION_0 -> Gravity.BOTTOM or Gravity.LEFT
Surface.ROTATION_90 -> Gravity.BOTTOM or Gravity.RIGHT
Surface.ROTATION_180 -> Gravity.TOP or Gravity.RIGHT
Surface.ROTATION_270 -> Gravity.TOP or Gravity.LEFT
else -> throw IllegalArgumentException("Invalid rotation $rotation")
}
} else {
when (rotation) {
Surface.ROTATION_0 -> Gravity.BOTTOM or Gravity.RIGHT
Surface.ROTATION_90 -> Gravity.TOP or Gravity.RIGHT
Surface.ROTATION_180 -> Gravity.TOP or Gravity.LEFT
Surface.ROTATION_270 -> Gravity.BOTTOM or Gravity.LEFT
else -> throw IllegalArgumentException("Invalid rotation $rotation")
}
}
}
@@ -0,0 +1,102 @@
/*
* Copyright (C) 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 com.android.systemui.shared.rotation;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.DimenRes;
import com.android.systemui.shared.navigationbar.KeyButtonRipple;
public class FloatingRotationButtonView extends ImageView {
private static final float BACKGROUND_ALPHA = 0.92f;
private KeyButtonRipple mRipple;
private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private final Configuration mLastConfiguration;
public FloatingRotationButtonView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FloatingRotationButtonView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLastConfiguration = getResources().getConfiguration();
setClickable(true);
setWillNotDraw(false);
forceHasOverlappingRendering(false);
}
public void setRipple(@DimenRes int rippleMaxWidthResource) {
mRipple = new KeyButtonRipple(getContext(), this, rippleMaxWidthResource);
setBackground(mRipple);
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility != View.VISIBLE) {
jumpDrawablesToCurrentState();
}
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
final int changes = mLastConfiguration.updateFrom(newConfig);
if ((changes & ActivityInfo.CONFIG_SCREEN_SIZE) != 0
|| ((changes & ActivityInfo.CONFIG_DENSITY) != 0)) {
if (mRipple != null) {
mRipple.updateResources();
}
}
}
public void setColors(int lightColor, int darkColor) {
getDrawable().setColorFilter(new PorterDuffColorFilter(lightColor, PorterDuff.Mode.SRC_IN));
final int ovalBackgroundColor = Color.valueOf(Color.red(darkColor),
Color.green(darkColor), Color.blue(darkColor), BACKGROUND_ALPHA).toArgb();
mOvalBgPaint.setColor(ovalBackgroundColor);
mRipple.setType(KeyButtonRipple.Type.OVAL);
}
public void setDarkIntensity(float darkIntensity) {
mRipple.setDarkIntensity(darkIntensity);
}
@Override
public void draw(Canvas canvas) {
int d = Math.min(getWidth(), getHeight());
canvas.drawOval(0, 0, d, d, mOvalBgPaint);
super.draw(canvas);
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (C) 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 com.android.systemui.shared.rotation;
import android.graphics.drawable.Drawable;
import android.view.View;
/**
* Interface of a rotation button that interacts {@link RotationButtonController}.
* This interface exists because of the two different styles of rotation button in Sysui,
* one in contextual for 3 button nav and a floating rotation button for gestural.
*/
public interface RotationButton {
default void setRotationButtonController(RotationButtonController rotationButtonController) { }
default void setUpdatesCallback(RotationButtonUpdatesCallback updatesCallback) { }
default View getCurrentView() {
return null;
}
default boolean show() { return false; }
default boolean hide() { return false; }
default boolean isVisible() {
return false;
}
default void onTaskbarStateChanged(boolean taskbarVisible, boolean taskbarStashed) {}
default void updateIcon(int lightIconColor, int darkIconColor) { }
default void setOnClickListener(View.OnClickListener onClickListener) { }
default void setOnHoverListener(View.OnHoverListener onHoverListener) { }
default Drawable getImageDrawable() {
return null;
}
default void setDarkIntensity(float darkIntensity) { }
default boolean acceptRotationProposal() {
return getCurrentView() != null;
}
/**
* Callback for updates provided by a rotation button
*/
interface RotationButtonUpdatesCallback {
default void onVisibilityChanged(boolean isVisible) {};
default void onPositionChanged() {};
}
}
@@ -0,0 +1,736 @@
/*
* 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 com.android.systemui.shared.rotation;
import static android.content.pm.PackageManager.FEATURE_PC;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION;
import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.annotation.SuppressLint;
import android.app.StatusBarManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.IRotationWatcher;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.WindowInsetsController;
import android.view.WindowManagerGlobal;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLoggerImpl;
import com.android.internal.view.RotationPolicy;
import com.android.systemui.shared.recents.utilities.Utilities;
import com.android.systemui.shared.recents.utilities.ViewRippler;
import com.android.systemui.shared.rotation.RotationButton.RotationButtonUpdatesCallback;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import java.io.PrintWriter;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.function.Supplier;
/**
* Contains logic that deals with showing a rotate suggestion button with animation.
*/
public class RotationButtonController {
public static final boolean DEBUG_ROTATION = false;
private static final String TAG = "RotationButtonController";
private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100;
private static final int NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS = 20000;
private static final boolean OEM_DISALLOW_ROTATION_IN_SUW =
SystemProperties.getBoolean("ro.setupwizard.rotation_locked", false);
private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3;
private final Context mContext;
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
private final UiEventLogger mUiEventLogger = new UiEventLoggerImpl();
private final ViewRippler mViewRippler = new ViewRippler();
private final Supplier<Integer> mWindowRotationProvider;
private RotationButton mRotationButton;
private boolean mIsRecentsAnimationRunning;
private boolean mDocked;
private boolean mHomeRotationEnabled;
private int mLastRotationSuggestion;
private boolean mPendingRotationSuggestion;
private boolean mHoveringRotationSuggestion;
private final AccessibilityManager mAccessibilityManager;
private final TaskStackListenerImpl mTaskStackListener;
private boolean mListenersRegistered = false;
private boolean mRotationWatcherRegistered = false;
private boolean mIsNavigationBarShowing;
@SuppressLint("InlinedApi")
private @WindowInsetsController.Behavior
int mBehavior = WindowInsetsController.BEHAVIOR_DEFAULT;
private int mNavBarMode;
private boolean mTaskBarVisible = false;
private boolean mSkipOverrideUserLockPrefsOnce;
private final int mLightIconColor;
private final int mDarkIconColor;
@DrawableRes
private final int mIconCcwStart0ResId;
@DrawableRes
private final int mIconCcwStart90ResId;
@DrawableRes
private final int mIconCwStart0ResId;
@DrawableRes
private final int mIconCwStart90ResId;
/** Defaults to mainExecutor if not set via {@link #setBgExecutor(Executor)}. */
private Executor mBgExecutor;
@DrawableRes
private int mIconResId;
private final Runnable mRemoveRotationProposal =
() -> setRotateSuggestionButtonState(false /* visible */);
private final Runnable mCancelPendingRotationProposal =
() -> mPendingRotationSuggestion = false;
private Animator mRotateHideAnimator;
private final BroadcastReceiver mDockedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateDockedState(intent);
}
};
private final IRotationWatcher.Stub mRotationWatcher = new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(final int rotation) {
// We need this to be scheduled as early as possible to beat the redrawing of
// window in response to the orientation change.
mMainThreadHandler.postAtFrontOfQueue(() -> {
onRotationWatcherChanged(rotation);
});
}
};
/**
* Determines if rotation suggestions disabled2 flag exists in flag
*
* @param disable2Flags see if rotation suggestion flag exists in this flag
* @return whether flag exists
*/
public static boolean hasDisable2RotateSuggestionFlag(int disable2Flags) {
return (disable2Flags & StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS) != 0;
}
public RotationButtonController(Context context,
@ColorInt int lightIconColor, @ColorInt int darkIconColor,
@DrawableRes int iconCcwStart0ResId,
@DrawableRes int iconCcwStart90ResId,
@DrawableRes int iconCwStart0ResId,
@DrawableRes int iconCwStart90ResId,
Supplier<Integer> windowRotationProvider) {
mContext = context;
mLightIconColor = lightIconColor;
mDarkIconColor = darkIconColor;
mIconCcwStart0ResId = iconCcwStart0ResId;
mIconCcwStart90ResId = iconCcwStart90ResId;
mIconCwStart0ResId = iconCwStart0ResId;
mIconCwStart90ResId = iconCwStart90ResId;
mIconResId = mIconCcwStart90ResId;
mAccessibilityManager = AccessibilityManager.getInstance(context);
mTaskStackListener = new TaskStackListenerImpl();
mWindowRotationProvider = windowRotationProvider;
mBgExecutor = context.getMainExecutor();
}
public void setRotationButton(RotationButton rotationButton,
RotationButtonUpdatesCallback updatesCallback) {
mRotationButton = rotationButton;
mRotationButton.setRotationButtonController(this);
mRotationButton.setOnClickListener(this::onRotateSuggestionClick);
mRotationButton.setOnHoverListener(this::onRotateSuggestionHover);
mRotationButton.setUpdatesCallback(updatesCallback);
}
public Context getContext() {
return mContext;
}
/**
* We should pass single threaded executor (rather than {@link ThreadPoolExecutor}) as we will
* make binder calls on that executor and ordering is vital.
*/
public void setBgExecutor(Executor bgExecutor) {
mBgExecutor = bgExecutor;
}
/**
* Called during Taskbar initialization.
*/
public void init() {
registerListeners(true /* registerRotationWatcher */);
if (mContext.getDisplay().getDisplayId() != DEFAULT_DISPLAY) {
// Currently there is no accelerometer sensor on non-default display, disable fixed
// rotation for non-default display
onDisable2FlagChanged(StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS);
}
}
/**
* Called during Taskbar uninitialization.
*/
public void onDestroy() {
unregisterListeners();
}
public void registerListeners(boolean registerRotationWatcher) {
if (mListenersRegistered || getContext().getPackageManager().hasSystemFeature(FEATURE_PC)) {
return;
}
mListenersRegistered = true;
mBgExecutor.execute(() -> {
if (registerRotationWatcher) {
try {
WindowManagerGlobal.getWindowManagerService()
.watchRotation(mRotationWatcher, DEFAULT_DISPLAY);
mRotationWatcherRegistered = true;
} catch (IllegalArgumentException e) {
Log.w(TAG, "RegisterListeners for the display failed", e);
} catch (RemoteException e) {
Log.e(TAG, "RegisterListeners caught a RemoteException", e);
}
}
final Intent intent = mContext.registerReceiver(mDockedReceiver,
new IntentFilter(Intent.ACTION_DOCK_EVENT));
mContext.getMainExecutor().execute(() -> updateDockedState(intent));
});
TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
}
public void unregisterListeners() {
if (!mListenersRegistered) {
return;
}
mListenersRegistered = false;
mBgExecutor.execute(() -> {
try {
mContext.unregisterReceiver(mDockedReceiver);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Docked receiver already unregistered", e);
}
if (mRotationWatcherRegistered) {
try {
WindowManagerGlobal.getWindowManagerService().removeRotationWatcher(
mRotationWatcher);
} catch (RemoteException e) {
Log.e(TAG, "UnregisterListeners caught a RemoteException", e);
}
}
});
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
}
public void setRotationLockedAtAngle(int rotationSuggestion, String caller) {
final Boolean isLocked = isRotationLocked();
if (isLocked == null) {
// Ignore if we can't read the setting for the current user
return;
}
RotationPolicy.setRotationLockAtAngle(mContext, /* enabled= */ isLocked,
/* rotation= */ rotationSuggestion, caller);
}
/**
* @return whether rotation is currently locked, or <code>null</code> if the setting couldn't
* be read
*/
public Boolean isRotationLocked() {
try {
return RotationPolicy.isRotationLocked(mContext);
} catch (SecurityException e) {
// TODO(b/279561841): RotationPolicy uses the current user to resolve the setting which
// may change before the rotation watcher can be unregistered
Log.e(TAG, "Failed to get isRotationLocked", e);
return null;
}
}
public void setRotateSuggestionButtonState(boolean visible) {
setRotateSuggestionButtonState(visible, false /* force */);
}
void setRotateSuggestionButtonState(final boolean visible, final boolean force) {
// At any point the button can become invisible because an a11y service became active.
// Similarly, a call to make the button visible may be rejected because an a11y service is
// active. Must account for this.
// Rerun a show animation to indicate change but don't rerun a hide animation
if (!visible && !mRotationButton.isVisible()) return;
final View view = mRotationButton.getCurrentView();
if (view == null) return;
final Drawable currentDrawable = mRotationButton.getImageDrawable();
if (currentDrawable == null) return;
// Clear any pending suggestion flag as it has either been nullified or is being shown
mPendingRotationSuggestion = false;
mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal);
// Handle the visibility change and animation
if (visible) { // Appear and change (cannot force)
// Stop and clear any currently running hide animations
if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
mRotateHideAnimator.cancel();
}
mRotateHideAnimator = null;
// Reset the alpha if any has changed due to hide animation
view.setAlpha(1f);
// Run the rotate icon's animation if it has one
if (currentDrawable instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) currentDrawable).reset();
((AnimatedVectorDrawable) currentDrawable).start();
}
// TODO(b/187754252): No idea why this doesn't work. If we remove the "false"
// we see the animation show the pressed state... but it only shows the first time.
if (!isRotateSuggestionIntroduced()) mViewRippler.start(view);
// Set visibility unless a11y service is active.
mRotationButton.show();
} else { // Hide
mViewRippler.stop(); // Prevent any pending ripples, force hide or not
if (force) {
// If a hide animator is running stop it and make invisible
if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
mRotateHideAnimator.pause();
}
mRotationButton.hide();
return;
}
// Don't start any new hide animations if one is running
if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", 0f);
fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS);
fadeOut.setInterpolator(LINEAR_INTERPOLATOR);
fadeOut.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mRotationButton.hide();
}
});
mRotateHideAnimator = fadeOut;
fadeOut.start();
}
}
public void setDarkIntensity(float darkIntensity) {
mRotationButton.setDarkIntensity(darkIntensity);
}
public void setRecentsAnimationRunning(boolean running) {
mIsRecentsAnimationRunning = running;
updateRotationButtonStateInOverview();
}
public void setHomeRotationEnabled(boolean enabled) {
mHomeRotationEnabled = enabled;
updateRotationButtonStateInOverview();
}
private void updateDockedState(Intent intent) {
if (intent == null) {
return;
}
mDocked = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED)
!= Intent.EXTRA_DOCK_STATE_UNDOCKED;
}
private void updateRotationButtonStateInOverview() {
if (mIsRecentsAnimationRunning && !mHomeRotationEnabled) {
setRotateSuggestionButtonState(false, true /* hideImmediately */);
}
}
public void onRotationProposal(int rotation, boolean isValid) {
boolean isUserSetupComplete = Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.USER_SETUP_COMPLETE, 0) != 0;
if (!isUserSetupComplete && OEM_DISALLOW_ROTATION_IN_SUW) {
return;
}
int windowRotation = mWindowRotationProvider.get();
if (!mRotationButton.acceptRotationProposal()) {
return;
}
if (!mHomeRotationEnabled && mIsRecentsAnimationRunning) {
return;
}
// This method will be called on rotation suggestion changes even if the proposed rotation
// is not valid for the top app. Use invalid rotation choices as a signal to remove the
// rotate button if shown.
if (!isValid) {
setRotateSuggestionButtonState(false /* visible */);
return;
}
// If window rotation matches suggested rotation, remove any current suggestions
if (rotation == windowRotation) {
mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
setRotateSuggestionButtonState(false /* visible */);
return;
}
// Prepare to show the navbar icon by updating the icon style to change anim params
Log.i(TAG, "onRotationProposal(rotation=" + rotation + ")");
mLastRotationSuggestion = rotation; // Remember rotation for click
final boolean rotationCCW = Utilities.isRotationAnimationCCW(windowRotation, rotation);
if (windowRotation == Surface.ROTATION_0 || windowRotation == Surface.ROTATION_180) {
mIconResId = rotationCCW ? mIconCcwStart0ResId : mIconCwStart0ResId;
} else { // 90 or 270
mIconResId = rotationCCW ? mIconCcwStart90ResId : mIconCwStart90ResId;
}
mRotationButton.updateIcon(mLightIconColor, mDarkIconColor);
if (canShowRotationButton()) {
// The navbar is visible / it's in visual immersive mode, so show the icon right away
showAndLogRotationSuggestion();
} else {
// If the navbar isn't shown, flag the rotate icon to be shown should the navbar become
// visible given some time limit.
mPendingRotationSuggestion = true;
mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal);
mMainThreadHandler.postDelayed(mCancelPendingRotationProposal,
NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS);
}
}
/**
* Called when the rotation watcher rotation changes, either from the watcher registered
* internally in this class, or a signal propagated from NavBarHelper.
*/
public void onRotationWatcherChanged(int rotation) {
if (!mListenersRegistered) {
// Ignore if not registered
return;
}
// If the screen rotation changes while locked, potentially update lock to flow with
// new screen rotation and hide any showing suggestions.
Boolean rotationLocked = isRotationLocked();
if (rotationLocked == null) {
// Ignore if we can't read the setting for the current user
return;
}
// The isVisible check makes the rotation button disappear when we are not locked
// (e.g. for tabletop auto-rotate).
if (rotationLocked || mRotationButton.isVisible()) {
// Do not allow a change in rotation to set user rotation when docked.
if (shouldOverrideUserLockPrefs(rotation) && rotationLocked && !mDocked) {
setRotationLockedAtAngle(rotation, /* caller= */
"RotationButtonController#onRotationWatcherChanged");
}
setRotateSuggestionButtonState(false /* visible */, true /* forced */);
}
}
public void onDisable2FlagChanged(int state2) {
final boolean rotateSuggestionsDisabled = hasDisable2RotateSuggestionFlag(state2);
if (rotateSuggestionsDisabled) onRotationSuggestionsDisabled();
}
public void onNavigationModeChanged(int mode) {
mNavBarMode = mode;
}
public void onBehaviorChanged(int displayId, @WindowInsetsController.Behavior int behavior) {
if (DEFAULT_DISPLAY != displayId) {
return;
}
if (mBehavior != behavior) {
mBehavior = behavior;
showPendingRotationButtonIfNeeded();
}
}
public void onNavigationBarWindowVisibilityChange(boolean showing) {
if (mIsNavigationBarShowing != showing) {
mIsNavigationBarShowing = showing;
showPendingRotationButtonIfNeeded();
}
}
public void onTaskbarStateChange(boolean visible, boolean stashed) {
mTaskBarVisible = visible;
if (getRotationButton() == null) {
return;
}
getRotationButton().onTaskbarStateChanged(visible, stashed);
}
private void showPendingRotationButtonIfNeeded() {
if (canShowRotationButton() && mPendingRotationSuggestion) {
showAndLogRotationSuggestion();
}
}
/**
* Return true when either the task bar is visible or it's in visual immersive mode.
*/
@SuppressLint("InlinedApi")
@VisibleForTesting
boolean canShowRotationButton() {
return mIsNavigationBarShowing
|| mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT
|| isGesturalMode(mNavBarMode);
}
@DrawableRes
public int getIconResId() {
return mIconResId;
}
@ColorInt
public int getLightIconColor() {
return mLightIconColor;
}
@ColorInt
public int getDarkIconColor() {
return mDarkIconColor;
}
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "RotationButtonController:");
pw.println(String.format(
"%s\tmIsRecentsAnimationRunning=%b", prefix, mIsRecentsAnimationRunning));
pw.println(String.format("%s\tmHomeRotationEnabled=%b", prefix, mHomeRotationEnabled));
pw.println(String.format(
"%s\tmLastRotationSuggestion=%d", prefix, mLastRotationSuggestion));
pw.println(String.format(
"%s\tmPendingRotationSuggestion=%b", prefix, mPendingRotationSuggestion));
pw.println(String.format(
"%s\tmHoveringRotationSuggestion=%b", prefix, mHoveringRotationSuggestion));
pw.println(String.format("%s\tmListenersRegistered=%b", prefix, mListenersRegistered));
pw.println(String.format(
"%s\tmIsNavigationBarShowing=%b", prefix, mIsNavigationBarShowing));
pw.println(String.format("%s\tmBehavior=%d", prefix, mBehavior));
pw.println(String.format(
"%s\tmSkipOverrideUserLockPrefsOnce=%b", prefix, mSkipOverrideUserLockPrefsOnce));
pw.println(String.format(
"%s\tmLightIconColor=0x%s", prefix, Integer.toHexString(mLightIconColor)));
pw.println(String.format(
"%s\tmDarkIconColor=0x%s", prefix, Integer.toHexString(mDarkIconColor)));
}
public RotationButton getRotationButton() {
return mRotationButton;
}
private void onRotateSuggestionClick(View v) {
mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_ACCEPTED);
incrementNumAcceptedRotationSuggestionsIfNeeded();
setRotationLockedAtAngle(mLastRotationSuggestion,
/* caller= */ "RotationButtonController#onRotateSuggestionClick");
Log.i(TAG, "onRotateSuggestionClick() mLastRotationSuggestion=" + mLastRotationSuggestion);
v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
private boolean onRotateSuggestionHover(View v, MotionEvent event) {
final int action = event.getActionMasked();
mHoveringRotationSuggestion = (action == MotionEvent.ACTION_HOVER_ENTER)
|| (action == MotionEvent.ACTION_HOVER_MOVE);
rescheduleRotationTimeout(true /* reasonHover */);
return false; // Must return false so a11y hover events are dispatched correctly.
}
private void onRotationSuggestionsDisabled() {
// Immediately hide the rotate button and clear any planned removal
setRotateSuggestionButtonState(false /* visible */, true /* force */);
mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
}
private void showAndLogRotationSuggestion() {
setRotateSuggestionButtonState(true /* visible */);
rescheduleRotationTimeout(false /* reasonHover */);
mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_SHOWN);
}
/**
* Makes {@link #shouldOverrideUserLockPrefs} always return {@code false} once. It is used to
* avoid losing original user rotation when display rotation is changed by entering the fixed
* orientation overview.
*/
public void setSkipOverrideUserLockPrefsOnce() {
// If live-tile is enabled (recents animation keeps running in overview), there is no
// activity switch so the display rotation is not changed, then it is no need to skip.
mSkipOverrideUserLockPrefsOnce = !mIsRecentsAnimationRunning;
}
private boolean shouldOverrideUserLockPrefs(final int rotation) {
if (mSkipOverrideUserLockPrefsOnce) {
mSkipOverrideUserLockPrefsOnce = false;
return false;
}
// Only override user prefs when returning to the natural rotation (normally portrait).
// Don't let apps that force landscape or 180 alter user lock.
return rotation == NATURAL_ROTATION;
}
private void rescheduleRotationTimeout(final boolean reasonHover) {
// May be called due to a new rotation proposal or a change in hover state
if (reasonHover) {
// Don't reschedule if a hide animator is running
if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
// Don't reschedule if not visible
if (!mRotationButton.isVisible()) return;
}
// Stop any pending removal
mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
// Schedule timeout
mMainThreadHandler.postDelayed(mRemoveRotationProposal,
computeRotationProposalTimeout());
}
private int computeRotationProposalTimeout() {
return mAccessibilityManager.getRecommendedTimeoutMillis(
mHoveringRotationSuggestion ? 16000 : 5000,
AccessibilityManager.FLAG_CONTENT_CONTROLS);
}
private boolean isRotateSuggestionIntroduced() {
ContentResolver cr = mContext.getContentResolver();
return Settings.Secure.getInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0)
>= NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION;
}
private void incrementNumAcceptedRotationSuggestionsIfNeeded() {
// Get the number of accepted suggestions
ContentResolver cr = mContext.getContentResolver();
final int numSuggestions = Settings.Secure.getInt(cr,
Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0);
// Increment the number of accepted suggestions only if it would change intro mode
if (numSuggestions < NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION) {
Settings.Secure.putInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED,
numSuggestions + 1);
}
}
private class TaskStackListenerImpl implements TaskStackChangeListener {
// Invalidate any rotation suggestion on task change or activity orientation change
// Note: all callbacks happen on main thread
@Override
public void onTaskStackChanged() {
setRotateSuggestionButtonState(false /* visible */);
}
@Override
public void onTaskRemoved(int taskId) {
setRotateSuggestionButtonState(false /* visible */);
}
@Override
public void onTaskMovedToFront(int taskId) {
setRotateSuggestionButtonState(false /* visible */);
}
@Override
public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
mBgExecutor.execute(() -> {
// Only hide the icon if the top task changes its requestedOrientation Launcher can
// alter its requestedOrientation while it's not on top, don't hide on this
Optional.ofNullable(ActivityManagerWrapper.getInstance())
.map(ActivityManagerWrapper::getRunningTask)
.ifPresent(a -> {
if (a.id == taskId) {
mMainThreadHandler.post(() ->
setRotateSuggestionButtonState(false /* visible */));
}
});
});
}
}
enum RotationButtonEvent implements UiEventLogger.UiEventEnum {
@UiEvent(doc = "The rotation button was shown")
ROTATION_SUGGESTION_SHOWN(206),
@UiEvent(doc = "The rotation button was clicked")
ROTATION_SUGGESTION_ACCEPTED(207);
private final int mId;
RotationButtonEvent(int id) {
mId = id;
}
@Override
public int getId() {
return mId;
}
}
}
@@ -0,0 +1,137 @@
/*
* Copyright (C) 2022 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 com.android.systemui.shared.shadow
import android.content.res.ColorStateList
import android.graphics.BlendMode
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.RenderEffect
import android.graphics.RenderNode
import android.graphics.Shader
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo
/** A component to draw an icon with two layers of shadows. */
class DoubleShadowIconDrawable(
keyShadowInfo: ShadowInfo,
ambientShadowInfo: ShadowInfo,
iconDrawable: Drawable,
iconSize: Int,
val iconInsetSize: Int
) : Drawable() {
private val mAmbientShadowInfo: ShadowInfo
private val mCanvasSize: Int
private val mKeyShadowInfo: ShadowInfo
private val mIconDrawable: InsetDrawable
private val mDoubleShadowNode: RenderNode
init {
mCanvasSize = iconSize + iconInsetSize * 2
mKeyShadowInfo = keyShadowInfo
mAmbientShadowInfo = ambientShadowInfo
setBounds(0, 0, mCanvasSize, mCanvasSize)
mIconDrawable = InsetDrawable(iconDrawable, iconInsetSize)
mIconDrawable.setBounds(0, 0, mCanvasSize, mCanvasSize)
mDoubleShadowNode = createShadowRenderNode()
}
private fun createShadowRenderNode(): RenderNode {
val renderNode = RenderNode("DoubleShadowNode")
renderNode.setPosition(0, 0, mCanvasSize, mCanvasSize)
// Create render effects
val ambientShadow =
createShadowRenderEffect(
mAmbientShadowInfo.blur,
mAmbientShadowInfo.offsetX,
mAmbientShadowInfo.offsetY,
mAmbientShadowInfo.alpha
)
val keyShadow =
createShadowRenderEffect(
mKeyShadowInfo.blur,
mKeyShadowInfo.offsetX,
mKeyShadowInfo.offsetY,
mKeyShadowInfo.alpha
)
val blend = RenderEffect.createBlendModeEffect(ambientShadow, keyShadow, BlendMode.DST_ATOP)
renderNode.setRenderEffect(blend)
return renderNode
}
private fun createShadowRenderEffect(
radius: Float,
offsetX: Float,
offsetY: Float,
alpha: Float
): RenderEffect {
return RenderEffect.createColorFilterEffect(
PorterDuffColorFilter(Color.argb(alpha, 0f, 0f, 0f), PorterDuff.Mode.MULTIPLY),
RenderEffect.createOffsetEffect(
offsetX,
offsetY,
RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)
)
)
}
override fun draw(canvas: Canvas) {
if (canvas.isHardwareAccelerated) {
if (!mDoubleShadowNode.hasDisplayList()) {
// Record render node if its display list is not recorded or discarded
// (which happens when it's no longer drawn by anything).
val recordingCanvas = mDoubleShadowNode.beginRecording()
mIconDrawable.draw(recordingCanvas)
mDoubleShadowNode.endRecording()
}
canvas.drawRenderNode(mDoubleShadowNode)
}
mIconDrawable.draw(canvas)
}
override fun getIntrinsicHeight(): Int {
return mCanvasSize
}
override fun getIntrinsicWidth(): Int {
return mCanvasSize
}
override fun getOpacity(): Int {
return PixelFormat.TRANSPARENT
}
override fun setAlpha(alpha: Int) {
mIconDrawable.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
mIconDrawable.colorFilter = colorFilter
}
override fun setTint(color: Int) {
mIconDrawable.setTint(color)
}
override fun setTintList(tint: ColorStateList?) {
mIconDrawable.setTintList(tint)
}
}
@@ -0,0 +1,148 @@
/*
* Copyright (C) 2022 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 com.android.systemui.shared.shadow
import android.content.Context
import android.content.res.Resources
import android.content.res.TypedArray
import android.graphics.Canvas
import android.util.AttributeSet
import android.widget.TextClock
import com.android.systemui.shared.R
import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo
import com.android.systemui.shared.shadow.DoubleShadowTextHelper.applyShadows
import kotlin.math.floor
/** Extension of [TextClock] which draws two shadows on the text (ambient and key shadows) */
class DoubleShadowTextClock
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0,
) : TextClock(context, attrs, defStyleAttr, defStyleRes) {
private lateinit var mAmbientShadowInfo: ShadowInfo
private lateinit var mKeyShadowInfo: ShadowInfo
private var attributesInput: TypedArray? = null
private var resources: Resources? = null
constructor(
resources: Resources,
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0,
attributesInput: TypedArray? = null
) : this(context, attrs, defStyleAttr, defStyleRes) {
this.attributesInput = attributesInput
this.resources = resources
this.initializeAttributes(attrs, defStyleAttr, defStyleRes)
}
init {
initializeAttributes(attrs, defStyleAttr, defStyleRes)
}
private fun initializeAttributes(attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) {
var attributes: TypedArray =
this.attributesInput
?: context.obtainStyledAttributes(
attrs,
R.styleable.DoubleShadowTextClock,
defStyleAttr,
defStyleRes
)
var resource: Resources = this.resources ?: context.resources
try {
val keyShadowBlur =
attributes.getDimensionPixelSize(R.styleable.DoubleShadowTextClock_keyShadowBlur, 0)
val keyShadowOffsetX =
attributes.getDimensionPixelSize(
R.styleable.DoubleShadowTextClock_keyShadowOffsetX,
0
)
val keyShadowOffsetY =
attributes.getDimensionPixelSize(
R.styleable.DoubleShadowTextClock_keyShadowOffsetY,
0
)
val keyShadowAlpha =
attributes.getFloat(R.styleable.DoubleShadowTextClock_keyShadowAlpha, 0f)
mKeyShadowInfo =
ShadowInfo(
keyShadowBlur.toFloat(),
keyShadowOffsetX.toFloat(),
keyShadowOffsetY.toFloat(),
keyShadowAlpha
)
val ambientShadowBlur =
attributes.getDimensionPixelSize(
R.styleable.DoubleShadowTextClock_ambientShadowBlur,
0
)
val ambientShadowOffsetX =
attributes.getDimensionPixelSize(
R.styleable.DoubleShadowTextClock_ambientShadowOffsetX,
0
)
val ambientShadowOffsetY =
attributes.getDimensionPixelSize(
R.styleable.DoubleShadowTextClock_ambientShadowOffsetY,
0
)
val ambientShadowAlpha =
attributes.getFloat(R.styleable.DoubleShadowTextClock_ambientShadowAlpha, 0f)
mAmbientShadowInfo =
ShadowInfo(
ambientShadowBlur.toFloat(),
ambientShadowOffsetX.toFloat(),
ambientShadowOffsetY.toFloat(),
ambientShadowAlpha
)
val removeTextDescent =
attributes.getBoolean(R.styleable.DoubleShadowTextClock_removeTextDescent, false)
val textDescentExtraPadding =
attributes.getDimensionPixelSize(
R.styleable.DoubleShadowTextClock_textDescentExtraPadding,
0
)
if (removeTextDescent) {
val addBottomPaddingToClock =
resource.getBoolean(R.bool.dream_overlay_complication_clock_bottom_padding)
val metrics = paint.fontMetrics
val padding =
if (addBottomPaddingToClock) {
textDescentExtraPadding +
floor(metrics.descent.toDouble()).toInt() / paddingDividedOffset
} else {
textDescentExtraPadding - floor(metrics.descent.toDouble()).toInt()
}
setPaddingRelative(0, 0, 0, padding)
}
} finally {
attributes.recycle()
}
}
companion object {
private val paddingDividedOffset = 2
}
public override fun onDraw(canvas: Canvas) {
applyShadows(mKeyShadowInfo, mAmbientShadowInfo, this, canvas) { super.onDraw(canvas) }
}
}
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2022 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 com.android.systemui.shared.shadow
import android.graphics.Canvas
import android.graphics.Color
import android.widget.TextView
object DoubleShadowTextHelper {
data class ShadowInfo(
val blur: Float,
val offsetX: Float = 0f,
val offsetY: Float = 0f,
val alpha: Float
)
fun applyShadows(
keyShadowInfo: ShadowInfo,
ambientShadowInfo: ShadowInfo,
view: TextView,
canvas: Canvas,
onDrawCallback: () -> Unit
) {
// We enhance the shadow by drawing the shadow twice
view.paint.setShadowLayer(
ambientShadowInfo.blur,
ambientShadowInfo.offsetX,
ambientShadowInfo.offsetY,
Color.argb(ambientShadowInfo.alpha, 0f, 0f, 0f)
)
onDrawCallback()
canvas.save()
canvas.clipRect(
view.scrollX,
view.scrollY + view.extendedPaddingTop,
view.scrollX + view.width,
view.scrollY + view.height
)
view.paint.setShadowLayer(
keyShadowInfo.blur,
keyShadowInfo.offsetX,
keyShadowInfo.offsetY,
Color.argb(keyShadowInfo.alpha, 0f, 0f, 0f)
)
onDrawCallback()
canvas.restore()
}
}
@@ -0,0 +1,107 @@
/*
* Copyright (C) 2022 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 com.android.systemui.shared.shadow
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.widget.TextView
import com.android.systemui.shared.R
import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo
import com.android.systemui.shared.shadow.DoubleShadowTextHelper.applyShadows
/** Extension of [TextView] which draws two shadows on the text (ambient and key shadows} */
open class DoubleShadowTextView
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : TextView(context, attrs, defStyleAttr, defStyleRes) {
private val mKeyShadowInfo: ShadowInfo
private val mAmbientShadowInfo: ShadowInfo
init {
val attributes =
context.obtainStyledAttributes(
attrs,
R.styleable.DoubleShadowTextView,
defStyleAttr,
defStyleRes
)
val drawableSize: Int
val drawableInsetSize: Int
try {
val keyShadowBlur =
attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowBlur, 0f)
val keyShadowOffsetX =
attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetX, 0f)
val keyShadowOffsetY =
attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetY, 0f)
val keyShadowAlpha =
attributes.getFloat(R.styleable.DoubleShadowTextView_keyShadowAlpha, 0f)
mKeyShadowInfo =
ShadowInfo(keyShadowBlur, keyShadowOffsetX, keyShadowOffsetY, keyShadowAlpha)
val ambientShadowBlur =
attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowBlur, 0f)
val ambientShadowOffsetX =
attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetX, 0f)
val ambientShadowOffsetY =
attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetY, 0f)
val ambientShadowAlpha =
attributes.getFloat(R.styleable.DoubleShadowTextView_ambientShadowAlpha, 0f)
mAmbientShadowInfo =
ShadowInfo(
ambientShadowBlur,
ambientShadowOffsetX,
ambientShadowOffsetY,
ambientShadowAlpha
)
drawableSize =
attributes.getDimensionPixelSize(
R.styleable.DoubleShadowTextView_drawableIconSize,
0
)
drawableInsetSize =
attributes.getDimensionPixelSize(
R.styleable.DoubleShadowTextView_drawableIconInsetSize,
0
)
} finally {
attributes.recycle()
}
val drawables = arrayOf<Drawable?>(null, null, null, null)
for ((index, drawable) in compoundDrawablesRelative.withIndex()) {
if (drawable == null) continue
drawables[index] =
DoubleShadowIconDrawable(
mKeyShadowInfo,
mAmbientShadowInfo,
drawable,
drawableSize,
drawableInsetSize
)
}
setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3])
}
public override fun onDraw(canvas: Canvas) {
applyShadows(mKeyShadowInfo, mAmbientShadowInfo, this, canvas) { super.onDraw(canvas) }
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 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 com.android.systemui.shared.system
import android.app.ActivityManager
/** Kotlin extensions for [ActivityManager] */
object ActivityManagerKt {
/**
* Returns `true` whether the app with the given package name has an activity at the top of the
* most recent task; `false` otherwise
*/
fun ActivityManager.isInForeground(packageName: String): Boolean {
val tasks: List<ActivityManager.RunningTaskInfo> = getRunningTasks(1)
return tasks.isNotEmpty() && packageName == tasks[0].topActivity?.packageName
}
}
@@ -0,0 +1,392 @@
/*
* Copyright (C) 2015 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 com.android.systemui.shared.system;
import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
import static android.app.ActivityTaskManager.getService;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityClient;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.AppGlobals;
import android.app.WindowConfiguration;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.Log;
import android.view.Display;
import android.view.IRecentsAnimationController;
import android.view.IRecentsAnimationRunner;
import android.view.RemoteAnimationTarget;
import android.window.TaskSnapshot;
import com.android.internal.app.IVoiceInteractionManagerService;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
import java.util.List;
import java.util.function.Consumer;
import app.lawnchair.compat.LawnchairQuickstepCompat;
public class ActivityManagerWrapper {
private static final String TAG = "ActivityManagerWrapper";
private static final int NUM_RECENT_ACTIVITIES_REQUEST = 3;
private static final ActivityManagerWrapper sInstance = new ActivityManagerWrapper();
// Should match the values in PhoneWindowManager
public static final String CLOSE_SYSTEM_WINDOWS_REASON_RECENTS = "recentapps";
public static final String CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY = "homekey";
// Should match the value in AssistManager
private static final String INVOCATION_TIME_MS_KEY = "invocation_time_ms";
private final ActivityTaskManager mAtm = ActivityTaskManager.getInstance();
private ActivityManagerWrapper() { }
public static ActivityManagerWrapper getInstance() {
return sInstance;
}
/**
* @return the current user's id.
*/
public int getCurrentUserId() {
UserInfo ui;
try {
ui = ActivityManager.getService().getCurrentUser();
return ui != null ? ui.id : 0;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* @return the top running task (can be {@code null}).
*/
public ActivityManager.RunningTaskInfo getRunningTask() {
return getRunningTask(false /* filterVisibleRecents */);
}
/**
* @return a list of the recents tasks.
*/
@NonNull
public List<ActivityManager.RecentTaskInfo> getRecentTasks(int numTasks, int userId) {
return LawnchairQuickstepCompat.getActivityManagerCompat().getRecentTasks(numTasks, userId);
}
/**
* @return the top running task filtering only for tasks that can be visible in the recent tasks
* list (can be {@code null}).
*/
public ActivityManager.RunningTaskInfo getRunningTask(boolean filterOnlyVisibleRecents) {
// Note: The set of running tasks from the system is ordered by recency
List<ActivityManager.RunningTaskInfo> tasks =
mAtm.getTasks(1, filterOnlyVisibleRecents);
if (tasks.isEmpty()) {
return null;
}
return tasks.get(0);
}
/**
* @see #getRunningTasks(boolean , int)
*/
public ActivityManager.RunningTaskInfo[] getRunningTasks(boolean filterOnlyVisibleRecents) {
return getRunningTasks(filterOnlyVisibleRecents, Display.INVALID_DISPLAY);
}
/**
* We ask for {@link #NUM_RECENT_ACTIVITIES_REQUEST} activities because when in split screen,
* we'll get back 2 activities for each split app and one for launcher. Launcher might be more
* "recently" used than one of the split apps so if we only request 2 tasks, then we might miss
* out on one of the split apps
*
* @return an array of up to {@link #NUM_RECENT_ACTIVITIES_REQUEST} running tasks
* filtering only for tasks that can be visible in the recent tasks list.
*/
public ActivityManager.RunningTaskInfo[] getRunningTasks(boolean filterOnlyVisibleRecents,
int displayId) {
// Note: The set of running tasks from the system is ordered by recency
List<ActivityManager.RunningTaskInfo> tasks =
mAtm.getTasks(NUM_RECENT_ACTIVITIES_REQUEST,
filterOnlyVisibleRecents, /* keepInExtras= */ false, displayId);
return tasks.toArray(new RunningTaskInfo[tasks.size()]);
}
/**
* @return the task snapshot for the given {@param taskId}.
*/
public @NonNull ThumbnailData getTaskThumbnail(int taskId, boolean isLowResolution) {
TaskSnapshot snapshot = null;
try {
snapshot = getService().getTaskSnapshot(taskId, isLowResolution);
} catch (RemoteException e) {
Log.w(TAG, "Failed to retrieve task snapshot", e);
}
if (snapshot != null) {
return ThumbnailData.fromSnapshot(snapshot);
} else {
return new ThumbnailData();
}
}
/**
* Requests for a new snapshot to be taken for the given task, stores it in the cache, and
* returns a {@link ThumbnailData} with the result.
*/
@NonNull
public ThumbnailData takeTaskThumbnail(int taskId) {
TaskSnapshot snapshot = null;
try {
snapshot = getService().takeTaskSnapshot(taskId, /* updateCache= */ true);
} catch (RemoteException e) {
Log.w(TAG, "Failed to take task snapshot", e);
}
if (snapshot != null) {
return ThumbnailData.fromSnapshot(snapshot);
} else {
return new ThumbnailData();
}
}
/**
* Removes the outdated snapshot of home task.
*
* @param homeActivity The home task activity, or null if you have the
* {@link android.Manifest.permission#MANAGE_ACTIVITY_TASKS} permission and
* want us to find the home task for you.
*/
public void invalidateHomeTaskSnapshot(@Nullable final Activity homeActivity) {
try {
ActivityClient.getInstance().invalidateHomeTaskSnapshot(
homeActivity == null ? null : homeActivity.getActivityToken());
} catch (Throwable e) {
Log.w(TAG, "Failed to invalidate home snapshot", e);
}
}
/**
* Starts the recents activity. The caller should manage the thread on which this is called.
*/
public void startRecentsActivity(Intent intent, long eventTime,
final RecentsAnimationListener animationHandler, final Consumer<Boolean> resultCallback,
Handler resultCallbackHandler) {
boolean result = startRecentsActivity(intent, eventTime, animationHandler);
if (resultCallback != null && resultCallbackHandler != null) {
resultCallbackHandler.post(new Runnable() {
@Override
public void run() {
resultCallback.accept(result);
}
});
}
}
/**
* Starts the recents activity. The caller should manage the thread on which this is called.
*/
public boolean startRecentsActivity(
Intent intent, long eventTime, RecentsAnimationListener animationHandler) {
try {
IRecentsAnimationRunner runner = null;
if (animationHandler != null) {
runner = new IRecentsAnimationRunner.Stub() {
@Override
public void onAnimationStart(IRecentsAnimationController controller,
RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
Rect homeContentInsets, Rect minimizedHomeBounds,
Bundle extras) {
final RecentsAnimationControllerCompat controllerCompat =
new RecentsAnimationControllerCompat(controller);
animationHandler.onAnimationStart(controllerCompat, apps,
wallpapers, homeContentInsets, minimizedHomeBounds, extras);
}
@Override
public void onAnimationCanceled(int[] taskIds, TaskSnapshot[] taskSnapshots) {
animationHandler.onAnimationCanceled(
ThumbnailData.wrap(taskIds, taskSnapshots));
}
@Override
public void onTasksAppeared(RemoteAnimationTarget[] apps) {
animationHandler.onTasksAppeared(apps);
}
};
}
getService().startRecentsActivity(intent, eventTime, runner);
return true;
} catch (Exception e) {
return false;
}
}
/**
* Cancels the remote recents animation started from {@link #startRecentsActivity}.
*/
public void cancelRecentsAnimation(boolean restoreHomeRootTaskPosition) {
try {
getService().cancelRecentsAnimation(restoreHomeRootTaskPosition);
} catch (RemoteException e) {
Log.e(TAG, "Failed to cancel recents animation", e);
}
}
/**
* Starts a task from Recents synchronously.
*/
public boolean startActivityFromRecents(Task.TaskKey taskKey, ActivityOptions options) {
return startActivityFromRecents(taskKey.id, options);
}
/**
* Starts a task from Recents synchronously.
*/
public boolean startActivityFromRecents(int taskId, ActivityOptions options) {
try {
Bundle optsBundle = options == null ? null : options.toBundle();
return ActivityManager.isStartResultSuccessful(
getService().startActivityFromRecents(
taskId, optsBundle));
} catch (Exception e) {
return false;
}
}
/**
* Requests that the system close any open system windows (including other SystemUI).
*/
public void closeSystemWindows(final String reason) {
try {
ActivityManager.getService().closeSystemDialogs(reason);
} catch (RemoteException e) {
Log.w(TAG, "Failed to close system windows", e);
}
}
/**
* Removes a task by id.
*/
public void removeTask(final int taskId) {
try {
getService().removeTask(taskId);
} catch (RemoteException e) {
Log.w(TAG, "Failed to remove task=" + taskId, e);
}
}
/**
* Removes all the recent tasks.
*/
public void removeAllRecentTasks() {
try {
getService().removeAllVisibleRecentTasks();
} catch (RemoteException e) {
Log.w(TAG, "Failed to remove all tasks", e);
}
}
/**
* @return whether screen pinning is enabled.
*/
public boolean isScreenPinningEnabled() {
final ContentResolver cr = AppGlobals.getInitialApplication().getContentResolver();
return Settings.System.getInt(cr, Settings.System.LOCK_TO_APP_ENABLED, 0) != 0;
}
/**
* @return whether there is currently a locked task (ie. in screen pinning).
*/
public boolean isLockToAppActive() {
try {
return getService().getLockTaskModeState() != LOCK_TASK_MODE_NONE;
} catch (RemoteException e) {
return false;
}
}
/**
* @return whether lock task mode is active in kiosk-mode (not screen pinning).
*/
public boolean isLockTaskKioskModeActive() {
try {
return getService().getLockTaskModeState() == LOCK_TASK_MODE_LOCKED;
} catch (RemoteException e) {
return false;
}
}
/**
* Shows a voice session identified by {@code token}
* @return true if the session was shown, false otherwise
*/
public boolean showVoiceSession(@NonNull IBinder token, @NonNull Bundle args, int flags,
@Nullable String attributionTag) {
IVoiceInteractionManagerService service = IVoiceInteractionManagerService.Stub.asInterface(
ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
if (service == null) {
return false;
}
args.putLong(INVOCATION_TIME_MS_KEY, SystemClock.elapsedRealtime());
try {
return service.showSessionFromSession(token, args, flags, attributionTag);
} catch (RemoteException e) {
return false;
}
}
/**
* Returns true if the system supports freeform multi-window.
*/
public boolean supportsFreeformMultiWindow(Context context) {
final boolean freeformDevOption = Settings.Global.getInt(context.getContentResolver(),
Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
return ActivityTaskManager.supportsMultiWindow(context)
&& (context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT)
|| freeformDevOption);
}
/**
* Returns true if the running task represents the home task
*/
public static boolean isHomeTask(RunningTaskInfo info) {
return info.configuration.windowConfiguration.getActivityType()
== WindowConfiguration.ACTIVITY_TYPE_HOME;
}
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2020 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 com.android.systemui.shared.system;
import static android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED;
import android.app.ActivityManager;
import android.os.SystemProperties;
public abstract class BlurUtils {
/**
* If this device can render blurs.
*
* @return {@code true} when supported.
*/
public static boolean supportsBlursOnWindows() {
return CROSS_WINDOW_BLUR_SUPPORTED && ActivityManager.isHighEndGfx()
&& !SystemProperties.getBoolean("persist.sysui.disableBlur", false);
}
}
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2019 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 com.android.systemui.shared.system;
import android.app.AppGlobals;
import android.app.admin.DevicePolicyManager;
/**
* Wrapper for {@link DevicePolicyManager}.
*/
public class DevicePolicyManagerWrapper {
private static final DevicePolicyManagerWrapper sInstance = new DevicePolicyManagerWrapper();
private static final DevicePolicyManager sDevicePolicyManager =
AppGlobals.getInitialApplication().getSystemService(DevicePolicyManager.class);
private DevicePolicyManagerWrapper() { }
public static DevicePolicyManagerWrapper getInstance() {
return sInstance;
}
/**
* Returns whether the given package is allowed to run in Lock Task mode.
*/
public boolean isLockTaskPermitted(String pkg) {
return sDevicePolicyManager.isLockTaskPermitted(pkg);
}
}
@@ -0,0 +1,101 @@
/**
* Copyright (C) 2019 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 com.android.systemui.shared.system;
import static android.os.Trace.TRACE_TAG_INPUT;
import android.os.Looper;
import android.os.Trace;
import android.util.Log;
import android.view.BatchedInputEventReceiver;
import android.view.Choreographer;
import android.view.InputChannel;
import android.view.InputEvent;
import android.view.MotionEvent;
/**
* @see android.view.InputChannel
*/
public class InputChannelCompat {
/**
* Callback for receiving event callbacks
*/
public interface InputEventListener {
/**
* @param ev event to be handled
*/
void onInputEvent(InputEvent ev);
}
/**
* Version of addBatch method which preserves time accuracy in nanoseconds instead of
* converting the time to milliseconds.
* @param src old MotionEvent where the target should be appended
* @param target new MotionEvent which should be added to the src
* @return true if the merge was successful
*
* @see MotionEvent#addBatch(MotionEvent)
*/
public static boolean mergeMotionEvent(MotionEvent src, MotionEvent target) {
return target.addBatch(src);
}
/**
* @see BatchedInputEventReceiver
*/
public static class InputEventReceiver {
private final String mName;
private final BatchedInputEventReceiver mReceiver;
@Deprecated
public InputEventReceiver(InputChannel inputChannel, Looper looper,
Choreographer choreographer, final InputEventListener listener) {
this("unknown", inputChannel, looper, choreographer, listener);
}
public InputEventReceiver(String name, InputChannel inputChannel, Looper looper,
Choreographer choreographer, final InputEventListener listener) {
mName = name;
mReceiver = new BatchedInputEventReceiver(inputChannel, looper, choreographer) {
@Override
public void onInputEvent(InputEvent event) {
listener.onInputEvent(event);
finishInputEvent(event, true /* handled */);
}
};
}
/**
* @see BatchedInputEventReceiver#setBatchingEnabled()
*/
public void setBatchingEnabled(boolean batchingEnabled) {
mReceiver.setBatchingEnabled(batchingEnabled);
}
/**
* @see BatchedInputEventReceiver#dispose()
*/
public void dispose() {
mReceiver.dispose();
Trace.instant(TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " receiver disposed");
Log.d(InputMonitorCompat.TAG, "Input event receiver for monitor (" + mName
+ ") disposed");
}
}
}
@@ -0,0 +1,178 @@
/*
* Copyright (C) 2017 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 com.android.systemui.shared.system;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION;
import android.os.Binder;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import android.view.BatchedInputEventReceiver;
import android.view.Choreographer;
import android.view.IWindowManager;
import android.view.InputChannel;
import android.view.InputEvent;
import android.view.WindowManagerGlobal;
import java.io.PrintWriter;
/**
* Manages the input consumer that allows the SystemUI to directly receive input.
* TODO: Refactor this for the gesture nav case
*/
public class InputConsumerController {
private static final String TAG = InputConsumerController.class.getSimpleName();
/**
* Listener interface for callers to subscribe to input events.
*/
public interface InputListener {
/** Handles any input event. */
boolean onInputEvent(InputEvent ev);
}
/**
* Listener interface for callers to learn when this class is registered or unregistered with
* window manager
*/
public interface RegistrationListener {
void onRegistrationChanged(boolean isRegistered);
}
/**
* Input handler used for the input consumer. Input events are batched and consumed with the
* SurfaceFlinger vsync.
*/
private final class InputEventReceiver extends BatchedInputEventReceiver {
InputEventReceiver(InputChannel inputChannel, Looper looper,
Choreographer choreographer) {
super(inputChannel, looper, choreographer);
}
@Override
public void onInputEvent(InputEvent event) {
boolean handled = true;
try {
if (mListener != null) {
handled = mListener.onInputEvent(event);
}
} finally {
finishInputEvent(event, handled);
}
}
}
private final IWindowManager mWindowManager;
private final IBinder mToken;
private final String mName;
private InputEventReceiver mInputEventReceiver;
private InputListener mListener;
private RegistrationListener mRegistrationListener;
/**
* @param name the name corresponding to the input consumer that is defined in the system.
*/
public InputConsumerController(IWindowManager windowManager, String name) {
mWindowManager = windowManager;
mToken = new Binder();
mName = name;
}
/**
* @return A controller for the recents animation input consumer.
*/
public static InputConsumerController getRecentsAnimationInputConsumer() {
return new InputConsumerController(WindowManagerGlobal.getWindowManagerService(),
INPUT_CONSUMER_RECENTS_ANIMATION);
}
/**
* Sets the input listener.
*/
public void setInputListener(InputListener listener) {
mListener = listener;
}
/**
* Sets the registration listener.
*/
public void setRegistrationListener(RegistrationListener listener) {
mRegistrationListener = listener;
if (mRegistrationListener != null) {
mRegistrationListener.onRegistrationChanged(mInputEventReceiver != null);
}
}
/**
* Check if the InputConsumer is currently registered with WindowManager
*
* @return {@code true} if registered, {@code false} if not.
*/
public boolean isRegistered() {
return mInputEventReceiver != null;
}
/**
* Registers the input consumer.
*/
public void registerInputConsumer() {
if (mInputEventReceiver == null) {
final InputChannel inputChannel = new InputChannel();
try {
mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY);
mWindowManager.createInputConsumer(mToken, mName, DEFAULT_DISPLAY, inputChannel);
} catch (RemoteException e) {
Log.e(TAG, "Failed to create input consumer", e);
}
mInputEventReceiver = new InputEventReceiver(inputChannel, Looper.myLooper(),
Choreographer.getInstance());
if (mRegistrationListener != null) {
mRegistrationListener.onRegistrationChanged(true /* isRegistered */);
}
}
}
/**
* Unregisters the input consumer.
*/
public void unregisterInputConsumer() {
if (mInputEventReceiver != null) {
try {
mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY);
} catch (RemoteException e) {
Log.e(TAG, "Failed to destroy input consumer", e);
}
mInputEventReceiver.dispose();
mInputEventReceiver = null;
if (mRegistrationListener != null) {
mRegistrationListener.onRegistrationChanged(false /* isRegistered */);
}
}
}
public void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
pw.println(innerPrefix + "registered=" + (mInputEventReceiver != null));
}
}
@@ -0,0 +1,84 @@
/**
* Copyright (C) 2019 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 com.android.systemui.shared.system;
import android.hardware.input.InputManagerGlobal;
import android.os.Looper;
import android.os.Trace;
import android.util.Log;
import android.view.Choreographer;
import android.view.InputMonitor;
import android.view.SurfaceControl;
import androidx.annotation.NonNull;
import com.android.systemui.shared.system.InputChannelCompat.InputEventListener;
import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
/**
* @see android.view.InputMonitor
*/
public class InputMonitorCompat {
static final String TAG = "InputMonitorCompat";
private final InputMonitor mInputMonitor;
private final String mName;
/**
* Monitor input on the specified display for gestures.
*/
public InputMonitorCompat(@NonNull String name, int displayId) {
mName = name + "-disp" + displayId;
mInputMonitor = InputManagerGlobal.getInstance()
.monitorGestureInput(name, displayId);
Trace.instant(Trace.TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " created");
Log.d(TAG, "Input monitor (" + mName + ") created");
}
/**
* @see InputMonitor#pilferPointers()
*/
public void pilferPointers() {
mInputMonitor.pilferPointers();
}
/**
* @see InputMonitor#getSurface()
*/
public SurfaceControl getSurface() {
return mInputMonitor.getSurface();
}
/**
* @see InputMonitor#dispose()
*/
public void dispose() {
mInputMonitor.dispose();
Trace.instant(Trace.TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " disposed");
Log.d(TAG, "Input monitor (" + mName + ") disposed");
}
/**
* @see InputMonitor#getInputChannel()
*/
public InputEventReceiver getInputReceiver(Looper looper, Choreographer choreographer,
InputEventListener listener) {
Trace.instant(Trace.TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " receiver created");
Log.d(TAG, "Input event receiver for monitor (" + mName + ") created");
return new InputEventReceiver(mName, mInputMonitor.getInputChannel(), looper, choreographer,
listener);
}
}
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2020 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 com.android.systemui.shared.system;
import android.os.Build;
import android.text.TextUtils;
import android.view.View;
import com.android.internal.jank.Cuj;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.jank.InteractionJankMonitor.Configuration;
public final class InteractionJankMonitorWrapper {
/**
* Begin a trace session.
*
* @param v an attached view.
* @param cujType the specific {@link Cuj.CujType}.
*/
public static void begin(View v, @Cuj.CujType int cujType) {
if (true) return;
InteractionJankMonitor.getInstance().begin(v, cujType);
}
/**
* Begin a trace session.
*
* @param v an attached view.
* @param cujType the specific {@link Cuj.CujType}.
* @param timeout duration to cancel the instrumentation in ms
*/
public static void begin(View v, @Cuj.CujType int cujType, long timeout) {
if (true) return;
Configuration.Builder builder =
Configuration.Builder.withView(cujType, v)
.setTimeout(timeout);
InteractionJankMonitor.getInstance().begin(builder);
}
/**
* Begin a trace session.
*
* @param v an attached view.
* @param cujType the specific {@link Cuj.CujType}.
* @param tag the tag to distinguish different flow of same type CUJ.
*/
public static void begin(View v, @Cuj.CujType int cujType, String tag) {
if (true) return;
Configuration.Builder builder =
Configuration.Builder.withView(cujType, v);
if (!TextUtils.isEmpty(tag)) {
builder.setTag(tag);
}
InteractionJankMonitor.getInstance().begin(builder);
}
/**
* End a trace session.
*
* @param cujType the specific {@link Cuj.CujType}.
*/
public static void end(@Cuj.CujType int cujType) {
if (true) return;
InteractionJankMonitor.getInstance().end(cujType);
}
/**
* Cancel the trace session.
*/
public static void cancel(@Cuj.CujType int cujType) {
if (true) return;
InteractionJankMonitor.getInstance().cancel(cujType);
}
/** Return true if currently instrumenting a trace session. */
public static boolean isInstrumenting(@Cuj.CujType int cujType) {
if (true) return true;
return InteractionJankMonitor.getInstance().isInstrumenting(cujType);
}
}
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2015 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 com.android.systemui.shared.system;
import android.app.AppGlobals;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ResolveInfoFlagsBits;
import android.content.pm.ResolveInfo;
import android.os.RemoteException;
import android.os.UserHandle;
import java.util.List;
public class PackageManagerWrapper {
private static final PackageManagerWrapper sInstance = new PackageManagerWrapper();
private static final IPackageManager mIPackageManager = AppGlobals.getPackageManager();
public static final String ACTION_PREFERRED_ACTIVITY_CHANGED =
Intent.ACTION_PREFERRED_ACTIVITY_CHANGED;
public static PackageManagerWrapper getInstance() {
return sInstance;
}
private PackageManagerWrapper() {}
/**
* @return the activity info for a given {@param componentName} and {@param userId}.
*/
public ActivityInfo getActivityInfo(ComponentName componentName, int userId) {
try {
return mIPackageManager.getActivityInfo(componentName, PackageManager.GET_META_DATA,
userId);
} catch (RemoteException e) {
e.printStackTrace();
return null;
}
}
/**
* Report the set of 'Home' activity candidates, plus (if any) which of them
* is the current "always use this one" setting.
*/
public ComponentName getHomeActivities(List<ResolveInfo> allHomeCandidates) {
try {
return mIPackageManager.getHomeActivities(allHomeCandidates);
} catch (RemoteException e) {
e.printStackTrace();
return null;
}
}
/**
* Determine the best Activity to perform for a given Intent.
*/
public ResolveInfo resolveActivity(Intent intent, @ResolveInfoFlagsBits int flags) {
final String resolvedType =
intent.resolveTypeIfNeeded(AppGlobals.getInitialApplication().getContentResolver());
try {
return mIPackageManager.resolveIntent(
intent, resolvedType, flags, UserHandle.getCallingUserId());
} catch (RemoteException e) {
e.printStackTrace();
return null;
}
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (C) 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 com.android.systemui.shared.system;
/**
* These strings are part of the {@link com.android.systemui.people.PeopleProvider} API
* contract. The API returns a People Tile preview that can be displayed by calling packages.
* The provider is part of the SystemUI service, and the strings live here for shared access with
* Launcher (caller).
*/
public class PeopleProviderUtils {
/**
* ContentProvider URI scheme.
* @hide
*/
public static final String PEOPLE_PROVIDER_SCHEME = "content://";
/**
* ContentProvider URI authority.
* @hide
*/
public static final String PEOPLE_PROVIDER_AUTHORITY =
"com.android.systemui.people.PeopleProvider";
/**
* Method name for getting People Tile preview.
* @hide
*/
public static final String GET_PEOPLE_TILE_PREVIEW_METHOD = "get_people_tile_preview";
/**
* Extras bundle key specifying shortcut Id of the People Tile preview requested.
* @hide
*/
public static final String EXTRAS_KEY_SHORTCUT_ID = "shortcut_id";
/**
* Extras bundle key specifying package name of the People Tile preview requested.
* @hide
*/
public static final String EXTRAS_KEY_PACKAGE_NAME = "package_name";
/**
* Extras bundle key specifying {@code UserHandle} of the People Tile preview requested.
* @hide
*/
public static final String EXTRAS_KEY_USER_HANDLE = "user_handle";
/**
* Response bundle key to access the returned People Tile preview.
* @hide
*/
public static final String RESPONSE_KEY_REMOTE_VIEWS = "remote_views";
/**
* Name of the permission needed to get a People Tile preview for a given conversation shortcut.
* @hide
*/
public static final String GET_PEOPLE_TILE_PREVIEW_PERMISSION =
"android.permission.GET_PEOPLE_TILE_PREVIEW";
}
@@ -0,0 +1,420 @@
/*
* Copyright (C) 2019 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 com.android.systemui.shared.system;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
import static com.android.systemui.shared.Flags.shadeAllowBackGesture;
import android.annotation.LongDef;
import android.content.Context;
import android.content.res.Resources;
import android.view.ViewConfiguration;
import android.view.WindowManagerPolicyConstants;
import com.android.internal.policy.ScreenDecorationsUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.StringJoiner;
import app.lawnchair.compat.LawnchairQuickstepCompat;
/**
* Various shared constants between Launcher and SysUI as part of quickstep
*/
public class QuickStepContract {
public static final String KEY_EXTRA_SYSUI_PROXY = "extra_sysui_proxy";
public static final String KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER = "extra_unfold_animation";
// See ISysuiUnlockAnimationController.aidl
public static final String KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER = "unlock_animation";
public static final String NAV_BAR_MODE_3BUTTON_OVERLAY =
WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY;
public static final String NAV_BAR_MODE_GESTURAL_OVERLAY =
WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY;
// Overview is disabled, either because the device is in lock task mode, or because the device
// policy has disabled the feature
public static final long SYSUI_STATE_SCREEN_PINNING = 1L << 0;
// The navigation bar is hidden due to immersive mode
public static final long SYSUI_STATE_NAV_BAR_HIDDEN = 1L << 1;
// The notification panel is expanded and interactive (either locked or unlocked), and the
// quick settings is not expanded
public static final long SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED = 1L << 2;
// The keyguard bouncer is showing
public static final long SYSUI_STATE_BOUNCER_SHOWING = 1L << 3;
// The navigation bar a11y button should be shown
public static final long SYSUI_STATE_A11Y_BUTTON_CLICKABLE = 1L << 4;
// The navigation bar a11y button shortcut is available
public static final long SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE = 1L << 5;
// The keyguard is showing and not occluded
public static final long SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING = 1L << 6;
// The recents feature is disabled (either by SUW/SysUI/device policy)
public static final long SYSUI_STATE_OVERVIEW_DISABLED = 1L << 7;
// The home feature is disabled (either by SUW/SysUI/device policy)
public static final long SYSUI_STATE_HOME_DISABLED = 1L << 8;
// The keyguard is showing, but occluded
public static final long SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED = 1L << 9;
// The search feature is disabled (either by SUW/SysUI/device policy)
public static final long SYSUI_STATE_SEARCH_DISABLED = 1L << 10;
// The notification panel is expanded and interactive (either locked or unlocked), and quick
// settings is expanded.
public static final long SYSUI_STATE_QUICK_SETTINGS_EXPANDED = 1L << 11;
// Winscope tracing is enabled
public static final long SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION = 1L << 12;
// The Assistant gesture should be constrained. It is up to the launcher implementation to
// decide how to constrain it
public static final long SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED = 1L << 13;
// The bubble stack is expanded. This means that the home gesture should be ignored, since a
// swipe up is an attempt to close the bubble stack, but that the back gesture should remain
// enabled (since it's used to navigate back within the bubbled app, or to collapse the bubble
// stack.
public static final long SYSUI_STATE_BUBBLES_EXPANDED = 1L << 14;
// A SysUI dialog is showing.
public static final long SYSUI_STATE_DIALOG_SHOWING = 1L << 15;
// The one-handed mode is active
public static final long SYSUI_STATE_ONE_HANDED_ACTIVE = 1L << 16;
// Allow system gesture no matter the system bar(s) is visible or not
public static final long SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY = 1L << 17;
// The IME is showing
public static final long SYSUI_STATE_IME_SHOWING = 1L << 18;
// The window magnification is overlapped with system gesture insets at the bottom.
public static final long SYSUI_STATE_MAGNIFICATION_OVERLAP = 1L << 19;
// ImeSwitcher is showing
public static final long SYSUI_STATE_IME_SWITCHER_SHOWING = 1L << 20;
// Device dozing/AOD state
public static final long SYSUI_STATE_DEVICE_DOZING = 1L << 21;
// The home feature is disabled (either by SUW/SysUI/device policy)
public static final long SYSUI_STATE_BACK_DISABLED = 1L << 22;
// The bubble stack is expanded AND the mange menu for bubbles is expanded on top of it.
public static final long SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED = 1L << 23;
// The voice interaction session window is showing
public static final long SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING = 1L << 25;
// Freeform windows are showing in desktop mode
public static final long SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1L << 26;
// Device dreaming state
public static final long SYSUI_STATE_DEVICE_DREAMING = 1L << 27;
// Whether the device is currently awake (as opposed to asleep, see WakefulnessLifecycle).
// Note that the device is awake on while waking up on, but not while going to sleep.
public static final long SYSUI_STATE_AWAKE = 1L << 28;
// Whether the device is currently transitioning between awake/asleep indicated by
// SYSUI_STATE_AWAKE.
public static final long SYSUI_STATE_WAKEFULNESS_TRANSITION = 1L << 29;
// The notification panel expansion fraction is > 0
public static final long SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE = 1L << 30;
// When keyguard will be dismissed but didn't start animation yet
public static final long SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY = 1L << 31;
// Physical keyboard shortcuts helper is showing
public static final long SYSUI_STATE_SHORTCUT_HELPER_SHOWING = 1L << 32;
// Touchpad gestures are disabled
public static final long SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED = 1L << 33;
// Mask for SystemUiStateFlags to isolate SYSUI_STATE_AWAKE and
// SYSUI_STATE_WAKEFULNESS_TRANSITION, to match WAKEFULNESS_* constants
public static final long SYSUI_STATE_WAKEFULNESS_MASK =
SYSUI_STATE_AWAKE | SYSUI_STATE_WAKEFULNESS_TRANSITION;
// Mirroring the WakefulnessLifecycle#Wakefulness states
public static final long WAKEFULNESS_ASLEEP = 0;
public static final long WAKEFULNESS_AWAKE = SYSUI_STATE_AWAKE;
public static final long WAKEFULNESS_GOING_TO_SLEEP = SYSUI_STATE_WAKEFULNESS_TRANSITION;
public static final long WAKEFULNESS_WAKING =
SYSUI_STATE_WAKEFULNESS_TRANSITION | SYSUI_STATE_AWAKE;
// Whether the back gesture is allowed (or ignored) by the Shade
public static final boolean ALLOW_BACK_GESTURE_IN_SHADE = false;
@Retention(RetentionPolicy.SOURCE)
@LongDef({SYSUI_STATE_SCREEN_PINNING,
SYSUI_STATE_NAV_BAR_HIDDEN,
SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
SYSUI_STATE_QUICK_SETTINGS_EXPANDED,
SYSUI_STATE_BOUNCER_SHOWING,
SYSUI_STATE_A11Y_BUTTON_CLICKABLE,
SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE,
SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING,
SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED,
SYSUI_STATE_OVERVIEW_DISABLED,
SYSUI_STATE_HOME_DISABLED,
SYSUI_STATE_SEARCH_DISABLED,
SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION,
SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED,
SYSUI_STATE_BUBBLES_EXPANDED,
SYSUI_STATE_DIALOG_SHOWING,
SYSUI_STATE_ONE_HANDED_ACTIVE,
SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY,
SYSUI_STATE_IME_SHOWING,
SYSUI_STATE_MAGNIFICATION_OVERLAP,
SYSUI_STATE_IME_SWITCHER_SHOWING,
SYSUI_STATE_DEVICE_DOZING,
SYSUI_STATE_BACK_DISABLED,
SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING,
SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE,
SYSUI_STATE_DEVICE_DREAMING,
SYSUI_STATE_AWAKE,
SYSUI_STATE_WAKEFULNESS_TRANSITION,
SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE,
SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY,
SYSUI_STATE_SHORTCUT_HELPER_SHOWING,
SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED,
})
public @interface SystemUiStateFlags {}
public static String getSystemUiStateString(long flags) {
StringJoiner str = new StringJoiner("|");
if ((flags & SYSUI_STATE_SCREEN_PINNING) != 0) {
str.add("screen_pinned");
}
if ((flags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
str.add("overview_disabled");
}
if ((flags & SYSUI_STATE_HOME_DISABLED) != 0) {
str.add("home_disabled");
}
if ((flags & SYSUI_STATE_SEARCH_DISABLED) != 0) {
str.add("search_disabled");
}
if ((flags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0) {
str.add("navbar_hidden");
}
if ((flags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) != 0) {
str.add("notif_expanded");
}
if ((flags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) != 0) {
str.add("qs_visible");
}
if ((flags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING) != 0) {
str.add("keygrd_visible");
}
if ((flags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) != 0) {
str.add("keygrd_occluded");
}
if ((flags & SYSUI_STATE_BOUNCER_SHOWING) != 0) {
str.add("bouncer_visible");
}
if ((flags & SYSUI_STATE_DIALOG_SHOWING) != 0) {
str.add("dialog_showing");
}
if ((flags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0) {
str.add("a11y_click");
}
if ((flags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0) {
str.add("a11y_long_click");
}
if ((flags & SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION) != 0) {
str.add("disable_gesture_split_invocation");
}
if ((flags & SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED) != 0) {
str.add("asst_gesture_constrain");
}
if ((flags & SYSUI_STATE_BUBBLES_EXPANDED) != 0) {
str.add("bubbles_expanded");
}
if ((flags & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0) {
str.add("one_handed_active");
}
if ((flags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) {
str.add("allow_gesture");
}
if ((flags & SYSUI_STATE_IME_SHOWING) != 0) {
str.add("ime_visible");
}
if ((flags & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0) {
str.add("magnification_overlap");
}
if ((flags & SYSUI_STATE_IME_SWITCHER_SHOWING) != 0) {
str.add("ime_switcher_showing");
}
if ((flags & SYSUI_STATE_DEVICE_DOZING) != 0) {
str.add("device_dozing");
}
if ((flags & SYSUI_STATE_BACK_DISABLED) != 0) {
str.add("back_disabled");
}
if ((flags & SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0) {
str.add("bubbles_mange_menu_expanded");
}
if ((flags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0) {
str.add("vis_win_showing");
}
if ((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0) {
str.add("freeform_active_in_desktop_mode");
}
if ((flags & SYSUI_STATE_DEVICE_DREAMING) != 0) {
str.add("device_dreaming");
}
if ((flags & SYSUI_STATE_WAKEFULNESS_TRANSITION) != 0) {
str.add("wakefulness_transition");
}
if ((flags & SYSUI_STATE_AWAKE) != 0) {
str.add("awake");
}
if ((flags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0) {
str.add("notif_visible");
}
if ((flags & SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY) != 0) {
str.add("keygrd_going_away");
}
if ((flags & SYSUI_STATE_SHORTCUT_HELPER_SHOWING) != 0) {
str.add("shortcut_helper_showing");
}
if ((flags & SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED) != 0) {
str.add("touchpad_gestures_disabled");
}
return str.toString();
}
/**
* Ratio of quickstep touch slop (when system takes over the touch) to view touch slop
*/
public static final float QUICKSTEP_TOUCH_SLOP_RATIO = 3;
/**
* Touch slop for quickstep gesture
*/
public static final float getQuickStepTouchSlopPx(Context context) {
return QUICKSTEP_TOUCH_SLOP_RATIO * ViewConfiguration.get(context).getScaledTouchSlop();
}
/**
* Returns whether the specified sysui state is such that the assistant gesture should be
* disabled.
*/
public static boolean isAssistantGestureDisabled(long sysuiStateFlags) {
if ((sysuiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) {
sysuiStateFlags &= ~SYSUI_STATE_NAV_BAR_HIDDEN;
}
// Disable when in quick settings, screen pinning, immersive, the bouncer is showing,
// or search is disabled
long disableFlags = SYSUI_STATE_SCREEN_PINNING
| SYSUI_STATE_NAV_BAR_HIDDEN
| SYSUI_STATE_BOUNCER_SHOWING
| SYSUI_STATE_SEARCH_DISABLED
| SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
if ((sysuiStateFlags & disableFlags) != 0) {
return true;
}
// Disable when notifications are showing (only if unlocked)
if ((sysuiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) != 0
&& (sysuiStateFlags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING) == 0) {
return true;
}
return false;
}
/**
* Returns whether the specified sysui state is such that the back gesture should be
* disabled.
*/
public static boolean isBackGestureDisabled(long sysuiStateFlags, boolean forTrackpad) {
// Always allow when the bouncer/global actions/voice session is showing (even on top of
// the keyguard)
if ((sysuiStateFlags & SYSUI_STATE_BOUNCER_SHOWING) != 0
|| (sysuiStateFlags & SYSUI_STATE_DIALOG_SHOWING) != 0
|| (sysuiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0) {
return false;
}
if ((sysuiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) {
sysuiStateFlags &= ~SYSUI_STATE_NAV_BAR_HIDDEN;
}
return (sysuiStateFlags & getBackGestureDisabledMask(forTrackpad)) != 0;
}
private static long getBackGestureDisabledMask(boolean forTrackpad) {
// Disable when in immersive, or the notifications are interactive
long disableFlags = SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
if (!forTrackpad) {
disableFlags |= SYSUI_STATE_NAV_BAR_HIDDEN;
}
// EdgeBackGestureHandler ignores Back gesture when SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED.
// To allow Shade to respond to Back, we're bypassing this check (behind a flag).
if (!ALLOW_BACK_GESTURE_IN_SHADE) {
disableFlags |= SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
}
return disableFlags;
}
/**
* @return whether this nav bar mode is edge to edge
*/
public static boolean isGesturalMode(int mode) {
return mode == NAV_BAR_MODE_GESTURAL;
}
/**
* @return whether this nav bar mode is swipe up
*/
public static boolean isSwipeUpMode(int mode) {
return mode == NAV_BAR_MODE_2BUTTON;
}
/**
* @return whether this nav bar mode is 3 button
*/
public static boolean isLegacyMode(int mode) {
return mode == NAV_BAR_MODE_3BUTTON;
}
/**
* Corner radius that should be used on windows in order to cover the display.
* These values are expressed in pixels because they should not respect display or font
* scaling. The corner radius may change when folding/unfolding the device.
*/
public static boolean sRecentsDisabled = false;
public static boolean sHasCustomCornerRadius = false;
public static float sCustomCornerRadius = 0f;
/**
* Corner radius that should be used on windows in order to cover the display.
* These values are expressed in pixels because they should not respect display or font
* scaling, this means that we don't have to reload them on config changes.
*/
public static float getWindowCornerRadius(Context context) {
if (sRecentsDisabled || !LawnchairQuickstepCompat.ATLEAST_S) {
return 0;
}
if (sHasCustomCornerRadius) {
return sCustomCornerRadius;
}
try {
return ScreenDecorationsUtils.getWindowCornerRadius(context);
} catch (Throwable t) {
return 0;
}
}
/**
* If live rounded corners are supported on windows.
*/
public static boolean supportsRoundedCornersOnWindows(Resources resources) {
try {
return ScreenDecorationsUtils.supportsRoundedCornersOnWindows(resources);
} catch (Throwable t) {
return false;
}
}
}
@@ -0,0 +1,166 @@
/*
* Copyright (C) 2018 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 com.android.systemui.shared.system;
import android.os.RemoteException;
import android.util.Log;
import android.view.IRecentsAnimationController;
import android.view.SurfaceControl;
import android.window.PictureInPictureSurfaceTransaction;
import android.window.TaskSnapshot;
import com.android.internal.os.IResultReceiver;
import com.android.systemui.shared.recents.model.ThumbnailData;
public class RecentsAnimationControllerCompat {
private static final String TAG = RecentsAnimationControllerCompat.class.getSimpleName();
private IRecentsAnimationController mAnimationController;
public RecentsAnimationControllerCompat() { }
public RecentsAnimationControllerCompat(IRecentsAnimationController animationController) {
mAnimationController = animationController;
}
public ThumbnailData screenshotTask(int taskId) {
try {
final TaskSnapshot snapshot = mAnimationController.screenshotTask(taskId);
if (snapshot != null) {
return ThumbnailData.fromSnapshot(snapshot);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to screenshot task", e);
}
return new ThumbnailData();
}
public void setInputConsumerEnabled(boolean enabled) {
try {
mAnimationController.setInputConsumerEnabled(enabled);
} catch (RemoteException e) {
Log.e(TAG, "Failed to set input consumer enabled state", e);
}
}
public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars) {
try {
mAnimationController.setAnimationTargetsBehindSystemBars(behindSystemBars);
} catch (RemoteException e) {
Log.e(TAG, "Failed to set whether animation targets are behind system bars", e);
}
}
/**
* Sets the final surface transaction on a Task. This is used by Launcher to notify the system
* that animating Activity to PiP has completed and the associated task surface should be
* updated accordingly. This should be called before `finish`
* @param taskId Task id of the Activity in PiP mode.
* @param finishTransaction leash operations for the final transform.
* @param overlay the surface control for an overlay being shown above the pip (can be null)
*/
public void setFinishTaskTransaction(int taskId,
PictureInPictureSurfaceTransaction finishTransaction,
SurfaceControl overlay) {
try {
mAnimationController.setFinishTaskTransaction(taskId, finishTransaction, overlay);
} catch (RemoteException e) {
Log.d(TAG, "Failed to set finish task bounds", e);
}
}
/**
* Finish the current recents animation.
* @param toHome Going to home or back to the previous app.
* @param sendUserLeaveHint determines whether userLeaveHint will be set true to the previous
* app.
*/
public void finish(boolean toHome, boolean sendUserLeaveHint, IResultReceiver finishCb) {
try {
mAnimationController.finish(toHome, sendUserLeaveHint, finishCb);
} catch (RemoteException e) {
Log.e(TAG, "Failed to finish recents animation", e);
try {
finishCb.send(0, null);
} catch (Exception ex) {
// Local call, can ignore
}
}
}
public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) {
try {
mAnimationController.setDeferCancelUntilNextTransition(defer, screenshot);
} catch (RemoteException e) {
Log.e(TAG, "Failed to set deferred cancel with screenshot", e);
}
}
public void cleanupScreenshot() {
try {
mAnimationController.cleanupScreenshot();
} catch (RemoteException e) {
Log.e(TAG, "Failed to clean up screenshot of recents animation", e);
}
}
/**
* @see {{@link IRecentsAnimationController#setWillFinishToHome(boolean)}}.
*/
public void setWillFinishToHome(boolean willFinishToHome) {
try {
mAnimationController.setWillFinishToHome(willFinishToHome);
} catch (RemoteException e) {
Log.e(TAG, "Failed to set overview reached state", e);
}
}
/**
* @see IRecentsAnimationController#removeTask
*/
public boolean removeTask(int taskId) {
try {
return mAnimationController.removeTask(taskId);
} catch (RemoteException e) {
Log.e(TAG, "Failed to remove remote animation target", e);
return false;
}
}
/**
* @see IRecentsAnimationController#detachNavigationBarFromApp
*/
public void detachNavigationBarFromApp(boolean moveHomeToTop) {
try {
mAnimationController.detachNavigationBarFromApp(moveHomeToTop);
} catch (RemoteException e) {
Log.e(TAG, "Failed to detach the navigation bar from app", e);
}
}
/**
* @see IRecentsAnimationController#animateNavigationBarToApp(long)
*/
public void animateNavigationBarToApp(long duration) {
try {
mAnimationController.animateNavigationBarToApp(duration);
} catch (RemoteException e) {
Log.e(TAG, "Failed to animate the navigation bar to app", e);
}
}
}
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2017 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 com.android.systemui.shared.system;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
import com.android.systemui.shared.recents.model.ThumbnailData;
import java.util.HashMap;
public interface RecentsAnimationListener {
/**
* Called when the animation into Recents can start. This call is made on the binder thread.
*/
void onAnimationStart(RecentsAnimationControllerCompat controller,
RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras);
// Introduced in NothingOS 2.5.5, needed in 2.6
void onAnimationStart(RecentsAnimationControllerCompat controller,
TransitionInfo transitionInfo, SurfaceControl.Transaction transaction,
RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
Rect homeContentInsets, Rect minimizedHomeBounds);
/**
* Called when the animation into Recents was canceled. This call is made on the binder thread.
*/
void onAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas);
/**
* Called when the task of an activity that has been started while the recents animation
* was running becomes ready for control.
*/
void onTasksAppeared(RemoteAnimationTarget[] app);
/**
* Called to request that the current task tile be switched out for a screenshot (if not
* already). Once complete, onFinished should be called.
* @return true if this impl will call onFinished. No other onSwitchToScreenshot impls will
* be called afterwards (to avoid multiple calls to onFinished).
*/
default boolean onSwitchToScreenshot(Runnable onFinished) {
return false;
}
}
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2020 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 com.android.systemui.shared.system;
import android.content.Context;
import android.graphics.PixelFormat;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Size;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.WindowManager;
import android.window.InputTransferToken;
/**
* A generic receiver that specifically handles SurfaceView request created by {@link
* com.android.systemui.shared.system.SurfaceViewRequestUtils}.
*/
public class SurfaceViewRequestReceiver {
private final int mOpacity;
private SurfaceControlViewHost mSurfaceControlViewHost;
public SurfaceViewRequestReceiver() {
this(PixelFormat.TRANSPARENT);
}
public SurfaceViewRequestReceiver(int opacity) {
mOpacity = opacity;
}
/** See {@link #onReceive(Context, Bundle, View, Size)}. */
public void onReceive(Context context, Bundle bundle, View view) {
onReceive(context, bundle, view, null);
}
/**
* Called whenever a surface view request is received.
* @param view the view rendering content, on the receiver end of the surface request.
* @param viewSize when {@param viewSize} is not specified, we will use the surface control size
* to attach the view to the window.
*/
public void onReceive(Context context, Bundle bundle, View view, Size viewSize) {
if (mSurfaceControlViewHost != null) {
mSurfaceControlViewHost.release();
}
SurfaceControl surfaceControl = SurfaceViewRequestUtils.getSurfaceControl(bundle);
if (surfaceControl != null) {
if (viewSize == null) {
viewSize = new Size(surfaceControl.getWidth(), surfaceControl.getHeight());
}
IBinder hostToken = SurfaceViewRequestUtils.getHostToken(bundle);
DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
mSurfaceControlViewHost = new SurfaceControlViewHost(context,
dm.getDisplay(SurfaceViewRequestUtils.getDisplayId(bundle)),
new InputTransferToken(hostToken), "SurfaceViewRequestReceiver");
WindowManager.LayoutParams layoutParams =
new WindowManager.LayoutParams(
viewSize.getWidth(),
viewSize.getHeight(),
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
mOpacity);
// This aspect scales the view to fit in the surface and centers it
final float scale = Math.min(surfaceControl.getWidth() / (float) viewSize.getWidth(),
surfaceControl.getHeight() / (float) viewSize.getHeight());
view.setScaleX(scale);
view.setScaleY(scale);
view.setPivotX(0);
view.setPivotY(0);
view.setTranslationX((surfaceControl.getWidth() - scale * viewSize.getWidth()) / 2);
view.setTranslationY((surfaceControl.getHeight() - scale * viewSize.getHeight()) / 2);
mSurfaceControlViewHost.setView(view, layoutParams);
}
}
}
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2020 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 com.android.systemui.shared.system;
import android.annotation.Nullable;
import android.os.Bundle;
import android.os.IBinder;
import android.view.SurfaceControl;
import android.view.SurfaceView;
/** Util class that wraps a SurfaceView request into a bundle. */
public class SurfaceViewRequestUtils {
private static final String KEY_HOST_TOKEN = "host_token";
private static final String KEY_SURFACE_CONTROL = "surface_control";
private static final String KEY_DISPLAY_ID = "display_id";
/** Creates a SurfaceView based bundle that stores the input host token and surface control. */
public static Bundle createSurfaceBundle(SurfaceView surfaceView) {
Bundle bundle = new Bundle();
bundle.putBinder(KEY_HOST_TOKEN, surfaceView.getHostToken());
bundle.putParcelable(KEY_SURFACE_CONTROL, surfaceView.getSurfaceControl());
bundle.putInt(KEY_DISPLAY_ID, surfaceView.getDisplay().getDisplayId());
return bundle;
}
/**
* Retrieves the SurfaceControl from a bundle created by
* {@link #createSurfaceBundle(SurfaceView)}.
*/
public static SurfaceControl getSurfaceControl(Bundle bundle) {
return bundle.getParcelable(KEY_SURFACE_CONTROL);
}
/**
* Retrieves the input token from a bundle created by {@link #createSurfaceBundle(SurfaceView)}.
*/
public static @Nullable IBinder getHostToken(Bundle bundle) {
return bundle.getBinder(KEY_HOST_TOKEN);
}
/**
* Retrieves the display id from a bundle created by {@link #createSurfaceBundle(SurfaceView)}.
*/
public static int getDisplayId(Bundle bundle) {
return bundle.getInt(KEY_DISPLAY_ID);
}
private SurfaceViewRequestUtils() {}
}
@@ -0,0 +1,117 @@
/*
* Copyright (C) 2017 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 com.android.systemui.shared.system;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ITaskStackListener;
import android.content.ComponentName;
import com.android.systemui.shared.recents.model.ThumbnailData;
/**
* An interface to track task stack changes. Classes should implement this instead of
* {@link android.app.ITaskStackListener} to reduce IPC calls from system services.
*/
public interface TaskStackChangeListener {
// Binder thread callbacks
default void onTaskStackChangedBackground() { }
// Main thread callbacks
default void onTaskStackChanged() { }
/**
* @return whether the snapshot is consumed and the lifecycle of the snapshot extends beyond
* the lifecycle of this callback.
*/
default boolean onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
return false;
}
default void onActivityPinned(String packageName, int userId, int taskId, int stackId) { }
default void onActivityUnpinned() { }
default void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible,
boolean clearedTask, boolean wasVisible) { }
default void onActivityForcedResizable(String packageName, int taskId, int reason) { }
default void onActivityDismissingDockedStack() { }
default void onActivityLaunchOnSecondaryDisplayFailed() { }
default void onActivityLaunchOnSecondaryDisplayFailed(RunningTaskInfo taskInfo) {
onActivityLaunchOnSecondaryDisplayFailed();
}
/**
* @see #onActivityLaunchOnSecondaryDisplayRerouted(RunningTaskInfo taskInfo)
*/
default void onActivityLaunchOnSecondaryDisplayRerouted() { }
/**
* Called when an activity was requested to be launched on a secondary display but was rerouted
* to default display.
*
* @param taskInfo info about the Activity's task
*/
default void onActivityLaunchOnSecondaryDisplayRerouted(RunningTaskInfo taskInfo) {
onActivityLaunchOnSecondaryDisplayRerouted();
}
default void onTaskProfileLocked(RunningTaskInfo taskInfo, int userId) { }
default void onTaskCreated(int taskId, ComponentName componentName) { }
default void onTaskRemoved(int taskId) { }
default void onTaskMovedToFront(int taskId) { }
default void onTaskMovedToFront(RunningTaskInfo taskInfo) {
onTaskMovedToFront(taskInfo.taskId);
}
/**
* Called when a tasks description is changed due to an activity calling
* ActivityManagerService.setTaskDescription
*
* @param taskInfo info about the task which changed, with
* {@link RunningTaskInfo#taskDescription}
*/
default void onTaskDescriptionChanged(RunningTaskInfo taskInfo) { }
default void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) { }
default void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { }
/**
* Called when a task is reparented to a stack on a different display.
*
* @param taskId id of the task which was moved to a different display.
* @param newDisplayId id of the new display.
*/
default void onTaskDisplayChanged(int taskId, int newDisplayId) { }
/**
* Called when any additions or deletions to the recent tasks list have been made.
*/
default void onRecentTaskListUpdated() { }
/** @see ITaskStackListener#onRecentTaskListFrozenChanged(boolean) */
default void onRecentTaskListFrozenChanged(boolean frozen) { }
/** @see ITaskStackListener#onActivityRotation(int)*/
default void onActivityRotation(int displayId) { }
/**
* Called when the lock task mode changes. See ActivityManager#LOCK_TASK_MODE_* and
* LockTaskController.
*/
default void onLockTaskModeChanged(int mode) { }
}
@@ -0,0 +1,537 @@
/*
* Copyright (C) 2017 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 com.android.systemui.shared.system;
import android.annotation.NonNull;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityTaskManager;
import android.app.TaskStackListener;
import android.content.ComponentName;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Trace;
import android.util.Log;
import android.window.TaskSnapshot;
import androidx.annotation.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.systemui.shared.recents.model.ThumbnailData;
import java.util.ArrayList;
import java.util.List;
/**
* Tracks all the task stack listeners
*/
public class TaskStackChangeListeners {
private static final String TAG = TaskStackChangeListeners.class.getSimpleName();
private static final TaskStackChangeListeners INSTANCE = new TaskStackChangeListeners();
private final Impl mImpl;
/**
* Proxies calls to the given handler callback synchronously for testing purposes.
*/
private static class TestSyncHandler extends Handler {
private Handler.Callback mCb;
public TestSyncHandler() {
super(Looper.getMainLooper());
}
public void setCallback(Handler.Callback cb) {
mCb = cb;
}
@Override
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
return mCb.handleMessage(msg);
}
}
private TaskStackChangeListeners() {
mImpl = new Impl(Looper.getMainLooper());
}
private TaskStackChangeListeners(Handler h) {
mImpl = new Impl(h);
}
public static TaskStackChangeListeners getInstance() {
return INSTANCE;
}
/**
* Returns an instance of the listeners that can be called upon synchronously for testsing
* purposes.
*/
@VisibleForTesting
public static TaskStackChangeListeners getTestInstance() {
TestSyncHandler h = new TestSyncHandler();
TaskStackChangeListeners l = new TaskStackChangeListeners(h);
h.setCallback(l.mImpl);
return l;
}
/**
* Registers a task stack listener with the system.
* This should be called on the main thread.
*/
public void registerTaskStackListener(TaskStackChangeListener listener) {
synchronized (mImpl) {
mImpl.addListener(listener);
}
}
/**
* Unregisters a task stack listener with the system.
* This should be called on the main thread.
*/
public void unregisterTaskStackListener(TaskStackChangeListener listener) {
synchronized (mImpl) {
mImpl.removeListener(listener);
}
}
/**
* Returns an instance of the listener to call upon from tests.
*/
@VisibleForTesting
public TaskStackListener getListenerImpl() {
return mImpl;
}
private class Impl extends TaskStackListener implements Handler.Callback {
private static final int ON_TASK_STACK_CHANGED = 1;
private static final int ON_TASK_SNAPSHOT_CHANGED = 2;
private static final int ON_ACTIVITY_PINNED = 3;
private static final int ON_ACTIVITY_RESTART_ATTEMPT = 4;
private static final int ON_ACTIVITY_FORCED_RESIZABLE = 6;
private static final int ON_ACTIVITY_DISMISSING_DOCKED_STACK = 7;
private static final int ON_TASK_PROFILE_LOCKED = 8;
private static final int ON_ACTIVITY_UNPINNED = 10;
private static final int ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_FAILED = 11;
private static final int ON_TASK_CREATED = 12;
private static final int ON_TASK_REMOVED = 13;
private static final int ON_TASK_MOVED_TO_FRONT = 14;
private static final int ON_ACTIVITY_REQUESTED_ORIENTATION_CHANGE = 15;
private static final int ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_REROUTED = 16;
private static final int ON_BACK_PRESSED_ON_TASK_ROOT = 17;
private static final int ON_TASK_DISPLAY_CHANGED = 18;
private static final int ON_TASK_LIST_UPDATED = 19;
private static final int ON_TASK_LIST_FROZEN_UNFROZEN = 20;
private static final int ON_TASK_DESCRIPTION_CHANGED = 21;
private static final int ON_ACTIVITY_ROTATION = 22;
private static final int ON_LOCK_TASK_MODE_CHANGED = 23;
private static final int ON_TASK_SNAPSHOT_INVALIDATED = 24;
/**
* List of {@link TaskStackChangeListener} registered from {@link #addListener}.
*/
private final List<TaskStackChangeListener> mTaskStackListeners = new ArrayList<>();
private final List<TaskStackChangeListener> mTmpListeners = new ArrayList<>();
private final Handler mHandler;
private boolean mRegistered;
private Impl(Looper looper) {
mHandler = new Handler(looper, this);
}
private Impl(Handler handler) {
mHandler = handler;
}
public void addListener(TaskStackChangeListener listener) {
synchronized (mTaskStackListeners) {
mTaskStackListeners.add(listener);
}
if (!mRegistered) {
// Register mTaskStackListener to IActivityManager only once if needed.
try {
// ActivityTaskManager.getService().registerTaskStackListener(this);
mRegistered = true;
} catch (Exception e) {
Log.w(TAG, "Failed to call registerTaskStackListener", e);
}
}
}
public void removeListener(TaskStackChangeListener listener) {
boolean isEmpty;
synchronized (mTaskStackListeners) {
mTaskStackListeners.remove(listener);
isEmpty = mTaskStackListeners.isEmpty();
}
if (isEmpty && mRegistered) {
// Unregister mTaskStackListener once we have no more listeners
try {
// ActivityTaskManager.getService().unregisterTaskStackListener(this);
mRegistered = false;
} catch (Exception e) {
Log.w(TAG, "Failed to call unregisterTaskStackListener", e);
}
}
}
@Override
public void onTaskStackChanged() {
// Call the task changed callback for the non-ui thread listeners first. Copy to a set
// of temp listeners so that we don't lock on mTaskStackListeners while calling all the
// callbacks. This call is always on the same binder thread, so we can just synchronize
// on the copying of the listener list.
synchronized (mTaskStackListeners) {
mTmpListeners.addAll(mTaskStackListeners);
}
for (int i = mTmpListeners.size() - 1; i >= 0; i--) {
mTmpListeners.get(i).onTaskStackChangedBackground();
}
mTmpListeners.clear();
mHandler.removeMessages(ON_TASK_STACK_CHANGED);
mHandler.sendEmptyMessage(ON_TASK_STACK_CHANGED);
}
@Override
public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
mHandler.removeMessages(ON_ACTIVITY_PINNED);
mHandler.obtainMessage(ON_ACTIVITY_PINNED,
new PinnedActivityInfo(packageName, userId, taskId, stackId)).sendToTarget();
}
@Override
public void onActivityUnpinned() {
mHandler.removeMessages(ON_ACTIVITY_UNPINNED);
mHandler.sendEmptyMessage(ON_ACTIVITY_UNPINNED);
}
@Override
public void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible,
boolean clearedTask, boolean wasVisible) {
final SomeArgs args = SomeArgs.obtain();
args.arg1 = task;
args.argi1 = homeTaskVisible ? 1 : 0;
args.argi2 = clearedTask ? 1 : 0;
args.argi3 = wasVisible ? 1 : 0;
mHandler.removeMessages(ON_ACTIVITY_RESTART_ATTEMPT);
mHandler.obtainMessage(ON_ACTIVITY_RESTART_ATTEMPT, args).sendToTarget();
}
@Override
public void onActivityForcedResizable(String packageName, int taskId, int reason) {
mHandler.obtainMessage(ON_ACTIVITY_FORCED_RESIZABLE, taskId, reason, packageName)
.sendToTarget();
}
@Override
public void onActivityDismissingDockedTask() {
mHandler.sendEmptyMessage(ON_ACTIVITY_DISMISSING_DOCKED_STACK);
}
@Override
public void onActivityLaunchOnSecondaryDisplayFailed(RunningTaskInfo taskInfo,
int requestedDisplayId) {
mHandler.obtainMessage(ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_FAILED,
requestedDisplayId,
0 /* unused */,
taskInfo).sendToTarget();
}
@Override
public void onActivityLaunchOnSecondaryDisplayRerouted(RunningTaskInfo taskInfo,
int requestedDisplayId) {
mHandler.obtainMessage(ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_REROUTED,
requestedDisplayId, 0 /* unused */, taskInfo).sendToTarget();
}
@Override
public void onTaskProfileLocked(RunningTaskInfo taskInfo, int userId) {
mHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, userId, 0, taskInfo).sendToTarget();
}
@Override
public void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) {
mHandler.obtainMessage(ON_TASK_SNAPSHOT_CHANGED, taskId, 0, snapshot).sendToTarget();
}
@Override
public void onTaskSnapshotInvalidated(int taskId) {
mHandler.obtainMessage(ON_TASK_SNAPSHOT_INVALIDATED, taskId, 0 /* unused */)
.sendToTarget();
}
@Override
public void onTaskCreated(int taskId, ComponentName componentName) {
mHandler.obtainMessage(ON_TASK_CREATED, taskId, 0, componentName).sendToTarget();
}
@Override
public void onTaskRemoved(int taskId) {
mHandler.obtainMessage(ON_TASK_REMOVED, taskId, 0).sendToTarget();
}
@Override
public void onTaskMovedToFront(RunningTaskInfo taskInfo) {
mHandler.obtainMessage(ON_TASK_MOVED_TO_FRONT, taskInfo).sendToTarget();
}
@Override
public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {
mHandler.obtainMessage(ON_BACK_PRESSED_ON_TASK_ROOT, taskInfo).sendToTarget();
}
@Override
public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
mHandler.obtainMessage(ON_ACTIVITY_REQUESTED_ORIENTATION_CHANGE, taskId,
requestedOrientation).sendToTarget();
}
@Override
public void onTaskDisplayChanged(int taskId, int newDisplayId) {
mHandler.obtainMessage(ON_TASK_DISPLAY_CHANGED, taskId, newDisplayId).sendToTarget();
}
@Override
public void onRecentTaskListUpdated() {
mHandler.obtainMessage(ON_TASK_LIST_UPDATED).sendToTarget();
}
@Override
public void onRecentTaskListFrozenChanged(boolean frozen) {
mHandler.obtainMessage(ON_TASK_LIST_FROZEN_UNFROZEN, frozen ? 1 : 0, 0 /* unused */)
.sendToTarget();
}
@Override
public void onTaskDescriptionChanged(RunningTaskInfo taskInfo) {
mHandler.obtainMessage(ON_TASK_DESCRIPTION_CHANGED, taskInfo).sendToTarget();
}
@Override
public void onActivityRotation(int displayId) {
mHandler.obtainMessage(ON_ACTIVITY_ROTATION, displayId, 0 /* unused */)
.sendToTarget();
}
@Override
public void onLockTaskModeChanged(int mode) {
mHandler.obtainMessage(ON_LOCK_TASK_MODE_CHANGED, mode, 0 /* unused */).sendToTarget();
}
@Override
public boolean handleMessage(Message msg) {
synchronized (mTaskStackListeners) {
switch (msg.what) {
case ON_TASK_STACK_CHANGED: {
Trace.beginSection("onTaskStackChanged");
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onTaskStackChanged();
}
Trace.endSection();
break;
}
case ON_TASK_SNAPSHOT_CHANGED: {
Trace.beginSection("onTaskSnapshotChanged");
final TaskSnapshot snapshot = (TaskSnapshot) msg.obj;
final ThumbnailData thumbnail = ThumbnailData.fromSnapshot(snapshot);
boolean snapshotConsumed = false;
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
boolean consumed = mTaskStackListeners.get(i).onTaskSnapshotChanged(
msg.arg1, thumbnail);
snapshotConsumed |= consumed;
}
if (!snapshotConsumed) {
thumbnail.recycleBitmap();
if (snapshot.getHardwareBuffer() != null) {
snapshot.getHardwareBuffer().close();
}
}
Trace.endSection();
break;
}
case ON_ACTIVITY_PINNED: {
final PinnedActivityInfo info = (PinnedActivityInfo) msg.obj;
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onActivityPinned(
info.mPackageName, info.mUserId, info.mTaskId,
info.mStackId);
}
break;
}
case ON_ACTIVITY_UNPINNED: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onActivityUnpinned();
}
break;
}
case ON_ACTIVITY_RESTART_ATTEMPT: {
final SomeArgs args = (SomeArgs) msg.obj;
final RunningTaskInfo task = (RunningTaskInfo) args.arg1;
final boolean homeTaskVisible = args.argi1 != 0;
final boolean clearedTask = args.argi2 != 0;
final boolean wasVisible = args.argi3 != 0;
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onActivityRestartAttempt(task,
homeTaskVisible, clearedTask, wasVisible);
}
break;
}
case ON_ACTIVITY_FORCED_RESIZABLE: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onActivityForcedResizable(
(String) msg.obj, msg.arg1, msg.arg2);
}
break;
}
case ON_ACTIVITY_DISMISSING_DOCKED_STACK: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onActivityDismissingDockedStack();
}
break;
}
case ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_FAILED: {
final RunningTaskInfo info = (RunningTaskInfo) msg.obj;
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i)
.onActivityLaunchOnSecondaryDisplayFailed(info);
}
break;
}
case ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_REROUTED: {
final RunningTaskInfo info = (RunningTaskInfo) msg.obj;
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i)
.onActivityLaunchOnSecondaryDisplayRerouted(info);
}
break;
}
case ON_TASK_PROFILE_LOCKED: {
final RunningTaskInfo info = (RunningTaskInfo) msg.obj;
final int userId = msg.arg1;
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onTaskProfileLocked(info, userId);
}
break;
}
case ON_TASK_CREATED: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onTaskCreated(msg.arg1,
(ComponentName) msg.obj);
}
break;
}
case ON_TASK_REMOVED: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onTaskRemoved(msg.arg1);
}
break;
}
case ON_TASK_MOVED_TO_FRONT: {
final RunningTaskInfo info = (RunningTaskInfo) msg.obj;
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onTaskMovedToFront(info);
}
break;
}
case ON_ACTIVITY_REQUESTED_ORIENTATION_CHANGE: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i)
.onActivityRequestedOrientationChanged(msg.arg1, msg.arg2);
}
break;
}
case ON_BACK_PRESSED_ON_TASK_ROOT: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onBackPressedOnTaskRoot(
(RunningTaskInfo) msg.obj);
}
break;
}
case ON_TASK_DISPLAY_CHANGED: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onTaskDisplayChanged(msg.arg1, msg.arg2);
}
break;
}
case ON_TASK_LIST_UPDATED: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onRecentTaskListUpdated();
}
break;
}
case ON_TASK_LIST_FROZEN_UNFROZEN: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onRecentTaskListFrozenChanged(
msg.arg1 != 0);
}
break;
}
case ON_TASK_DESCRIPTION_CHANGED: {
final RunningTaskInfo info = (RunningTaskInfo) msg.obj;
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onTaskDescriptionChanged(info);
}
break;
}
case ON_ACTIVITY_ROTATION: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onActivityRotation(msg.arg1);
}
break;
}
case ON_LOCK_TASK_MODE_CHANGED: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onLockTaskModeChanged(msg.arg1);
}
break;
}
case ON_TASK_SNAPSHOT_INVALIDATED: {
Trace.beginSection("onTaskSnapshotInvalidated");
final ThumbnailData thumbnail = new ThumbnailData();
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onTaskSnapshotChanged(msg.arg1, thumbnail);
}
Trace.endSection();
break;
}
}
}
if (msg.obj instanceof SomeArgs) {
((SomeArgs) msg.obj).recycle();
}
return true;
}
}
private static class PinnedActivityInfo {
final String mPackageName;
final int mUserId;
final int mTaskId;
final int mStackId;
PinnedActivityInfo(String packageName, int userId, int taskId, int stackId) {
mPackageName = packageName;
mUserId = userId;
mTaskId = taskId;
mStackId = stackId;
}
}
}
@@ -0,0 +1,71 @@
package com.android.systemui.shared.system
import android.util.Log
import java.lang.Thread.UncaughtExceptionHandler
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
import javax.inject.Singleton
/**
* Sets the global (static var in Thread) uncaught exception pre-handler to an implementation that
* delegates to each item in a list of registered UncaughtExceptionHandlers.
*/
@Singleton
class UncaughtExceptionPreHandlerManager @Inject constructor() {
private val handlers: MutableList<UncaughtExceptionHandler> = CopyOnWriteArrayList()
private val globalUncaughtExceptionPreHandler = GlobalUncaughtExceptionHandler()
/**
* Adds an exception pre-handler to the list of handlers. If this has not yet set the global
* (static var in Thread) uncaught exception pre-handler yet, it will do so.
*/
fun registerHandler(handler: UncaughtExceptionHandler) {
checkGlobalHandlerSetup()
addHandler(handler)
}
/**
* Verifies that the global handler is set in Thread. If not, sets is up.
*/
private fun checkGlobalHandlerSetup() {
val currentHandler = Thread.getDefaultUncaughtExceptionHandler()
if (currentHandler != globalUncaughtExceptionPreHandler) {
if (currentHandler is GlobalUncaughtExceptionHandler) {
throw IllegalStateException("Two UncaughtExceptionPreHandlerManagers created")
}
currentHandler?.let { addHandler(it) }
Thread.setDefaultUncaughtExceptionHandler(globalUncaughtExceptionPreHandler)
}
}
/**
* Adds a handler if it has not already been added, preserving order.
*/
private fun addHandler(it: UncaughtExceptionHandler) {
if (it !in handlers) {
handlers.add(it)
}
}
/**
* Calls uncaughtException on all registered handlers, catching and logging any new exceptions.
*/
fun handleUncaughtException(thread: Thread?, throwable: Throwable?) {
for (handler in handlers) {
try {
handler.uncaughtException(thread, throwable)
} catch (e: Exception) {
Log.wtf("Uncaught exception pre-handler error", e)
}
}
}
/**
* UncaughtExceptionHandler impl that will be set as Thread's pre-handler static variable.
*/
inner class GlobalUncaughtExceptionHandler : UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread?, throwable: Throwable?) {
handleUncaughtException(thread, throwable)
}
}
}
@@ -0,0 +1,47 @@
/*
* Copyright (C) 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 com.android.systemui.shared.system.smartspace;
import android.graphics.Rect;
import com.android.systemui.shared.system.smartspace.SmartspaceState;
// Methods for System UI to interface with Launcher to perform the unlock animation.
interface ILauncherUnlockAnimationController {
// Prepares Launcher for the unlock animation by setting scale/alpha/etc. to their starting
// values.
void prepareForUnlock(boolean animateSmartspace, in Rect lockscreenSmartspaceBounds,
int selectedPage);
// Set the unlock percentage. This is used when System UI is controlling each frame of the
// unlock animation, such as during a swipe to unlock touch gesture. Will not apply this change
// if the unlock amount is animating unless forceIfAnimating is true.
oneway void setUnlockAmount(float amount, boolean forceIfAnimating);
// Play a full unlock animation from 0f to 1f. This is used when System UI is unlocking from a
// single action, such as biometric auth, and doesn't need to control individual frames.
oneway void playUnlockAnimation(boolean unlocked, long duration, long startDelay);
// Set the selected page on Launcher's smartspace.
oneway void setSmartspaceSelectedPage(int selectedPage);
// Set the visibility of Launcher's smartspace.
void setSmartspaceVisibility(int visibility);
// Tell SystemUI the smartspace's current state. Launcher code should call this whenever the
// smartspace state may have changed.
oneway void dispatchSmartspaceStateToSysui();
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 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 com.android.systemui.shared.system.smartspace;
import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController;
import com.android.systemui.shared.system.smartspace.SmartspaceState;
// System UI unlock controller. Launcher will provide a LauncherUnlockAnimationController to this
// controller, which System UI will use to control the unlock animation within the Launcher window.
interface ISysuiUnlockAnimationController {
// Provides an implementation of the LauncherUnlockAnimationController to System UI, so that
// SysUI can use it to control the unlock animation in the launcher window.
oneway void setLauncherUnlockController(
String activityClass, ILauncherUnlockAnimationController callback);
// Called by Launcher whenever anything happens to change the state of its smartspace. System UI
// proactively saves this and uses it to perform the unlock animation without needing to make a
// blocking query to Launcher asking about the smartspace state.
oneway void onLauncherSmartspaceStateUpdated(in SmartspaceState state);
}
@@ -0,0 +1,19 @@
/*
* Copyright (C) 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 com.android.systemui.shared.system.smartspace;
parcelable SmartspaceState;
@@ -0,0 +1,64 @@
/*
* Copyright (C) 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 com.android.systemui.shared.system.smartspace
import android.graphics.Rect
import android.os.Parcel
import android.os.Parcelable
/**
* Represents the state of a SmartSpace, including its location on screen and the index of the
* currently selected page. This object contains all of the information needed to synchronize two
* SmartSpace instances so that we can perform shared-element transitions between them.
*/
class SmartspaceState() : Parcelable {
var boundsOnScreen: Rect = Rect()
var selectedPage = 0
var visibleOnScreen = false
constructor(parcel: Parcel) : this() {
this.boundsOnScreen = parcel.readParcelable(Rect::javaClass.javaClass.classLoader) ?: Rect()
this.selectedPage = parcel.readInt()
this.visibleOnScreen = parcel.readBoolean()
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeParcelable(boundsOnScreen, 0)
dest.writeInt(selectedPage)
dest.writeBoolean(visibleOnScreen)
}
override fun describeContents(): Int {
return 0
}
override fun toString(): String {
return "boundsOnScreen: $boundsOnScreen, " +
"selectedPage: $selectedPage, " +
"visibleOnScreen: $visibleOnScreen"
}
companion object CREATOR : Parcelable.Creator<SmartspaceState> {
override fun createFromParcel(parcel: Parcel): SmartspaceState {
return SmartspaceState(parcel)
}
override fun newArray(size: Int): Array<SmartspaceState?> {
return arrayOfNulls(size)
}
}
}
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2016 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 com.android.systemui.statusbar.policy;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
/**
* Implementation of the collection used and thread guarantees are left to the discretion of the
* client. Consider using {@link com.android.systemui.util.ListenerSet} to prevent concurrent
* modification exceptions.
*/
public interface CallbackController<T> {
/** Add a callback */
void addCallback(@NonNull T listener);
/** Remove a callback */
void removeCallback(@NonNull T listener);
/**
* Wrapper to {@link #addCallback(Object)} when a lifecycle is in the resumed state
* and {@link #removeCallback(Object)} when not resumed automatically.
*/
default T observe(LifecycleOwner owner, T listener) {
return observe(owner.getLifecycle(), listener);
}
/**
* Wrapper to {@link #addCallback(Object)} when a lifecycle is in the resumed state
* and {@link #removeCallback(Object)} when not resumed automatically.
*/
default T observe(Lifecycle lifecycle, T listener) {
lifecycle.addObserver((LifecycleEventObserver) (lifecycleOwner, event) -> {
if (event == Event.ON_RESUME) {
addCallback(listener);
} else if (event == Event.ON_PAUSE) {
removeCallback(listener);
}
});
return listener;
}
}
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2022 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 com.android.systemui.unfold.system
import android.app.ActivityManager
import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration
import android.os.Trace
import com.android.systemui.shared.system.TaskStackChangeListener
import com.android.systemui.shared.system.TaskStackChangeListeners
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ActivityManagerActivityTypeProvider
@Inject
constructor(private val activityManager: ActivityManager) : CurrentActivityTypeProvider {
override val isHomeActivity: Boolean?
get() = _isHomeActivity
@Volatile private var _isHomeActivity: Boolean? = null
override fun init() {
_isHomeActivity = activityManager.isOnHomeActivity()
TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener)
}
override fun uninit() {
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
}
private val taskStackChangeListener =
object : TaskStackChangeListener {
override fun onTaskMovedToFront(taskInfo: RunningTaskInfo) {
_isHomeActivity = taskInfo.isHomeActivity()
}
}
private fun RunningTaskInfo.isHomeActivity(): Boolean =
topActivityType == WindowConfiguration.ACTIVITY_TYPE_HOME
private fun ActivityManager.isOnHomeActivity(): Boolean? {
try {
Trace.beginSection("isOnHomeActivity")
return getRunningTasks(/* maxNum= */ 1)?.firstOrNull()?.isHomeActivity()
} finally {
Trace.endSection()
}
}
}
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2022 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 com.android.systemui.unfold.system
import android.content.Context
import android.hardware.devicestate.DeviceStateManager
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DeviceStateManagerFoldProvider
@Inject
constructor(private val deviceStateManager: DeviceStateManager, private val context: Context) :
FoldProvider {
private val callbacks =
ConcurrentHashMap<FoldCallback, DeviceStateManager.DeviceStateCallback>()
override fun registerCallback(callback: FoldCallback, executor: Executor) {
val listener = FoldStateListener(context, callback)
deviceStateManager.registerCallback(executor, listener)
callbacks[callback] = listener
}
override fun unregisterCallback(callback: FoldCallback) {
val listener = callbacks.remove(callback)
listener?.let { deviceStateManager.unregisterCallback(it) }
}
private inner class FoldStateListener(context: Context, listener: FoldCallback) :
DeviceStateManager.FoldStateListener(context, { listener.onFoldUpdated(it) })
}
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 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 com.android.systemui.unfold.system
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
/** Provides whether the device is folded. */
interface DeviceStateRepository {
val isFolded: Flow<Boolean>
}
@Singleton
class DeviceStateRepositoryImpl
@Inject
constructor(
private val foldProvider: FoldProvider,
@UnfoldMain private val executor: Executor,
) : DeviceStateRepository {
override val isFolded: Flow<Boolean>
get() =
callbackFlow {
val callback = FoldCallback { isFolded -> trySend(isFolded) }
foldProvider.registerCallback(callback, executor)
awaitClose { foldProvider.unregisterCallback(callback) }
}
.buffer(capacity = Channel.CONFLATED)
}
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2022 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 com.android.systemui.unfold.system
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.os.Process
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.dagger.UnfoldBg
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import java.util.concurrent.Executor
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.android.asCoroutineDispatcher
/**
* Dagger module with system-only dependencies for the unfold animation. The code that is used to
* calculate unfold transition progress depends on some hidden APIs that are not available in normal
* apps. In order to re-use this code and use alternative implementations of these classes in other
* apps and hidden APIs here.
*/
@Module
abstract class SystemUnfoldSharedModule {
@Binds
abstract fun activityTypeProvider(executor: ActivityManagerActivityTypeProvider):
CurrentActivityTypeProvider
@Binds
abstract fun config(config: ResourceUnfoldTransitionConfig): UnfoldTransitionConfig
@Binds
abstract fun foldState(provider: DeviceStateManagerFoldProvider): FoldProvider
@Binds
abstract fun deviceStateRepository(provider: DeviceStateRepositoryImpl): DeviceStateRepository
@Binds
@UnfoldMain
abstract fun mainExecutor(@Main executor: Executor): Executor
@Binds
@UnfoldMain
abstract fun mainHandler(@Main handler: Handler): Handler
@Binds
@UnfoldSingleThreadBg
abstract fun backgroundExecutor(@UiBackground executor: Executor): Executor
companion object {
@Provides
@UnfoldBg
@Singleton
fun unfoldBgProgressHandler(@UnfoldBg looper: Looper): Handler {
return Handler(looper)
}
@Provides
@UnfoldBg
@Singleton
fun unfoldBgDispatcher(@UnfoldBg handler: Handler): CoroutineDispatcher {
return handler.asCoroutineDispatcher("@UnfoldBg Dispatcher")
}
@Provides
@UnfoldBg
@Singleton
fun provideBgLooper(): Looper {
return HandlerThread("UnfoldBg", Process.THREAD_PRIORITY_FOREGROUND)
.apply { start() }
.looper
}
}
}
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2022 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 com.android.systemui.unfold.util
import android.view.View
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor.CUJ_UNFOLD_ANIM
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import java.util.function.Supplier
class JankMonitorTransitionProgressListener(private val attachedViewProvider: Supplier<View>) :
TransitionProgressListener {
private val interactionJankMonitor = InteractionJankMonitor.getInstance()
override fun onTransitionStarted() {
interactionJankMonitor.begin(attachedViewProvider.get(), CUJ_UNFOLD_ANIM)
}
override fun onTransitionFinished() {
interactionJankMonitor.end(CUJ_UNFOLD_ANIM)
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2022 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 com.android.systemui.unfold.util
import android.content.Context
import android.view.Surface
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
/**
* [UnfoldTransitionProgressProvider] that emits transition progress only when the display has
* default rotation or 180 degrees opposite rotation (ROTATION_0 or ROTATION_180). It could be
* helpful to run the animation only when the display's rotation is perpendicular to the fold.
*/
class NaturalRotationUnfoldProgressProvider(
private val context: Context,
private val rotationChangeProvider: RotationChangeProvider,
unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider
) : UnfoldTransitionProgressProvider {
private val scopedUnfoldTransitionProgressProvider =
ScopedUnfoldTransitionProgressProvider(unfoldTransitionProgressProvider)
private var isNaturalRotation: Boolean = false
fun init() {
rotationChangeProvider.addCallback(rotationListener)
context.display?.rotation?.let { rotationListener.onRotationChanged(it) }
}
private val rotationListener = RotationListener { rotation ->
val isNewRotationNatural =
rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
if (isNaturalRotation != isNewRotationNatural) {
isNaturalRotation = isNewRotationNatural
scopedUnfoldTransitionProgressProvider.setReadyToHandleTransition(isNewRotationNatural)
}
}
override fun destroy() {
rotationChangeProvider.removeCallback(rotationListener)
scopedUnfoldTransitionProgressProvider.destroy()
}
override fun addCallback(listener: TransitionProgressListener) {
scopedUnfoldTransitionProgressProvider.addCallback(listener)
}
override fun removeCallback(listener: TransitionProgressListener) {
scopedUnfoldTransitionProgressProvider.removeCallback(listener)
}
}
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2023 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 com.android.systemui.unfold.util
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
import java.util.concurrent.Executor
/**
* [UnfoldTransitionProgressProvider] that emits transition progress only when unfolding but not
* when folding, so we can play the animation only one way but not the other way.
*/
class UnfoldOnlyProgressProvider(
foldProvider: FoldProvider,
@Main private val executor: Executor,
private val sourceProvider: UnfoldTransitionProgressProvider,
private val scopedProvider: ScopedUnfoldTransitionProgressProvider =
ScopedUnfoldTransitionProgressProvider(sourceProvider)
) : UnfoldTransitionProgressProvider by scopedProvider {
private var isFolded = false
init {
foldProvider.registerCallback(FoldListener(), executor)
sourceProvider.addCallback(SourceTransitionListener())
}
private inner class SourceTransitionListener : TransitionProgressListener {
override fun onTransitionFinished() {
// Disable scoped progress provider after the first unfold animation, so fold animation
// will not be propagated. It will be re-enabled after folding so we can play
// the unfold animation again.
if (!isFolded) {
scopedProvider.setReadyToHandleTransition(false)
}
}
}
private inner class FoldListener : FoldCallback {
override fun onFoldUpdated(isFolded: Boolean) {
if (isFolded) {
scopedProvider.setReadyToHandleTransition(true)
}
this@UnfoldOnlyProgressProvider.isFolded = isFolded
}
}
}