Updated project directories for unified 'systemUI' folder (#5034)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
+120
@@ -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),
|
||||
}
|
||||
}
|
||||
+195
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+359
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+219
@@ -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 */);
|
||||
}
|
||||
};
|
||||
}
|
||||
+41
@@ -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);
|
||||
}
|
||||
}
|
||||
+93
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
@@ -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() {};
|
||||
}
|
||||
}
|
||||
+736
@@ -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);
|
||||
}
|
||||
}
|
||||
+43
@@ -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);
|
||||
}
|
||||
}
|
||||
+94
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+166
@@ -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;
|
||||
}
|
||||
}
|
||||
+97
@@ -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 task’s 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -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();
|
||||
}
|
||||
+34
@@ -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);
|
||||
}
|
||||
+19
@@ -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;
|
||||
}
|
||||
}
|
||||
+64
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -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)
|
||||
}
|
||||
}
|
||||
+67
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user