Merge "Add feature flag for typeface redesign" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 5db0772..dfacbc4 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -140,11 +140,17 @@
"ravenwood-presubmit": [
{
"name": "CtsUtilTestCasesRavenwood",
- "host": true
+ "host": true,
+ "file_patterns": [
+ "[Rr]avenwood"
+ ]
},
{
"name": "RavenwoodBivalentTest",
- "host": true
+ "host": true,
+ "file_patterns": [
+ "[Rr]avenwood"
+ ]
}
],
"postsubmit-managedprofile-stress": [
diff --git a/core/api/current.txt b/core/api/current.txt
index f96e4fd..c7df662 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9875,6 +9875,7 @@
field public static final int RESULT_DISCOVERY_TIMEOUT = 2; // 0x2
field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3
field public static final int RESULT_OK = -1; // 0xffffffff
+ field @FlaggedApi("android.companion.association_failure_code") public static final int RESULT_SECURITY_ERROR = 4; // 0x4
field public static final int RESULT_USER_REJECTED = 1; // 0x1
}
@@ -9884,7 +9885,7 @@
method public void onAssociationPending(@NonNull android.content.IntentSender);
method @Deprecated public void onDeviceFound(@NonNull android.content.IntentSender);
method public abstract void onFailure(@Nullable CharSequence);
- method @FlaggedApi("android.companion.association_failure_code") public void onFailure(int);
+ method @FlaggedApi("android.companion.association_failure_code") public void onFailure(int, @Nullable CharSequence);
}
public abstract class CompanionDeviceService extends android.app.Service {
@@ -46100,7 +46101,7 @@
method public int getCardIdForDefaultEuicc();
method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @WorkerThread public android.os.PersistableBundle getCarrierConfig();
method public int getCarrierIdFromSimMccMnc();
- method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public void getCarrierRestrictionStatus(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @RequiresPermission(anyOf={android.Manifest.permission.READ_BASIC_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public void getCarrierRestrictionStatus(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) public android.telephony.CellLocation getCellLocation();
method public int getDataActivity();
method @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.READ_BASIC_PHONE_STATE}) public int getDataNetworkType();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index ce0d38f..8dd4adc 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3676,6 +3676,7 @@
public static final class Display.Mode implements android.os.Parcelable {
ctor public Display.Mode(int, int, float);
+ method public boolean isSynthetic();
method public boolean matches(int, int, float);
}
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index 0ff5514..1e9a79b 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -210,6 +210,11 @@
public static final int START_TIMESTAMP_SURFACEFLINGER_COMPOSITION_COMPLETE = 7;
/**
+ * @see #getMonoticCreationTimeMs
+ */
+ private long mMonoticCreationTimeMs;
+
+ /**
* @see #getStartupState
*/
private @StartupState int mStartupState;
@@ -487,6 +492,15 @@
}
/**
+ * Monotonic elapsed time persisted across reboots.
+ *
+ * @hide
+ */
+ public long getMonoticCreationTimeMs() {
+ return mMonoticCreationTimeMs;
+ }
+
+ /**
* The process id.
*
* <p class="note"> Note: field will be set for any {@link #getStartupState} value.</p>
@@ -669,7 +683,9 @@
}
/** @hide */
- public ApplicationStartInfo() {}
+ public ApplicationStartInfo(long monotonicCreationTimeMs) {
+ mMonoticCreationTimeMs = monotonicCreationTimeMs;
+ }
/** @hide */
public ApplicationStartInfo(ApplicationStartInfo other) {
@@ -686,6 +702,7 @@
mStartIntent = other.mStartIntent;
mLaunchMode = other.mLaunchMode;
mWasForceStopped = other.mWasForceStopped;
+ mMonoticCreationTimeMs = other.mMonoticCreationTimeMs;
}
private ApplicationStartInfo(@NonNull Parcel in) {
@@ -708,6 +725,7 @@
in.readParcelable(Intent.class.getClassLoader(), android.content.Intent.class);
mLaunchMode = in.readInt();
mWasForceStopped = in.readBoolean();
+ mMonoticCreationTimeMs = in.readLong();
}
private static String intern(@Nullable String source) {
@@ -786,6 +804,7 @@
}
proto.write(ApplicationStartInfoProto.LAUNCH_MODE, mLaunchMode);
proto.write(ApplicationStartInfoProto.WAS_FORCE_STOPPED, mWasForceStopped);
+ proto.write(ApplicationStartInfoProto.MONOTONIC_CREATION_TIME_MS, mMonoticCreationTimeMs);
proto.end(token);
}
@@ -869,6 +888,10 @@
mWasForceStopped = proto.readBoolean(
ApplicationStartInfoProto.WAS_FORCE_STOPPED);
break;
+ case (int) ApplicationStartInfoProto.MONOTONIC_CREATION_TIME_MS:
+ mMonoticCreationTimeMs = proto.readLong(
+ ApplicationStartInfoProto.MONOTONIC_CREATION_TIME_MS);
+ break;
}
}
proto.end(token);
@@ -881,6 +904,8 @@
sb.append(prefix)
.append("ApplicationStartInfo ").append(seqSuffix).append(':')
.append('\n')
+ .append(" monotonicCreationTimeMs=").append(mMonoticCreationTimeMs)
+ .append('\n')
.append(" pid=").append(mPid)
.append(" realUid=").append(mRealUid)
.append(" packageUid=").append(mPackageUid)
@@ -949,14 +974,15 @@
&& mDefiningUid == o.mDefiningUid && mReason == o.mReason
&& mStartupState == o.mStartupState && mStartType == o.mStartType
&& mLaunchMode == o.mLaunchMode && TextUtils.equals(mProcessName, o.mProcessName)
- && timestampsEquals(o) && mWasForceStopped == o.mWasForceStopped;
+ && timestampsEquals(o) && mWasForceStopped == o.mWasForceStopped
+ && mMonoticCreationTimeMs == o.mMonoticCreationTimeMs;
}
@Override
public int hashCode() {
return Objects.hash(mPid, mRealUid, mPackageUid, mDefiningUid, mReason, mStartupState,
- mStartType, mLaunchMode, mProcessName,
- mStartupTimestampsNs);
+ mStartType, mLaunchMode, mProcessName, mStartupTimestampsNs,
+ mMonoticCreationTimeMs);
}
private boolean timestampsEquals(@NonNull ApplicationStartInfo other) {
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 965e3c4..ba1dc56 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -4334,11 +4334,13 @@
*/
@RequiresPermission(value = MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional = true)
@FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ @UserHandleAware
public @ContentProtectionPolicy int getContentProtectionPolicy(@Nullable ComponentName admin) {
throwIfParentInstance("getContentProtectionPolicy");
if (mService != null) {
try {
- return mService.getContentProtectionPolicy(admin, mContext.getPackageName());
+ return mService.getContentProtectionPolicy(admin, mContext.getPackageName(),
+ myUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index c393a9e..d4e5c99 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -621,7 +621,7 @@
void calculateHasIncompatibleAccounts();
void setContentProtectionPolicy(in ComponentName who, String callerPackageName, int policy);
- int getContentProtectionPolicy(in ComponentName who, String callerPackageName);
+ int getContentProtectionPolicy(in ComponentName who, String callerPackageName, int userId);
int[] getSubscriptionIds(String callerPackageName);
diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
index a50425e..db3de62 100644
--- a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
+++ b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
@@ -40,8 +40,8 @@
public ExecuteAppFunctionRequest createFromParcel(Parcel parcel) {
String targetPackageName = parcel.readString8();
String functionIdentifier = parcel.readString8();
- GenericDocument parameters;
- parameters = GenericDocument.createFromParcel(parcel);
+ GenericDocumentWrapper parameters = GenericDocumentWrapper
+ .CREATOR.createFromParcel(parcel);
Bundle extras = parcel.readBundle(Bundle.class.getClassLoader());
return new ExecuteAppFunctionRequest(
targetPackageName, functionIdentifier, extras, parameters);
@@ -75,17 +75,17 @@
*
* <p>The document may have missing parameters. Developers are advised to implement defensive
* handling measures.
- *
+ * <p>
* TODO(b/357551503): Document how function parameters can be obtained for function execution
*/
@NonNull
- private final GenericDocument mParameters;
+ private final GenericDocumentWrapper mParameters;
private ExecuteAppFunctionRequest(
@NonNull String targetPackageName,
@NonNull String functionIdentifier,
@NonNull Bundle extras,
- @NonNull GenericDocument parameters) {
+ @NonNull GenericDocumentWrapper parameters) {
mTargetPackageName = Objects.requireNonNull(targetPackageName);
mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
mExtras = Objects.requireNonNull(extras);
@@ -117,7 +117,7 @@
*/
@NonNull
public GenericDocument getParameters() {
- return mParameters;
+ return mParameters.getValue();
}
/**
@@ -152,7 +152,8 @@
@NonNull
private Bundle mExtras = Bundle.EMPTY;
@NonNull
- private GenericDocument mParameters = new GenericDocument.Builder<>("", "", "").build();
+ private GenericDocument mParameters =
+ new GenericDocument.Builder<>("", "", "").build();
public Builder(@NonNull String targetPackageName, @NonNull String functionIdentifier) {
mTargetPackageName = Objects.requireNonNull(targetPackageName);
@@ -173,7 +174,8 @@
*/
@NonNull
public Builder setParameters(@NonNull GenericDocument parameters) {
- mParameters = Objects.requireNonNull(parameters);
+ Objects.requireNonNull(parameters);
+ mParameters = parameters;
return this;
}
@@ -183,7 +185,8 @@
@NonNull
public ExecuteAppFunctionRequest build() {
return new ExecuteAppFunctionRequest(
- mTargetPackageName, mFunctionIdentifier, mExtras, mParameters);
+ mTargetPackageName, mFunctionIdentifier, mExtras,
+ new GenericDocumentWrapper(mParameters));
}
}
}
diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
index 872327d..9fb3375 100644
--- a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
+++ b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
@@ -41,13 +41,16 @@
new Creator<ExecuteAppFunctionResponse>() {
@Override
public ExecuteAppFunctionResponse createFromParcel(Parcel parcel) {
- GenericDocument result =
- Objects.requireNonNull(GenericDocument.createFromParcel(parcel));
+ GenericDocumentWrapper resultWrapper =
+ Objects.requireNonNull(
+ GenericDocumentWrapper
+ .CREATOR.createFromParcel(parcel));
Bundle extras = Objects.requireNonNull(
parcel.readBundle(Bundle.class.getClassLoader()));
int resultCode = parcel.readInt();
String errorMessage = parcel.readString8();
- return new ExecuteAppFunctionResponse(result, extras, resultCode, errorMessage);
+ return new ExecuteAppFunctionResponse(
+ resultWrapper, extras, resultCode, errorMessage);
}
@Override
@@ -127,7 +130,7 @@
* <p>See {@link #getResultDocument} for more information on extracting the return value.
*/
@NonNull
- private final GenericDocument mResultDocument;
+ private final GenericDocumentWrapper mResultDocumentWrapper;
/**
* Returns the additional metadata data relevant to this function execution response.
@@ -135,17 +138,30 @@
@NonNull
private final Bundle mExtras;
- private ExecuteAppFunctionResponse(@NonNull GenericDocument resultDocument,
+ private ExecuteAppFunctionResponse(@NonNull GenericDocumentWrapper resultDocumentWrapper,
@NonNull Bundle extras,
@ResultCode int resultCode,
@Nullable String errorMessage) {
- mResultDocument = Objects.requireNonNull(resultDocument);
+ mResultDocumentWrapper = Objects.requireNonNull(resultDocumentWrapper);
mExtras = Objects.requireNonNull(extras);
mResultCode = resultCode;
mErrorMessage = errorMessage;
}
/**
+ * Returns result codes from throwable.
+ *
+ * @hide
+ */
+ @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
+ static @ResultCode int getResultCode(@NonNull Throwable t) {
+ if (t instanceof IllegalArgumentException) {
+ return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT;
+ }
+ return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR;
+ }
+
+ /**
* Returns a generic document containing the return value of the executed function.
*
* <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value.</p>
@@ -166,7 +182,7 @@
*/
@NonNull
public GenericDocument getResultDocument() {
- return mResultDocument;
+ return mResultDocumentWrapper.getValue();
}
/**
@@ -210,7 +226,7 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
- mResultDocument.writeToParcel(dest, flags);
+ mResultDocumentWrapper.writeToParcel(dest, flags);
dest.writeBundle(mExtras);
dest.writeInt(mResultCode);
dest.writeString8(mErrorMessage);
@@ -236,24 +252,13 @@
}
/**
- * Returns result codes from throwable.
- *
- * @hide
- */
- static @ResultCode int getResultCode(@NonNull Throwable t) {
- if (t instanceof IllegalArgumentException) {
- return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT;
- }
- return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR;
- }
-
- /**
* The builder for creating {@link ExecuteAppFunctionResponse} instances.
*/
@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
public static final class Builder {
@NonNull
- private GenericDocument mResultDocument = new GenericDocument.Builder<>("", "", "").build();
+ private GenericDocument mResultDocument =
+ new GenericDocument.Builder<>("", "", "").build();
@NonNull
private Bundle mExtras = Bundle.EMPTY;
private int mResultCode;
@@ -271,7 +276,8 @@
* with a result code of {@link #RESULT_OK} and a resultDocument.
*/
public Builder(@NonNull GenericDocument resultDocument) {
- mResultDocument = Objects.requireNonNull(resultDocument);
+ Objects.requireNonNull(resultDocument);
+ mResultDocument = resultDocument;
mResultCode = RESULT_OK;
}
@@ -300,7 +306,8 @@
@NonNull
public ExecuteAppFunctionResponse build() {
return new ExecuteAppFunctionResponse(
- mResultDocument, mExtras, mResultCode, mErrorMessage);
+ new GenericDocumentWrapper(mResultDocument),
+ mExtras, mResultCode, mErrorMessage);
}
}
}
diff --git a/core/java/android/app/appfunctions/GenericDocumentWrapper.java b/core/java/android/app/appfunctions/GenericDocumentWrapper.java
new file mode 100644
index 0000000..8c76c8e
--- /dev/null
+++ b/core/java/android/app/appfunctions/GenericDocumentWrapper.java
@@ -0,0 +1,91 @@
+/*
+ * 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 android.app.appfunctions;
+
+import android.app.appsearch.GenericDocument;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import java.util.Objects;
+
+/**
+ * The Parcelable object contains a {@link GenericDocument} to allow the parcelization of it
+ * exceeding the binder limit.
+ *
+ * <p>{#link {@link Parcel#writeBlob(byte[])}} could take care of whether to pass data via binder
+ * directly or Android shared memory if the data is large.
+ *
+ * @hide
+ * @see Parcel#writeBlob(byte[])
+ */
+public final class GenericDocumentWrapper implements Parcelable {
+ public static final Creator<GenericDocumentWrapper> CREATOR =
+ new Creator<>() {
+ @Override
+ public GenericDocumentWrapper createFromParcel(Parcel in) {
+ byte[] dataBlob = Objects.requireNonNull(in.readBlob());
+ Parcel unmarshallParcel = Parcel.obtain();
+ try {
+ unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length);
+ unmarshallParcel.setDataPosition(0);
+ return new GenericDocumentWrapper(
+ GenericDocument.createFromParcel(unmarshallParcel));
+ } finally {
+ unmarshallParcel.recycle();
+ }
+ }
+
+ @Override
+ public GenericDocumentWrapper[] newArray(int size) {
+ return new GenericDocumentWrapper[size];
+ }
+ };
+ @NonNull
+ private final GenericDocument mGenericDocument;
+
+ public GenericDocumentWrapper(@NonNull GenericDocument genericDocument) {
+ mGenericDocument = Objects.requireNonNull(genericDocument);
+ }
+
+ /**
+ * Returns the wrapped {@link android.app.appsearch.GenericDocument}
+ */
+ @NonNull
+ public GenericDocument getValue() {
+ return mGenericDocument;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Parcel parcel = Parcel.obtain();
+ try {
+ mGenericDocument.writeToParcel(parcel, flags);
+ byte[] bytes = parcel.marshall();
+ dest.writeBlob(bytes);
+ } finally {
+ parcel.recycle();
+ }
+
+ }
+}
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 1529842..1cdf3b1 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -21,7 +21,6 @@
import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_COMPUTER;
import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH;
-import static java.util.Collections.unmodifiableMap;
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
@@ -58,7 +57,6 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
-import android.util.ArrayMap;
import android.util.ExceptionUtils;
import android.util.Log;
import android.util.SparseArray;
@@ -78,7 +76,6 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
@@ -146,12 +143,19 @@
/**
* The result code to propagate back to the user activity, indicates the internal error
* in CompanionDeviceManager.
- * E.g. Missing necessary permissions or duplicate {@link AssociationRequest}s when create the
- * {@link AssociationInfo}.
*/
public static final int RESULT_INTERNAL_ERROR = 3;
/**
+ * The result code to propagate back to the user activity and
+ * {@link Callback#onFailure(int, CharSequence)}, indicates app is not allow to create the
+ * association due to the security issue.
+ * E.g. There are missing necessary permissions when creating association.
+ */
+ @FlaggedApi(Flags.FLAG_ASSOCIATION_FAILURE_CODE)
+ public static final int RESULT_SECURITY_ERROR = 4;
+
+ /**
* Requesting applications will receive the String in {@link Callback#onFailure} if the
* association dialog is explicitly declined by the users. E.g. press the Don't allow
* button.
@@ -374,7 +378,6 @@
*/
public void onAssociationCreated(@NonNull AssociationInfo associationInfo) {}
- //TODO(b/331459560): Add deprecated and remove abstract after API cut for W.
/**
* Invoked if the association could not be created.
*
@@ -385,11 +388,15 @@
/**
* Invoked if the association could not be created.
*
- * @param resultCode indicate the particular reason why the association
- * could not be created.
+ * Please note that both {@link #onFailure(CharSequence error)} and this
+ * API will be called if the association could not be created.
+ *
+ * @param errorCode indicate the particular error code why the association
+ * could not be created.
+ * @param error error message.
*/
@FlaggedApi(Flags.FLAG_ASSOCIATION_FAILURE_CODE)
- public void onFailure(@ResultCode int resultCode) {}
+ public void onFailure(@ResultCode int errorCode, @Nullable CharSequence error) {}
}
private final ICompanionDeviceManager mService;
@@ -1825,12 +1832,12 @@
}
@Override
- public void onFailure(@ResultCode int resultCode) {
+ public void onFailure(@ResultCode int errorCode, @Nullable CharSequence error) {
if (Flags.associationFailureCode()) {
- execute(mCallback::onFailure, resultCode);
+ execute(mCallback::onFailure, errorCode, error);
}
- execute(mCallback::onFailure, RESULT_CODE_TO_REASON.get(resultCode));
+ execute(mCallback::onFailure, error);
}
private <T> void execute(Consumer<T> callback, T arg) {
@@ -1840,6 +1847,12 @@
mHandler.post(() -> callback.accept(arg));
}
}
+
+ private <T, U> void execute(BiConsumer<T, U> callback, T arg1, U arg2) {
+ if (mExecutor != null) {
+ mExecutor.execute(() -> callback.accept(arg1, arg2));
+ }
+ }
}
private static class OnAssociationsChangedListenerProxy
@@ -2014,15 +2027,4 @@
}
}
}
-
- private static final Map<Integer, String> RESULT_CODE_TO_REASON;
- static {
- final Map<Integer, String> map = new ArrayMap<>();
- map.put(RESULT_CANCELED, REASON_CANCELED);
- map.put(RESULT_USER_REJECTED, REASON_USER_REJECTED);
- map.put(RESULT_DISCOVERY_TIMEOUT, REASON_DISCOVERY_TIMEOUT);
- map.put(RESULT_INTERNAL_ERROR, REASON_INTERNAL_ERROR);
-
- RESULT_CODE_TO_REASON = unmodifiableMap(map);
- }
}
diff --git a/core/java/android/companion/IAssociationRequestCallback.aidl b/core/java/android/companion/IAssociationRequestCallback.aidl
index b1be30a..a6f86a5 100644
--- a/core/java/android/companion/IAssociationRequestCallback.aidl
+++ b/core/java/android/companion/IAssociationRequestCallback.aidl
@@ -25,5 +25,5 @@
oneway void onAssociationCreated(in AssociationInfo associationInfo);
- oneway void onFailure(in int resultCode);
+ oneway void onFailure(in int errorCode, in CharSequence error);
}
\ No newline at end of file
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index cb57c7b..abb0d8d 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -3767,7 +3767,7 @@
* <p>The Intent will have the following extra value:</p>
* <ul>
* <li><em>{@link android.content.Intent#EXTRA_PHONE_NUMBER}</em> -
- * the phone number originally intended to be dialed.</li>
+ * the phone number dialed.</li>
* </ul>
* <p class="note">Starting in Android 15, this broadcast is no longer sent as an ordered
* broadcast. The <code>resultData</code> no longer has any effect and will not determine the
@@ -3800,6 +3800,14 @@
* {@link android.Manifest.permission#PROCESS_OUTGOING_CALLS}
* permission to receive this Intent.</p>
*
+ * <p class="note">Starting in {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, this broadcast is
+ * no longer sent as an ordered broadcast, and does not allow activity launches. This means
+ * that receivers may no longer change the phone number for the outgoing call, or cancel the
+ * outgoing call. This functionality is only possible using the
+ * {@link android.telecom.CallRedirectionService} API. Although background receivers are
+ * woken up to handle this intent, no guarantee is made as to the timeliness of the broadcast.
+ * </p>
+ *
* <p class="note">This is a protected intent that can only be sent
* by the system.
*
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index cd3ce87..5779a44 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -2501,33 +2501,19 @@
return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true, indent);
}
- private void addIndentOrComma(StringBuilder sb, String indent) {
- if (indent != null) {
- sb.append("\n ");
- sb.append(indent);
- } else {
- sb.append(", ");
- }
+ /** @hide */
+ public String toSimpleString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(mId);
+ addReadableFlags(sb);
+ return sb.toString();
}
- private String toStringInner(boolean secure, boolean includeInternalData, String indent) {
- final StringBuilder sb = new StringBuilder();
-
- if (indent != null) {
- sb.append(indent);
- }
-
- sb.append("ShortcutInfo {");
-
- sb.append("id=");
- sb.append(secure ? "***" : mId);
-
- sb.append(", flags=0x");
- sb.append(Integer.toHexString(mFlags));
+ private void addReadableFlags(StringBuilder sb) {
sb.append(" [");
if ((mFlags & FLAG_SHADOW) != 0) {
- // Note the shadow flag isn't actually used anywhere and it's just for dumpsys, so
- // we don't have an isXxx for this.
+ // Note the shadow flag isn't actually used anywhere and it's
+ // just for dumpsys, so we don't have an isXxx for this.
sb.append("Sdw");
}
if (!isEnabled()) {
@@ -2576,7 +2562,32 @@
sb.append("Hid-L");
}
sb.append("]");
+ }
+ private void addIndentOrComma(StringBuilder sb, String indent) {
+ if (indent != null) {
+ sb.append("\n ");
+ sb.append(indent);
+ } else {
+ sb.append(", ");
+ }
+ }
+
+ private String toStringInner(boolean secure, boolean includeInternalData, String indent) {
+ final StringBuilder sb = new StringBuilder();
+
+ if (indent != null) {
+ sb.append(indent);
+ }
+
+ sb.append("ShortcutInfo {");
+
+ sb.append("id=");
+ sb.append(secure ? "***" : mId);
+
+ sb.append(", flags=0x");
+ sb.append(Integer.toHexString(mFlags));
+ addReadableFlags(sb);
addIndentOrComma(sb, indent);
sb.append("packageName=");
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index 7665fe8..04a810a 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -461,6 +461,12 @@
@SuppressLint("NonUserGetterCalled")
public boolean registerClient(Context ctx, IBinder token, int extension,
String cameraId, Map<String, CameraMetadataNative> characteristicsMapNative) {
+ if (!SystemProperties.getBoolean("ro.camerax.extensions.enabled",
+ /*default*/ false)) {
+ Log.v(TAG, "Disabled camera extension property!");
+ return false;
+ }
+
boolean ret = registerClientHelper(ctx, token, extension, false /*useFallback*/);
if (Flags.concertMode()) {
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index 48d2785..a60c48e 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -80,7 +80,6 @@
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -355,14 +354,7 @@
mCameraId = cameraId;
if (Flags.singleThreadExecutor()) {
mDeviceCallback = new ClientStateCallback(executor, callback);
- mDeviceExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
- @Override
- public Thread newThread(Runnable r) {
- Thread thread = Executors.defaultThreadFactory().newThread(r);
- thread.setName("CameraDeviceExecutor");
- return thread;
- }
- });
+ mDeviceExecutor = Executors.newSingleThreadExecutor();
} else {
mDeviceCallback = callback;
mDeviceExecutor = executor;
diff --git a/core/java/android/hardware/radio/flags.aconfig b/core/java/android/hardware/radio/flags.aconfig
index c9ab62d..c99dc04 100644
--- a/core/java/android/hardware/radio/flags.aconfig
+++ b/core/java/android/hardware/radio/flags.aconfig
@@ -8,3 +8,11 @@
description: "Feature flag for improved HD radio support with less vendor extensions"
bug: "280300929"
}
+
+flag {
+ name: "hd_radio_emergency_alert_system"
+ is_exported: true
+ namespace: "car_framework"
+ description: "Feature flag for HD radio emergency alert system support"
+ bug: "361348719"
+}
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 4cc057a..e80efd2 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -41,7 +41,6 @@
import android.net.Uri;
import android.os.MessageQueue.OnFileDescriptorEventListener;
import android.ravenwood.annotation.RavenwoodKeepWholeClass;
-import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass;
import android.ravenwood.annotation.RavenwoodReplace;
import android.ravenwood.annotation.RavenwoodThrow;
import android.system.ErrnoException;
@@ -77,8 +76,6 @@
* you to close it when done with it.
*/
@RavenwoodKeepWholeClass
-@RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.ParcelFileDescriptor_host")
public class ParcelFileDescriptor implements Parcelable, Closeable {
private static final String TAG = "ParcelFileDescriptor";
@@ -206,11 +203,11 @@
}
mWrapped = null;
mFd = fd;
- setFdOwner(mFd);
+ IoUtils.setFdOwner(mFd, this);
mCommFd = commChannel;
if (mCommFd != null) {
- setFdOwner(mCommFd);
+ IoUtils.setFdOwner(mCommFd, this);
}
mGuard.open("close");
@@ -298,7 +295,7 @@
public static @NonNull ParcelFileDescriptor wrap(@NonNull ParcelFileDescriptor pfd,
@NonNull Handler handler, @NonNull OnCloseListener listener) throws IOException {
final FileDescriptor original = new FileDescriptor();
- setFdInt(original, pfd.detachFd());
+ original.setInt$(pfd.detachFd());
return fromFd(original, handler, listener);
}
@@ -363,18 +360,10 @@
}
}
- @RavenwoodReplace
private static void closeInternal(FileDescriptor fd) {
IoUtils.closeQuietly(fd);
}
- private static void closeInternal$ravenwood(FileDescriptor fd) {
- try {
- Os.close(fd);
- } catch (ErrnoException ignored) {
- }
- }
-
/**
* Create a new ParcelFileDescriptor that is a dup of an existing
* FileDescriptor. This obeys standard POSIX semantics, where the
@@ -385,7 +374,7 @@
try {
final FileDescriptor fd = new FileDescriptor();
int intfd = Os.fcntlInt(orig, (isAtLeastQ() ? F_DUPFD_CLOEXEC : F_DUPFD), 0);
- setFdInt(fd, intfd);
+ fd.setInt$(intfd);
return new ParcelFileDescriptor(fd);
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
@@ -418,12 +407,12 @@
*/
public static ParcelFileDescriptor fromFd(int fd) throws IOException {
final FileDescriptor original = new FileDescriptor();
- setFdInt(original, fd);
+ original.setInt$(fd);
try {
final FileDescriptor dup = new FileDescriptor();
int intfd = Os.fcntlInt(original, (isAtLeastQ() ? F_DUPFD_CLOEXEC : F_DUPFD), 0);
- setFdInt(dup, intfd);
+ dup.setInt$(intfd);
return new ParcelFileDescriptor(dup);
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
@@ -446,7 +435,7 @@
*/
public static ParcelFileDescriptor adoptFd(int fd) {
final FileDescriptor fdesc = new FileDescriptor();
- setFdInt(fdesc, fd);
+ fdesc.setInt$(fd);
return new ParcelFileDescriptor(fdesc);
}
@@ -703,7 +692,7 @@
@RavenwoodThrow(reason = "Os.readlink() and Os.stat()")
public static File getFile(FileDescriptor fd) throws IOException {
try {
- final String path = Os.readlink("/proc/self/fd/" + getFdInt(fd));
+ final String path = Os.readlink("/proc/self/fd/" + fd.getInt$());
if (OsConstants.S_ISREG(Os.stat(path).st_mode)
|| OsConstants.S_ISCHR(Os.stat(path).st_mode)) {
return new File(path);
@@ -783,7 +772,7 @@
if (mClosed) {
throw new IllegalStateException("Already closed");
}
- return getFdInt(mFd);
+ return mFd.getInt$();
}
}
@@ -805,7 +794,7 @@
if (mClosed) {
throw new IllegalStateException("Already closed");
}
- int fd = acquireRawFd(mFd);
+ int fd = IoUtils.acquireRawFd(mFd);
writeCommStatusAndClose(Status.DETACHED, null);
mClosed = true;
mGuard.close();
@@ -1265,38 +1254,6 @@
}
}
- private static native void setFdInt$ravenwood(FileDescriptor fd, int fdInt);
- private static native int getFdInt$ravenwood(FileDescriptor fd);
-
- @RavenwoodReplace
- private static void setFdInt(FileDescriptor fd, int fdInt) {
- fd.setInt$(fdInt);
- }
-
- @RavenwoodReplace
- private static int getFdInt(FileDescriptor fd) {
- return fd.getInt$();
- }
-
- @RavenwoodReplace
- private void setFdOwner(FileDescriptor fd) {
- IoUtils.setFdOwner(fd, this);
- }
-
- private void setFdOwner$ravenwood(FileDescriptor fd) {
- // FD owners currently unsupported under Ravenwood; ignored
- }
-
- @RavenwoodReplace
- private int acquireRawFd(FileDescriptor fd) {
- return IoUtils.acquireRawFd(fd);
- }
-
- private int acquireRawFd$ravenwood(FileDescriptor fd) {
- // FD owners currently unsupported under Ravenwood; return FD directly
- return getFdInt(fd);
- }
-
@RavenwoodReplace
private static boolean isAtLeastQ() {
return (VMRuntime.getRuntime().getTargetSdkVersion() >= Build.VERSION_CODES.Q);
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index db06a6b..3b2041b 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -838,16 +838,11 @@
/**
* Returns true if the current process is a 64-bit runtime.
*/
- @android.ravenwood.annotation.RavenwoodReplace
+ @android.ravenwood.annotation.RavenwoodKeep
public static final boolean is64Bit() {
return VMRuntime.getRuntime().is64Bit();
}
- /** @hide */
- public static final boolean is64Bit$ravenwood() {
- return "amd64".equals(System.getProperty("os.arch"));
- }
-
private static volatile ThreadLocal<SomeArgs> sIdentity$ravenwood;
/** @hide */
diff --git a/core/java/android/security/OWNERS b/core/java/android/security/OWNERS
index 8bd6c85..c38ee08 100644
--- a/core/java/android/security/OWNERS
+++ b/core/java/android/security/OWNERS
@@ -3,6 +3,7 @@
brambonne@google.com
eranm@google.com
jeffv@google.com
+tweek@google.com
per-file *NetworkSecurityPolicy.java = file:net/OWNERS
per-file Confirmation*.java = file:/keystore/OWNERS
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 17d2790..013ec5f 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -28,7 +28,9 @@
import android.util.Log;
import android.view.WindowManager;
+import java.lang.ref.WeakReference;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
@@ -52,43 +54,51 @@
// An {@link IDreamOverlayClient} implementation that identifies itself when forwarding
// requests to the {@link DreamOverlayService}
private static class OverlayClient extends IDreamOverlayClient.Stub {
- private final DreamOverlayService mService;
+ private final WeakReference<DreamOverlayService> mService;
private boolean mShowComplications;
private ComponentName mDreamComponent;
IDreamOverlayCallback mDreamOverlayCallback;
- OverlayClient(DreamOverlayService service) {
+ OverlayClient(WeakReference<DreamOverlayService> service) {
mService = service;
}
+ private void applyToDream(Consumer<DreamOverlayService> consumer) {
+ final DreamOverlayService service = mService.get();
+
+ if (service != null) {
+ consumer.accept(service);
+ }
+ }
+
@Override
public void startDream(WindowManager.LayoutParams params, IDreamOverlayCallback callback,
String dreamComponent, boolean shouldShowComplications) throws RemoteException {
mDreamComponent = ComponentName.unflattenFromString(dreamComponent);
mShowComplications = shouldShowComplications;
mDreamOverlayCallback = callback;
- mService.startDream(this, params);
+ applyToDream(dreamOverlayService -> dreamOverlayService.startDream(this, params));
}
@Override
public void wakeUp() {
- mService.wakeUp(this);
+ applyToDream(dreamOverlayService -> dreamOverlayService.wakeUp(this));
}
@Override
public void endDream() {
- mService.endDream(this);
+ applyToDream(dreamOverlayService -> dreamOverlayService.endDream(this));
}
@Override
public void comeToFront() {
- mService.comeToFront(this);
+ applyToDream(dreamOverlayService -> dreamOverlayService.comeToFront(this));
}
@Override
public void onWakeRequested() {
if (Flags.dreamWakeRedirect()) {
- mService.onWakeRequested();
+ applyToDream(DreamOverlayService::onWakeRequested);
}
}
@@ -161,17 +171,24 @@
});
}
- private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
+ private static class DreamOverlay extends IDreamOverlay.Stub {
+ private final WeakReference<DreamOverlayService> mService;
+
+ DreamOverlay(DreamOverlayService service) {
+ mService = new WeakReference<>(service);
+ }
+
@Override
public void getClient(IDreamOverlayClientCallback callback) {
try {
- callback.onDreamOverlayClient(
- new OverlayClient(DreamOverlayService.this));
+ callback.onDreamOverlayClient(new OverlayClient(mService));
} catch (RemoteException e) {
Log.e(TAG, "could not send client to callback", e);
}
}
- };
+ }
+
+ private final IDreamOverlay mDreamOverlay = new DreamOverlay(this);
public DreamOverlayService() {
}
@@ -195,6 +212,12 @@
}
}
+ @Override
+ public void onDestroy() {
+ mCurrentClient = null;
+ super.onDestroy();
+ }
+
@Nullable
@Override
public final IBinder onBind(@NonNull Intent intent) {
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 1734223..ad457ce 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -25,6 +25,7 @@
import static android.graphics.Matrix.MSKEW_Y;
import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.view.flags.Flags.disableDrawWakeLock;
import static com.android.window.flags.Flags.FLAG_OFFLOAD_COLOR_EXTRACTION;
import static com.android.window.flags.Flags.noDuplicateSurfaceDestroyedEvents;
@@ -51,6 +52,7 @@
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
+import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.Intent;
@@ -208,6 +210,15 @@
private boolean mIsWearOs;
/**
+ * This change disables the {@code DRAW_WAKE_LOCK}, an internal wakelock acquired per-frame
+ * duration display DOZE. It was added to allow animation during AOD. This wakelock consumes
+ * battery severely if the animation is too heavy, so, it will be removed.
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private static final long DISABLE_DRAW_WAKE_LOCK_WALLPAPER = 361433696L;
+
+ /**
* Wear products currently force a slight scaling transition to wallpapers
* when the QSS is opened. However, on Wear 6 (SDK 35) and above, 1P watch faces
* will be expected to either implement their own scaling, or to override this
@@ -362,6 +373,8 @@
private SurfaceControl mScreenshotSurfaceControl;
private Point mScreenshotSize = new Point();
+ private final boolean mDisableDrawWakeLock;
+
final BaseSurfaceHolder mSurfaceHolder = new BaseSurfaceHolder() {
{
mRequestedFormat = PixelFormat.RGBX_8888;
@@ -406,6 +419,9 @@
}
private void prepareToDraw() {
+ if (mDisableDrawWakeLock) {
+ return;
+ }
if (mDisplayState == Display.STATE_DOZE) {
try {
mSession.pokeDrawLock(mWindow);
@@ -546,6 +562,8 @@
public Engine(Supplier<Long> clockFunction, Handler handler) {
mClockFunction = clockFunction;
mHandler = handler;
+ mDisableDrawWakeLock = CompatChanges.isChangeEnabled(DISABLE_DRAW_WAKE_LOCK_WALLPAPER)
+ && disableDrawWakeLock();
}
/**
diff --git a/core/java/android/util/LruCache.java b/core/java/android/util/LruCache.java
index be1ec41..9845f9e 100644
--- a/core/java/android/util/LruCache.java
+++ b/core/java/android/util/LruCache.java
@@ -18,7 +18,6 @@
import android.compat.annotation.UnsupportedAppUsage;
-import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -226,16 +225,10 @@
}
}
- @android.ravenwood.annotation.RavenwoodReplace
private Map.Entry<K, V> eldest() {
return map.eldest();
}
- private Map.Entry<K, V> eldest$ravenwood() {
- final Iterator<Map.Entry<K, V>> it = map.entrySet().iterator();
- return it.hasNext() ? it.next() : null;
- }
-
/**
* Removes the entry for {@code key} if it exists.
*
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 15b0c13..1f7ed8b 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -2344,6 +2344,8 @@
* SurfaceControl.DisplayMode
* @hide
*/
+ @SuppressWarnings("UnflaggedApi") // For testing only
+ @TestApi
public boolean isSynthetic() {
return mIsSynthetic;
}
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 6568912..91e9230 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -35,8 +35,8 @@
import static android.view.InsetsController.LayoutInsetsDuringAnimation;
import static android.view.InsetsSource.ID_IME;
import static android.view.InsetsSource.SIDE_BOTTOM;
-import static android.view.InsetsSource.SIDE_NONE;
import static android.view.InsetsSource.SIDE_LEFT;
+import static android.view.InsetsSource.SIDE_NONE;
import static android.view.InsetsSource.SIDE_RIGHT;
import static android.view.InsetsSource.SIDE_TOP;
import static android.view.WindowInsets.Type.ime;
@@ -100,6 +100,8 @@
private @InsetsType int mControllingTypes;
private final InsetsAnimationControlCallbacks mController;
private final WindowInsetsAnimation mAnimation;
+ private final long mDurationMs;
+ private final Interpolator mInterpolator;
/** @see WindowInsetsAnimationController#hasZeroInsetsIme */
private final boolean mHasZeroInsetsIme;
private final CompatibilityInfo.Translator mTranslator;
@@ -120,8 +122,8 @@
@VisibleForTesting
public InsetsAnimationControlImpl(SparseArray<InsetsSourceControl> controls,
@Nullable Rect frame, InsetsState state, WindowInsetsAnimationControlListener listener,
- @InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
- Interpolator interpolator, @AnimationType int animationType,
+ @InsetsType int types, InsetsAnimationControlCallbacks controller,
+ InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
CompatibilityInfo.Translator translator, @Nullable ImeTracker.Token statsToken) {
mControls = controls;
@@ -155,8 +157,10 @@
}
mPendingInsets = mCurrentInsets;
- mAnimation = new WindowInsetsAnimation(mTypes, interpolator,
- durationMs);
+ mDurationMs = insetsAnimationSpec.getDurationMs(mHasZeroInsetsIme);
+ mInterpolator = insetsAnimationSpec.getInsetsInterpolator(mHasZeroInsetsIme);
+
+ mAnimation = new WindowInsetsAnimation(mTypes, mInterpolator, mDurationMs);
mAnimation.setAlpha(getCurrentAlpha());
mAnimationType = animationType;
mLayoutInsetsDuringAnimation = layoutInsetsDuringAnimation;
@@ -186,6 +190,16 @@
}
@Override
+ public long getDurationMs() {
+ return mDurationMs;
+ }
+
+ @Override
+ public Interpolator getInsetsInterpolator() {
+ return mInterpolator;
+ }
+
+ @Override
public void setReadyDispatched(boolean dispatched) {
mReadyDispatched = dispatched;
}
diff --git a/core/java/android/view/InsetsAnimationSpec.java b/core/java/android/view/InsetsAnimationSpec.java
new file mode 100644
index 0000000..7ad6661
--- /dev/null
+++ b/core/java/android/view/InsetsAnimationSpec.java
@@ -0,0 +1,39 @@
+/*
+ * 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 android.view;
+
+import android.view.animation.Interpolator;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Used by {@link InsetsAnimationControlImpl}
+ * @hide
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public interface InsetsAnimationSpec {
+ /**
+ * @param hasZeroInsetsIme whether IME has no insets (floating, fullscreen or non-overlapping).
+ * @return Duration of animation in {@link java.util.concurrent.TimeUnit#MILLISECONDS}
+ */
+ long getDurationMs(boolean hasZeroInsetsIme);
+ /**
+ * @param hasZeroInsetsIme whether IME has no insets (floating, fullscreen or non-overlapping).
+ * @return The interpolator used for the animation
+ */
+ Interpolator getInsetsInterpolator(boolean hasZeroInsetsIme);
+}
diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java
index 1b3b3eb..fc185bc 100644
--- a/core/java/android/view/InsetsAnimationThreadControlRunner.java
+++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java
@@ -33,7 +33,6 @@
import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsAnimation.Bounds;
-import android.view.animation.Interpolator;
import android.view.inputmethod.ImeTracker;
/**
@@ -110,15 +109,15 @@
@UiThread
public InsetsAnimationThreadControlRunner(SparseArray<InsetsSourceControl> controls,
@Nullable Rect frame, InsetsState state, WindowInsetsAnimationControlListener listener,
- @InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
- Interpolator interpolator, @AnimationType int animationType,
+ @InsetsType int types, InsetsAnimationControlCallbacks controller,
+ InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
CompatibilityInfo.Translator translator, Handler mainThreadHandler,
@Nullable ImeTracker.Token statsToken) {
mMainThreadHandler = mainThreadHandler;
mOuterCallbacks = controller;
mControl = new InsetsAnimationControlImpl(controls, frame, state, listener, types,
- mCallbacks, durationMs, interpolator, animationType, layoutInsetsDuringAnimation,
+ mCallbacks, insetsAnimationSpec, animationType, layoutInsetsDuringAnimation,
translator, statsToken);
InsetsAnimationThread.getHandler().post(() -> {
if (mControl.isCancelled()) {
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 7896cbd..b1df51f 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -366,7 +366,7 @@
* animate insets.
*/
public static class InternalAnimationControlListener
- implements WindowInsetsAnimationControlListener {
+ implements WindowInsetsAnimationControlListener, InsetsAnimationSpec {
private WindowInsetsAnimationController mController;
private ValueAnimator mAnimator;
@@ -374,7 +374,6 @@
private final boolean mHasAnimationCallbacks;
private final @InsetsType int mRequestedTypes;
private final @Behavior int mBehavior;
- private final long mDurationMs;
private final boolean mDisable;
private final int mFloatingImeBottomInset;
private final WindowInsetsAnimationControlListener mLoggingListener;
@@ -388,7 +387,6 @@
mHasAnimationCallbacks = hasAnimationCallbacks;
mRequestedTypes = requestedTypes;
mBehavior = behavior;
- mDurationMs = calculateDurationMs();
mDisable = disable;
mFloatingImeBottomInset = floatingImeBottomInset;
mLoggingListener = loggingListener;
@@ -407,13 +405,14 @@
onAnimationFinish();
return;
}
+ final boolean hasZeroInsetsIme = controller.hasZeroInsetsIme();
mAnimator = ValueAnimator.ofFloat(0f, 1f);
- mAnimator.setDuration(mDurationMs);
+ mAnimator.setDuration(controller.getDurationMs());
mAnimator.setInterpolator(new LinearInterpolator());
Insets hiddenInsets = controller.getHiddenStateInsets();
// IME with zero insets is a special case: it will animate-in from offscreen and end
// with final insets of zero and vice-versa.
- hiddenInsets = controller.hasZeroInsetsIme()
+ hiddenInsets = hasZeroInsetsIme
? Insets.of(hiddenInsets.left, hiddenInsets.top, hiddenInsets.right,
mFloatingImeBottomInset)
: hiddenInsets;
@@ -423,7 +422,7 @@
Insets end = mShow
? controller.getShownStateInsets()
: hiddenInsets;
- Interpolator insetsInterpolator = getInsetsInterpolator();
+ Interpolator insetsInterpolator = controller.getInsetsInterpolator();
Interpolator alphaInterpolator = getAlphaInterpolator();
mAnimator.addUpdateListener(animation -> {
float rawFraction = animation.getAnimatedFraction();
@@ -486,9 +485,10 @@
}
}
- protected Interpolator getInsetsInterpolator() {
+ @Override
+ public Interpolator getInsetsInterpolator(boolean hasZeroInsetsIme) {
if ((mRequestedTypes & ime()) != 0) {
- if (mHasAnimationCallbacks) {
+ if (mHasAnimationCallbacks && hasZeroInsetsIme) {
return SYNC_IME_INTERPOLATOR;
} else if (mShow) {
return LINEAR_OUT_SLOW_IN_INTERPOLATOR;
@@ -507,10 +507,9 @@
Interpolator getAlphaInterpolator() {
if ((mRequestedTypes & ime()) != 0) {
- if (mHasAnimationCallbacks) {
+ if (mHasAnimationCallbacks && !mController.hasZeroInsetsIme()) {
return input -> 1f;
} else if (mShow) {
-
// Alpha animation takes half the time with linear interpolation;
return input -> Math.min(1f, 2 * input);
} else {
@@ -534,16 +533,10 @@
if (DEBUG) Log.d(TAG, "onAnimationFinish showOnFinish: " + mShow);
}
- /**
- * To get the animation duration in MS.
- */
- public long getDurationMs() {
- return mDurationMs;
- }
-
- private long calculateDurationMs() {
+ @Override
+ public long getDurationMs(boolean hasZeroInsetsIme) {
if ((mRequestedTypes & ime()) != 0) {
- if (mHasAnimationCallbacks) {
+ if (mHasAnimationCallbacks && hasZeroInsetsIme) {
return ANIMATION_DURATION_SYNC_IME_MS;
} else {
return ANIMATION_DURATION_UNSYNC_IME_MS;
@@ -593,13 +586,13 @@
private static class PendingControlRequest {
PendingControlRequest(@InsetsType int types, WindowInsetsAnimationControlListener listener,
- long durationMs, Interpolator interpolator, @AnimationType int animationType,
+ InsetsAnimationSpec insetsAnimationSpec,
+ @AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
CancellationSignal cancellationSignal, boolean useInsetsAnimationThread) {
this.types = types;
this.listener = listener;
- this.durationMs = durationMs;
- this.interpolator = interpolator;
+ this.mInsetsAnimationSpec = insetsAnimationSpec;
this.animationType = animationType;
this.layoutInsetsDuringAnimation = layoutInsetsDuringAnimation;
this.cancellationSignal = cancellationSignal;
@@ -608,8 +601,7 @@
@InsetsType int types;
final WindowInsetsAnimationControlListener listener;
- final long durationMs;
- final Interpolator interpolator;
+ final InsetsAnimationSpec mInsetsAnimationSpec;
final @AnimationType int animationType;
final @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation;
final CancellationSignal cancellationSignal;
@@ -1201,12 +1193,10 @@
// We are about to playing the default animation. Passing a null frame indicates the
// controlled types should be animated regardless of the frame.
- controlAnimationUnchecked(
- pendingRequest.types, pendingRequest.cancellationSignal,
- pendingRequest.listener, null /* frame */,
- true /* fromIme */, pendingRequest.durationMs, pendingRequest.interpolator,
- pendingRequest.animationType,
- pendingRequest.layoutInsetsDuringAnimation,
+ controlAnimationUnchecked(pendingRequest.types, pendingRequest.cancellationSignal,
+ pendingRequest.listener, null /* frame */, true /* fromIme */,
+ pendingRequest.mInsetsAnimationSpec,
+ pendingRequest.animationType, pendingRequest.layoutInsetsDuringAnimation,
pendingRequest.useInsetsAnimationThread, statsToken);
}
@@ -1327,18 +1317,26 @@
mHost.getInputMethodManager(), null /* icProto */);
}
+ InsetsAnimationSpec spec = new InsetsAnimationSpec() {
+ @Override
+ public long getDurationMs(boolean hasZeroInsetsIme) {
+ return durationMs;
+ }
+ @Override
+ public Interpolator getInsetsInterpolator(boolean hasZeroInsetsIme) {
+ return interpolator;
+ }
+ };
// TODO(b/342111149): Create statsToken here once ImeTracker#onStart becomes async.
- controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs,
- interpolator, animationType,
- getLayoutInsetsDuringAnimationMode(types, fromPredictiveBack),
+ controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, spec,
+ animationType, getLayoutInsetsDuringAnimationMode(types, fromPredictiveBack),
false /* useInsetsAnimationThread */, null);
}
private void controlAnimationUnchecked(@InsetsType int types,
@Nullable CancellationSignal cancellationSignal,
WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme,
- long durationMs, Interpolator interpolator,
- @AnimationType int animationType,
+ InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) {
final boolean visible = layoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
@@ -1349,7 +1347,7 @@
// However, we might reject the request in some cases, such as delaying showing IME or
// rejecting showing IME.
controlAnimationUncheckedInner(types, cancellationSignal, listener, frame, fromIme,
- durationMs, interpolator, animationType, layoutInsetsDuringAnimation,
+ insetsAnimationSpec, animationType, layoutInsetsDuringAnimation,
useInsetsAnimationThread, statsToken);
// We are finishing setting the requested visible types. Report them to the server
@@ -1360,8 +1358,7 @@
private void controlAnimationUncheckedInner(@InsetsType int types,
@Nullable CancellationSignal cancellationSignal,
WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme,
- long durationMs, Interpolator interpolator,
- @AnimationType int animationType,
+ InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) {
if ((types & mTypesBeingCancelled) != 0) {
@@ -1418,8 +1415,8 @@
// TODO (b/323319146) remove layoutInsetsDuringAnimation from
// PendingControlRequest, as it is now only used for showing
final PendingControlRequest request = new PendingControlRequest(types,
- listener, durationMs,
- interpolator, animationType, LAYOUT_INSETS_DURING_ANIMATION_SHOWN,
+ listener, insetsAnimationSpec, animationType,
+ LAYOUT_INSETS_DURING_ANIMATION_SHOWN,
cancellationSignal, false /* useInsetsAnimationThread */);
mPendingImeControlRequest = request;
// only add a timeout when the control is not currently showing
@@ -1460,11 +1457,9 @@
if (!imeReady) {
// IME isn't ready, all requested types will be animated once IME is ready
abortPendingImeControlRequest();
- final PendingControlRequest request = new PendingControlRequest(types,
- listener, durationMs,
- interpolator, animationType, layoutInsetsDuringAnimation,
- cancellationSignal,
- useInsetsAnimationThread);
+ final PendingControlRequest request = new PendingControlRequest(types, listener,
+ insetsAnimationSpec, animationType, layoutInsetsDuringAnimation,
+ cancellationSignal, useInsetsAnimationThread);
mPendingImeControlRequest = request;
mHandler.postDelayed(mPendingControlTimeout, PENDING_CONTROL_TIMEOUT_MS);
if (DEBUG) Log.d(TAG, "Ime not ready. Create pending request");
@@ -1520,11 +1515,11 @@
final InsetsAnimationControlRunner runner = useInsetsAnimationThread
? new InsetsAnimationThreadControlRunner(controls,
- frame, mState, listener, typesReady, this, durationMs, interpolator,
- animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
- mHost.getHandler(), statsToken)
+ frame, mState, listener, typesReady, this,
+ insetsAnimationSpec, animationType, layoutInsetsDuringAnimation,
+ mHost.getTranslator(), mHost.getHandler(), statsToken)
: new InsetsAnimationControlImpl(controls,
- frame, mState, listener, typesReady, this, durationMs, interpolator,
+ frame, mState, listener, typesReady, this, insetsAnimationSpec,
animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
statsToken);
if ((typesReady & WindowInsets.Type.ime()) != 0) {
@@ -2023,7 +2018,7 @@
// the controlled types should be animated regardless of the frame.
controlAnimationUnchecked(
types, null /* cancellationSignal */, listener, null /* frame */, fromIme,
- listener.getDurationMs(), listener.getInsetsInterpolator(),
+ listener /* insetsAnimationSpec */,
show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE,
show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
!hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken);
diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java
index 6e62221..f90b841 100644
--- a/core/java/android/view/InsetsResizeAnimationRunner.java
+++ b/core/java/android/view/InsetsResizeAnimationRunner.java
@@ -233,6 +233,16 @@
}
@Override
+ public long getDurationMs() {
+ return 0;
+ }
+
+ @Override
+ public Interpolator getInsetsInterpolator() {
+ return null;
+ }
+
+ @Override
public void setReadyDispatched(boolean dispatched) {
}
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index 72d2d3b..326e34b 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -128,7 +128,16 @@
* ev.getPointerId(p), ev.getX(p), ev.getY(p));
* }
* }
- * </code></pre></p>
+ * </code></pre></p><p>
+ * Developers should keep in mind that it is especially important to consume all samples
+ * in a batched event when processing relative values that report changes since the last
+ * event or sample. Examples of such relative axes include {@link #AXIS_RELATIVE_X},
+ * {@link #AXIS_RELATIVE_Y}, and many of the axes prefixed with {@code AXIS_GESTURE_}.
+ * In these cases, developers should first consume all historical values using
+ * {@link #getHistoricalAxisValue(int, int)} and then consume the current values using
+ * {@link #getAxisValue(int)} like in the example above, as these relative values are
+ * not accumulated in a batched event.
+ * </p>
*
* <h3>Device Types</h3>
* <p>
@@ -1117,6 +1126,9 @@
* the location but this axis reports the difference which allows the app to see
* how the mouse is moved.
* </ul>
+ * </p><p>
+ * These values are relative to the state from the last sample, not accumulated, so developers
+ * should make sure to process this axis value for all batched historical samples.
* </p>
*
* @see #getAxisValue(int, int)
@@ -1130,6 +1142,9 @@
* Axis constant: The movement of y position of a motion event.
* <p>
* This is similar to {@link #AXIS_RELATIVE_X} but for y-axis.
+ * </p><p>
+ * These values are relative to the state from the last sample, not accumulated, so developers
+ * should make sure to process this axis value for all batched historical samples.
* </p>
*
* @see #getAxisValue(int, int)
@@ -1324,8 +1339,8 @@
* swipe gesture starts at X = 500 then moves to X = 400, this axis would have a value of
* -0.1.
* </ul>
- * These values are relative to the state from the last event, not accumulated, so developers
- * should make sure to process this axis value for all batched historical events.
+ * These values are relative to the state from the last sample, not accumulated, so developers
+ * should make sure to process this axis value for all batched historical samples.
* <p>
* This axis is only set on the first pointer in a motion event.
*/
@@ -1345,8 +1360,8 @@
* <li>For a touch pad, reports the distance that should be scrolled in the X axis as a result
* of the user's two-finger scroll gesture, in display pixels.
* </ul>
- * These values are relative to the state from the last event, not accumulated, so developers
- * should make sure to process this axis value for all batched historical events.
+ * These values are relative to the state from the last sample, not accumulated, so developers
+ * should make sure to process this axis value for all batched historical samples.
* <p>
* This axis is only set on the first pointer in a motion event.
*/
@@ -1367,8 +1382,8 @@
* making a pinch gesture, as a proportion of the previous distance. For example, if the fingers
* were 50 units apart and are now 52 units apart, the scale factor would be 1.04.
* </ul>
- * These values are relative to the state from the last event, not accumulated, so developers
- * should make sure to process this axis value for all batched historical events.
+ * These values are relative to the state from the last sample, not accumulated, so developers
+ * should make sure to process this axis value for all batched historical samples.
* <p>
* This axis is only set on the first pointer in a motion event.
*/
diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java
index 1535145..815fd1c 100644
--- a/core/java/android/view/PointerIcon.java
+++ b/core/java/android/view/PointerIcon.java
@@ -556,12 +556,9 @@
+ "is a different type from the others. All frames should be the "
+ "same type.");
}
- if (drawableFrame.getIntrinsicWidth() != width ||
- drawableFrame.getIntrinsicHeight() != height) {
- throw new IllegalArgumentException("The bitmap size of " + i + "-th frame "
- + "is different. All frames should have the exact same size and "
- + "share the same hotspot.");
- }
+ // TODO(b/361232935): Check when bitmap size of the ith frame is different
+ // drawableFrame.getIntrinsicWidth() != width ||
+ // drawableFrame.getIntrinsicHeight() != height
if (isVectorAnimation) {
drawableFrame = getBitmapDrawableFromVectorDrawable(resources,
(VectorDrawable) drawableFrame, pointerScale);
diff --git a/core/java/android/view/WindowInsetsAnimationController.java b/core/java/android/view/WindowInsetsAnimationController.java
index 6578e9b..d3ea982 100644
--- a/core/java/android/view/WindowInsetsAnimationController.java
+++ b/core/java/android/view/WindowInsetsAnimationController.java
@@ -23,6 +23,7 @@
import android.graphics.Insets;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsAnimation.Bounds;
+import android.view.animation.Interpolator;
/**
* Controller for app-driven animation of system windows.
@@ -188,4 +189,16 @@
* fullscreen or non-overlapping).
*/
boolean hasZeroInsetsIme();
+
+ /**
+ * @hide
+ * @return The duration of the animation in {@link java.util.concurrent.TimeUnit#MILLISECONDS}.
+ */
+ long getDurationMs();
+
+ /**
+ * @hide
+ * @return The interpolator of the animation.
+ */
+ Interpolator getInsetsInterpolator();
}
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 1922327..eb35817 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -83,7 +83,6 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.StrictMode;
-import android.os.Trace;
import android.os.UserHandle;
import android.system.Os;
import android.text.TextUtils;
@@ -6247,18 +6246,6 @@
private View inflateView(Context context, RemoteViews rv, @Nullable ViewGroup parent,
@StyleRes int applyThemeResId, @Nullable ColorResources colorResources) {
- try {
- Trace.beginSection(rv.hasDrawInstructions()
- ? "RemoteViews#inflateViewWithDrawInstructions"
- : "RemoteViews#inflateView");
- return inflateViewInternal(context, rv, parent, applyThemeResId, colorResources);
- } finally {
- Trace.endSection();
- }
- }
-
- private View inflateViewInternal(Context context, RemoteViews rv, @Nullable ViewGroup parent,
- @StyleRes int applyThemeResId, @Nullable ColorResources colorResources) {
// RemoteViews may be built by an application installed in another
// user. So build a context that loads resources from that user but
// still returns the current users userId so settings like data / time formats
@@ -6397,7 +6384,7 @@
private View mResult;
private ViewTree mTree;
- private List<Action> mActions;
+ private Action[] mActions;
private Exception mError;
private AsyncApplyTask(
@@ -6424,20 +6411,11 @@
if (mRV.mActions != null) {
int count = mRV.mActions.size();
- mActions = new ArrayList<>(count);
- try {
- Trace.beginSection(hasDrawInstructions()
- ? "RemoteViews#initActionAsyncWithDrawInstructions"
- : "RemoteViews#initActionAsync");
- for (Action action : mRV.mActions) {
- if (isCancelled()) {
- break;
- }
- // TODO: check if isCancelled in nested views.
- mActions.add(action.initActionAsync(mTree, mParent, mApplyParams));
- }
- } finally {
- Trace.endSection();
+ mActions = new Action[count];
+ for (int i = 0; i < count && !isCancelled(); i++) {
+ // TODO: check if isCancelled in nested views.
+ mActions[i] = mRV.mActions.get(i)
+ .initActionAsync(mTree, mParent, mApplyParams);
}
} else {
mActions = null;
@@ -6459,7 +6437,14 @@
try {
if (mActions != null) {
- mRV.performApply(viewTree.mRoot, mParent, mApplyParams, mActions);
+
+ ActionApplyParams applyParams = mApplyParams.clone();
+ if (applyParams.handler == null) {
+ applyParams.handler = DEFAULT_INTERACTION_HANDLER;
+ }
+ for (Action a : mActions) {
+ a.apply(viewTree.mRoot, mParent, applyParams);
+ }
}
// If the parent of the view is has is a root, resolve the recycling.
if (mTopLevel && mResult instanceof ViewGroup) {
@@ -6635,11 +6620,6 @@
}
private void performApply(View v, ViewGroup parent, ActionApplyParams params) {
- performApply(v, parent, params, mActions);
- }
-
- private void performApply(
- View v, ViewGroup parent, ActionApplyParams params, List<Action> actions) {
params = params.clone();
if (params.handler == null) {
params.handler = DEFAULT_INTERACTION_HANDLER;
@@ -6650,15 +6630,8 @@
}
if (mActions != null) {
final int count = mActions.size();
- try {
- Trace.beginSection(hasDrawInstructions()
- ? "RemoteViews#applyActionsWithDrawInstructions"
- : "RemoteViews#applyActions");
- for (int i = 0; i < count; i++) {
- mActions.get(i).apply(v, parent, params);
- }
- } finally {
- Trace.endSection();
+ for (int i = 0; i < count; i++) {
+ mActions.get(i).apply(v, parent, params);
}
}
}
diff --git a/core/java/com/android/internal/accessibility/TEST_MAPPING b/core/java/com/android/internal/accessibility/TEST_MAPPING
index 1c67399..b2b3041 100644
--- a/core/java/com/android/internal/accessibility/TEST_MAPPING
+++ b/core/java/com/android/internal/accessibility/TEST_MAPPING
@@ -2,6 +2,9 @@
"imports": [
{
"path": "frameworks/base/services/accessibility/TEST_MAPPING"
+ },
+ {
+ "path": "frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/TEST_MAPPING"
}
]
}
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index 7bfb800..1204ef3 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -218,8 +218,25 @@
*/
public static final int CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE = 117;
+ /**
+ * Track attempting to snap resize a desktop window via button or drag.
+ *
+ * <p>CUJ has 3 different tags:
+ * <ul>
+ * <li>snap resizing resizable apps via maximize menu button: maximize_menu_resizable </li>
+ * <li>snap resizing resizable via drag: drag_resizable </li>
+ * <li>snap resizing non-resizable via drag: drag_non_resizable </li>
+ * </ul>
+ *
+ * <p>For non-resizable apps, the desktop window won't actually be resized, instead will return
+ * to its pre-dragged position. Attempting to snap resize a non-resizable app via the
+ * maximize menu will just result in no change, and a toast explaining the app can't be resized.
+ *
+ */
+ public static final int CUJ_DESKTOP_MODE_SNAP_RESIZE = 118;
+
// When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
- @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE;
+ @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_SNAP_RESIZE;
/** @hide */
@IntDef({
@@ -328,7 +345,8 @@
CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE,
CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH,
CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE,
- CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE
+ CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE,
+ CUJ_DESKTOP_MODE_SNAP_RESIZE
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {}
@@ -448,6 +466,7 @@
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_SNAP_RESIZE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_SNAP_RESIZE;
}
private Cuj() {
@@ -678,6 +697,8 @@
return "DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE";
case CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE:
return "DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE";
+ case CUJ_DESKTOP_MODE_SNAP_RESIZE:
+ return "DESKTOP_MODE_SNAP_RESIZE";
}
return "UNKNOWN";
}
diff --git a/core/java/com/android/internal/os/FuseAppLoop.java b/core/java/com/android/internal/os/FuseAppLoop.java
index 1c6c6a7..656d4c7 100644
--- a/core/java/com/android/internal/os/FuseAppLoop.java
+++ b/core/java/com/android/internal/os/FuseAppLoop.java
@@ -211,6 +211,7 @@
if (mInstance != 0) {
native_replySimple(mInstance, unique, FUSE_OK);
}
+ mCallbackMap.remove(checkInode(inode));
mBytesMap.stopUsing(inode);
recycleLocked(args);
}
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 0d0207f..e14249c 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -2513,9 +2513,16 @@
}
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
+
+ // For floating windows that are *allowed* to fill the screen (like Wear) content
+ // should still be wrapped if they're not explicitly requested as fullscreen.
+ final boolean isFloatingAndFullscreen = mIsFloating
+ && mAllowFloatingWindowsFillScreen
+ && a.getBoolean(R.styleable.Window_windowFullscreen, false);
+
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
- if (mIsFloating && !mAllowFloatingWindowsFillScreen) {
+ if (mIsFloating && !isFloatingAndFullscreen) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
diff --git a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
index 2feb3d5..8771cde 100644
--- a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
@@ -29,6 +29,7 @@
import static com.android.internal.protolog.ProtoLogMessage.SINT64_PARAMS;
import static com.android.internal.protolog.ProtoLogMessage.STR_PARAMS;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.ShellCommand;
import android.os.SystemClock;
@@ -49,6 +50,7 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
@@ -419,6 +421,12 @@
return group.isLogToLogcat() || (group.isLogToProto() && isProtoEnabled());
}
+ @Override
+ @NonNull
+ public List<IProtoLogGroup> getRegisteredGroups() {
+ return mLogGroups.values().stream().toList();
+ }
+
public void registerGroups(IProtoLogGroup... protoLogGroups) {
for (IProtoLogGroup group : protoLogGroups) {
mLogGroups.put(group.name(), group);
diff --git a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java b/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java
index e8d5195..b82c660 100644
--- a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java
@@ -18,6 +18,7 @@
import static com.android.internal.protolog.ProtoLog.REQUIRE_PROTOLOGTOOL;
+import android.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
@@ -26,6 +27,9 @@
import com.android.internal.protolog.common.IProtoLogGroup;
import com.android.internal.protolog.common.LogLevel;
+import java.util.Collections;
+import java.util.List;
+
/**
* Class only create and used to server temporarily for when there is source code pre-processing by
* the ProtoLog tool, when the tracing to Perfetto flag is off, and the static REQUIRE_PROTOLOGTOOL
@@ -79,4 +83,10 @@
public boolean isEnabled(IProtoLogGroup group, LogLevel level) {
return true;
}
+
+ @Override
+ @NonNull
+ public List<IProtoLogGroup> getRegisteredGroups() {
+ return Collections.emptyList();
+ }
}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 49ed55d..5517967 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -306,6 +306,12 @@
|| group.isLogToLogcat();
}
+ @Override
+ @NonNull
+ public List<IProtoLogGroup> getRegisteredGroups() {
+ return mLogGroups.values().stream().toList();
+ }
+
private void registerGroupsLocally(@NonNull IProtoLogGroup[] protoLogGroups) {
final var groupsLoggingToLogcat = new ArrayList<String>();
for (IProtoLogGroup protoLogGroup : protoLogGroups) {
diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java
index f9b9894..660d3c9 100644
--- a/core/java/com/android/internal/protolog/ProtoLog.java
+++ b/core/java/com/android/internal/protolog/ProtoLog.java
@@ -20,6 +20,9 @@
import com.android.internal.protolog.common.IProtoLogGroup;
import com.android.internal.protolog.common.LogLevel;
+import java.util.ArrayList;
+import java.util.Arrays;
+
/**
* ProtoLog API - exposes static logging methods. Usage of this API is similar
* to {@code android.utils.Log} class. Instead of plain text log messages each call consists of
@@ -49,6 +52,8 @@
private static IProtoLog sProtoLogInstance;
+ private static final Object sInitLock = new Object();
+
/**
* Initialize ProtoLog in this process.
* <p>
@@ -59,7 +64,17 @@
*/
public static void init(IProtoLogGroup... groups) {
if (android.tracing.Flags.perfettoProtologTracing()) {
- sProtoLogInstance = new PerfettoProtoLogImpl(groups);
+ synchronized (sInitLock) {
+ if (sProtoLogInstance != null) {
+ // The ProtoLog instance has already been initialized in this process
+ final var alreadyRegisteredGroups = sProtoLogInstance.getRegisteredGroups();
+ final var allGroups = new ArrayList<>(alreadyRegisteredGroups);
+ allGroups.addAll(Arrays.stream(groups).toList());
+ groups = allGroups.toArray(new IProtoLogGroup[0]);
+ }
+
+ sProtoLogInstance = new PerfettoProtoLogImpl(groups);
+ }
} else {
// The first call to ProtoLog is likely to flip REQUIRE_PROTOLOGTOOL, which is when this
// static block will be executed before REQUIRE_PROTOLOGTOOL is actually set.
diff --git a/core/java/com/android/internal/protolog/common/IProtoLog.java b/core/java/com/android/internal/protolog/common/IProtoLog.java
index d5c2ac1..f06f08a 100644
--- a/core/java/com/android/internal/protolog/common/IProtoLog.java
+++ b/core/java/com/android/internal/protolog/common/IProtoLog.java
@@ -16,6 +16,8 @@
package com.android.internal.protolog.common;
+import java.util.List;
+
/**
* Interface for ProtoLog implementations.
*/
@@ -68,4 +70,9 @@
* @return If we need to log this group and level to either ProtoLog or Logcat.
*/
boolean isEnabled(IProtoLogGroup group, LogLevel level);
+
+ /**
+ * @return an immutable list of the registered ProtoLog groups in this ProtoLog instance.
+ */
+ List<IProtoLogGroup> getRegisteredGroups();
}
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 1d43f6f..c834dde 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -209,11 +209,6 @@
void onDisplayReady(int displayId);
/**
- * Notifies System UI whether the recents animation is running or not.
- */
- void onRecentsAnimationStateChanged(boolean running);
-
- /**
* Notifies System UI side of system bar attribute change on the specified display.
*
* @param displayId the ID of the display to notify.
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java
index 1e2cad4..1e965c5d 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -49,81 +49,41 @@
private ArrayUtils() { /* cannot be instantiated */ }
- @android.ravenwood.annotation.RavenwoodReplace
public static byte[] newUnpaddedByteArray(int minLen) {
return (byte[])VMRuntime.getRuntime().newUnpaddedArray(byte.class, minLen);
}
- @android.ravenwood.annotation.RavenwoodReplace
public static char[] newUnpaddedCharArray(int minLen) {
return (char[])VMRuntime.getRuntime().newUnpaddedArray(char.class, minLen);
}
- @android.ravenwood.annotation.RavenwoodReplace
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static int[] newUnpaddedIntArray(int minLen) {
return (int[])VMRuntime.getRuntime().newUnpaddedArray(int.class, minLen);
}
- @android.ravenwood.annotation.RavenwoodReplace
public static boolean[] newUnpaddedBooleanArray(int minLen) {
return (boolean[])VMRuntime.getRuntime().newUnpaddedArray(boolean.class, minLen);
}
- @android.ravenwood.annotation.RavenwoodReplace
public static long[] newUnpaddedLongArray(int minLen) {
return (long[])VMRuntime.getRuntime().newUnpaddedArray(long.class, minLen);
}
- @android.ravenwood.annotation.RavenwoodReplace
public static float[] newUnpaddedFloatArray(int minLen) {
return (float[])VMRuntime.getRuntime().newUnpaddedArray(float.class, minLen);
}
- @android.ravenwood.annotation.RavenwoodReplace
public static Object[] newUnpaddedObjectArray(int minLen) {
return (Object[])VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen);
}
- @android.ravenwood.annotation.RavenwoodReplace
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@SuppressWarnings("unchecked")
public static <T> T[] newUnpaddedArray(Class<T> clazz, int minLen) {
return (T[])VMRuntime.getRuntime().newUnpaddedArray(clazz, minLen);
}
- public static byte[] newUnpaddedByteArray$ravenwood(int minLen) {
- return new byte[minLen];
- }
-
- public static char[] newUnpaddedCharArray$ravenwood(int minLen) {
- return new char[minLen];
- }
-
- public static int[] newUnpaddedIntArray$ravenwood(int minLen) {
- return new int[minLen];
- }
-
- public static boolean[] newUnpaddedBooleanArray$ravenwood(int minLen) {
- return new boolean[minLen];
- }
-
- public static long[] newUnpaddedLongArray$ravenwood(int minLen) {
- return new long[minLen];
- }
-
- public static float[] newUnpaddedFloatArray$ravenwood(int minLen) {
- return new float[minLen];
- }
-
- public static Object[] newUnpaddedObjectArray$ravenwood(int minLen) {
- return new Object[minLen];
- }
-
- public static <T> T[] newUnpaddedArray$ravenwood(Class<T> clazz, int minLen) {
- return (T[]) Array.newInstance(clazz, minLen);
- }
-
/**
* Checks if the beginnings of two byte arrays are equal.
*
diff --git a/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java b/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java
index bc729f1..b6383d9 100644
--- a/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java
+++ b/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java
@@ -437,9 +437,8 @@
// Initialize x ensuring that the toolbar isn't rendered behind the nav bar in
// landscape.
- final int x = Math.min(
- contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2,
- mViewPortOnScreen.right - mPopupWindow.getWidth());
+ final int x = Math.clamp(contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2,
+ mViewPortOnScreen.left, mViewPortOnScreen.right - mPopupWindow.getWidth());
final int y;
diff --git a/core/proto/android/app/appstartinfo.proto b/core/proto/android/app/appstartinfo.proto
index 8de5458..78cf6f4 100644
--- a/core/proto/android/app/appstartinfo.proto
+++ b/core/proto/android/app/appstartinfo.proto
@@ -40,4 +40,5 @@
optional bytes start_intent = 10;
optional AppStartLaunchMode launch_mode = 11;
optional bool was_force_stopped = 12;
+ optional int64 monotonic_creation_time_ms = 13;
}
diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto
index 58f39a9..42c591b 100644
--- a/core/proto/android/server/activitymanagerservice.proto
+++ b/core/proto/android/server/activitymanagerservice.proto
@@ -1045,7 +1045,7 @@
repeated Package packages = 2;
}
-// sync with com.android.server.am.am.ProcessList.java
+// LINT.IfChange
message AppsStartInfoProto {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
@@ -1064,4 +1064,6 @@
repeated User users = 2;
}
repeated Package packages = 2;
+ optional int64 monotonic_time = 3;
}
+// LINT.ThenChange(/services/core/java/com/android/server/am/AppStartInfoTracker.java)
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f795406..2dd560c 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5605,12 +5605,13 @@
<!-- This permission is required among systems services to always keep the
binding with TvInputManagerService.
<p>This should only be used by the OEM TvInputService.
+ @FlaggedApi("android.media.tv.flags.tif_unbind_inactive_tis")
<p>Protection level: signature|privileged|vendorPrivileged
@hide
-->
<permission android:name="android.permission.ALWAYS_BOUND_TV_INPUT"
android:protectionLevel="signature|privileged|vendorPrivileged"
- android:featureFlag="android.media.tv.flags.tis_always_bound_permission"/>
+ android:featureFlag="android.media.tv.flags.tif_unbind_inactive_tis"/>
<!-- Must be required by a {@link android.media.tv.interactive.TvInteractiveAppService}
to ensure that only the system can bind to it.
diff --git a/core/res/OWNERS b/core/res/OWNERS
index b2b58d5..5293131 100644
--- a/core/res/OWNERS
+++ b/core/res/OWNERS
@@ -78,6 +78,11 @@
per-file res/values/config_telephony.xml = file:/platform/frameworks/opt/telephony:/OWNERS
per-file res/xml/sms_short_codes.xml = file:/platform/frameworks/opt/telephony:/OWNERS
+# Input Method Framework
+per-file res/*/*input_method* = file:/services/core/java/com/android/server/inputmethod/OWNERS
+per-file res/*/*_ime* = file:/services/core/java/com/android/server/inputmethod/OWNERS
+per-file res/*/ime_* = file:/services/core/java/com/android/server/inputmethod/OWNERS
+
# TV Input Framework
per-file res/values/config_tv_external_input_logging.xml = file:/services/core/java/com/android/server/tv/OWNERS
diff --git a/core/res/res/color/input_method_switch_on_item.xml b/core/res/res/color/input_method_switch_on_item.xml
new file mode 100644
index 0000000..49fe081
--- /dev/null
+++ b/core/res/res/color/input_method_switch_on_item.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_activated="true" android:color="?attr/materialColorOnSecondaryContainer" />
+ <item android:color="?attr/materialColorOnSurface" />
+</selector>
diff --git a/core/res/res/layout/input_method_switch_item_new.xml b/core/res/res/layout/input_method_switch_item_new.xml
index 09ed650..10d938c 100644
--- a/core/res/res/layout/input_method_switch_item_new.xml
+++ b/core/res/res/layout/input_method_switch_item_new.xml
@@ -70,6 +70,7 @@
android:ellipsize="marquee"
android:singleLine="true"
android:fontFamily="google-sans-text"
+ android:textColor="@color/input_method_switch_on_item"
android:textAppearance="?attr/textAppearanceListItem"/>
</LinearLayout>
@@ -81,7 +82,7 @@
android:gravity="center_vertical"
android:layout_marginStart="12dp"
android:src="@drawable/ic_check_24dp"
- android:tint="?attr/materialColorOnSurface"
+ android:tint="@color/input_method_switch_on_item"
android:visibility="gone"
android:importantForAccessibility="no"/>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index de7477e..b6468ee 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3080,6 +3080,11 @@
<!-- Whether UI for multi user should be shown -->
<bool name="config_enableMultiUserUI">false</bool>
+ <!-- Whether to boot system with the headless system user, i.e. user 0. If set to true,
+ system will be booted with the headless system user, or user 0. It has no effect if device
+ is not in Headless System User Mode (HSUM). -->
+ <bool name="config_bootToHeadlessSystemUser">false</bool>
+
<!-- Whether multiple admins are allowed on the device. If set to true, new users can be created
with admin privileges and admin privileges can be granted/revoked from existing users. -->
<bool name="config_enableMultipleAdmins">false</bool>
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 118acac..a7240ff 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -443,4 +443,20 @@
<!-- Telephony satellite gateway intent for handling carrier roaming to satellite is using ESOS messaging. -->
<string name="config_satellite_carrier_roaming_esos_provisioned_intent_action" translatable="false"></string>
<java-symbol type="string" name="config_satellite_carrier_roaming_esos_provisioned_intent_action" />
+
+ <!-- The time duration in minutes to wait before retry validating a possible change
+ in satellite allowed region. The default value is 10 minutes. -->
+ <integer name="config_satellite_delay_minutes_before_retry_validating_possible_change_in_allowed_region">10</integer>
+ <java-symbol type="integer" name="config_satellite_delay_minutes_before_retry_validating_possible_change_in_allowed_region" />
+
+ <!-- The maximum retry count to validate a possible change in satellite allowed region.
+ The default value is 3 minutes. -->
+ <integer name="config_satellite_max_retry_count_for_validating_possible_change_in_allowed_region">3</integer>
+ <java-symbol type="integer" name="config_satellite_max_retry_count_for_validating_possible_change_in_allowed_region" />
+
+ <!-- The time duration in minutes for location query throttle interval.
+ The default value is 10 minutes. -->
+ <integer name="config_satellite_location_query_throttle_interval_minutes">10</integer>
+ <java-symbol type="integer" name="config_satellite_location_query_throttle_interval_minutes" />
+
</resources>
diff --git a/core/res/res/values/config_tv_external_input_logging.xml b/core/res/res/values/config_tv_external_input_logging.xml
index 72e30be..293a183 100644
--- a/core/res/res/values/config_tv_external_input_logging.xml
+++ b/core/res/res/values/config_tv_external_input_logging.xml
@@ -24,27 +24,39 @@
entries do not follow the convention, but all new entries should. -->
<resources>
- <bool name="config_tvExternalInputLoggingDisplayNameFilterEnabled">false</bool>
+ <bool name="config_tvExternalInputLoggingDisplayNameFilterEnabled">true</bool>
<string-array name="config_tvExternalInputLoggingDeviceOnScreenDisplayNames">
- <item>Chromecast</item>
+ <item>ADT-4</item>
<item>Chromecast HD</item>
- <item>SHIELD</item>
- <item>Roku</item>
- <item>Roku Express 4</item>
- <item>Home Theater</item>
<item>Fire TV Stick</item>
- <item>PlayStation 5</item>
+ <item>Freebox Player</item>
+ <item>Home Theater</item>
+ <item>Jarvis</item>
<item>NintendoSwitch</item>
+ <item>onn. 4K Plus S</item>
+ <item>onn. Streaming</item>
+ <item>PlayStation 4</item>
+ <item>PlayStation 5</item>
+ <item>Roku 3</item>
+ <item>Roku Express 4</item>
</string-array>
<string-array name="config_tvExternalInputLoggingDeviceBrandNames">
- <item>Chromecast</item>
- <item>SHIELD</item>
- <item>Roku</item>
<item>Apple</item>
+ <item>Chromecast</item>
<item>Fire TV</item>
- <item>PlayStation</item>
+ <item>Freebox</item>
+ <item>Google</item>
+ <item>MiBOX</item>
+ <item>Microsoft</item>
<item>Nintendo</item>
+ <item>NVIDIA</item>
+ <item>onn.</item>
+ <item>PlayStation</item>
+ <item>Roku</item>
+ <item>SHIELD</item>
+ <item>Sony</item>
+ <item>XBOX</item>
</string-array>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 0d16e9c..9a52bd4 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -363,6 +363,7 @@
<java-symbol type="bool" name="config_canSwitchToHeadlessSystemUser"/>
<java-symbol type="bool" name="config_enableMultiUserUI"/>
<java-symbol type="bool" name="config_enableMultipleAdmins"/>
+ <java-symbol type="bool" name="config_bootToHeadlessSystemUser"/>
<java-symbol type="bool" name="config_omnipresentCommunalUser"/>
<java-symbol type="bool" name="config_enableNewAutoSelectNetworkUI"/>
<java-symbol type="bool" name="config_disableUsbPermissionDialogs"/>
diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
index 668487d..786f1e8 100644
--- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
+++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
@@ -40,6 +40,7 @@
import android.util.SparseArray;
import android.view.SurfaceControl.Transaction;
import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
+import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -117,11 +118,21 @@
SparseArray<InsetsSourceControl> controls = new SparseArray<>();
controls.put(ID_STATUS_BAR, topConsumer.getControl());
controls.put(ID_NAVIGATION_BAR, navConsumer.getControl());
+ InsetsAnimationSpec spec = new InsetsAnimationSpec() {
+ @Override
+ public long getDurationMs(boolean hasZeroInsetsIme) {
+ return 10;
+ }
+ @Override
+ public Interpolator getInsetsInterpolator(boolean hasZeroInsetsIme) {
+ return new LinearInterpolator();
+ }
+ };
+
mController = new InsetsAnimationControlImpl(controls,
new Rect(0, 0, 500, 500), mInsetsState, mMockListener, systemBars(),
- mMockController, 10 /* durationMs */, new LinearInterpolator(),
- 0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */,
- null /* statsToken */);
+ mMockController, spec /* insetsAnimationSpecCreator */, 0 /* animationType */,
+ 0 /* layoutInsetsDuringAnimation */, null /* translator */, null /* statsToken */);
mController.setReadyDispatched(true);
}
diff --git a/core/tests/resourceflaggingtests/Android.bp b/core/tests/resourceflaggingtests/Android.bp
index efb8437..40bdc2b 100644
--- a/core/tests/resourceflaggingtests/Android.bp
+++ b/core/tests/resourceflaggingtests/Android.bp
@@ -26,6 +26,7 @@
name: "ResourceFlaggingTests",
srcs: [
"src/**/*.java",
+ ":resource-flagging-test-app-r-java",
],
platform_apis: true,
certificate: "platform",
diff --git a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
index 471b402..005538a 100644
--- a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
+++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
@@ -19,15 +19,22 @@
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.ApkAssets;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.FileUtils;
import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
+import com.android.intenal.flaggedresources.R;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -46,7 +53,14 @@
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getTargetContext();
AssetManager assets = new AssetManager();
- assertThat(assets.addAssetPath(extractApkAndGetPath(R.raw.resapp))).isNotEqualTo(0);
+ assets.setApkAssets(
+ new ApkAssets[]{
+ ApkAssets.loadFromPath(
+ extractApkAndGetPath(
+ com.android.resourceflaggingtests.R.raw.resapp
+ )
+ )
+ }, true);
final DisplayMetrics dm = new DisplayMetrics();
dm.setToDefaults();
@@ -55,54 +69,60 @@
@Test
public void testFlagDisabled() {
- assertThat(getBoolean("res1")).isTrue();
+ assertThat(mResources.getBoolean(R.bool.bool1)).isTrue();
}
@Test
public void testFlagEnabled() {
- assertThat(getBoolean("res2")).isTrue();
+ assertThat(mResources.getBoolean(R.bool.bool2)).isTrue();
}
@Test
public void testFlagEnabledDifferentCompilationUnit() {
- assertThat(getBoolean("res3")).isTrue();
+ assertThat(mResources.getBoolean(R.bool.bool3)).isTrue();
}
@Test
public void testFlagDisabledStringArrayElement() {
- assertThat(getStringArray("strarr1")).isEqualTo(new String[]{"one", "two", "three"});
+ assertThat(mResources.getStringArray(R.array.strarr1))
+ .isEqualTo(new String[]{"one", "two", "three"});
}
@Test
public void testFlagDisabledIntArrayElement() {
- assertThat(getIntArray("intarr1")).isEqualTo(new int[]{1, 2, 3});
+ assertThat(mResources.getIntArray(R.array.intarr1)).isEqualTo(new int[]{1, 2, 3});
}
- private boolean getBoolean(String name) {
- int resId = mResources.getIdentifier(
- name,
- "bool",
- "com.android.intenal.flaggedresources");
- assertThat(resId).isNotEqualTo(0);
- return mResources.getBoolean(resId);
+ @Test
+ public void testLayoutWithDisabledElements() {
+ LinearLayout ll = (LinearLayout) getLayoutInflater().inflate(R.layout.layout1, null);
+ assertThat(ll).isNotNull();
+ assertThat((View) ll.findViewById(R.id.text1)).isNotNull();
+ assertThat((View) ll.findViewById(R.id.disabled_text)).isNull();
+ assertThat((View) ll.findViewById(R.id.text2)).isNotNull();
}
- private String[] getStringArray(String name) {
- int resId = mResources.getIdentifier(
- name,
- "array",
- "com.android.intenal.flaggedresources");
- assertThat(resId).isNotEqualTo(0);
- return mResources.getStringArray(resId);
- }
+ private LayoutInflater getLayoutInflater() {
+ ContextWrapper c = new ContextWrapper(mContext) {
+ private LayoutInflater mInflater;
- private int[] getIntArray(String name) {
- int resId = mResources.getIdentifier(
- name,
- "array",
- "com.android.intenal.flaggedresources");
- assertThat(resId).isNotEqualTo(0);
- return mResources.getIntArray(resId);
+ @Override
+ public Resources getResources() {
+ return mResources;
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (LAYOUT_INFLATER_SERVICE.equals(name)) {
+ if (mInflater == null) {
+ mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
+ }
+ return mInflater;
+ }
+ return super.getSystemService(name);
+ }
+ };
+ return LayoutInflater.from(c);
}
private String extractApkAndGetPath(int id) throws Exception {
diff --git a/data/fonts/Android.bp b/data/fonts/Android.bp
index 1a3a0f6..cbaac21 100644
--- a/data/fonts/Android.bp
+++ b/data/fonts/Android.bp
@@ -71,6 +71,8 @@
},
}
+// TODO(nona): Change this to use generate_font_fallback to be able to generate XML from
+// per family JSON config
prebuilt_fonts_xml {
name: "font_fallback.xml",
src: "font_fallback.xml",
@@ -94,3 +96,33 @@
"DroidSansMono.ttf",
],
}
+
+genrule {
+ name: "generate_font_fallback",
+ tools: [":generate_fonts_xml"],
+ tool_files: [
+ "alias.json",
+ "fallback_order.json",
+ ],
+ srcs: [
+ ":CarroisGothicSC",
+ ":ComingSoon",
+ ":CutiveMono",
+ ":DancingScript",
+ ":DroidSansMono",
+ ":Roboto",
+ ":RobotoFlex",
+ ":SourceSansPro",
+ ":noto-fonts",
+ ],
+ exclude_srcs: [
+ "alias.json",
+ "fallback_order.json",
+ ],
+ out: ["font_fallback.xml"],
+ cmd: "$(location :generate_fonts_xml) " +
+ "--alias=$(location alias.json) " +
+ "--fallback=$(location fallback_order.json) " +
+ "$(in) " +
+ "-o $(out)",
+}
diff --git a/data/fonts/alias.json b/data/fonts/alias.json
new file mode 100644
index 0000000..b5b867a
--- /dev/null
+++ b/data/fonts/alias.json
@@ -0,0 +1,37 @@
+[
+ // sans-serif aliases
+ { "name": "arial", "to": "sans-serif" },
+ { "name": "helvetica", "to": "sans-serif" },
+ { "name": "tahoma", "to": "sans-serif" },
+ { "name": "verdana", "to": "sans-serif" },
+ { "name": "sans-serif-black", "to": "sans-serif", "weight": "900" },
+ { "name": "sans-serif-light", "to": "sans-serif", "weight": "300" },
+ { "name": "sans-serif-medium", "to": "sans-serif", "weight": "500" },
+ { "name": "sans-serif-thin", "to": "sans-serif", "weight": "100" },
+
+ // sans-serif-condensed aliases
+ { "name": "sans-serif-condensed-light", "to": "sans-serif-condensed", "weight": "300" },
+ { "name": "sans-serif-condensed-medium", "to": "sans-serif-condensed", "weight": "500" },
+
+ // serif aliases
+ { "name": "ITC Stone Serif", "to": "serif" },
+ { "name": "baskerville", "to": "serif" },
+ { "name": "fantasy", "to": "serif" },
+ { "name": "georgia", "to": "serif" },
+ { "name": "goudy", "to": "serif" },
+ { "name": "palatino", "to": "serif" },
+ { "name": "times new roman", "to": "serif" },
+ { "name": "times", "to": "serif" },
+ { "name": "serif-bold", "to": "serif", "weight": "700" },
+
+ // monospace aliases
+ { "name": "monaco", "to": "monospace" },
+ { "name": "sans-serif-monospace", "to": "monospace" },
+
+ // serif-monospace aliases
+ { "name": "courier new", "to": "serif-monospace" },
+ { "name": "courier", "to": "serif-monospace" },
+
+ // source-sans-pro aliases
+ { "name": "source-sans-pro-semi-bold", "to": "source-sans-pro", "weight": "600" }
+]
diff --git a/data/fonts/fallback_order.json b/data/fonts/fallback_order.json
new file mode 100644
index 0000000..2fc3f3e
--- /dev/null
+++ b/data/fonts/fallback_order.json
@@ -0,0 +1,136 @@
+[
+ { "lang": "und-Arab" },
+ { "lang": "und-Ethi" },
+ { "lang": "und-Hebr" },
+ { "lang": "und-Thai" },
+ { "lang": "und-Armn" },
+ { "lang": "und-Geor,und-Geok" },
+ { "lang": "und-Deva" },
+ { "lang": "und-Gujr" },
+ { "lang": "und-Guru" },
+ { "lang": "und-Taml" },
+ { "lang": "und-Mlym" },
+ { "lang": "und-Beng" },
+ { "lang": "und-Telu" },
+ { "lang": "und-Knda" },
+ { "lang": "und-Orya" },
+ { "lang": "und-Sinh" },
+ { "lang": "und-Khmr" },
+ { "lang": "und-Laoo" },
+ { "lang": "und-Mymr" },
+ { "lang": "und-Thaa" },
+ { "lang": "und-Cham" },
+ { "lang": "und-Ahom" },
+ { "lang": "und-Adlm" },
+ { "lang": "und-Avst" },
+ { "lang": "und-Bali" },
+ { "lang": "und-Bamu" },
+ { "lang": "und-Batk" },
+ { "lang": "und-Brah" },
+ { "lang": "und-Bugi" },
+ { "lang": "und-Buhd" },
+ { "lang": "und-Cans" },
+ { "lang": "und-Cari" },
+ { "lang": "und-Cakm" },
+ { "lang": "und-Cher" },
+ { "lang": "und-Copt" },
+ { "lang": "und-Xsux" },
+ { "lang": "und-Cprt" },
+ { "lang": "und-Dsrt" },
+ { "lang": "und-Egyp" },
+ { "lang": "und-Elba" },
+ { "lang": "und-Glag" },
+ { "lang": "und-Goth" },
+ { "lang": "und-Hano" },
+ { "lang": "und-Armi" },
+ { "lang": "und-Phli" },
+ { "lang": "und-Prti" },
+ { "lang": "und-Java" },
+ { "lang": "und-Kthi" },
+ { "lang": "und-Kali" },
+ { "lang": "und-Khar" },
+ { "lang": "und-Lepc" },
+ { "lang": "und-Limb" },
+ { "lang": "und-Linb" },
+ { "lang": "und-Lisu" },
+ { "lang": "und-Lyci" },
+ { "lang": "und-Lydi" },
+ { "lang": "und-Mand" },
+ { "lang": "und-Mtei" },
+ { "lang": "und-Talu" },
+ { "lang": "und-Nkoo" },
+ { "lang": "und-Ogam" },
+ { "lang": "und-Olck" },
+ { "lang": "und-Ital" },
+ { "lang": "und-Xpeo" },
+ { "lang": "und-Sarb" },
+ { "lang": "und-Orkh" },
+ { "lang": "und-Osge" },
+ { "lang": "und-Osma" },
+ { "lang": "und-Phnx" },
+ { "lang": "und-Rjng" },
+ { "lang": "und-Runr" },
+ { "lang": "und-Samr" },
+ { "lang": "und-Saur" },
+ { "lang": "und-Shaw" },
+ { "lang": "und-Sund" },
+ { "lang": "und-Sylo" },
+ { "lang": "und-Syre" },
+ { "lang": "und-Syrn" },
+ { "lang": "und-Syrj" },
+ { "lang": "und-Tglg" },
+ { "lang": "und-Tagb" },
+ { "lang": "und-Lana" },
+ { "lang": "und-Tavt" },
+ { "lang": "und-Tibt" },
+ { "lang": "und-Tfng" },
+ { "lang": "und-Ugar" },
+ { "lang": "und-Vaii" },
+ // NotoSansSymbol-Regular-Subsetted doesn't have any language but should be
+ // placed before the CJK fonts for reproducing the same fallback order.
+ { "id": "NotoSansSymbols-Regular-Subsetted" },
+ { "lang": "zh-Hans" },
+ { "lang": "zh-Hant,zh-Bopo" },
+ { "lang": "ja" },
+ { "lang": "ko" },
+ { "lang": "und-Zsye" },
+ { "lang": "und-Zsym" },
+ { "lang": "und-Tale" },
+ { "lang": "und-Yiii" },
+ { "lang": "und-Mong" },
+ { "lang": "und-Phag" },
+ { "lang": "und-Hluw" },
+ { "lang": "und-Bass" },
+ { "lang": "und-Bhks" },
+ { "lang": "und-Hatr" },
+ { "lang": "und-Lina" },
+ { "lang": "und-Mani" },
+ { "lang": "und-Marc" },
+ { "lang": "und-Merc" },
+ { "lang": "und-Plrd" },
+ { "lang": "und-Mroo" },
+ { "lang": "und-Mult" },
+ { "lang": "und-Nbat" },
+ { "lang": "und-Newa" },
+ { "lang": "und-Narb" },
+ { "lang": "und-Perm" },
+ { "lang": "und-Hmng" },
+ { "lang": "und-Palm" },
+ { "lang": "und-Pauc" },
+ { "lang": "und-Shrd" },
+ { "lang": "und-Sora" },
+ { "lang": "und-Gong" },
+ { "lang": "und-Rohg" },
+ { "lang": "und-Khoj" },
+ { "lang": "und-Gonm" },
+ { "lang": "und-Wcho" },
+ { "lang": "und-Wara" },
+ { "lang": "und-Gran" },
+ { "lang": "und-Modi" },
+ { "lang": "und-Dogr" },
+ { "lang": "und-Medf" },
+ { "lang": "und-Soyo" },
+ { "lang": "und-Takr" },
+ { "lang": "und-Hmnp" },
+ { "lang": "und-Yezi" }
+]
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
index 434885f..47d5274 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
@@ -22,10 +22,14 @@
import com.android.window.flags.Flags
/*
- * A shared class to check desktop mode flags state.
+ * An enum to check desktop mode flags state.
*
- * The class computes whether a Desktop Windowing flag should be enabled by using the aconfig flag
- * value and the developer option override state (if applicable).
+ * This enum provides a centralized way to control the behavior of flags related to desktop
+ * windowing features which are aiming for developer preview before their release. It allows
+ * developer option to override the default behavior of these flags.
+ *
+ * NOTE: Flags should only be added to this enum when they have received Product and UX
+ * alignment that the feature is ready for developer preview, otherwise just do a flag check.
*/
enum class DesktopModeFlags(
// Function called to obtain aconfig flag value.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index 8ce7837..17869e9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -19,8 +19,6 @@
import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
-import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
@@ -40,6 +38,7 @@
import com.android.wm.shell.compatui.api.CompatUIEvent;
import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonAppeared;
import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import java.util.function.Consumer;
@@ -83,7 +82,7 @@
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mCallback = callback;
mHasSizeCompat = taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat();
- if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+ if (DesktopModeStatus.canEnterDesktopMode(context)
&& DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(context)) {
// Don't show the SCM button for freeform tasks
mHasSizeCompat &= !taskInfo.isFreeform();
@@ -139,7 +138,7 @@
boolean canShow) {
final boolean prevHasSizeCompat = mHasSizeCompat;
mHasSizeCompat = taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat();
- if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)
&& DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)) {
// Don't show the SCM button for freeform tasks
mHasSizeCompat &= !taskInfo.isFreeform();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt
new file mode 100644
index 0000000..a489c4f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.wm.shell.dagger
+
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import dagger.Module
+import dagger.Provides
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.asCoroutineDispatcher
+
+/** Providers for various WmShell-specific coroutines-related constructs. */
+@Module
+class WMShellCoroutinesModule {
+ @Provides
+ @ShellMainThread
+ fun provideMainDispatcher(@ShellMainThread mainExecutor: ShellExecutor): CoroutineDispatcher =
+ mainExecutor.asCoroutineDispatcher()
+
+ @Provides
+ @ShellBackgroundThread
+ fun provideBackgroundDispatcher(
+ @ShellBackgroundThread backgroundExecutor: ShellExecutor
+ ): CoroutineDispatcher = backgroundExecutor.asCoroutineDispatcher()
+
+ @Provides
+ @WMSingleton
+ @ShellMainThread
+ fun provideApplicationScope(
+ @ShellMainThread applicationDispatcher: CoroutineDispatcher,
+ ): CoroutineScope = CoroutineScope(applicationDispatcher)
+
+ @Provides
+ @WMSingleton
+ @ShellBackgroundThread
+ fun provideBackgroundCoroutineScope(
+ @ShellBackgroundThread backgroundDispatcher: CoroutineDispatcher,
+ ): CoroutineScope = CoroutineScope(backgroundDispatcher)
+
+ @Provides
+ @WMSingleton
+ @ShellBackgroundThread
+ fun provideBackgroundCoroutineContext(
+ @ShellBackgroundThread backgroundDispatcher: CoroutineDispatcher
+ ): CoroutineContext = backgroundDispatcher + SupervisorJob()
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index b6f2a25..02cbe01 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -132,7 +132,8 @@
sessionId,
taskUpdate.minimizeReason?.reason ?: UNSET_MINIMIZE_REASON,
taskUpdate.unminimizeReason?.reason ?: UNSET_UNMINIMIZE_REASON,
-
+ /* visible_task_count */
+ taskUpdate.visibleTaskCount
)
}
@@ -159,6 +160,7 @@
val taskY: Int,
val minimizeReason: MinimizeReason? = null,
val unminimizeReason: UnminimizeReason? = null,
+ val visibleTaskCount: Int,
)
// Default value used when the task was not minimized.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index 2e36ffe..336e5e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -295,7 +295,8 @@
postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>
) {
postTransitionVisibleFreeformTasks.forEach { taskId, taskInfo ->
- val currentTaskUpdate = buildTaskUpdateForTask(taskInfo)
+ val currentTaskUpdate = buildTaskUpdateForTask(taskInfo,
+ postTransitionVisibleFreeformTasks.size())
val previousTaskInfo = preTransitionVisibleFreeformTasks[taskId]
when {
// new tasks added
@@ -309,15 +310,17 @@
}
// old tasks that were resized or repositioned
// TODO(b/347935387): Log changes only once they are stable.
- buildTaskUpdateForTask(previousTaskInfo) != currentTaskUpdate ->
- desktopModeEventLogger.logTaskInfoChanged(sessionId, currentTaskUpdate)
+ buildTaskUpdateForTask(previousTaskInfo, postTransitionVisibleFreeformTasks.size())
+ != currentTaskUpdate ->
+ desktopModeEventLogger.logTaskInfoChanged(sessionId, currentTaskUpdate)
}
}
// find old tasks that were removed
preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo ->
if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) {
- desktopModeEventLogger.logTaskRemoved(sessionId, buildTaskUpdateForTask(taskInfo))
+ desktopModeEventLogger.logTaskRemoved(sessionId,
+ buildTaskUpdateForTask(taskInfo, postTransitionVisibleFreeformTasks.size()))
Trace.setCounter(
Trace.TRACE_TAG_WINDOW_MANAGER,
VISIBLE_TASKS_COUNTER_NAME,
@@ -327,7 +330,7 @@
}
}
- private fun buildTaskUpdateForTask(taskInfo: TaskInfo): TaskUpdate {
+ private fun buildTaskUpdateForTask(taskInfo: TaskInfo, visibleTasks: Int): TaskUpdate {
val screenBounds = taskInfo.configuration.windowConfiguration.bounds
val positionInParent = taskInfo.positionInParent
return TaskUpdate(
@@ -337,6 +340,7 @@
taskWidth = screenBounds.width(),
taskX = positionInParent.x,
taskY = positionInParent.y,
+ visibleTaskCount = visibleTasks,
)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index c97066a..ffd534b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -53,6 +53,7 @@
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
+import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.policy.ScreenDecorationsUtils
import com.android.internal.protolog.ProtoLog
@@ -322,7 +323,7 @@
logW("moveBackgroundTaskToDesktop taskId=%d not found", taskId)
return false
}
- logV("moveBackgroundTaskToDesktop with taskId=%d, displayId=%d", taskId)
+ logV("moveBackgroundTaskToDesktop with taskId=%d", taskId)
// TODO(342378842): Instead of using default display, support multiple displays
val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
DEFAULT_DISPLAY, wct, taskId)
@@ -693,6 +694,10 @@
) {
releaseVisualIndicator()
if (!taskInfo.isResizeable && DesktopModeFlags.DISABLE_SNAP_RESIZE.isEnabled(context)) {
+ interactionJankMonitor.begin(
+ taskSurface, context, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_non_resizable"
+ )
+
// reposition non-resizable app back to its original position before being dragged
returnToDragStartAnimator.start(
taskInfo.taskId,
@@ -701,6 +706,9 @@
endBounds = dragStartBounds
)
} else {
+ interactionJankMonitor.begin(
+ taskSurface, context, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_resizable"
+ )
snapToHalfScreen(taskInfo, currentDragBounds, position)
}
}
@@ -1104,12 +1112,11 @@
addMoveToDesktopChanges(wct, task)
// In some launches home task is moved behind new task being launched. Make sure
// that's not the case for launches in desktop.
- moveHomeTask(wct, toTop = false)
- // Move existing minimized tasks behind Home
- taskRepository.getFreeformTasksInZOrder(task.displayId)
- .filter { taskId -> taskRepository.isMinimizedTask(taskId) }
- .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) }
- .forEach { taskInfo -> wct.reorder(taskInfo.token, /* onTop= */ false) }
+ if (task.baseIntent.flags.and(Intent.FLAG_ACTIVITY_TASK_ON_HOME) != 0) {
+ bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
+ wct.reorder(task.token, true)
+ }
+
// Desktop Mode is already showing and we're launching a new Task - we might need to
// minimize another Task.
val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt
index 24a7d77..4c5258f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt
@@ -24,6 +24,7 @@
import android.view.SurfaceControl
import android.widget.Toast
import androidx.core.animation.addListener
+import com.android.internal.jank.Cuj
import com.android.internal.jank.InteractionJankMonitor
import com.android.wm.shell.R
import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener
@@ -85,7 +86,7 @@
R.string.desktop_mode_non_resizable_snap_text,
Toast.LENGTH_SHORT
).show()
- // TODO(b/339582583) - add Jank CUJ using interactionJankMonitor
+ interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE)
}
)
addUpdateListener { anim ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
index bf185a4..96719fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
@@ -117,8 +117,8 @@
finishCallback.onTransitionFinished(null)
initialBounds = null
boundsAnimator = null
- interactionJankMonitor.end(
- Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW)
+ interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW)
+ interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE)
}
)
addUpdateListener { anim ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 7ba6ec4..b102e40 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -41,6 +41,7 @@
import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection;
import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection;
import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_CLEANUP_PIP_EXIT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
@@ -122,6 +123,8 @@
@Nullable
private IBinder mMoveToBackTransition;
private IBinder mRequestedEnterTransition;
+ private IBinder mCleanupTransition;
+
private WindowContainerToken mRequestedEnterTask;
/** The Task window that is currently in PIP windowing mode. */
@Nullable
@@ -232,10 +235,12 @@
// Exiting PIP.
final int type = info.getType();
- if (transition.equals(mExitTransition) || transition.equals(mMoveToBackTransition)) {
+ if (transition.equals(mExitTransition) || transition.equals(mMoveToBackTransition)
+ || transition.equals(mCleanupTransition)) {
mExitDestinationBounds.setEmpty();
mExitTransition = null;
mMoveToBackTransition = null;
+ mCleanupTransition = null;
mHasFadeOut = false;
if (mFinishCallback != null) {
callFinishCallback(null /* wct */);
@@ -269,6 +274,9 @@
removePipImmediately(info, startTransaction, finishTransaction, finishCallback,
pipTaskInfo);
break;
+ case TRANSIT_CLEANUP_PIP_EXIT:
+ cleanupPipExitTransition(startTransaction, finishCallback);
+ break;
default:
throw new IllegalStateException("mExitTransition with unexpected transit type="
+ transitTypeToString(type));
@@ -768,7 +776,19 @@
mPipAnimationController.resetAnimatorState();
finishTransaction.remove(pipLeash);
}
- finishCallback.onTransitionFinished(wct);
+
+ if (mFixedRotationState == FIXED_ROTATION_TRANSITION) {
+ // TODO(b/358226697): start a new transition with the WCT instead of applying it in
+ // the {@link finishCallback}, to ensure shell creates a transition for it.
+ finishCallback.onTransitionFinished(wct);
+ } else {
+ // Apply wct in separate transition so that it can be correctly handled by the
+ // {@link FreeformTaskTransitionObserver} when desktop windowing (which does not
+ // utilize fixed rotation transitions for exiting pip) is enabled (See b/288910069).
+ mCleanupTransition = mTransitions.startTransition(
+ TRANSIT_CLEANUP_PIP_EXIT, wct, this);
+ finishCallback.onTransitionFinished(null);
+ }
};
mFinishTransaction = finishTransaction;
@@ -914,6 +934,16 @@
finishCallback.onTransitionFinished(null);
}
+ /**
+ * For {@link Transitions#TRANSIT_CLEANUP_PIP_EXIT} which applies final config changes needed
+ * after the exit from pip transition animation finishes.
+ */
+ private void cleanupPipExitTransition(@NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ startTransaction.apply();
+ finishCallback.onTransitionFinished(null);
+ }
+
/** Whether we should handle the given {@link TransitionInfo} animation as entering PIP. */
private boolean isEnteringPip(@NonNull TransitionInfo info) {
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index a9a4e10..4fc6c44 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -18,8 +18,8 @@
import static android.app.ActivityOptions.ANIM_CLIP_REVEAL;
import static android.app.ActivityOptions.ANIM_CUSTOM;
-import static android.app.ActivityOptions.ANIM_FROM_STYLE;
import static android.app.ActivityOptions.ANIM_NONE;
+import static android.app.ActivityOptions.ANIM_FROM_STYLE;
import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS;
import static android.app.ActivityOptions.ANIM_SCALE_UP;
import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index f0d3668..7dc336b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -193,6 +193,9 @@
/** Remote Transition that split accepts but ultimately needs to be animated by the remote. */
public static final int TRANSIT_SPLIT_PASSTHROUGH = TRANSIT_FIRST_CUSTOM + 18;
+ /** Transition to set windowing mode after exit pip transition is finished animating. */
+ public static final int TRANSIT_CLEANUP_PIP_EXIT = WindowManager.TRANSIT_FIRST_CUSTOM + 19;
+
/** Transition type for desktop mode transitions. */
public static final int TRANSIT_DESKTOP_MODE_TYPES =
WindowManager.TRANSIT_FIRST_CUSTOM + 100;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 501e856..11976ae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -27,7 +27,6 @@
import android.app.ActivityManager.RunningTaskInfo;
import android.content.ContentResolver;
import android.content.Context;
-import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
@@ -181,7 +180,6 @@
}
decoration.relayout(taskInfo);
- setupCaptionColor(taskInfo, decoration);
}
@Override
@@ -243,15 +241,6 @@
decoration.close();
}
- private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) {
- if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
- decoration.setCaptionColor(Color.TRANSPARENT);
- } else {
- final int statusBarColor = taskInfo.taskDescription.getStatusBarColor();
- decoration.setCaptionColor(statusBarColor);
- }
- }
-
private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
return true;
@@ -320,7 +309,6 @@
windowDecoration.setTaskDragResizer(taskPositioner);
windowDecoration.relayout(taskInfo, startT, finishT,
false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */);
- setupCaptionColor(taskInfo, windowDecoration);
}
private class CaptionTouchEventListener implements
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 231570f..349ee0b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -35,7 +35,6 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.VectorDrawable;
import android.os.Handler;
import android.util.Size;
import android.view.Choreographer;
@@ -310,6 +309,9 @@
}
private void bindData(View rootView, RunningTaskInfo taskInfo) {
+ // Set up the tint first so that the drawable can be stylized when loaded.
+ setupCaptionColor(taskInfo);
+
final boolean isFullscreen =
taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
rootView.findViewById(R.id.maximize_window)
@@ -317,7 +319,16 @@
: R.drawable.decor_maximize_button_dark);
}
- void setCaptionColor(int captionColor) {
+ private void setupCaptionColor(RunningTaskInfo taskInfo) {
+ if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
+ setCaptionColor(Color.TRANSPARENT);
+ } else {
+ final int statusBarColor = taskInfo.taskDescription.getStatusBarColor();
+ setCaptionColor(statusBarColor);
+ }
+ }
+
+ private void setCaptionColor(int captionColor) {
if (mResult.mRootView == null) {
return;
}
@@ -334,20 +345,16 @@
caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */);
final View back = caption.findViewById(R.id.back_button);
- final VectorDrawable backBackground = (VectorDrawable) back.getBackground();
- backBackground.setTintList(buttonTintColor);
+ back.setBackgroundTintList(buttonTintColor);
final View minimize = caption.findViewById(R.id.minimize_window);
- final VectorDrawable minimizeBackground = (VectorDrawable) minimize.getBackground();
- minimizeBackground.setTintList(buttonTintColor);
+ minimize.setBackgroundTintList(buttonTintColor);
final View maximize = caption.findViewById(R.id.maximize_window);
- final VectorDrawable maximizeBackground = (VectorDrawable) maximize.getBackground();
- maximizeBackground.setTintList(buttonTintColor);
+ maximize.setBackgroundTintList(buttonTintColor);
final View close = caption.findViewById(R.id.close_window);
- final VectorDrawable closeBackground = (VectorDrawable) close.getBackground();
- closeBackground.setTintList(buttonTintColor);
+ close.setBackgroundTintList(buttonTintColor);
}
boolean isHandlingDragResize() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index b1cb834..20a406f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -480,6 +480,8 @@
Toast.makeText(mContext,
R.string.desktop_mode_non_resizable_snap_text, Toast.LENGTH_SHORT).show();
} else {
+ mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext,
+ Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE, "maximize_menu_resizable");
mDesktopTasksController.snapToHalfScreen(decoration.mTaskInfo,
decoration.mTaskInfo.configuration.windowConfiguration.getBounds(),
left ? SnapPosition.LEFT : SnapPosition.RIGHT);
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
index 8584b59..3fb67cd 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
@@ -24,14 +24,19 @@
import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd
import android.tools.flicker.assertors.assertions.AppWindowHasSizeOfAtLeast
import android.tools.flicker.assertors.assertions.AppWindowIsInvisibleAtEnd
+import android.tools.flicker.assertors.assertions.AppWindowIsVisibleAlways
+import android.tools.flicker.assertors.assertions.AppWindowMaintainsAspectRatioAlways
import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd
import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart
+import android.tools.flicker.assertors.assertions.AppWindowRemainInsideDisplayBounds
+import android.tools.flicker.assertors.assertions.AppWindowReturnsToStartBoundsAndPosition
import android.tools.flicker.assertors.assertions.LauncherWindowReplacesAppAsTopWindow
import android.tools.flicker.config.AssertionTemplates
import android.tools.flicker.config.FlickerConfigEntry
import android.tools.flicker.config.ScenarioId
-import android.tools.flicker.config.desktopmode.Components
+import android.tools.flicker.config.desktopmode.Components.DESKTOP_MODE_APP
import android.tools.flicker.config.desktopmode.Components.DESKTOP_WALLPAPER
+import android.tools.flicker.config.desktopmode.Components.NON_RESIZABLE_APP
import android.tools.flicker.extractors.ITransitionMatcher
import android.tools.flicker.extractors.ShellTransitionScenarioExtractor
import android.tools.flicker.extractors.TaggedCujTransitionMatcher
@@ -62,13 +67,11 @@
assertions =
AssertionTemplates.COMMON_ASSERTIONS +
listOf(
- AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP),
- AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP),
- AppWindowHasDesktopModeInitialBoundsAtTheEnd(
- Components.DESKTOP_MODE_APP
- ),
- AppWindowBecomesVisible(DESKTOP_WALLPAPER)
- )
+ AppLayerIsVisibleAlways(DESKTOP_MODE_APP),
+ AppWindowOnTopAtEnd(DESKTOP_MODE_APP),
+ AppWindowHasDesktopModeInitialBoundsAtTheEnd(DESKTOP_MODE_APP),
+ AppWindowBecomesVisible(DESKTOP_WALLPAPER)
+ )
.associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
)
@@ -97,11 +100,10 @@
assertions =
AssertionTemplates.COMMON_ASSERTIONS +
listOf(
- AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP),
- AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP),
- AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP),
- )
- .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ AppWindowOnTopAtStart(DESKTOP_MODE_APP),
+ AppLayerIsVisibleAtStart(DESKTOP_MODE_APP),
+ AppLayerIsInvisibleAtEnd(DESKTOP_MODE_APP)
+ ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
)
val CLOSE_LAST_APP =
@@ -125,10 +127,10 @@
assertions =
AssertionTemplates.COMMON_ASSERTIONS +
listOf(
- AppWindowIsInvisibleAtEnd(Components.DESKTOP_MODE_APP),
- LauncherWindowReplacesAppAsTopWindow(Components.DESKTOP_MODE_APP),
- AppWindowIsInvisibleAtEnd(DESKTOP_WALLPAPER)
- )
+ AppWindowIsInvisibleAtEnd(DESKTOP_MODE_APP),
+ LauncherWindowReplacesAppAsTopWindow(DESKTOP_MODE_APP),
+ AppWindowIsInvisibleAtEnd(DESKTOP_WALLPAPER)
+ )
.associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
)
@@ -156,9 +158,28 @@
)
.build(),
assertions =
- AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
- listOf(AppWindowHasSizeOfAtLeast(Components.DESKTOP_MODE_APP, 770, 700))
+ AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
+ listOf(AppWindowHasSizeOfAtLeast(DESKTOP_MODE_APP, 770, 700))
.associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
)
+
+ val SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE"),
+ extractor =
+ TaggedScenarioExtractorBuilder()
+ .setTargetTag(CujType.CUJ_DESKTOP_MODE_SNAP_RESIZE)
+ .setTransitionMatcher(
+ TaggedCujTransitionMatcher(associatedTransitionRequired = false)
+ )
+ .build(),
+ assertions = listOf(
+ AppWindowIsVisibleAlways(NON_RESIZABLE_APP),
+ AppWindowOnTopAtEnd(NON_RESIZABLE_APP),
+ AppWindowRemainInsideDisplayBounds(NON_RESIZABLE_APP),
+ AppWindowMaintainsAspectRatioAlways(NON_RESIZABLE_APP),
+ AppWindowReturnsToStartBoundsAndPosition(NON_RESIZABLE_APP)
+ ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
}
}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowLeftWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowLeftWithDrag.kt
new file mode 100644
index 0000000..582658f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowLeftWithDrag.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.wm.shell.flicker
+
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE
+import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Snap resize non-resizable app window by dragging it to the left edge of the screen.
+ *
+ * Assert that the app window keeps the same size and returns to its original pre-drag position.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SnapResizeNonResizableAppWindowLeftWithDrag :
+ SnapResizeAppWindowWithDrag(toLeft = true, isResizable = false) {
+ @ExpectedScenarios(["SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE"])
+ @Test
+ override fun snapResizeAppWindowWithDrag() = super.snapResizeAppWindowWithDrag()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+ .use(SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE)
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowRightWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowRightWithDrag.kt
new file mode 100644
index 0000000..7205ec4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowRightWithDrag.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.wm.shell.flicker
+
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE
+import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Snap resize non-resizable app window by dragging it to the right edge of the screen.
+ *
+ * Assert that the app window keeps the same size and returns to its original pre-drag position.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SnapResizeNonResizableAppWindowRightWithDrag :
+ SnapResizeAppWindowWithDrag(toLeft = false, isResizable = false) {
+ @ExpectedScenarios(["SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE"])
+ @Test
+ override fun snapResizeAppWindowWithDrag() = super.snapResizeAppWindowWithDrag()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+ .use(SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE)
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
index e3660fe..b812c59 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
@@ -25,6 +25,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.window.flags.Flags
import com.android.wm.shell.Utils
@@ -38,15 +39,20 @@
@RunWith(BlockJUnit4ClassRunner::class)
@Postsubmit
open class MaximizeAppWindow
-{
+@JvmOverloads
+constructor(rotation: Rotation = Rotation.ROTATION_0, isResizable: Boolean = true) {
+
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
private val tapl = LauncherInstrumentation()
private val wmHelper = WindowManagerStateHelper(instrumentation)
private val device = UiDevice.getInstance(instrumentation)
- private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+ private val testApp = if (isResizable) {
+ DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+ } else {
+ DesktopModeAppHelper(NonResizeableAppHelper(instrumentation))
+ }
- @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL,
- Rotation.ROTATION_0)
+ @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
@Before
fun setup() {
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt
index 63e7387..03d970f 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt
@@ -25,6 +25,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.window.flags.Flags
import com.android.wm.shell.Utils
@@ -40,15 +41,24 @@
@Postsubmit
open class ResizeAppWithCornerResize
@JvmOverloads
-constructor(val rotation: Rotation = Rotation.ROTATION_0,
+constructor(
+ val rotation: Rotation = Rotation.ROTATION_0,
val horizontalChange: Int = 50,
- val verticalChange: Int = -50) {
+ val verticalChange: Int = -50,
+ val appProperty: AppProperty = AppProperty.STANDARD
+) {
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
private val tapl = LauncherInstrumentation()
private val wmHelper = WindowManagerStateHelper(instrumentation)
private val device = UiDevice.getInstance(instrumentation)
- private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+ private val testApp =
+ DesktopModeAppHelper(
+ when (appProperty) {
+ AppProperty.STANDARD -> SimpleAppHelper(instrumentation)
+ AppProperty.NON_RESIZABLE -> NonResizeableAppHelper(instrumentation)
+ }
+ )
@Rule
@JvmField
@@ -64,15 +74,24 @@
@Test
open fun resizeAppWithCornerResize() {
- testApp.cornerResize(wmHelper,
+ testApp.cornerResize(
+ wmHelper,
device,
DesktopModeAppHelper.Corners.RIGHT_TOP,
horizontalChange,
- verticalChange)
+ verticalChange
+ )
}
@After
fun teardown() {
testApp.exit(wmHelper)
}
+
+ companion object {
+ enum class AppProperty {
+ STANDARD,
+ NON_RESIZABLE
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt
new file mode 100644
index 0000000..685a3ba
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.wm.shell.scenarios
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class SnapResizeAppWindowWithButton
+@JvmOverloads
+constructor(private val toLeft: Boolean = true, private val isResizable: Boolean = true) {
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val tapl = LauncherInstrumentation()
+ private val wmHelper = WindowManagerStateHelper(instrumentation)
+ private val device = UiDevice.getInstance(instrumentation)
+ private val testApp = if (isResizable) {
+ DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+ } else {
+ DesktopModeAppHelper(NonResizeableAppHelper(instrumentation))
+ }
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+ testApp.enterDesktopWithDrag(wmHelper, device)
+ }
+
+ @Test
+ open fun snapResizeAppWindowWithButton() {
+ testApp.snapResizeDesktopApp(wmHelper, device, instrumentation.context, toLeft)
+ }
+
+ @After
+ fun teardown() {
+ testApp.exit(wmHelper)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt
new file mode 100644
index 0000000..8a4aa63
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.wm.shell.scenarios
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class SnapResizeAppWindowWithDrag
+@JvmOverloads
+constructor(private val toLeft: Boolean = true, private val isResizable: Boolean = true) {
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val tapl = LauncherInstrumentation()
+ private val wmHelper = WindowManagerStateHelper(instrumentation)
+ private val device = UiDevice.getInstance(instrumentation)
+ private val testApp = if (isResizable) {
+ DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+ } else {
+ DesktopModeAppHelper(NonResizeableAppHelper(instrumentation))
+ }
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+ testApp.enterDesktopWithDrag(wmHelper, device)
+ }
+
+ @Test
+ open fun snapResizeAppWindowWithDrag() {
+ testApp.dragToSnapResizeRegion(wmHelper, device, toLeft)
+ }
+
+ @After
+ fun teardown() {
+ testApp.exit(wmHelper)
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
index 70b3661..ca97229 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
@@ -104,7 +104,9 @@
/* session_id */
eq(SESSION_ID),
eq(UNSET_MINIMIZE_REASON),
- eq(UNSET_UNMINIMIZE_REASON))
+ eq(UNSET_UNMINIMIZE_REASON),
+ /* visible_task_count */
+ eq(TASK_COUNT))
}
}
@@ -131,7 +133,9 @@
/* session_id */
eq(SESSION_ID),
eq(UNSET_MINIMIZE_REASON),
- eq(UNSET_UNMINIMIZE_REASON))
+ eq(UNSET_UNMINIMIZE_REASON),
+ /* visible_task_count */
+ eq(TASK_COUNT))
}
}
@@ -159,7 +163,9 @@
/* session_id */
eq(SESSION_ID),
eq(UNSET_MINIMIZE_REASON),
- eq(UNSET_UNMINIMIZE_REASON))
+ eq(UNSET_UNMINIMIZE_REASON),
+ /* visible_task_count */
+ eq(TASK_COUNT))
}
}
@@ -190,7 +196,9 @@
/* minimize_reason */
eq(MinimizeReason.TASK_LIMIT.reason),
/* unminimize_reason */
- eq(UNSET_UNMINIMIZE_REASON))
+ eq(UNSET_UNMINIMIZE_REASON),
+ /* visible_task_count */
+ eq(TASK_COUNT))
}
}
@@ -221,7 +229,9 @@
/* minimize_reason */
eq(UNSET_MINIMIZE_REASON),
/* unminimize_reason */
- eq(UnminimizeReason.TASKBAR_TAP.reason))
+ eq(UnminimizeReason.TASKBAR_TAP.reason),
+ /* visible_task_count */
+ eq(TASK_COUNT))
}
}
@@ -233,15 +243,17 @@
private const val TASK_Y = 0
private const val TASK_HEIGHT = 100
private const val TASK_WIDTH = 100
+ private const val TASK_COUNT = 1
private val TASK_UPDATE = TaskUpdate(
- TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y
+ TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y,
+ visibleTaskCount = TASK_COUNT,
)
private fun createTaskUpdate(
minimizeReason: MinimizeReason? = null,
unminimizeReason: UnminimizeReason? = null,
) = TaskUpdate(TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y, minimizeReason,
- unminimizeReason)
+ unminimizeReason, TASK_COUNT)
}
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
index 74fc7b0..e49eb36 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -138,7 +138,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.APP_FREEFORM_INTENT, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.APP_FREEFORM_INTENT,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -152,7 +153,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -165,7 +167,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_MENU_BUTTON, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_MENU_BUTTON,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -178,7 +181,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -191,7 +195,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.KEYBOARD_SHORTCUT_ENTER, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.KEYBOARD_SHORTCUT_ENTER,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -201,7 +206,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -231,7 +237,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -261,7 +268,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -291,7 +299,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -324,7 +333,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -335,7 +345,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.UNKNOWN_ENTER, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.UNKNOWN_ENTER,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -345,7 +356,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -364,7 +376,8 @@
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -386,7 +399,8 @@
.build()
callOnTransitionReady(transitionInfo)
- verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -522,7 +536,8 @@
val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
callOnTransitionReady(transitionInfo2)
- verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE)
+ verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW,
+ DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
}
@Test
@@ -538,7 +553,8 @@
callOnTransitionReady(transitionInfo)
verify(desktopModeEventLogger, times(1))
- .logTaskAdded(eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2)))
+ .logTaskAdded(eq(sessionId),
+ eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2, visibleTaskCount = 2)))
verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
}
@@ -560,7 +576,8 @@
verify(desktopModeEventLogger, times(1))
.logTaskInfoChanged(
- eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100)))
+ eq(sessionId),
+ eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 1)))
verifyZeroInteractions(desktopModeEventLogger)
}
@@ -589,7 +606,8 @@
eq(sessionId),
eq(
DEFAULT_TASK_UPDATE.copy(
- taskWidth = DEFAULT_TASK_WIDTH + 100, taskHeight = DEFAULT_TASK_HEIGHT - 100)))
+ taskWidth = DEFAULT_TASK_WIDTH + 100, taskHeight = DEFAULT_TASK_HEIGHT - 100,
+ visibleTaskCount = 1)))
verifyZeroInteractions(desktopModeEventLogger)
}
@@ -613,7 +631,8 @@
verify(desktopModeEventLogger, times(1))
.logTaskInfoChanged(
- eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100)))
+ eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(
+ taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 2)))
verifyZeroInteractions(desktopModeEventLogger)
// task 2 resize
@@ -637,7 +656,9 @@
DEFAULT_TASK_UPDATE.copy(
instanceId = 2,
taskWidth = DEFAULT_TASK_WIDTH + 100,
- taskHeight = DEFAULT_TASK_HEIGHT - 100)))
+ taskHeight = DEFAULT_TASK_HEIGHT - 100,
+ visibleTaskCount = 2)),
+ )
verifyZeroInteractions(desktopModeEventLogger)
}
@@ -655,7 +676,8 @@
callOnTransitionReady(transitionInfo)
verify(desktopModeEventLogger, times(1))
- .logTaskRemoved(eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2)))
+ .logTaskRemoved(eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(
+ instanceId = 2, visibleTaskCount = 1)))
verify(desktopModeEventLogger, never()).logSessionExit(any(), any())
}
@@ -694,6 +716,7 @@
const val DEFAULT_TASK_WIDTH = 200
const val DEFAULT_TASK_X = 30
const val DEFAULT_TASK_Y = 70
+ const val DEFAULT_VISIBLE_TASK_COUNT = 0
val DEFAULT_TASK_UPDATE =
TaskUpdate(
DEFAULT_TASK_ID,
@@ -702,6 +725,7 @@
DEFAULT_TASK_WIDTH,
DEFAULT_TASK_X,
DEFAULT_TASK_Y,
+ visibleTaskCount = DEFAULT_VISIBLE_TASK_COUNT,
)
fun createTaskInfo(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 2e0af273..058a26a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -1515,8 +1515,34 @@
assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FREEFORM)
- assertThat(wct.hierarchyOps).hasSize(2)
- wct.assertReorderAt(1, homeTask, toTop = false)
+ assertThat(wct.hierarchyOps).hasSize(1)
+ }
+
+ @Test
+ fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val homeTask = setUpHomeTask()
+ val freeformTask = setUpFreeformTask()
+ markTaskVisible(freeformTask)
+ val fullscreenTask = createFullscreenTask()
+ fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
+
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ assertNotNull(wct, "should handle request")
+ assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+
+ // There are 5 hops that are happening in this case:
+ // 1. Moving the fullscreen task to top as we add moveToDesktop() changes
+ // 2. Bringing home task to front
+ // 3. Pending intent for the wallpaper
+ // 4. Bringing the existing freeform task to top
+ // 5. Bringing the fullscreen task back at the top
+ assertThat(wct.hierarchyOps).hasSize(5)
+ wct.assertReorderAt(1, homeTask, toTop = true)
+ wct.assertReorderAt(4, fullscreenTask, toTop = true)
}
@Test
@@ -1541,19 +1567,34 @@
val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
freeformTasks.forEach { markTaskVisible(it) }
val fullscreenTask = createFullscreenTask()
- val homeTask = setUpHomeTask(DEFAULT_DISPLAY)
val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
// Make sure we reorder the new task to top, and the back task to the bottom
- assertThat(wct!!.hierarchyOps.size).isEqualTo(3)
+ assertThat(wct!!.hierarchyOps.size).isEqualTo(2)
wct.assertReorderAt(0, fullscreenTask, toTop = true)
- wct.assertReorderAt(1, homeTask, toTop = false)
- wct.assertReorderAt(2, freeformTasks[0], toTop = false)
+ wct.assertReorderAt(1, freeformTasks[0], toTop = false)
}
@Test
- fun handleRequest_fullscreenTaskToFreeform_alreadyBeyondLimit_existingAndNewTasksAreMinimized() {
+ fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+ freeformTasks.forEach { markTaskVisible(it) }
+ val fullscreenTask = createFullscreenTask()
+ fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
+
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ // Make sure we reorder the new task to top, and the back task to the bottom
+ assertThat(wct!!.hierarchyOps.size).isEqualTo(9)
+ wct.assertReorderAt(0, fullscreenTask, toTop = true)
+ wct.assertReorderAt(8, freeformTasks[0], toTop = false)
+ }
+
+ @Test
+ fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() {
assumeTrue(ENABLE_SHELL_TRANSITIONS)
val minimizedTask = setUpFreeformTask()
@@ -1562,15 +1603,16 @@
freeformTasks.forEach { markTaskVisible(it) }
val homeTask = setUpHomeTask()
val fullscreenTask = createFullscreenTask()
+ fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
- assertThat(wct!!.hierarchyOps.size).isEqualTo(4)
+ assertThat(wct!!.hierarchyOps.size).isEqualTo(10)
wct.assertReorderAt(0, fullscreenTask, toTop = true)
- // Make sure we reorder the home task to the bottom, and minimized tasks below the home task.
- wct.assertReorderAt(1, homeTask, toTop = false)
- wct.assertReorderAt(2, minimizedTask, toTop = false)
- wct.assertReorderAt(3, freeformTasks[0], toTop = false)
+ // Make sure we reorder the home task to the top, desktop tasks to top of them and minimized
+ // task is under the home task.
+ wct.assertReorderAt(1, homeTask, toTop = true)
+ wct.assertReorderAt(9, freeformTasks[0], toTop = false)
}
@Test
diff --git a/libs/hwui/ColorFilter.h b/libs/hwui/ColorFilter.h
index 31c9db7..3a3bfb47 100644
--- a/libs/hwui/ColorFilter.h
+++ b/libs/hwui/ColorFilter.h
@@ -106,7 +106,7 @@
private:
sk_sp<SkColorFilter> createInstance() override {
- return SkColorFilters::Matrix(mMatrix.data());
+ return SkColorFilters::Matrix(mMatrix.data(), SkColorFilters::Clamp::kNo);
}
private:
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 8bb11ba..dfda25d 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -761,8 +761,8 @@
if (mExpectSurfaceStats) {
reportMetricsWithPresentTime();
{ // acquire lock
- std::lock_guard lock(mLast4FrameMetricsInfosMutex);
- FrameMetricsInfo& next = mLast4FrameMetricsInfos.next();
+ std::lock_guard lock(mLastFrameMetricsInfosMutex);
+ FrameMetricsInfo& next = mLastFrameMetricsInfos.next();
next.frameInfo = mCurrentFrameInfo;
next.frameNumber = frameCompleteNr;
next.surfaceId = mSurfaceControlGenerationId;
@@ -816,12 +816,12 @@
int32_t surfaceControlId;
{ // acquire lock
- std::scoped_lock lock(mLast4FrameMetricsInfosMutex);
- if (mLast4FrameMetricsInfos.size() != mLast4FrameMetricsInfos.capacity()) {
+ std::scoped_lock lock(mLastFrameMetricsInfosMutex);
+ if (mLastFrameMetricsInfos.size() != mLastFrameMetricsInfos.capacity()) {
// Not enough frames yet
return;
}
- auto frameMetricsInfo = mLast4FrameMetricsInfos.front();
+ auto frameMetricsInfo = mLastFrameMetricsInfos.front();
forthBehind = frameMetricsInfo.frameInfo;
frameNumber = frameMetricsInfo.frameNumber;
surfaceControlId = frameMetricsInfo.surfaceId;
@@ -869,12 +869,12 @@
}
}
-FrameInfo* CanvasContext::getFrameInfoFromLast4(uint64_t frameNumber, uint32_t surfaceControlId) {
- std::scoped_lock lock(mLast4FrameMetricsInfosMutex);
- for (size_t i = 0; i < mLast4FrameMetricsInfos.size(); i++) {
- if (mLast4FrameMetricsInfos[i].frameNumber == frameNumber &&
- mLast4FrameMetricsInfos[i].surfaceId == surfaceControlId) {
- return mLast4FrameMetricsInfos[i].frameInfo;
+FrameInfo* CanvasContext::getFrameInfoFromLastFew(uint64_t frameNumber, uint32_t surfaceControlId) {
+ std::scoped_lock lock(mLastFrameMetricsInfosMutex);
+ for (size_t i = 0; i < mLastFrameMetricsInfos.size(); i++) {
+ if (mLastFrameMetricsInfos[i].frameNumber == frameNumber &&
+ mLastFrameMetricsInfos[i].surfaceId == surfaceControlId) {
+ return mLastFrameMetricsInfos[i].frameInfo;
}
}
@@ -894,7 +894,7 @@
}
uint64_t frameNumber = functions.getFrameNumberFunc(stats);
- FrameInfo* frameInfo = instance->getFrameInfoFromLast4(frameNumber, surfaceControlId);
+ FrameInfo* frameInfo = instance->getFrameInfoFromLastFew(frameNumber, surfaceControlId);
if (frameInfo != nullptr) {
std::scoped_lock lock(instance->mFrameInfoMutex);
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index e2e3fa3..cb37538 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -260,7 +260,7 @@
void finishFrame(FrameInfo* frameInfo);
/**
- * Invoke 'reportFrameMetrics' on the last frame stored in 'mLast4FrameInfos'.
+ * Invoke 'reportFrameMetrics' on the last frame stored in 'mLastFrameInfos'.
* Populate the 'presentTime' field before calling.
*/
void reportMetricsWithPresentTime();
@@ -271,7 +271,7 @@
int32_t surfaceId;
};
- FrameInfo* getFrameInfoFromLast4(uint64_t frameNumber, uint32_t surfaceControlId);
+ FrameInfo* getFrameInfoFromLastFew(uint64_t frameNumber, uint32_t surfaceControlId);
Frame getFrame();
@@ -336,9 +336,9 @@
// List of data of frames that are awaiting GPU completion reporting. Used to compute frame
// metrics and determine whether or not to report the metrics.
- RingBuffer<FrameMetricsInfo, 4> mLast4FrameMetricsInfos
- GUARDED_BY(mLast4FrameMetricsInfosMutex);
- std::mutex mLast4FrameMetricsInfosMutex;
+ RingBuffer<FrameMetricsInfo, 6> mLastFrameMetricsInfos
+ GUARDED_BY(mLastFrameMetricsInfosMutex);
+ std::mutex mLastFrameMetricsInfosMutex;
std::string mName;
JankTracker mJankTracker;
diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index 0829a90e..93bca21 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -26,11 +26,11 @@
}
flag {
- name: "tis_always_bound_permission"
+ name: "tif_unbind_inactive_tis"
is_exported: true
namespace: "media_tv"
- description: "Introduce ALWAYS_BOUND_TV_INPUT for TIS."
- bug: "332201346"
+ description: "Unbind hardware TIS when not needed"
+ bug: "279189366"
}
flag {
diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
index a0b3469..08155dd 100644
--- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
+++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
@@ -133,7 +133,8 @@
<LinearLayout
android:id="@+id/negative_multiple_devices_layout"
android:layout_width="wrap_content"
- android:layout_height="48dp"
+ android:layout_height="match_parent"
+ android:padding="6dp"
android:gravity="center"
android:visibility="gone">
diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml
index e8e24f4..fe7cfc6 100644
--- a/packages/CompanionDeviceManager/res/values/styles.xml
+++ b/packages/CompanionDeviceManager/res/values/styles.xml
@@ -103,11 +103,10 @@
<style name="NegativeButtonMultipleDevices"
parent="@android:style/Widget.Material.Button.Colored">
<item name="android:layout_width">wrap_content</item>
- <item name="android:layout_height">36dp</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:minHeight">36dp</item>>
<item name="android:textAllCaps">false</item>
<item name="android:textSize">14sp</item>
- <item name="android:paddingLeft">6dp</item>
- <item name="android:paddingRight">6dp</item>
<item name="android:background">@drawable/btn_negative_multiple_devices</item>
<item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
</style>
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
index 6117330..7974a37 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
@@ -19,6 +19,7 @@
import static android.companion.CompanionDeviceManager.RESULT_CANCELED;
import static android.companion.CompanionDeviceManager.RESULT_DISCOVERY_TIMEOUT;
import static android.companion.CompanionDeviceManager.RESULT_INTERNAL_ERROR;
+import static android.companion.CompanionDeviceManager.RESULT_SECURITY_ERROR;
import static android.companion.CompanionDeviceManager.RESULT_USER_REJECTED;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
@@ -33,6 +34,7 @@
import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_TITLES;
import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_PROFILES;
import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_SELF_MANAGED_PROFILES;
+import static com.android.companiondevicemanager.Utils.RESULT_CODE_TO_REASON;
import static com.android.companiondevicemanager.Utils.getApplicationLabel;
import static com.android.companiondevicemanager.Utils.getHtmlFromResources;
import static com.android.companiondevicemanager.Utils.getIcon;
@@ -51,6 +53,7 @@
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
+import android.companion.Flags;
import android.companion.IAssociationRequestCallback;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -231,7 +234,7 @@
boolean forCancelDialog = intent.getBooleanExtra(EXTRA_FORCE_CANCEL_CONFIRMATION, false);
if (forCancelDialog) {
Slog.i(TAG, "Cancelling the user confirmation");
- cancel(RESULT_CANCELED);
+ cancel(RESULT_CANCELED, null);
return;
}
@@ -243,10 +246,15 @@
if (appCallback == null) {
return;
}
- Slog.e(TAG, "More than one AssociationRequests are processing.");
try {
- appCallback.onFailure(RESULT_INTERNAL_ERROR);
+ if (Flags.associationFailureCode()) {
+ appCallback.onFailure(
+ RESULT_SECURITY_ERROR, "More than one AssociationRequests are processing.");
+ } else {
+ appCallback.onFailure(
+ RESULT_INTERNAL_ERROR, "More than one AssociationRequests are processing.");
+ }
} catch (RemoteException ignore) {
}
}
@@ -257,7 +265,7 @@
// TODO: handle config changes without cancelling.
if (!isDone()) {
- cancel(RESULT_CANCELED); // will finish()
+ cancel(RESULT_CANCELED, null); // will finish()
}
}
@@ -331,7 +339,7 @@
&& CompanionDeviceDiscoveryService.getScanResult().getValue().isEmpty()) {
synchronized (LOCK) {
if (sDiscoveryStarted) {
- cancel(RESULT_DISCOVERY_TIMEOUT);
+ cancel(RESULT_DISCOVERY_TIMEOUT, null);
}
}
}
@@ -371,7 +379,7 @@
mCdmServiceReceiver.send(RESULT_CODE_ASSOCIATION_APPROVED, data);
}
- private void cancel(int failureCode) {
+ private void cancel(int errorCode, @Nullable CharSequence error) {
if (isDone()) {
Slog.w(TAG, "Already done: " + (mApproved ? "Approved" : "Cancelled"));
return;
@@ -385,13 +393,14 @@
// First send callback to the app directly...
try {
- Slog.i(TAG, "Sending onFailure to app due to failureCode=" + failureCode);
- mAppCallback.onFailure(failureCode);
+ CharSequence errorMessage = error != null
+ ? error : RESULT_CODE_TO_REASON.get(errorCode);
+ mAppCallback.onFailure(errorCode, errorMessage);
} catch (RemoteException ignore) {
}
// ... then set result and finish ("sending" onActivityResult()).
- setResultAndFinish(null, failureCode);
+ setResultAndFinish(null, errorCode);
}
private void setResultAndFinish(@Nullable AssociationInfo association, int resultCode) {
@@ -436,7 +445,7 @@
}
} catch (PackageManager.NameNotFoundException e) {
Slog.e(TAG, "Package u" + userId + "/" + packageName + " not found.");
- cancel(RESULT_INTERNAL_ERROR);
+ cancel(RESULT_INTERNAL_ERROR, e.getMessage());
return;
}
@@ -625,7 +634,7 @@
// Disable the button, to prevent more clicks.
v.setEnabled(false);
- cancel(RESULT_USER_REJECTED);
+ cancel(RESULT_USER_REJECTED, null);
}
private void onShowHelperDialog(View view) {
@@ -755,8 +764,8 @@
};
@Override
- public void onShowHelperDialogFailed() {
- cancel(RESULT_INTERNAL_ERROR);
+ public void onShowHelperDialogFailed(CharSequence errorMessage) {
+ cancel(RESULT_INTERNAL_ERROR, errorMessage);
}
@Override
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java
index ec92987..b2d78da 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java
@@ -54,7 +54,7 @@
private Button mButton;
interface CompanionVendorHelperDialogListener {
- void onShowHelperDialogFailed();
+ void onShowHelperDialogFailed(CharSequence error);
void onHelperDialogDismissed();
}
@@ -110,7 +110,7 @@
appLabel = getApplicationLabel(getContext(), packageName, userId);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Package u" + userId + "/" + packageName + " not found.");
- mListener.onShowHelperDialogFailed();
+ mListener.onShowHelperDialogFailed(e.getMessage());
return;
}
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java
index 8c14f80..2f97132 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java
@@ -16,6 +16,15 @@
package com.android.companiondevicemanager;
+import static android.companion.CompanionDeviceManager.REASON_CANCELED;
+import static android.companion.CompanionDeviceManager.REASON_DISCOVERY_TIMEOUT;
+import static android.companion.CompanionDeviceManager.REASON_USER_REJECTED;
+import static android.companion.CompanionDeviceManager.RESULT_CANCELED;
+import static android.companion.CompanionDeviceManager.RESULT_DISCOVERY_TIMEOUT;
+import static android.companion.CompanionDeviceManager.RESULT_USER_REJECTED;
+
+import static java.util.Collections.unmodifiableMap;
+
import android.annotation.NonNull;
import android.annotation.StringRes;
import android.content.Context;
@@ -31,6 +40,9 @@
import android.os.ResultReceiver;
import android.text.Html;
import android.text.Spanned;
+import android.util.ArrayMap;
+
+import java.util.Map;
/**
* Utilities.
@@ -40,6 +52,17 @@
"android.companion.vendor_icon";
private static final String COMPANION_DEVICE_ACTIVITY_VENDOR_NAME =
"android.companion.vendor_name";
+ // This map solely the common error messages that occur during the Association
+ // creation process.
+ static final Map<Integer, String> RESULT_CODE_TO_REASON;
+ static {
+ final Map<Integer, String> map = new ArrayMap<>();
+ map.put(RESULT_CANCELED, REASON_CANCELED);
+ map.put(RESULT_USER_REJECTED, REASON_USER_REJECTED);
+ map.put(RESULT_DISCOVERY_TIMEOUT, REASON_DISCOVERY_TIMEOUT);
+
+ RESULT_CODE_TO_REASON = unmodifiableMap(map);
+ }
/**
* Convert an instance of a "locally-defined" ResultReceiver to an instance of
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt
index e58de64..5bd26e113 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt
@@ -48,45 +48,14 @@
/* Used as credential suggestion or user action chip. */
@Composable
fun CredentialsScreenChip(
- label: String,
+ primaryText: @Composable () -> Unit,
+ secondaryText: (@Composable () -> Unit)? = null,
onClick: () -> Unit,
- secondaryLabel: String? = null,
icon: Drawable? = null,
isAuthenticationEntryLocked: Boolean? = null,
- textAlign: TextAlign = TextAlign.Center,
modifier: Modifier = Modifier,
colors: ChipColors = ChipDefaults.secondaryChipColors()
) {
- return CredentialsScreenChip(
- onClick,
- text = {
- WearButtonText(
- text = label,
- textAlign = textAlign,
- maxLines = 2
- )
- },
- secondaryLabel,
- icon,
- isAuthenticationEntryLocked,
- modifier,
- colors
- )
-}
-
-
-
-/* Used as credential suggestion or user action chip. */
-@Composable
-fun CredentialsScreenChip(
- onClick: () -> Unit,
- text: @Composable () -> Unit,
- secondaryLabel: String? = null,
- icon: Drawable? = null,
- isAuthenticationEntryLocked: Boolean? = null,
- modifier: Modifier = Modifier,
- colors: ChipColors = ChipDefaults.primaryChipColors(),
- ) {
val labelParam: (@Composable RowScope.() -> Unit) =
{
var horizontalArrangement = Arrangement.Start
@@ -94,19 +63,15 @@
horizontalArrangement = Arrangement.Center
}
Row(horizontalArrangement = horizontalArrangement, modifier = modifier.fillMaxWidth()) {
- text()
+ primaryText()
}
}
val secondaryLabelParam: (@Composable RowScope.() -> Unit)? =
- secondaryLabel?.let {
+ secondaryText?.let {
{
Row {
- WearSecondaryLabel(
- text = secondaryLabel,
- color = WearMaterialTheme.colors.onSurfaceVariant
- )
-
+ secondaryText()
if (isAuthenticationEntryLocked != null) {
if (isAuthenticationEntryLocked) {
Icon(
@@ -156,9 +121,19 @@
@Composable
fun CredentialsScreenChipPreview() {
CredentialsScreenChip(
- label = "Elisa Beckett",
+ primaryText = {
+ WearButtonText(
+ text = "Elisa Beckett",
+ textAlign = TextAlign.Start,
+ )
+ },
onClick = { },
- secondaryLabel = "beckett_bakery@gmail.com",
+ secondaryText = {
+ WearSecondaryLabel(
+ text = "beckett_bakery@gmail.com",
+ color = WearMaterialTheme.colors.onSurfaceVariant
+ )
+ },
icon = null,
)
}
@@ -166,8 +141,13 @@
@Composable
fun SignInOptionsChip(onClick: () -> Unit) {
CredentialsScreenChip(
- label = stringResource(R.string.dialog_sign_in_options_button),
- textAlign = TextAlign.Start,
+ primaryText = {
+ WearButtonText(
+ text = stringResource(R.string.dialog_sign_in_options_button),
+ textAlign = TextAlign.Center,
+ maxLines = 2
+ )
+ },
onClick = onClick,
)
}
@@ -182,7 +162,7 @@
fun ContinueChip(onClick: () -> Unit) {
CredentialsScreenChip(
onClick = onClick,
- text = {
+ primaryText = {
WearButtonText(
text = stringResource(R.string.dialog_continue_button),
textAlign = TextAlign.Center,
@@ -202,14 +182,21 @@
@Composable
fun DismissChip(onClick: () -> Unit) {
CredentialsScreenChip(
- label = stringResource(R.string.dialog_dismiss_button),
+ primaryText = {
+ WearButtonText(
+ text = stringResource(R.string.dialog_dismiss_button),
+ textAlign = TextAlign.Center,
+ maxLines = 2
+ )
+ },
onClick = onClick,
)
}
@Composable
fun LockedProviderChip(
authenticationEntryInfo: AuthenticationEntryInfo,
- onClick: () -> Unit
+ secondaryMaxLines: Int = 1,
+ onClick: () -> Unit,
) {
val secondaryLabel = stringResource(
if (authenticationEntryInfo.isUnlockedAndEmpty)
@@ -218,10 +205,21 @@
)
CredentialsScreenChip(
- label = authenticationEntryInfo.title,
+ primaryText = {
+ WearButtonText(
+ text = authenticationEntryInfo.title,
+ textAlign = TextAlign.Start,
+ maxLines = 2,
+ )
+ },
icon = authenticationEntryInfo.icon,
- secondaryLabel = secondaryLabel,
- textAlign = TextAlign.Start,
+ secondaryText = {
+ WearSecondaryLabel(
+ text = secondaryLabel,
+ color = WearMaterialTheme.colors.onSurfaceVariant,
+ maxLines = secondaryMaxLines
+ )
+ },
isAuthenticationEntryLocked = !authenticationEntryInfo.isUnlockedAndEmpty,
onClick = onClick,
)
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt
index a7b13ad..a1dc568 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt
@@ -16,7 +16,6 @@
package com.android.credentialmanager.common.ui.components
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Text
@@ -93,15 +92,16 @@
fun WearSecondaryLabel(
text: String,
color: Color = WearMaterialTheme.colors.onSurface,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ maxLines: Int = 1,
) {
Text(
- modifier = modifier.fillMaxSize(),
+ modifier = modifier.wrapContentSize(),
text = text,
color = color,
style = WearMaterialTheme.typography.caption1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
- maxLines = 1,
+ maxLines = maxLines,
)
}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt
index ef32c94..932b345 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt
@@ -24,13 +24,13 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.Alignment
import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry
import com.android.credentialmanager.FlowEngine
import com.android.credentialmanager.R
import com.android.credentialmanager.common.ui.components.WearButtonText
+import com.android.credentialmanager.ui.components.LockedProviderChip
import com.android.credentialmanager.common.ui.components.WearSecondaryLabel
import com.android.credentialmanager.model.get.CredentialEntryInfo
import com.android.credentialmanager.ui.components.CredentialsScreenChipSpacer
@@ -38,6 +38,8 @@
import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.rememberColumnState
import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
+import androidx.compose.ui.text.style.TextAlign
+import androidx.wear.compose.material.MaterialTheme as WearMaterialTheme
/**
* Screen that shows multiple credentials to select from, grouped by accounts
@@ -69,6 +71,7 @@
text = stringResource(R.string.sign_in_options_title),
textAlign = TextAlign.Center,
modifier = Modifier.weight(0.854f).fillMaxSize(),
+ maxLines = 2,
)
Spacer(Modifier.weight(0.073f)) // 7.3% side margin
}
@@ -94,19 +97,39 @@
userNameEntries.sortedCredentialEntryList.forEach { credential: CredentialEntryInfo ->
item {
CredentialsScreenChip(
- label = credential.userName,
+ primaryText = {
+ WearButtonText(
+ text = credential.userName,
+ textAlign = TextAlign.Start,
+ maxLines = 2,
+ )
+ },
onClick = { selectEntry(credential, false) },
- secondaryLabel =
- credential.credentialTypeDisplayName.ifEmpty {
- credential.providerDisplayName
+ secondaryText =
+ {
+ WearSecondaryLabel(
+ text = credential.credentialTypeDisplayName.ifEmpty {
+ credential.providerDisplayName
+ },
+ color = WearMaterialTheme.colors.onSurfaceVariant,
+ maxLines = 2
+ )
},
icon = credential.icon,
- textAlign = TextAlign.Start
)
CredentialsScreenChipSpacer()
}
}
+
+ credentialSelectorUiState.authenticationEntryList.forEach { authenticationEntryInfo ->
+ item {
+ LockedProviderChip(authenticationEntryInfo, secondaryMaxLines = 2) {
+ selectEntry(authenticationEntryInfo, false)
+ }
+ CredentialsScreenChipSpacer()
+ }
+ }
}
if (credentialSelectorUiState.actionEntryList.isNotEmpty()) {
@@ -120,7 +143,8 @@
bottom = 4.dp,
start = 0.dp,
end = 0.dp
- ).fillMaxWidth(0.87f)
+ ).fillMaxWidth(0.87f),
+ maxLines = 2
)
Spacer(Modifier.weight(0.0624f)) // 6.24% side margin
}
@@ -128,9 +152,15 @@
credentialSelectorUiState.actionEntryList.forEach { actionEntry ->
item {
CredentialsScreenChip(
- label = actionEntry.title,
+ primaryText = {
+ WearButtonText(
+ text = actionEntry.title,
+ textAlign = TextAlign.Start,
+ maxLines = 2
+ )
+ },
onClick = { selectEntry(actionEntry, false) },
- secondaryLabel = null,
+ secondaryText = null,
icon = actionEntry.icon,
)
CredentialsScreenChipSpacer()
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
index 38307b0..b56b982b 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
@@ -17,7 +17,6 @@
package com.android.credentialmanager.ui.screens.multiple
import androidx.compose.foundation.layout.Row
-import androidx.compose.ui.text.style.TextAlign
import androidx.compose.foundation.layout.fillMaxSize
import com.android.credentialmanager.R
import androidx.compose.ui.res.stringResource
@@ -40,6 +39,10 @@
import com.android.credentialmanager.model.CredentialType
import com.android.credentialmanager.ui.components.BottomSpacer
import com.android.credentialmanager.ui.components.CredentialsScreenChipSpacer
+import com.android.credentialmanager.common.ui.components.WearButtonText
+import com.android.credentialmanager.common.ui.components.WearSecondaryLabel
+import androidx.compose.ui.text.style.TextAlign
+import androidx.wear.compose.material.MaterialTheme as WearMaterialTheme
/**
* Screen that shows multiple credentials to select from.
@@ -82,14 +85,25 @@
credentials.forEach { credential: CredentialEntryInfo ->
item {
CredentialsScreenChip(
- label = credential.userName,
+ primaryText =
+ {
+ WearButtonText(
+ text = credential.userName,
+ textAlign = TextAlign.Start,
+ maxLines = 2
+ )
+ },
onClick = { selectEntry(credential, false) },
- secondaryLabel =
- credential.credentialTypeDisplayName.ifEmpty {
- credential.providerDisplayName
+ secondaryText = {
+ WearSecondaryLabel(
+ text = credential.credentialTypeDisplayName.ifEmpty {
+ credential.providerDisplayName
+ },
+ color = WearMaterialTheme.colors.onSurfaceVariant,
+ maxLines = 1 // See b/359649621 for context
+ )
},
icon = credential.icon,
- textAlign = TextAlign.Start
)
CredentialsScreenChipSpacer()
}
diff --git a/packages/PrintSpooler/res/values-zh-rCN/strings.xml b/packages/PrintSpooler/res/values-zh-rCN/strings.xml
index 3c95fd8..94d835b 100644
--- a/packages/PrintSpooler/res/values-zh-rCN/strings.xml
+++ b/packages/PrintSpooler/res/values-zh-rCN/strings.xml
@@ -29,7 +29,7 @@
<string name="label_pages" msgid="7768589729282182230">"页数"</string>
<string name="destination_default_text" msgid="5422708056807065710">"选择打印机"</string>
<string name="template_all_pages" msgid="3322235982020148762">"全部<xliff:g id="PAGE_COUNT">%1$s</xliff:g>页"</string>
- <string name="template_page_range" msgid="428638530038286328">"<xliff:g id="PAGE_COUNT">%1$s</xliff:g>页"</string>
+ <string name="template_page_range" msgid="428638530038286328">"范围 <xliff:g id="PAGE_COUNT">%1$s</xliff:g>页"</string>
<string name="pages_range_example" msgid="8558694453556945172">"例如:1-5、8、11-13"</string>
<string name="print_preview" msgid="8010217796057763343">"打印预览"</string>
<string name="install_for_print_preview" msgid="6366303997385509332">"安装 PDF 查看器以便预览"</string>
diff --git a/packages/PrintSpooler/res/values-zh-rTW/strings.xml b/packages/PrintSpooler/res/values-zh-rTW/strings.xml
index 10932be..5ca4579 100644
--- a/packages/PrintSpooler/res/values-zh-rTW/strings.xml
+++ b/packages/PrintSpooler/res/values-zh-rTW/strings.xml
@@ -29,7 +29,7 @@
<string name="label_pages" msgid="7768589729282182230">"頁面"</string>
<string name="destination_default_text" msgid="5422708056807065710">"選取印表機"</string>
<string name="template_all_pages" msgid="3322235982020148762">"全部 <xliff:g id="PAGE_COUNT">%1$s</xliff:g> 頁"</string>
- <string name="template_page_range" msgid="428638530038286328">"<xliff:g id="PAGE_COUNT">%1$s</xliff:g> 頁"</string>
+ <string name="template_page_range" msgid="428638530038286328">"範圍是 <xliff:g id="PAGE_COUNT">%1$s</xliff:g> 頁"</string>
<string name="pages_range_example" msgid="8558694453556945172">"例如:1—5,8,11—13"</string>
<string name="print_preview" msgid="8010217796057763343">"列印預覽"</string>
<string name="install_for_print_preview" msgid="6366303997385509332">"安裝預覽所需的 PDF 檢視器"</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
index 6198d80..d71b337 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
@@ -181,7 +181,7 @@
* admin status.
*/
public Dialog createDialog(Activity activity,
- ActivityStarter activityStarter, boolean canCreateAdminUser,
+ @NonNull ActivityStarter activityStarter, boolean canCreateAdminUser,
NewUserData successCallback, Runnable cancelCallback) {
mActivity = activity;
mCustomDialogHelper = new CustomDialogHelper(activity);
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
index 46f2290..c4c4ed8e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
@@ -31,6 +31,7 @@
import android.widget.EditText;
import android.widget.ImageView;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -126,9 +127,11 @@
* @param activityStarter - ActivityStarter is called with appropriate intents and request
* codes to take photo/choose photo/crop photo.
*/
- public Dialog createDialog(Activity activity, ActivityStarter activityStarter,
- @Nullable Drawable oldUserIcon, String defaultUserName,
- BiConsumer<String, Drawable> successCallback, Runnable cancelCallback) {
+ public @NonNull Dialog createDialog(@NonNull Activity activity,
+ @NonNull ActivityStarter activityStarter, @Nullable Drawable oldUserIcon,
+ @Nullable String defaultUserName,
+ @Nullable BiConsumer<String, Drawable> successCallback,
+ @Nullable Runnable cancelCallback) {
LayoutInflater inflater = LayoutInflater.from(activity);
View content = inflater.inflate(R.layout.edit_user_info_dialog_content, null);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
index 006e644..62401a1 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
+++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
@@ -81,3 +81,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "sync_local_overrides_removal_new_storage"
+ namespace: "core_experiments_team_internal"
+ description: "When DeviceConfig overrides are deleted, delete new storage overrides too."
+ bug: "361643653"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 2e98c1f..f98b29a 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -484,6 +484,7 @@
<activity android:name=".touchpad.tutorial.ui.view.TouchpadTutorialActivity"
android:exported="true"
+ android:showForAllUsers="true"
android:screenOrientation="userLandscape"
android:theme="@style/Theme.AppCompat.NoActionBar">
<intent-filter>
@@ -494,6 +495,7 @@
<activity android:name=".inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity"
android:exported="true"
+ android:showForAllUsers="true"
android:screenOrientation="userLandscape"
android:theme="@style/Theme.AppCompat.NoActionBar">
<intent-filter>
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 7c0db8d..d0fb279 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -541,6 +541,16 @@
}
flag {
+ name: "clipboard_shared_transitions"
+ namespace: "systemui"
+ description: "Show shared transitions from clipboard"
+ bug: "360843770"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "clipboard_image_timeout"
namespace: "systemui"
description: "Wait for clipboard image to load before showing UI"
@@ -1022,6 +1032,16 @@
}
flag {
+ name: "communal_edit_widgets_activity_finish_fix"
+ namespace: "systemui"
+ description: "finish edit widgets activity when stopping"
+ bug: "354725145"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "app_clips_backlinks"
namespace: "systemui"
description: "Enables Backlinks improvement feature in App Clips"
@@ -1073,6 +1093,13 @@
}
flag {
+ name: "media_controls_posts_optimization"
+ namespace: "systemui"
+ description: "Ignore duplicate media notifications posted"
+ bug: "358645640"
+}
+
+flag {
namespace: "systemui"
name: "enable_view_capture_tracing"
description: "Enables view capture tracing in System UI."
@@ -1346,3 +1373,14 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "face_message_defer_update"
+ namespace: "systemui"
+ description: "Only analyze the last n frames when determining whether to defer a face auth help message like low light"
+ bug: "351863611"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
diff --git a/packages/SystemUI/animation/build.gradle b/packages/SystemUI/animation/build.gradle
index 939455f..16ba42f 100644
--- a/packages/SystemUI/animation/build.gradle
+++ b/packages/SystemUI/animation/build.gradle
@@ -10,13 +10,6 @@
}
}
- compileSdk 33
-
- defaultConfig {
- minSdk 33
- targetSdk 33
- }
-
lintOptions {
abortOnError false
}
@@ -24,10 +17,6 @@
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
}
- kotlinOptions {
- jvmTarget = '1.8'
- freeCompilerArgs = ["-Xjvm-default=all"]
- }
}
dependencies {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 91a88bc..6f415ea 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -1154,7 +1154,7 @@
.then(selectableModifier)
.thenIf(!viewModel.isEditMode && !model.inQuietMode) {
Modifier.pointerInput(Unit) {
- observeTaps { viewModel.onTapWidget(model.componentName, model.priority) }
+ observeTaps { viewModel.onTapWidget(model.componentName, model.rank) }
}
}
.thenIf(!viewModel.isEditMode && model.inQuietMode) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
index 38a3474..1137357 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
@@ -34,11 +34,11 @@
return remember(communalContent) {
ContentListState(
communalContent,
- { componentName, user, priority ->
+ { componentName, user, rank ->
viewModel.onAddWidget(
componentName,
user,
- priority,
+ rank,
widgetConfigurator,
)
},
@@ -56,10 +56,9 @@
class ContentListState
internal constructor(
communalContent: List<CommunalContentModel>,
- private val onAddWidget:
- (componentName: ComponentName, user: UserHandle, priority: Int) -> Unit,
- private val onDeleteWidget: (id: Int, componentName: ComponentName, priority: Int) -> Unit,
- private val onReorderWidgets: (widgetIdToPriorityMap: Map<Int, Int>) -> Unit,
+ private val onAddWidget: (componentName: ComponentName, user: UserHandle, rank: Int) -> Unit,
+ private val onDeleteWidget: (id: Int, componentName: ComponentName, rank: Int) -> Unit,
+ private val onReorderWidgets: (widgetIdToRankMap: Map<Int, Int>) -> Unit,
) {
var list = communalContent.toMutableStateList()
private set
@@ -74,7 +73,7 @@
if (list[indexToRemove].isWidgetContent()) {
val widget = list[indexToRemove] as CommunalContentModel.WidgetContent
list.apply { removeAt(indexToRemove) }
- onDeleteWidget(widget.appWidgetId, widget.componentName, widget.priority)
+ onDeleteWidget(widget.appWidgetId, widget.componentName, widget.rank)
}
}
@@ -94,24 +93,24 @@
newItemUser: UserHandle? = null,
newItemIndex: Int? = null
) {
- // filters placeholder, but, maintains the indices of the widgets as if the placeholder was
- // in the list. When persisted in DB, this leaves space for the new item (to be added) at
- // the correct priority.
- val widgetIdToPriorityMap: Map<Int, Int> =
+ // New widget added to the grid. Other widgets are shifted as needed at the database level.
+ if (newItemComponentName != null && newItemUser != null && newItemIndex != null) {
+ onAddWidget(newItemComponentName, newItemUser, /* rank= */ newItemIndex)
+ return
+ }
+
+ // No new widget, only reorder existing widgets.
+ val widgetIdToRankMap: Map<Int, Int> =
list
.mapIndexedNotNull { index, item ->
if (item is CommunalContentModel.WidgetContent) {
- item.appWidgetId to list.size - index
+ item.appWidgetId to index
} else {
null
}
}
.toMap()
- // reorder and then add the new widget
- onReorderWidgets(widgetIdToPriorityMap)
- if (newItemComponentName != null && newItemUser != null && newItemIndex != null) {
- onAddWidget(newItemComponentName, newItemUser, /* priority= */ list.size - newItemIndex)
- }
+ onReorderWidgets(widgetIdToRankMap)
}
/** Returns true if the item at given index is editable. */
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
index 0c29394..f2f7c87 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
@@ -193,7 +193,7 @@
val widgetExtra = event.maybeWidgetExtra() ?: return false
val (componentName, user) = widgetExtra
if (componentName != null && user != null) {
- // Placeholder isn't removed yet to allow the setting the right priority for items
+ // Placeholder isn't removed yet to allow the setting the right rank for items
// before adding in the new item.
contentListState.onSaveList(
newItemComponentName = componentName,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index a3e0701..3e73057 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -76,6 +76,11 @@
viewModel
.areNotificationsVisible(contentKey)
.collectAsStateWithLifecycle(initialValue = false)
+ val isBypassEnabled by viewModel.isBypassEnabled.collectAsStateWithLifecycle()
+
+ if (isBypassEnabled) {
+ with(notificationSection) { HeadsUpNotifications() }
+ }
LockscreenLongPress(
viewModel = viewModel.touchHandling,
@@ -110,7 +115,7 @@
}
)
}
- if (isShadeLayoutWide) {
+ if (isShadeLayoutWide && !isBypassEnabled) {
with(notificationSection) {
Notifications(
areNotificationsVisible = areNotificationsVisible,
@@ -124,7 +129,7 @@
}
}
}
- if (!isShadeLayoutWide) {
+ if (!isShadeLayoutWide && !isBypassEnabled) {
with(notificationSection) {
Notifications(
areNotificationsVisible = areNotificationsVisible,
@@ -175,7 +180,7 @@
},
modifier = Modifier.fillMaxSize(),
) { measurables, constraints ->
- check(measurables.size == 6)
+ check(measurables.size == 6) { "Expected 6 measurables, got: ${measurables.size}" }
val aboveLockIconMeasurable = measurables[0]
val lockIconMeasurable = measurables[1]
val belowLockIconMeasurable = measurables[2]
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
index bcdb259..eae46e9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
@@ -111,7 +111,7 @@
1f
}
- val dir = if (transition.toScene == splitShadeLargeClockScene) -1f else 1f
+ val dir = if (transition.toContent == splitShadeLargeClockScene) -1f else 1f
val distance = dir * getClockCenteringDistance()
val largeClock = checkNotNull(currentClock).largeClock
largeClock.animations.onPositionUpdated(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 6801cf2..5f4dc6e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -20,7 +20,6 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
@@ -34,6 +33,7 @@
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack
+import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace
import com.android.systemui.shade.LargeScreenHeaderHelper
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
@@ -79,6 +79,14 @@
)
}
+ @Composable
+ fun SceneScope.HeadsUpNotifications() {
+ SnoozeableHeadsUpNotificationSpace(
+ stackScrollView = stackScrollView.get(),
+ viewModel = rememberViewModel { viewModelFactory.create() },
+ )
+ }
+
/**
* @param burnInParams params to make this view adaptive to burn-in, `null` to disable burn-in
* adjustment
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt
index 70c0db1..5dccb68 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt
@@ -21,7 +21,7 @@
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneTransitionLayoutState
import com.android.compose.animation.scene.StaticElementContentPicker
-import com.android.compose.animation.scene.content.state.ContentState
+import com.android.compose.animation.scene.content.state.TransitionState
import com.android.systemui.scene.shared.model.Scenes
/** [ElementContentPicker] implementation for the media carousel object. */
@@ -38,7 +38,7 @@
override fun contentDuringTransition(
element: ElementKey,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
fromContentZIndex: Float,
toContentZIndex: Float
): ContentKey {
@@ -64,7 +64,7 @@
}
/** Returns true when the media should be laid on top of the rest for the given [transition]. */
- fun shouldElevateMedia(transition: ContentState.Transition<*>): Boolean {
+ fun shouldElevateMedia(transition: TransitionState.Transition): Boolean {
return transition.isTransitioningBetween(Scenes.Lockscreen, Scenes.Shade)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 0bef05dc..9e292d0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -68,7 +68,6 @@
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.Dp
@@ -82,7 +81,6 @@
import com.android.compose.animation.scene.NestedScrollBehavior
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.thenIf
-import com.android.internal.policy.SystemBarUtils
import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.res.R
@@ -137,14 +135,16 @@
.notificationHeadsUpHeight(stackScrollView)
.debugBackground(viewModel, DEBUG_HUN_COLOR)
.onGloballyPositioned { coordinates: LayoutCoordinates ->
+ val positionInWindow = coordinates.positionInWindow()
val boundsInWindow = coordinates.boundsInWindow()
debugLog(viewModel) {
"HUNS onGloballyPositioned:" +
" size=${coordinates.size}" +
" bounds=$boundsInWindow"
}
- // Note: boundsInWindow doesn't scroll off the screen
- stackScrollView.setHeadsUpTop(boundsInWindow.top)
+ // Note: boundsInWindow doesn't scroll off the screen, so use positionInWindow
+ // for top bound, which can scroll off screen while snoozing
+ stackScrollView.setHeadsUpTop(positionInWindow.y)
stackScrollView.setHeadsUpBottom(boundsInWindow.bottom)
}
)
@@ -159,16 +159,10 @@
stackScrollView: NotificationScrollView,
viewModel: NotificationsPlaceholderViewModel,
) {
- val context = LocalContext.current
- val density = LocalDensity.current
- val statusBarHeight = SystemBarUtils.getStatusBarHeight(context)
- val headsUpPadding =
- with(density) { dimensionResource(id = R.dimen.heads_up_status_bar_padding).roundToPx() }
-
val isHeadsUp by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false)
var scrollOffset by remember { mutableFloatStateOf(0f) }
- val minScrollOffset = -(statusBarHeight + headsUpPadding.toFloat())
+ val minScrollOffset = -(stackScrollView.getHeadsUpInset().toFloat())
val maxScrollOffset = 0f
val scrollableState = rememberScrollableState { delta ->
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index f399436..1921624 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -98,29 +98,31 @@
else -> QSSceneAdapter.State.CLOSED
}
}
- is TransitionState.Transition ->
+ is TransitionState.Transition.ChangeCurrentScene ->
with(transitionState) {
when {
isSplitShade -> UnsquishingQS(squishiness)
fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> {
- Expanding(progress)
+ Expanding { progress }
}
fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> {
- Collapsing(progress)
+ Collapsing { progress }
}
- fromScene == Scenes.Shade || toScene == Scenes.Shade -> {
+ fromContent == Scenes.Shade || toContent == Scenes.Shade -> {
UnsquishingQQS(squishiness)
}
- fromScene == Scenes.QuickSettings || toScene == Scenes.QuickSettings -> {
+ fromContent == Scenes.QuickSettings || toContent == Scenes.QuickSettings -> {
QSSceneAdapter.State.QS
}
else ->
error(
- "Bad transition for QuickSettings: fromScene=$fromScene," +
- " toScene=$toScene"
+ "Bad transition for QuickSettings: fromContent=$fromContent," +
+ " toScene=$toContent"
)
}
}
+ is TransitionState.Transition.OverlayTransition ->
+ TODO("b/359173565: Handle overlay transitions")
}
}
@@ -212,7 +214,8 @@
addView(view)
}
},
- // When the view changes (e.g. due to a theme change), this will be recomposed
+ // When the view changes (e.g. due to a theme change), this will be
+ // recomposed
// if needed and the new view will be attached to the FrameLayout here.
update = {
qsSceneAdapter.setState(state())
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index e064724..d16ba0d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -253,7 +253,7 @@
val isScrollable =
when (val state = layoutState.transitionState) {
is TransitionState.Idle -> true
- is TransitionState.Transition -> state.fromScene == Scenes.QuickSettings
+ is TransitionState.Transition -> state.fromContent == Scenes.QuickSettings
}
LaunchedEffect(isCustomizing, scrollState) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt
index 5eabd22..2bc8c87 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt
@@ -19,14 +19,14 @@
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.SpringSpec
-import com.android.compose.animation.scene.content.state.ContentState
+import com.android.compose.animation.scene.content.state.TransitionState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
internal fun CoroutineScope.animateContent(
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
oneOffAnimation: OneOffAnimation,
targetProgress: Float,
startTransition: () -> Unit,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index ae5a84b..7eef5d6 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -393,11 +393,12 @@
transition: TransitionState.Transition?,
): T? {
if (transition == null) {
- return sharedValue[layoutImpl.state.transitionState.currentScene]
+ return sharedValue[content]
+ ?: sharedValue[layoutImpl.state.transitionState.currentScene]
}
- val fromValue = sharedValue[transition.fromScene]
- val toValue = sharedValue[transition.toScene]
+ val fromValue = sharedValue[transition.fromContent]
+ val toValue = sharedValue[transition.toContent]
return if (fromValue != null && toValue != null) {
if (fromValue == toValue) {
// Optimization: avoid reading progress if the values are the same, so we don't
@@ -411,7 +412,7 @@
if (canOverflow) transition.progress
else transition.progress.fastCoerceIn(0f, 1f)
}
- overscrollSpec.scene == transition.toScene -> 1f
+ overscrollSpec.scene == transition.toContent -> 1f
else -> 0f
}
@@ -424,14 +425,16 @@
val targetValues = sharedValue.targetValues
val transition =
if (element != null) {
- layoutImpl.elements[element]?.stateByContent?.let { sceneStates ->
- layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
- transition.fromScene in sceneStates || transition.toScene in sceneStates
- }
+ layoutImpl.elements[element]?.let { element ->
+ elementState(
+ layoutImpl.state.transitionStates,
+ isInContent = { it in element.stateByContent },
+ )
+ as? TransitionState.Transition
}
} else {
layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
- transition.fromScene in targetValues || transition.toScene in targetValues
+ transition.fromContent in targetValues || transition.toContent in targetValues
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index 920c234..8aa0690 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -28,7 +28,7 @@
layoutState: MutableSceneTransitionLayoutStateImpl,
target: SceneKey,
transitionKey: TransitionKey?,
-): TransitionState.Transition? {
+): TransitionState.Transition.ChangeCurrentScene? {
val transitionState = layoutState.transitionState
if (transitionState.currentScene == target) {
// This can happen in 3 different situations, for which there isn't anything else to do:
@@ -43,16 +43,19 @@
}
return when (transitionState) {
- is TransitionState.Idle -> {
+ is TransitionState.Idle,
+ is TransitionState.Transition.ShowOrHideOverlay,
+ is TransitionState.Transition.ReplaceOverlay -> {
animateToScene(
layoutState,
target,
transitionKey,
isInitiatedByUserInput = false,
+ fromScene = transitionState.currentScene,
replacedTransition = null,
)
}
- is TransitionState.Transition -> {
+ is TransitionState.Transition.ChangeCurrentScene -> {
val isInitiatedByUserInput = transitionState.isInitiatedByUserInput
// A transition is currently running: first check whether `transition.toScene` or
@@ -136,7 +139,7 @@
reversed: Boolean = false,
fromScene: SceneKey = layoutState.transitionState.currentScene,
chain: Boolean = true,
-): TransitionState.Transition {
+): TransitionState.Transition.ChangeCurrentScene {
val oneOffAnimation = OneOffAnimation()
val targetProgress = if (reversed) 0f else 1f
val transition =
@@ -181,7 +184,7 @@
override val isInitiatedByUserInput: Boolean,
replacedTransition: TransitionState.Transition?,
private val oneOffAnimation: OneOffAnimation,
-) : TransitionState.Transition(fromScene, toScene, replacedTransition) {
+) : TransitionState.Transition.ChangeCurrentScene(fromScene, toScene, replacedTransition) {
override val progress: Float
get() = oneOffAnimation.progress
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 83db724..7787458 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -31,9 +31,8 @@
import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastCoerceIn
import com.android.compose.animation.scene.content.Scene
-import com.android.compose.animation.scene.content.state.ContentState
-import com.android.compose.animation.scene.content.state.ContentState.HasOverscrollProperties.Companion.DistanceUnspecified
import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
@@ -582,8 +581,8 @@
replacedTransition: SwipeTransition?,
var lastDistance: Float = DistanceUnspecified,
) :
- TransitionState.Transition(_fromScene.key, _toScene.key, replacedTransition),
- ContentState.HasOverscrollProperties {
+ TransitionState.Transition.ChangeCurrentScene(_fromScene.key, _toScene.key, replacedTransition),
+ TransitionState.HasOverscrollProperties {
var _currentScene by mutableStateOf(_fromScene)
override val currentScene: SceneKey
get() = _currentScene.key
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index e619814..6ea0285 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -46,10 +46,9 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastCoerceIn
-import androidx.compose.ui.util.fastLastOrNull
+import androidx.compose.ui.util.fastForEachReversed
import androidx.compose.ui.util.lerp
import com.android.compose.animation.scene.content.Content
-import com.android.compose.animation.scene.content.state.ContentState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.SharedElementTransformation
@@ -69,7 +68,7 @@
* The last transition that was used when computing the state (size, position and alpha) of this
* element in any content, or `null` if it was last laid out when idle.
*/
- var lastTransition: ContentState.Transition<*>? = null
+ var lastTransition: TransitionState.Transition? = null
/** Whether this element was ever drawn in a content. */
var wasDrawnInAnyContent = false
@@ -146,8 +145,9 @@
// layout/drawing.
// TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once
// we can ensure that SceneTransitionLayoutImpl will compose new contents first.
- val currentTransitions = layoutImpl.state.currentTransitions
- return then(ElementModifier(layoutImpl, currentTransitions, content, key)).testTag(key.testTag)
+ val currentTransitionStates = layoutImpl.state.transitionStates
+ return then(ElementModifier(layoutImpl, currentTransitionStates, content, key))
+ .testTag(key.testTag)
}
/**
@@ -156,20 +156,21 @@
*/
private data class ElementModifier(
private val layoutImpl: SceneTransitionLayoutImpl,
- private val currentTransitions: List<TransitionState.Transition>,
+ private val currentTransitionStates: List<TransitionState>,
private val content: Content,
private val key: ElementKey,
) : ModifierNodeElement<ElementNode>() {
- override fun create(): ElementNode = ElementNode(layoutImpl, currentTransitions, content, key)
+ override fun create(): ElementNode =
+ ElementNode(layoutImpl, currentTransitionStates, content, key)
override fun update(node: ElementNode) {
- node.update(layoutImpl, currentTransitions, content, key)
+ node.update(layoutImpl, currentTransitionStates, content, key)
}
}
internal class ElementNode(
private var layoutImpl: SceneTransitionLayoutImpl,
- private var currentTransitions: List<TransitionState.Transition>,
+ private var currentTransitionStates: List<TransitionState>,
private var content: Content,
private var key: ElementKey,
) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode {
@@ -227,12 +228,12 @@
fun update(
layoutImpl: SceneTransitionLayoutImpl,
- currentTransitions: List<TransitionState.Transition>,
+ currentTransitionStates: List<TransitionState>,
content: Content,
key: ElementKey,
) {
check(layoutImpl == this.layoutImpl && content == this.content)
- this.currentTransitions = currentTransitions
+ this.currentTransitionStates = currentTransitionStates
removeNodeFromContentState()
@@ -288,31 +289,72 @@
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
- val transitions = currentTransitions
- val transition = elementTransition(layoutImpl, element, transitions)
+ val elementState = elementState(layoutImpl, element, currentTransitionStates)
+ if (elementState == null) {
+ // If the element is not part of any transition, place it normally in its idle scene.
+ val currentState = currentTransitionStates.last()
+ val placeInThisContent =
+ elementContentWhenIdle(
+ layoutImpl,
+ currentState.currentScene,
+ currentState.currentOverlays,
+ isInContent = { it in element.stateByContent },
+ ) == content.key
- // If this element is not supposed to be laid out now, either because it is not part of any
- // ongoing transition or the other content of its transition is overscrolling, then lay out
- // the element normally and don't place it.
+ return if (placeInThisContent) {
+ placeNormally(measurable, constraints)
+ } else {
+ doNotPlace(measurable, constraints)
+ }
+ }
+
+ val transition = elementState as? TransitionState.Transition
+
+ // If this element is not supposed to be laid out now because the other content of its
+ // transition is overscrolling, then lay out the element normally and don't place it.
val overscrollScene = transition?.currentOverscrollSpec?.scene
val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != content.key
- val isNotPartOfAnyOngoingTransitions = transitions.isNotEmpty() && transition == null
- if (isNotPartOfAnyOngoingTransitions || isOtherSceneOverscrolling) {
- recursivelyClearPlacementValues()
- stateInContent.lastSize = Element.SizeUnspecified
-
- val placeable = measurable.measure(constraints)
- return layout(placeable.width, placeable.height) { /* Do not place */ }
+ if (isOtherSceneOverscrolling) {
+ return doNotPlace(measurable, constraints)
}
val placeable =
measure(layoutImpl, element, transition, stateInContent, measurable, constraints)
stateInContent.lastSize = placeable.size()
- return layout(placeable.width, placeable.height) { place(transition, placeable) }
+ return layout(placeable.width, placeable.height) { place(elementState, placeable) }
+ }
+
+ private fun ApproachMeasureScope.doNotPlace(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ recursivelyClearPlacementValues()
+ stateInContent.lastSize = Element.SizeUnspecified
+
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) { /* Do not place */ }
+ }
+
+ private fun ApproachMeasureScope.placeNormally(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ stateInContent.lastSize = placeable.size()
+ return layout(placeable.width, placeable.height) {
+ coordinates?.let {
+ with(layoutImpl.lookaheadScope) {
+ stateInContent.lastOffset =
+ lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero)
+ }
+ }
+
+ placeable.place(0, 0)
+ }
}
private fun Placeable.PlacementScope.place(
- transition: ContentState.Transition<*>?,
+ elementState: TransitionState,
placeable: Placeable,
) {
with(layoutImpl.lookaheadScope) {
@@ -322,11 +364,12 @@
coordinates ?: error("Element ${element.key} does not have any coordinates")
// No need to place the element in this content if we don't want to draw it anyways.
- if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) {
+ if (!shouldPlaceElement(layoutImpl, content.key, element, elementState)) {
recursivelyClearPlacementValues()
return
}
+ val transition = elementState as? TransitionState.Transition
val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
val targetOffset =
computeValue(
@@ -392,11 +435,15 @@
return@placeWithLayer
}
- val transition = elementTransition(layoutImpl, element, currentTransitions)
- if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) {
+ val elementState = elementState(layoutImpl, element, currentTransitionStates)
+ if (
+ elementState == null ||
+ !shouldPlaceElement(layoutImpl, content.key, element, elementState)
+ ) {
return@placeWithLayer
}
+ val transition = elementState as? TransitionState.Transition
alpha = elementAlpha(layoutImpl, element, transition, stateInContent)
compositingStrategy = CompositingStrategy.ModulateAlpha
}
@@ -426,7 +473,9 @@
override fun ContentDrawScope.draw() {
element.wasDrawnInAnyContent = true
- val transition = elementTransition(layoutImpl, element, currentTransitions)
+ val transition =
+ elementState(layoutImpl, element, currentTransitionStates)
+ as? TransitionState.Transition
val drawScale = getDrawScale(layoutImpl, element, transition, stateInContent)
if (drawScale == Scale.Default) {
drawContent()
@@ -469,21 +518,15 @@
}
}
-/**
- * The transition that we should consider for [element]. This is the last transition where one of
- * its contents contains the element.
- */
-private fun elementTransition(
+/** The [TransitionState] that we should consider for [element]. */
+private fun elementState(
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
- transitions: List<TransitionState.Transition>,
-): ContentState.Transition<*>? {
- val transition =
- transitions.fastLastOrNull { transition ->
- transition.fromScene in element.stateByContent ||
- transition.toScene in element.stateByContent
- }
+ transitionStates: List<TransitionState>,
+): TransitionState? {
+ val state = elementState(transitionStates, isInContent = { it in element.stateByContent })
+ val transition = state as? TransitionState.Transition
val previousTransition = element.lastTransition
element.lastTransition = transition
@@ -498,14 +541,73 @@
}
}
- return transition
+ return state
+}
+
+internal inline fun elementState(
+ transitionStates: List<TransitionState>,
+ isInContent: (ContentKey) -> Boolean,
+): TransitionState? {
+ val lastState = transitionStates.last()
+ if (lastState is TransitionState.Idle) {
+ check(transitionStates.size == 1)
+ return lastState
+ }
+
+ // Find the last transition with a content that contains the element.
+ transitionStates.fastForEachReversed { state ->
+ val transition = state as TransitionState.Transition
+ if (isInContent(transition.fromContent) || isInContent(transition.toContent)) {
+ return transition
+ }
+ }
+
+ return null
+}
+
+internal inline fun elementContentWhenIdle(
+ layoutImpl: SceneTransitionLayoutImpl,
+ idle: TransitionState.Idle,
+ isInContent: (ContentKey) -> Boolean,
+): ContentKey {
+ val currentScene = idle.currentScene
+ val overlays = idle.currentOverlays
+ return elementContentWhenIdle(layoutImpl, currentScene, overlays, isInContent)
+}
+
+private inline fun elementContentWhenIdle(
+ layoutImpl: SceneTransitionLayoutImpl,
+ currentScene: SceneKey,
+ overlays: Set<OverlayKey>,
+ isInContent: (ContentKey) -> Boolean,
+): ContentKey {
+ if (overlays.isEmpty()) {
+ return currentScene
+ }
+
+ // Find the overlay with highest zIndex that contains the element.
+ // TODO(b/353679003): Should we cache enabledOverlays into a List<> to avoid a lot of
+ // allocations here?
+ var currentOverlay: OverlayKey? = null
+ for (overlay in overlays) {
+ if (
+ isInContent(overlay) &&
+ (currentOverlay == null ||
+ (layoutImpl.overlay(overlay).zIndex >
+ layoutImpl.overlay(currentOverlay).zIndex))
+ ) {
+ currentOverlay = overlay
+ }
+ }
+
+ return currentOverlay ?: currentScene
}
private fun prepareInterruption(
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
- transition: ContentState.Transition<*>,
- previousTransition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
+ previousTransition: TransitionState.Transition,
) {
if (transition.replacedTransition == previousTransition) {
return
@@ -552,7 +654,7 @@
*/
private fun reconcileStates(
element: Element,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
) {
val fromContentState = element.stateByContent[transition.fromContent] ?: return
val toContentState = element.stateByContent[transition.toContent] ?: return
@@ -621,7 +723,7 @@
*/
private inline fun <T> computeInterruptedValue(
layoutImpl: SceneTransitionLayoutImpl,
- transition: ContentState.Transition<*>?,
+ transition: TransitionState.Transition?,
value: T,
unspecifiedValue: T,
zeroValue: T,
@@ -668,7 +770,7 @@
private inline fun <T> setPlacementInterruptionDelta(
element: Element,
stateInContent: Element.State,
- transition: ContentState.Transition<*>?,
+ transition: TransitionState.Transition?,
delta: T,
setter: (Element.State, T) -> Unit,
) {
@@ -694,12 +796,20 @@
layoutImpl: SceneTransitionLayoutImpl,
content: ContentKey,
element: Element,
- transition: ContentState.Transition<*>?,
+ elementState: TransitionState,
): Boolean {
- // Always place the element if we are idle.
- if (transition == null) {
- return true
- }
+ val transition =
+ when (elementState) {
+ is TransitionState.Idle -> {
+ return content ==
+ elementContentWhenIdle(
+ layoutImpl,
+ elementState,
+ isInContent = { it in element.stateByContent },
+ )
+ }
+ is TransitionState.Transition -> elementState
+ }
// Don't place the element in this content if this content is not part of the current element
// transition.
@@ -732,7 +842,7 @@
layoutImpl: SceneTransitionLayoutImpl,
content: ContentKey,
element: ElementKey,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
): Boolean {
// If we are overscrolling, only place/compose the element in the overscrolling scene.
val overscrollScene = transition.currentOverscrollSpec?.scene
@@ -742,30 +852,26 @@
val scenePicker = element.contentPicker
val pickedScene =
- when (transition) {
- is TransitionState.Transition -> {
- scenePicker.contentDuringTransition(
- element = element,
- transition = transition,
- fromContentZIndex = layoutImpl.scene(transition.fromScene).zIndex,
- toContentZIndex = layoutImpl.scene(transition.toScene).zIndex,
- )
- }
- }
+ scenePicker.contentDuringTransition(
+ element = element,
+ transition = transition,
+ fromContentZIndex = layoutImpl.content(transition.fromContent).zIndex,
+ toContentZIndex = layoutImpl.content(transition.toContent).zIndex,
+ )
return pickedScene == content
}
private fun isSharedElementEnabled(
element: ElementKey,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
): Boolean {
return sharedElementTransformation(element, transition)?.enabled ?: true
}
internal fun sharedElementTransformation(
element: ElementKey,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
): SharedElementTransformation? {
val transformationSpec = transition.transformationSpec
val sharedInFromContent =
@@ -793,7 +899,7 @@
private fun isElementOpaque(
content: Content,
element: Element,
- transition: ContentState.Transition<*>?,
+ transition: TransitionState.Transition?,
): Boolean {
if (transition == null) {
return true
@@ -827,7 +933,7 @@
private fun elementAlpha(
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
- transition: ContentState.Transition<*>?,
+ transition: TransitionState.Transition?,
stateInContent: Element.State,
): Float {
val alpha =
@@ -858,7 +964,7 @@
private fun interruptedAlpha(
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
- transition: ContentState.Transition<*>?,
+ transition: TransitionState.Transition?,
stateInContent: Element.State,
alpha: Float,
): Float {
@@ -888,7 +994,7 @@
private fun measure(
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
- transition: ContentState.Transition<*>?,
+ transition: TransitionState.Transition?,
stateInContent: Element.State,
measurable: Measurable,
constraints: Constraints,
@@ -952,7 +1058,7 @@
private fun ContentDrawScope.getDrawScale(
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
- transition: ContentState.Transition<*>?,
+ transition: TransitionState.Transition?,
stateInContent: Element.State,
): Scale {
val scale =
@@ -1048,7 +1154,7 @@
layoutImpl: SceneTransitionLayoutImpl,
currentContentState: Element.State,
element: Element,
- transition: ContentState.Transition<*>?,
+ transition: TransitionState.Transition?,
contentValue: (Element.State) -> T,
transformation: (ElementTransformations) -> PropertyTransformation<T>?,
currentValue: () -> T,
@@ -1076,7 +1182,7 @@
}
val currentContent = currentContentState.content
- if (transition is ContentState.HasOverscrollProperties) {
+ if (transition is TransitionState.HasOverscrollProperties) {
val overscroll = transition.currentOverscrollSpec
if (overscroll?.scene == currentContent) {
val elementSpec =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt
index bf70ca9..6181cfb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt
@@ -37,7 +37,7 @@
* @see InterruptionResult
*/
fun onInterruption(
- interrupted: TransitionState.Transition,
+ interrupted: TransitionState.Transition.ChangeCurrentScene,
newTargetScene: SceneKey,
): InterruptionResult?
}
@@ -76,7 +76,7 @@
*/
object DefaultInterruptionHandler : InterruptionHandler {
override fun onInterruption(
- interrupted: TransitionState.Transition,
+ interrupted: TransitionState.Transition.ChangeCurrentScene,
newTargetScene: SceneKey,
): InterruptionResult {
return InterruptionResult(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index acb436e..3f8f5e7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -63,6 +63,18 @@
}
}
+/** Key for an overlay. */
+class OverlayKey(
+ debugName: String,
+ identity: Any = Object(),
+) : ContentKey(debugName, identity) {
+ override val testTag: String = "overlay:$debugName"
+
+ override fun toString(): String {
+ return "OverlayKey(debugName=$debugName)"
+ }
+}
+
/** Key for an element. */
open class ElementKey(
debugName: String,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
index abecdd7..715222c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
@@ -26,7 +26,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.util.fastLastOrNull
import com.android.compose.animation.scene.content.Content
import com.android.compose.animation.scene.content.state.TransitionState
@@ -58,6 +57,13 @@
modifier: Modifier,
content: @Composable ElementScope<MovableElementContentScope>.() -> Unit,
) {
+ check(key.contentPicker.contents.contains(sceneOrOverlay.key)) {
+ val elementName = key.debugName
+ val contentName = sceneOrOverlay.key.debugName
+ "MovableElement $elementName was composed in content $contentName but the " +
+ "MovableElementKey($elementName).contentPicker.contents does not contain $contentName"
+ }
+
Box(modifier.element(layoutImpl, sceneOrOverlay, key)) {
val contentScope = sceneOrOverlay.scope
val boxScope = this
@@ -153,13 +159,20 @@
// size* as its movable content, i.e. the same *size when idle*. During transitions,
// this size will be used to interpolate the transition size, during the intermediate
// layout pass.
+ //
+ // Important: Like in Modifier.element(), we read the transition states during
+ // composition then pass them to Layout to make sure that composition sees new states
+ // before layout and drawing.
+ val transitionStates = layoutImpl.state.transitionStates
Layout { _, _ ->
// No need to measure or place anything.
val size =
placeholderContentSize(
- layoutImpl,
- contentKey,
- layoutImpl.elements.getValue(element),
+ layoutImpl = layoutImpl,
+ content = contentKey,
+ element = layoutImpl.elements.getValue(element),
+ elementKey = element,
+ transitionStates = transitionStates,
)
layout(size.width, size.height) {}
}
@@ -172,28 +185,43 @@
content: ContentKey,
element: MovableElementKey,
): Boolean {
- val transitions = layoutImpl.state.currentTransitions
- if (transitions.isEmpty()) {
- // If we are idle, there is only one [scene] that is composed so we can compose our
- // movable content here. We still check that [scene] is equal to the current idle scene, to
- // make sure we only compose it there.
- return layoutImpl.state.transitionState.currentScene == content
+ return when (
+ val elementState = movableElementState(element, layoutImpl.state.transitionStates)
+ ) {
+ null -> false
+ is TransitionState.Idle ->
+ movableElementContentWhenIdle(layoutImpl, element, elementState) == content
+ is TransitionState.Transition -> {
+ // During transitions, always compose movable elements in the scene picked by their
+ // content picker.
+ shouldPlaceOrComposeSharedElement(
+ layoutImpl,
+ content,
+ element,
+ elementState,
+ )
+ }
}
+}
- // The current transition for this element is the last transition in which either fromScene or
- // toScene contains the element.
+private fun movableElementState(
+ element: MovableElementKey,
+ transitionStates: List<TransitionState>,
+): TransitionState? {
+ val content = element.contentPicker.contents
+ return elementState(transitionStates, isInContent = { content.contains(it) })
+}
+
+private fun movableElementContentWhenIdle(
+ layoutImpl: SceneTransitionLayoutImpl,
+ element: MovableElementKey,
+ elementState: TransitionState.Idle,
+): ContentKey {
val contents = element.contentPicker.contents
- val transition =
- transitions.fastLastOrNull { transition ->
- transition.fromScene in contents || transition.toScene in contents
- } ?: return false
-
- // Always compose movable elements in the scene picked by their scene picker.
- return shouldPlaceOrComposeSharedElement(
+ return elementContentWhenIdle(
layoutImpl,
- content,
- element,
- transition,
+ elementState,
+ isInContent = { contents.contains(it) },
)
}
@@ -205,6 +233,8 @@
layoutImpl: SceneTransitionLayoutImpl,
content: ContentKey,
element: Element,
+ elementKey: MovableElementKey,
+ transitionStates: List<TransitionState>,
): IntSize {
// If the content of the movable element was already composed in this scene before, use that
// target size.
@@ -213,19 +243,21 @@
return targetValueInScene
}
- // This code is only run during transitions (otherwise the content would be composed and the
- // placeholder would not), so it's ok to cast the state into a Transition directly.
- val transition = layoutImpl.state.transitionState as TransitionState.Transition
+ // If the element content was already composed in the other overlay/scene, we use that
+ // target size assuming it doesn't change between scenes.
+ // TODO(b/317026105): Provide a way to give a hint size/content for cases where this is
+ // not true.
+ val otherContent =
+ when (val state = movableElementState(elementKey, transitionStates)) {
+ null -> return IntSize.Zero
+ is TransitionState.Idle -> movableElementContentWhenIdle(layoutImpl, elementKey, state)
+ is TransitionState.Transition ->
+ if (state.fromContent == content) state.toContent else state.fromContent
+ }
- // If the content was already composed in the other scene, we use that target size assuming it
- // doesn't change between scenes.
- // TODO(b/317026105): Provide a way to give a hint size/content for cases where this is not
- // true.
- val otherScene =
- if (transition.fromScene == content) transition.toScene else transition.fromScene
- val targetValueInOtherScene = element.stateByContent[otherScene]?.targetSize
- if (targetValueInOtherScene != null && targetValueInOtherScene != Element.SizeUnspecified) {
- return targetValueInOtherScene
+ val targetValueInOtherContent = element.stateByContent[otherContent]?.targetSize
+ if (targetValueInOtherContent != null && targetValueInOtherContent != Element.SizeUnspecified) {
+ return targetValueInOtherContent
}
return IntSize.Zero
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
index 8f1a4141..236e202 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
@@ -43,7 +43,9 @@
fun currentScene(): Flow<SceneKey> {
return when (this) {
is Idle -> flowOf(currentScene)
- is Transition -> currentScene
+ is Transition.ChangeCurrentScene -> currentScene
+ is Transition.ShowOrHideOverlay -> flowOf(currentScene)
+ is Transition.ReplaceOverlay -> flowOf(currentScene)
}
}
@@ -51,10 +53,11 @@
data class Idle(val currentScene: SceneKey) : ObservableTransitionState
/** There is a transition animating between two scenes. */
- class Transition(
- val fromScene: SceneKey,
- val toScene: SceneKey,
- val currentScene: Flow<SceneKey>,
+ sealed class Transition(
+ // TODO(b/353679003): Rename these to fromContent and toContent.
+ open val fromScene: ContentKey,
+ open val toScene: ContentKey,
+ val currentOverlays: Flow<Set<OverlayKey>>,
val progress: Flow<Float>,
/**
@@ -76,10 +79,10 @@
val isUserInputOngoing: Flow<Boolean>,
/** Current progress of the preview part of the transition */
- val previewProgress: Flow<Float> = flowOf(0f),
+ val previewProgress: Flow<Float>,
/** Whether the transition is currently in the preview stage or not */
- val isInPreviewStage: Flow<Boolean> = flowOf(false),
+ val isInPreviewStage: Flow<Boolean>,
) : ObservableTransitionState {
override fun toString(): String =
"""Transition
@@ -89,13 +92,109 @@
| isUserInputOngoing=$isUserInputOngoing
|)"""
.trimMargin()
+
+ /** A transition animating between [fromScene] and [toScene]. */
+ class ChangeCurrentScene(
+ override val fromScene: SceneKey,
+ override val toScene: SceneKey,
+ val currentScene: Flow<SceneKey>,
+ currentOverlays: Flow<Set<OverlayKey>>,
+ progress: Flow<Float>,
+ isInitiatedByUserInput: Boolean,
+ isUserInputOngoing: Flow<Boolean>,
+ previewProgress: Flow<Float>,
+ isInPreviewStage: Flow<Boolean>,
+ ) :
+ Transition(
+ fromScene,
+ toScene,
+ currentOverlays,
+ progress,
+ isInitiatedByUserInput,
+ isUserInputOngoing,
+ previewProgress,
+ isInPreviewStage,
+ )
+
+ /** The [overlay] is either showing from [currentScene] or hiding into [currentScene]. */
+ class ShowOrHideOverlay(
+ val overlay: OverlayKey,
+ fromContent: ContentKey,
+ toContent: ContentKey,
+ val currentScene: SceneKey,
+ currentOverlays: Flow<Set<OverlayKey>>,
+ progress: Flow<Float>,
+ isInitiatedByUserInput: Boolean,
+ isUserInputOngoing: Flow<Boolean>,
+ previewProgress: Flow<Float>,
+ isInPreviewStage: Flow<Boolean>,
+ ) :
+ Transition(
+ fromContent,
+ toContent,
+ currentOverlays,
+ progress,
+ isInitiatedByUserInput,
+ isUserInputOngoing,
+ previewProgress,
+ isInPreviewStage,
+ )
+
+ /** We are transitioning from [fromOverlay] to [toOverlay]. */
+ class ReplaceOverlay(
+ val fromOverlay: OverlayKey,
+ val toOverlay: OverlayKey,
+ val currentScene: SceneKey,
+ currentOverlays: Flow<Set<OverlayKey>>,
+ progress: Flow<Float>,
+ isInitiatedByUserInput: Boolean,
+ isUserInputOngoing: Flow<Boolean>,
+ previewProgress: Flow<Float>,
+ isInPreviewStage: Flow<Boolean>,
+ ) :
+ Transition(
+ fromOverlay,
+ toOverlay,
+ currentOverlays,
+ progress,
+ isInitiatedByUserInput,
+ isUserInputOngoing,
+ previewProgress,
+ isInPreviewStage,
+ )
+
+ companion object {
+ operator fun invoke(
+ fromScene: SceneKey,
+ toScene: SceneKey,
+ currentScene: Flow<SceneKey>,
+ progress: Flow<Float>,
+ isInitiatedByUserInput: Boolean,
+ isUserInputOngoing: Flow<Boolean>,
+ previewProgress: Flow<Float> = flowOf(0f),
+ isInPreviewStage: Flow<Boolean> = flowOf(false),
+ currentOverlays: Flow<Set<OverlayKey>> = flowOf(emptySet()),
+ ): ChangeCurrentScene {
+ return ChangeCurrentScene(
+ fromScene,
+ toScene,
+ currentScene,
+ currentOverlays,
+ progress,
+ isInitiatedByUserInput,
+ isUserInputOngoing,
+ previewProgress,
+ isInPreviewStage,
+ )
+ }
+ }
}
fun isIdle(scene: SceneKey?): Boolean {
return this is Idle && (scene == null || this.currentScene == scene)
}
- fun isTransitioning(from: SceneKey? = null, to: SceneKey? = null): Boolean {
+ fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean {
return this is Transition &&
(from == null || this.fromScene == from) &&
(to == null || this.toScene == to)
@@ -111,16 +210,45 @@
return snapshotFlow {
when (val state = transitionState) {
is TransitionState.Idle -> ObservableTransitionState.Idle(state.currentScene)
- is TransitionState.Transition -> {
- ObservableTransitionState.Transition(
+ is TransitionState.Transition.ChangeCurrentScene -> {
+ ObservableTransitionState.Transition.ChangeCurrentScene(
fromScene = state.fromScene,
toScene = state.toScene,
currentScene = snapshotFlow { state.currentScene },
+ currentOverlays = flowOf(state.currentOverlays),
progress = snapshotFlow { state.progress },
isInitiatedByUserInput = state.isInitiatedByUserInput,
isUserInputOngoing = snapshotFlow { state.isUserInputOngoing },
previewProgress = snapshotFlow { state.previewProgress },
- isInPreviewStage = snapshotFlow { state.isInPreviewStage }
+ isInPreviewStage = snapshotFlow { state.isInPreviewStage },
+ )
+ }
+ is TransitionState.Transition.ShowOrHideOverlay -> {
+ check(state.fromOrToScene == state.currentScene)
+ ObservableTransitionState.Transition.ShowOrHideOverlay(
+ overlay = state.overlay,
+ fromContent = state.fromContent,
+ toContent = state.toContent,
+ currentScene = state.currentScene,
+ currentOverlays = snapshotFlow { state.currentOverlays },
+ progress = snapshotFlow { state.progress },
+ isInitiatedByUserInput = state.isInitiatedByUserInput,
+ isUserInputOngoing = snapshotFlow { state.isUserInputOngoing },
+ previewProgress = snapshotFlow { state.previewProgress },
+ isInPreviewStage = snapshotFlow { state.isInPreviewStage },
+ )
+ }
+ is TransitionState.Transition.ReplaceOverlay -> {
+ ObservableTransitionState.Transition.ReplaceOverlay(
+ fromOverlay = state.fromOverlay,
+ toOverlay = state.toOverlay,
+ currentScene = state.currentScene,
+ currentOverlays = snapshotFlow { state.currentOverlays },
+ progress = snapshotFlow { state.progress },
+ isInitiatedByUserInput = state.isInitiatedByUserInput,
+ isUserInputOngoing = snapshotFlow { state.isUserInputOngoing },
+ previewProgress = snapshotFlow { state.previewProgress },
+ isInPreviewStage = snapshotFlow { state.isInPreviewStage },
)
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
index cc53a28..e7e6b2a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
@@ -70,7 +70,7 @@
val coroutineScope: CoroutineScope,
fromScene: SceneKey,
toScene: SceneKey,
-) : TransitionState.Transition(fromScene, toScene) {
+) : TransitionState.Transition.ChangeCurrentScene(fromScene, toScene) {
override var currentScene by mutableStateOf(fromScene)
private set
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 65a7367..aaa2546 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -49,7 +49,7 @@
* if any.
* @param transitionInterceptionThreshold used during a scene transition. For the scene to be
* intercepted, the progress value must be above the threshold, and below (1 - threshold).
- * @param scenes the configuration of the different scenes of this layout.
+ * @param builder the configuration of the different scenes and overlays of this layout.
*/
@Composable
fun SceneTransitionLayout(
@@ -58,7 +58,7 @@
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
- scenes: SceneTransitionLayoutScope.() -> Unit,
+ builder: SceneTransitionLayoutScope.() -> Unit,
) {
SceneTransitionLayoutForTesting(
state,
@@ -67,7 +67,7 @@
swipeDetector,
transitionInterceptionThreshold,
onLayoutImpl = null,
- scenes,
+ builder,
)
}
@@ -86,6 +86,31 @@
userActions: Map<UserAction, UserActionResult> = emptyMap(),
content: @Composable ContentScope.() -> Unit,
)
+
+ /**
+ * Add an overlay to this layout, identified by [key].
+ *
+ * Overlays are displayed above scenes and can be toggled using
+ * [MutableSceneTransitionLayoutState.showOverlay] and
+ * [MutableSceneTransitionLayoutState.hideOverlay].
+ *
+ * Overlays will have a maximum size that is the size of the layout without overlays, i.e. an
+ * overlay can be fillMaxSize() to match the layout size but it won't make the layout bigger.
+ *
+ * By default overlays are centered in their layout but they can be aligned differently using
+ * [alignment].
+ *
+ * Important: overlays must be defined after all scenes. Overlay order along the z-axis follows
+ * call order. Calling overlay(A) followed by overlay(B) will mean that overlay B renders
+ * after/above overlay A.
+ */
+ // TODO(b/353679003): Allow to specify user actions. When overlays are shown, the user actions
+ // of the top-most overlay in currentOverlays will be used.
+ fun overlay(
+ key: OverlayKey,
+ alignment: Alignment = Alignment.Center,
+ content: @Composable ContentScope.() -> Unit,
+ )
}
/**
@@ -239,7 +264,7 @@
/**
* Animate some value at the content level.
*
- * @param value the value of this shared value in the current scene.
+ * @param value the value of this shared value in the current content.
* @param key the key of this shared value.
* @param type the [SharedValueType] of this animated value.
* @param canOverflow whether this value can overflow past the values it is interpolated
@@ -292,7 +317,7 @@
/**
* Animate some value associated to this element.
*
- * @param value the value of this shared value in the current scene.
+ * @param value the value of this shared value in the current content.
* @param key the key of this shared value.
* @param type the [SharedValueType] of this animated value.
* @param canOverflow whether this value can overflow past the values it is interpolated
@@ -509,7 +534,7 @@
swipeDetector: SwipeDetector = DefaultSwipeDetector,
transitionInterceptionThreshold: Float = 0f,
onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
- scenes: SceneTransitionLayoutScope.() -> Unit,
+ builder: SceneTransitionLayoutScope.() -> Unit,
) {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
@@ -521,7 +546,7 @@
layoutDirection = layoutDirection,
swipeSourceDetector = swipeSourceDetector,
transitionInterceptionThreshold = transitionInterceptionThreshold,
- builder = scenes,
+ builder = builder,
coroutineScope = coroutineScope,
)
.also { onLayoutImpl?.invoke(it) }
@@ -529,7 +554,7 @@
// TODO(b/317014852): Move this into the SideEffect {} again once STLImpl.scenes is not a
// SnapshotStateMap anymore.
- layoutImpl.updateScenes(scenes, layoutDirection)
+ layoutImpl.updateContents(builder, layoutDirection)
SideEffect {
if (state != layoutImpl.state) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 062d553..21f11e4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -18,10 +18,12 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.key
import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ApproachLayoutModifierNode
@@ -36,8 +38,11 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachReversed
+import androidx.compose.ui.zIndex
import com.android.compose.animation.scene.content.Content
+import com.android.compose.animation.scene.content.Overlay
import com.android.compose.animation.scene.content.Scene
+import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.ui.util.lerp
import kotlinx.coroutines.CoroutineScope
@@ -59,7 +64,17 @@
*
* TODO(b/317014852): Make this a normal MutableMap instead.
*/
- internal val scenes = SnapshotStateMap<SceneKey, Scene>()
+ private val scenes = SnapshotStateMap<SceneKey, Scene>()
+
+ /**
+ * The map of [Overlays].
+ *
+ * Note: We lazily create this map to avoid instantiation an expensive SnapshotStateMap in the
+ * common case where there is no overlay in this layout.
+ */
+ private var _overlays: MutableMap<OverlayKey, Overlay>? = null
+ private val overlays
+ get() = _overlays ?: SnapshotStateMap<OverlayKey, Overlay>().also { _overlays = it }
/**
* The map of [Element]s.
@@ -118,7 +133,7 @@
private set
init {
- updateScenes(builder, layoutDirection)
+ updateContents(builder, layoutDirection)
// DraggableHandlerImpl must wait for the scenes to be initialized, in order to access the
// current scene (required for SwipeTransition).
@@ -151,22 +166,32 @@
return scenes[key] ?: error("Scene $key is not configured")
}
+ internal fun sceneOrNull(key: SceneKey): Scene? = scenes[key]
+
+ internal fun overlay(key: OverlayKey): Overlay {
+ return overlays[key] ?: error("Overlay $key is not configured")
+ }
+
internal fun content(key: ContentKey): Content {
return when (key) {
is SceneKey -> scene(key)
+ is OverlayKey -> overlay(key)
}
}
- internal fun updateScenes(
+ internal fun updateContents(
builder: SceneTransitionLayoutScope.() -> Unit,
layoutDirection: LayoutDirection,
) {
- // Keep a reference of the current scenes. After processing [builder], the scenes that were
- // not configured will be removed.
+ // Keep a reference of the current contents. After processing [builder], the contents that
+ // were not configured will be removed.
val scenesToRemove = scenes.keys.toMutableSet()
+ val overlaysToRemove =
+ if (_overlays == null) mutableSetOf() else overlays.keys.toMutableSet()
// The incrementing zIndex of each scene.
var zIndex = 0f
+ var overlaysDefined = false
object : SceneTransitionLayoutScope {
override fun scene(
@@ -174,6 +199,8 @@
userActions: Map<UserAction, UserActionResult>,
content: @Composable ContentScope.() -> Unit,
) {
+ require(!overlaysDefined) { "all scenes must be defined before overlays" }
+
scenesToRemove.remove(key)
val resolvedUserActions =
@@ -198,10 +225,42 @@
zIndex++
}
+
+ override fun overlay(
+ key: OverlayKey,
+ alignment: Alignment,
+ content: @Composable (ContentScope.() -> Unit)
+ ) {
+ overlaysDefined = true
+ overlaysToRemove.remove(key)
+
+ val overlay = overlays[key]
+ if (overlay != null) {
+ // Update an existing overlay.
+ overlay.content = content
+ overlay.zIndex = zIndex
+ overlay.alignment = alignment
+ } else {
+ // New overlay.
+ overlays[key] =
+ Overlay(
+ key,
+ this@SceneTransitionLayoutImpl,
+ content,
+ // TODO(b/353679003): Allow to specify user actions
+ actions = emptyMap(),
+ zIndex,
+ alignment,
+ )
+ }
+
+ zIndex++
+ }
}
.builder()
scenesToRemove.forEach { scenes.remove(it) }
+ overlaysToRemove.forEach { overlays.remove(it) }
}
@Composable
@@ -219,8 +278,8 @@
lookaheadScope = this
BackHandler()
-
- scenesToCompose().fastForEach { scene -> key(scene.key) { scene.Content() } }
+ Scenes()
+ Overlays()
}
}
}
@@ -232,6 +291,11 @@
PredictiveBackHandler(state, coroutineScope, targetSceneForBack)
}
+ @Composable
+ private fun Scenes() {
+ scenesToCompose().fastForEach { scene -> key(scene.key) { scene.Content() } }
+ }
+
private fun scenesToCompose(): List<Scene> {
val transitions = state.currentTransitions
return if (transitions.isEmpty()) {
@@ -247,16 +311,79 @@
// Compose the new scene we are going to first.
transitions.fastForEachReversed { transition ->
- maybeAdd(transition.toScene)
- maybeAdd(transition.fromScene)
+ when (transition) {
+ is TransitionState.Transition.ChangeCurrentScene -> {
+ maybeAdd(transition.toScene)
+ maybeAdd(transition.fromScene)
+ }
+ is TransitionState.Transition.ShowOrHideOverlay ->
+ maybeAdd(transition.fromOrToScene)
+ is TransitionState.Transition.ReplaceOverlay -> {}
+ }
}
+
+ // Make sure that the current scene is always composed.
+ maybeAdd(transitions.last().currentScene)
}
}
}
+ @Composable
+ private fun BoxScope.Overlays() {
+ val overlaysOrderedByZIndex = overlaysToComposeOrderedByZIndex()
+ if (overlaysOrderedByZIndex.isEmpty()) {
+ return
+ }
+
+ // We put the overlays inside a Box that is matching the layout size so that overlays are
+ // measured after all scenes and that their max size is the size of the layout without the
+ // overlays.
+ Box(Modifier.matchParentSize().zIndex(overlaysOrderedByZIndex.first().zIndex)) {
+ overlaysOrderedByZIndex.fastForEach { overlay ->
+ key(overlay.key) { overlay.Content(Modifier.align(overlay.alignment)) }
+ }
+ }
+ }
+
+ private fun overlaysToComposeOrderedByZIndex(): List<Overlay> {
+ if (_overlays == null) return emptyList()
+
+ val transitions = state.currentTransitions
+ return if (transitions.isEmpty()) {
+ state.transitionState.currentOverlays.map { overlay(it) }
+ } else {
+ buildList {
+ val visited = mutableSetOf<OverlayKey>()
+ fun maybeAdd(key: OverlayKey) {
+ if (visited.add(key)) {
+ add(overlay(key))
+ }
+ }
+
+ transitions.fastForEach { transition ->
+ when (transition) {
+ is TransitionState.Transition.ChangeCurrentScene -> {}
+ is TransitionState.Transition.ShowOrHideOverlay ->
+ maybeAdd(transition.overlay)
+ is TransitionState.Transition.ReplaceOverlay -> {
+ maybeAdd(transition.fromOverlay)
+ maybeAdd(transition.toOverlay)
+ }
+ }
+ }
+
+ // Make sure that all current overlays are composed.
+ transitions.last().currentOverlays.forEach { maybeAdd(it) }
+ }
+ }
+ .sortedBy { it.zIndex }
+ }
+
internal fun setScenesTargetSizeForTest(size: IntSize) {
scenes.values.forEach { it.targetSize = size }
}
+
+ internal fun overlaysOrNullForTest(): Map<OverlayKey, Overlay>? = _overlays
}
private data class LayoutElement(private val layoutImpl: SceneTransitionLayoutImpl) :
@@ -284,7 +411,8 @@
val width: Int
val height: Int
- val transition = layoutImpl.state.currentTransition
+ val transition =
+ layoutImpl.state.currentTransition as? TransitionState.Transition.ChangeCurrentScene
if (transition == null) {
width = placeable.width
height = placeable.height
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 44f5964f..74cd136 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -25,7 +25,6 @@
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
-import com.android.compose.animation.scene.content.state.ContentState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transition.link.LinkedTransition
import com.android.compose.animation.scene.transition.link.StateLink
@@ -40,6 +39,21 @@
@Stable
sealed interface SceneTransitionLayoutState {
/**
+ * The current effective scene. If a new transition is triggered, it will start from this scene.
+ */
+ val currentScene: SceneKey
+
+ /**
+ * The current set of overlays. This represents the set of overlays that will be visible on
+ * screen once all [currentTransitions] are finished.
+ *
+ * @see MutableSceneTransitionLayoutState.showOverlay
+ * @see MutableSceneTransitionLayoutState.hideOverlay
+ * @see MutableSceneTransitionLayoutState.replaceOverlay
+ */
+ val currentOverlays: Set<OverlayKey>
+
+ /**
* The current [TransitionState]. All values read here are backed by the Snapshot system.
*
* To observe those values outside of Compose/the Snapshot system, use
@@ -111,7 +125,50 @@
): TransitionState.Transition?
/** Immediately snap to the given [scene]. */
- fun snapToScene(scene: SceneKey)
+ fun snapToScene(
+ scene: SceneKey,
+ currentOverlays: Set<OverlayKey> = transitionState.currentOverlays,
+ )
+
+ /**
+ * Request to show [overlay] so that it animates in from [currentScene] and ends up being
+ * visible on screen.
+ *
+ * After this returns, this overlay will be included in [currentOverlays]. This does nothing if
+ * [overlay] is already in [currentOverlays].
+ */
+ fun showOverlay(
+ overlay: OverlayKey,
+ animationScope: CoroutineScope,
+ transitionKey: TransitionKey? = null,
+ )
+
+ /**
+ * Request to hide [overlay] so that it animates out to [currentScene] and ends up *not* being
+ * visible on screen.
+ *
+ * After this returns, this overlay will not be included in [currentOverlays]. This does nothing
+ * if [overlay] is not in [currentOverlays].
+ */
+ fun hideOverlay(
+ overlay: OverlayKey,
+ animationScope: CoroutineScope,
+ transitionKey: TransitionKey? = null,
+ )
+
+ /**
+ * Replace [from] by [to] so that [from] ends up not being visible on screen and [to] ends up
+ * being visible.
+ *
+ * This throws if [from] is not currently in [currentOverlays] or if [to] is already in
+ * [currentOverlays].
+ */
+ fun replaceOverlay(
+ from: OverlayKey,
+ to: OverlayKey,
+ animationScope: CoroutineScope,
+ transitionKey: TransitionKey? = null,
+ )
}
/**
@@ -129,6 +186,7 @@
fun MutableSceneTransitionLayoutState(
initialScene: SceneKey,
transitions: SceneTransitions = SceneTransitions.Empty,
+ initialOverlays: Set<OverlayKey> = emptySet(),
canChangeScene: (SceneKey) -> Boolean = { true },
stateLinks: List<StateLink> = emptyList(),
enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
@@ -136,6 +194,7 @@
return MutableSceneTransitionLayoutStateImpl(
initialScene,
transitions,
+ initialOverlays,
canChangeScene,
stateLinks,
enableInterruptions,
@@ -146,6 +205,7 @@
internal class MutableSceneTransitionLayoutStateImpl(
initialScene: SceneKey,
override var transitions: SceneTransitions = transitions {},
+ initialOverlays: Set<OverlayKey> = emptySet(),
internal val canChangeScene: (SceneKey) -> Boolean = { true },
private val stateLinks: List<StateLink> = emptyList(),
@@ -159,13 +219,18 @@
* 1. A list with a single [TransitionState.Idle] element, when we are idle.
* 2. A list with one or more [TransitionState.Transition], when we are transitioning.
*/
- @VisibleForTesting
internal var transitionStates: List<TransitionState> by
- mutableStateOf(listOf(TransitionState.Idle(initialScene)))
+ mutableStateOf(listOf(TransitionState.Idle(initialScene, initialOverlays)))
private set
+ override val currentScene: SceneKey
+ get() = transitionState.currentScene
+
+ override val currentOverlays: Set<OverlayKey>
+ get() = transitionState.currentOverlays
+
override val transitionState: TransitionState
- get() = transitionStates.last()
+ get() = transitionStates[transitionStates.lastIndex]
override val currentTransitions: List<TransitionState.Transition>
get() {
@@ -209,7 +274,7 @@
targetScene: SceneKey,
coroutineScope: CoroutineScope,
transitionKey: TransitionKey?,
- ): TransitionState.Transition? {
+ ): TransitionState.Transition.ChangeCurrentScene? {
checkThread()
return coroutineScope.animateToScene(
@@ -228,13 +293,21 @@
*
* Important: you *must* call [finishTransition] once the transition is finished.
*/
- internal fun startTransition(transition: TransitionState.Transition, chain: Boolean = true) {
+ internal fun startTransition(
+ transition: TransitionState.Transition.ChangeCurrentScene,
+ chain: Boolean = true,
+ ) {
checkThread()
+ // Set the current scene and overlays on the transition.
+ val currentState = transitionState
+ transition.currentSceneWhenTransitionStarted = currentState.currentScene
+ transition.currentOverlaysWhenTransitionStarted = currentState.currentOverlays
+
// Compute the [TransformationSpec] when the transition starts.
val fromScene = transition.fromScene
val toScene = transition.toScene
- val orientation = (transition as? ContentState.HasOverscrollProperties)?.orientation
+ val orientation = (transition as? TransitionState.HasOverscrollProperties)?.orientation
// Update the transition specs.
transition.transformationSpec =
@@ -312,8 +385,8 @@
appendLine(" Transitions (size=${transitionStates.size}):")
transitionStates.fastForEach { state ->
val transition = state as TransitionState.Transition
- val from = transition.fromScene
- val to = transition.toScene
+ val from = transition.fromContent
+ val to = transition.toContent
val indicator = if (finishedTransitions.contains(transition)) "x" else " "
appendLine(" [$indicator] $from => $to ($transition)")
}
@@ -329,27 +402,32 @@
}
private fun setupTransitionLinks(transition: TransitionState.Transition) {
- stateLinks.fastForEach { stateLink ->
- val matchingLinks =
- stateLink.transitionLinks.fastFilter { it.isMatchingLink(transition) }
- if (matchingLinks.isEmpty()) return@fastForEach
- if (matchingLinks.size > 1) error("More than one link matched.")
+ when (transition) {
+ is TransitionState.Transition.ChangeCurrentScene -> {
+ stateLinks.fastForEach { stateLink ->
+ val matchingLinks =
+ stateLink.transitionLinks.fastFilter { it.isMatchingLink(transition) }
+ if (matchingLinks.isEmpty()) return@fastForEach
+ if (matchingLinks.size > 1) error("More than one link matched.")
- val targetCurrentScene = stateLink.target.transitionState.currentScene
- val matchingLink = matchingLinks[0]
+ val targetCurrentScene = stateLink.target.transitionState.currentScene
+ val matchingLink = matchingLinks[0]
- if (!matchingLink.targetIsInValidState(targetCurrentScene)) return@fastForEach
+ if (!matchingLink.targetIsInValidState(targetCurrentScene)) return@fastForEach
- val linkedTransition =
- LinkedTransition(
- originalTransition = transition,
- fromScene = targetCurrentScene,
- toScene = matchingLink.targetTo,
- key = matchingLink.targetTransitionKey,
- )
+ val linkedTransition =
+ LinkedTransition(
+ originalTransition = transition,
+ fromScene = targetCurrentScene,
+ toScene = matchingLink.targetTo,
+ key = matchingLink.targetTransitionKey,
+ )
- stateLink.target.startTransition(linkedTransition)
- transition.activeTransitionLinks[stateLink] = linkedTransition
+ stateLink.target.startTransition(linkedTransition)
+ transition.activeTransitionLinks[stateLink] = linkedTransition
+ }
+ }
+ else -> error("transition links are not supported with overlays yet")
}
}
@@ -402,23 +480,28 @@
// If all transitions are finished, we are idle.
if (i == nStates) {
check(finishedTransitions.isEmpty())
- this.transitionStates = listOf(TransitionState.Idle(lastTransition.currentScene))
+ this.transitionStates =
+ listOf(
+ TransitionState.Idle(
+ lastTransition.currentScene,
+ lastTransition.currentOverlays,
+ )
+ )
} else if (i > 0) {
this.transitionStates = transitionStates.subList(fromIndex = i, toIndex = nStates)
}
}
- override fun snapToScene(scene: SceneKey) {
+ override fun snapToScene(scene: SceneKey, currentOverlays: Set<OverlayKey>) {
checkThread()
// Force finish all transitions.
while (currentTransitions.isNotEmpty()) {
- val transition = transitionStates[0] as TransitionState.Transition
- finishTransition(transition)
+ finishTransition(transitionStates[0] as TransitionState.Transition)
}
check(transitionStates.size == 1)
- transitionStates = listOf(TransitionState.Idle(scene))
+ transitionStates = listOf(TransitionState.Idle(scene, currentOverlays))
}
private fun finishActiveTransitionLinks(transition: TransitionState.Transition) {
@@ -451,8 +534,8 @@
}
val shouldSnap =
- (isProgressCloseTo(0f) && transition.currentScene == transition.fromScene) ||
- (isProgressCloseTo(1f) && transition.currentScene == transition.toScene)
+ (isProgressCloseTo(0f) && transition.currentScene == transition.fromContent) ||
+ (isProgressCloseTo(1f) && transition.currentScene == transition.toContent)
return if (shouldSnap) {
finishAllTransitions()
true
@@ -460,6 +543,57 @@
false
}
}
+
+ override fun showOverlay(
+ overlay: OverlayKey,
+ animationScope: CoroutineScope,
+ transitionKey: TransitionKey?
+ ) {
+ checkThread()
+
+ // Overlay is already shown, do nothing.
+ if (overlay in transitionState.currentOverlays) {
+ return
+ }
+
+ // TODO(b/353679003): Animate the overlay instead of instantly snapping to an Idle state.
+ snapToScene(transitionState.currentScene, transitionState.currentOverlays + overlay)
+ }
+
+ override fun hideOverlay(
+ overlay: OverlayKey,
+ animationScope: CoroutineScope,
+ transitionKey: TransitionKey?
+ ) {
+ checkThread()
+
+ // Overlay is not shown, do nothing.
+ if (!transitionState.currentOverlays.contains(overlay)) {
+ return
+ }
+
+ // TODO(b/353679003): Animate the overlay instead of instantly snapping to an Idle state.
+ snapToScene(transitionState.currentScene, transitionState.currentOverlays - overlay)
+ }
+
+ override fun replaceOverlay(
+ from: OverlayKey,
+ to: OverlayKey,
+ animationScope: CoroutineScope,
+ transitionKey: TransitionKey?
+ ) {
+ checkThread()
+ require(from in currentOverlays) {
+ "Overlay ${from.debugName} is not shown so it can't be replaced by ${to.debugName}"
+ }
+ require(to !in currentOverlays) {
+ "Overlay ${to.debugName} is already shown so it can't replace ${from.debugName}"
+ }
+
+ // TODO(b/353679003): Animate from into to instead of hiding/showing the overlays
+ // separately.
+ snapToScene(transitionState.currentScene, transitionState.currentOverlays - from + to)
+ }
}
private const val TAG = "SceneTransitionLayoutState"
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index e38c849..5cc194d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -25,7 +25,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import com.android.compose.animation.scene.content.state.ContentState
+import com.android.compose.animation.scene.content.state.TransitionState
import kotlin.math.tanh
/** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
@@ -262,7 +262,7 @@
*/
fun contentDuringTransition(
element: ElementKey,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
fromContentZIndex: Float,
toContentZIndex: Float,
): ContentKey
@@ -278,7 +278,7 @@
*/
fun pickSingleContentIn(
contents: Set<ContentKey>,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
element: ElementKey,
): ContentKey {
val fromContent = transition.fromContent
@@ -331,7 +331,7 @@
object HighestZIndexContentPicker : ElementContentPicker {
override fun contentDuringTransition(
element: ElementKey,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
fromContentZIndex: Float,
toContentZIndex: Float
): ContentKey {
@@ -352,7 +352,7 @@
override fun contentDuringTransition(
element: ElementKey,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
fromContentZIndex: Float,
toContentZIndex: Float
): ContentKey {
@@ -373,7 +373,7 @@
object LowestZIndexContentPicker : ElementContentPicker {
override fun contentDuringTransition(
element: ElementKey,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
fromContentZIndex: Float,
toContentZIndex: Float
): ContentKey {
@@ -394,7 +394,7 @@
override fun contentDuringTransition(
element: ElementKey,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
fromContentZIndex: Float,
toContentZIndex: Float
): ContentKey {
@@ -428,7 +428,7 @@
) : StaticElementContentPicker {
override fun contentDuringTransition(
element: ElementKey,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
fromContentZIndex: Float,
toContentZIndex: Float,
): ContentKey {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
index 0f66804..9851b32 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
@@ -35,7 +35,7 @@
}
override fun SceneKey.targetSize(): IntSize? {
- return layoutImpl.scenes[this]?.targetSize.takeIf { it != IntSize.Zero }
+ return layoutImpl.sceneOrNull(this)?.targetSize.takeIf { it != IntSize.Zero }
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Overlay.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Overlay.kt
new file mode 100644
index 0000000..ccec9e8
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Overlay.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.compose.animation.scene.content
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.OverlayKey
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+
+/** An overlay defined in a [SceneTransitionLayout]. */
+@Stable
+internal class Overlay(
+ override val key: OverlayKey,
+ layoutImpl: SceneTransitionLayoutImpl,
+ content: @Composable ContentScope.() -> Unit,
+ actions: Map<UserAction.Resolved, UserActionResult>,
+ zIndex: Float,
+ alignment: Alignment,
+) : Content(key, layoutImpl, content, actions, zIndex) {
+ var alignment by mutableStateOf(alignment)
+
+ override fun toString(): String {
+ return "Overlay(key=$key)"
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/ContentState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/ContentState.kt
deleted file mode 100644
index 0bd676b..0000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/ContentState.kt
+++ /dev/null
@@ -1,242 +0,0 @@
-/*
- * 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.compose.animation.scene.content.state
-
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.spring
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.runtime.Stable
-import com.android.compose.animation.scene.ContentKey
-import com.android.compose.animation.scene.OverscrollScope
-import com.android.compose.animation.scene.OverscrollSpecImpl
-import com.android.compose.animation.scene.ProgressVisibilityThreshold
-import com.android.compose.animation.scene.SceneTransitionLayoutImpl
-import com.android.compose.animation.scene.TransformationSpec
-import com.android.compose.animation.scene.TransformationSpecImpl
-import com.android.compose.animation.scene.TransitionKey
-import com.android.compose.animation.scene.transition.link.LinkedTransition
-import com.android.compose.animation.scene.transition.link.StateLink
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-
-/** The state associated to one or more contents. */
-@Stable
-sealed interface ContentState<out T : ContentKey> {
- /** The [content] is idle, it does not animate. */
- sealed class Idle<T : ContentKey>(val content: T) : ContentState<T>
-
- /** The content is transitioning with another content. */
- sealed class Transition<out T : ContentKey>(
- val fromContent: T,
- val toContent: T,
- internal val replacedTransition: Transition<T>?,
- ) : ContentState<T> {
- /**
- * The key of this transition. This should usually be null, but it can be specified to use a
- * specific set of transformations associated to this transition.
- */
- open val key: TransitionKey? = null
-
- /**
- * The progress of the transition. This is usually in the `[0; 1]` range, but it can also be
- * less than `0` or greater than `1` when using transitions with a spring AnimationSpec or
- * when flinging quickly during a swipe gesture.
- */
- abstract val progress: Float
-
- /** The current velocity of [progress], in progress units. */
- abstract val progressVelocity: Float
-
- /** Whether the transition was triggered by user input rather than being programmatic. */
- abstract val isInitiatedByUserInput: Boolean
-
- /** Whether user input is currently driving the transition. */
- abstract val isUserInputOngoing: Boolean
-
- /**
- * The progress of the preview transition. This is usually in the `[0; 1]` range, but it can
- * also be less than `0` or greater than `1` when using transitions with a spring
- * AnimationSpec or when flinging quickly during a swipe gesture.
- */
- internal open val previewProgress: Float = 0f
-
- /** The current velocity of [previewProgress], in progress units. */
- internal open val previewProgressVelocity: Float = 0f
-
- /** Whether the transition is currently in the preview stage */
- internal open val isInPreviewStage: Boolean = false
-
- /**
- * The current [TransformationSpecImpl] and [OverscrollSpecImpl] associated to this
- * transition.
- *
- * Important: These will be set exactly once, when this transition is
- * [started][MutableSceneTransitionLayoutStateImpl.startTransition].
- */
- internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
- internal var previewTransformationSpec: TransformationSpecImpl? = null
- private var fromOverscrollSpec: OverscrollSpecImpl? = null
- private var toOverscrollSpec: OverscrollSpecImpl? = null
-
- /** The current [OverscrollSpecImpl], if this transition is currently overscrolling. */
- internal val currentOverscrollSpec: OverscrollSpecImpl?
- get() {
- if (this !is HasOverscrollProperties) return null
- val progress = progress
- val bouncingContent = bouncingContent
- return when {
- progress < 0f || bouncingContent == fromContent -> fromOverscrollSpec
- progress > 1f || bouncingContent == toContent -> toOverscrollSpec
- else -> null
- }
- }
-
- /**
- * An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden
- * jump of values when this transitions interrupts another one.
- */
- private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null
-
- /** The map of active links that connects this transition to other transitions. */
- internal val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()
-
- init {
- check(fromContent != toContent)
- check(
- replacedTransition == null ||
- (replacedTransition.fromContent == fromContent &&
- replacedTransition.toContent == toContent)
- )
- }
-
- /**
- * Force this transition to finish and animate to an [Idle] state.
- *
- * Important: Once this is called, the effective state of the transition should remain
- * unchanged. For instance, in the case of a [TransitionState.Transition], its
- * [currentScene][TransitionState.Transition.currentScene] should never change once [finish]
- * is called.
- *
- * @return the [Job] that animates to the idle state. It can be used to wait until the
- * animation is complete or cancel it to snap the animation. Calling [finish] multiple
- * times will return the same [Job].
- */
- internal abstract fun finish(): Job
-
- /**
- * Whether we are transitioning. If [from] or [to] is empty, we will also check that they
- * match the contents we are animating from and/or to.
- */
- fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean {
- return (from == null || fromContent == from) && (to == null || toContent == to)
- }
-
- /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */
- fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean {
- return isTransitioning(from = content, to = other) ||
- isTransitioning(from = other, to = content)
- }
-
- internal fun updateOverscrollSpecs(
- fromSpec: OverscrollSpecImpl?,
- toSpec: OverscrollSpecImpl?,
- ) {
- fromOverscrollSpec = fromSpec
- toOverscrollSpec = toSpec
- }
-
- /** Returns if the [progress] value of this transition can go beyond range `[0; 1]` */
- internal fun isWithinProgressRange(progress: Float): Boolean {
- // If the properties are missing we assume that every [Transition] can overscroll
- if (this !is HasOverscrollProperties) return true
- // [OverscrollSpec] for the current scene, even if it hasn't started overscrolling yet.
- val specForCurrentScene =
- when {
- progress <= 0f -> fromOverscrollSpec
- progress >= 1f -> toOverscrollSpec
- else -> null
- } ?: return true
-
- return specForCurrentScene.transformationSpec.transformations.isNotEmpty()
- }
-
- internal open fun interruptionProgress(
- layoutImpl: SceneTransitionLayoutImpl,
- ): Float {
- if (!layoutImpl.state.enableInterruptions) {
- return 0f
- }
-
- if (replacedTransition != null) {
- return replacedTransition.interruptionProgress(layoutImpl)
- }
-
- fun create(): Animatable<Float, AnimationVector1D> {
- val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold)
- layoutImpl.coroutineScope.launch {
- val swipeSpec = layoutImpl.state.transitions.defaultSwipeSpec
- val progressSpec =
- spring(
- stiffness = swipeSpec.stiffness,
- dampingRatio = swipeSpec.dampingRatio,
- visibilityThreshold = ProgressVisibilityThreshold,
- )
- animatable.animateTo(0f, progressSpec)
- }
-
- return animatable
- }
-
- val animatable = interruptionDecay ?: create().also { interruptionDecay = it }
- return animatable.value
- }
- }
-
- interface HasOverscrollProperties {
- /**
- * The position of the [Transition.toContent].
- *
- * Used to understand the direction of the overscroll.
- */
- val isUpOrLeft: Boolean
-
- /**
- * The relative orientation between [Transition.fromContent] and [Transition.toContent].
- *
- * Used to understand the orientation of the overscroll.
- */
- val orientation: Orientation
-
- /**
- * Scope which can be used in the Overscroll DSL to define a transformation based on the
- * distance between [Transition.fromContent] and [Transition.toContent].
- */
- val overscrollScope: OverscrollScope
-
- /**
- * The content (scene or overlay) around which the transition is currently bouncing. When
- * not `null`, this transition is currently oscillating around this content and will soon
- * settle to that content.
- */
- val bouncingContent: ContentKey?
-
- companion object {
- const val DistanceUnspecified = 0f
- }
- }
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index 77de22c..fdb019f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -16,16 +16,35 @@
package com.android.compose.animation.scene.content.state
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
+import com.android.compose.animation.scene.OverlayKey
+import com.android.compose.animation.scene.OverscrollScope
+import com.android.compose.animation.scene.OverscrollSpecImpl
+import com.android.compose.animation.scene.ProgressVisibilityThreshold
import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransformationSpec
+import com.android.compose.animation.scene.TransformationSpecImpl
+import com.android.compose.animation.scene.TransitionKey
+import com.android.compose.animation.scene.transition.link.LinkedTransition
+import com.android.compose.animation.scene.transition.link.StateLink
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
-/** The state associated to one or more scenes. */
-// TODO(b/353679003): Rename to SceneState.
+/** The state associated to a [SceneTransitionLayout] at some specific point in time. */
@Stable
-sealed interface TransitionState : ContentState<SceneKey> {
+sealed interface TransitionState {
/**
- * The current effective scene. If a new transition was triggered, it would start from this
- * scene.
+ * The current effective scene. If a new scene transition was triggered, it would start from
+ * this scene.
*
* For instance, when swiping from scene A to scene B, the [currentScene] is A when the swipe
* gesture starts, but then if the user flings their finger and commits the transition to scene
@@ -34,20 +53,353 @@
*/
val currentScene: SceneKey
+ /**
+ * The current set of overlays. This represents the set of overlays that will be visible on
+ * screen once all transitions are finished.
+ *
+ * @see MutableSceneTransitionLayoutState.showOverlay
+ * @see MutableSceneTransitionLayoutState.hideOverlay
+ * @see MutableSceneTransitionLayoutState.replaceOverlay
+ */
+ val currentOverlays: Set<OverlayKey>
+
/** The scene [currentScene] is idle. */
data class Idle(
override val currentScene: SceneKey,
- ) : TransitionState, ContentState.Idle<SceneKey>(currentScene)
+ override val currentOverlays: Set<OverlayKey> = emptySet(),
+ ) : TransitionState
- /** There is a transition animating between [fromScene] and [toScene]. */
- abstract class Transition(
- /** The scene this transition is starting from. Can't be the same as toScene */
- val fromScene: SceneKey,
+ sealed class Transition(
+ val fromContent: ContentKey,
+ val toContent: ContentKey,
+ val replacedTransition: Transition? = null,
+ ) : TransitionState {
+ /** A transition animating between [fromScene] and [toScene]. */
+ abstract class ChangeCurrentScene(
+ /** The scene this transition is starting from. Can't be the same as toScene */
+ val fromScene: SceneKey,
- /** The scene this transition is going to. Can't be the same as fromScene */
- val toScene: SceneKey,
+ /** The scene this transition is going to. Can't be the same as fromScene */
+ val toScene: SceneKey,
- /** The transition that `this` transition is replacing, if any. */
- replacedTransition: Transition? = null,
- ) : TransitionState, ContentState.Transition<SceneKey>(fromScene, toScene, replacedTransition)
+ /** The transition that `this` transition is replacing, if any. */
+ replacedTransition: Transition? = null,
+ ) : Transition(fromScene, toScene, replacedTransition) {
+ final override val currentOverlays: Set<OverlayKey>
+ get() {
+ // The set of overlays does not change in a [ChangeCurrentScene] transition.
+ return currentOverlaysWhenTransitionStarted
+ }
+ }
+
+ /**
+ * A transition that is animating one or more overlays and for which [currentOverlays] will
+ * change over the course of the transition.
+ */
+ sealed class OverlayTransition(
+ fromContent: ContentKey,
+ toContent: ContentKey,
+ replacedTransition: Transition?,
+ ) : Transition(fromContent, toContent, replacedTransition) {
+ final override val currentScene: SceneKey
+ get() {
+ // The current scene does not change during overlay transitions.
+ return currentSceneWhenTransitionStarted
+ }
+
+ // Note: We use deriveStateOf() so that the computed set is cached and reused when the
+ // inputs of the computations don't change, to avoid recomputing and allocating a new
+ // set every time currentOverlays is called (which is every frame and for each element).
+ final override val currentOverlays: Set<OverlayKey> by derivedStateOf {
+ computeCurrentOverlays()
+ }
+
+ protected abstract fun computeCurrentOverlays(): Set<OverlayKey>
+ }
+
+ /** The [overlay] is either showing from [fromOrToScene] or hiding into [fromOrToScene]. */
+ abstract class ShowOrHideOverlay(
+ val overlay: OverlayKey,
+ val fromOrToScene: SceneKey,
+ fromContent: ContentKey,
+ toContent: ContentKey,
+ replacedTransition: Transition? = null,
+ ) : OverlayTransition(fromContent, toContent, replacedTransition) {
+ /**
+ * Whether [overlay] is effectively shown. For instance, this will be `false` when
+ * starting a swipe transition to show [overlay] and will be `true` only once the swipe
+ * transition is committed.
+ */
+ protected abstract val isEffectivelyShown: Boolean
+
+ init {
+ check(
+ (fromContent == fromOrToScene && toContent == overlay) ||
+ (fromContent == overlay && toContent == fromOrToScene)
+ )
+ }
+
+ final override fun computeCurrentOverlays(): Set<OverlayKey> {
+ return if (isEffectivelyShown) {
+ currentOverlaysWhenTransitionStarted + overlay
+ } else {
+ currentOverlaysWhenTransitionStarted - overlay
+ }
+ }
+ }
+
+ /** We are transitioning from [fromOverlay] to [toOverlay]. */
+ abstract class ReplaceOverlay(
+ val fromOverlay: OverlayKey,
+ val toOverlay: OverlayKey,
+ replacedTransition: Transition? = null,
+ ) :
+ OverlayTransition(
+ fromContent = fromOverlay,
+ toContent = toOverlay,
+ replacedTransition,
+ ) {
+ /**
+ * The current effective overlay, either [fromOverlay] or [toOverlay]. For instance,
+ * this will be [fromOverlay] when starting a swipe transition that replaces
+ * [fromOverlay] by [toOverlay] and will [toOverlay] once the swipe transition is
+ * committed.
+ */
+ protected abstract val effectivelyShownOverlay: OverlayKey
+
+ init {
+ check(fromOverlay != toOverlay)
+ }
+
+ final override fun computeCurrentOverlays(): Set<OverlayKey> {
+ return when (effectivelyShownOverlay) {
+ fromOverlay ->
+ computeCurrentOverlays(include = fromOverlay, exclude = toOverlay)
+ toOverlay -> computeCurrentOverlays(include = toOverlay, exclude = fromOverlay)
+ else ->
+ error(
+ "effectivelyShownOverlay=$effectivelyShownOverlay, should be " +
+ "equal to fromOverlay=$fromOverlay or toOverlay=$toOverlay"
+ )
+ }
+ }
+
+ private fun computeCurrentOverlays(
+ include: OverlayKey,
+ exclude: OverlayKey
+ ): Set<OverlayKey> {
+ return buildSet {
+ addAll(currentOverlaysWhenTransitionStarted)
+ remove(exclude)
+ add(include)
+ }
+ }
+ }
+
+ /**
+ * The current scene and overlays observed right when this transition started. These are set
+ * when this transition is started in
+ * [com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl.startTransition].
+ */
+ internal lateinit var currentSceneWhenTransitionStarted: SceneKey
+ internal lateinit var currentOverlaysWhenTransitionStarted: Set<OverlayKey>
+
+ /**
+ * The key of this transition. This should usually be null, but it can be specified to use a
+ * specific set of transformations associated to this transition.
+ */
+ open val key: TransitionKey? = null
+
+ /**
+ * The progress of the transition. This is usually in the `[0; 1]` range, but it can also be
+ * less than `0` or greater than `1` when using transitions with a spring AnimationSpec or
+ * when flinging quickly during a swipe gesture.
+ */
+ abstract val progress: Float
+
+ /** The current velocity of [progress], in progress units. */
+ abstract val progressVelocity: Float
+
+ /** Whether the transition was triggered by user input rather than being programmatic. */
+ abstract val isInitiatedByUserInput: Boolean
+
+ /** Whether user input is currently driving the transition. */
+ abstract val isUserInputOngoing: Boolean
+
+ /**
+ * The progress of the preview transition. This is usually in the `[0; 1]` range, but it can
+ * also be less than `0` or greater than `1` when using transitions with a spring
+ * AnimationSpec or when flinging quickly during a swipe gesture.
+ */
+ internal open val previewProgress: Float = 0f
+
+ /** The current velocity of [previewProgress], in progress units. */
+ internal open val previewProgressVelocity: Float = 0f
+
+ /** Whether the transition is currently in the preview stage */
+ internal open val isInPreviewStage: Boolean = false
+
+ /**
+ * The current [TransformationSpecImpl] and [OverscrollSpecImpl] associated to this
+ * transition.
+ *
+ * Important: These will be set exactly once, when this transition is
+ * [started][MutableSceneTransitionLayoutStateImpl.startTransition].
+ */
+ internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
+ internal var previewTransformationSpec: TransformationSpecImpl? = null
+ private var fromOverscrollSpec: OverscrollSpecImpl? = null
+ private var toOverscrollSpec: OverscrollSpecImpl? = null
+
+ /** The current [OverscrollSpecImpl], if this transition is currently overscrolling. */
+ internal val currentOverscrollSpec: OverscrollSpecImpl?
+ get() {
+ if (this !is HasOverscrollProperties) return null
+ val progress = progress
+ val bouncingContent = bouncingContent
+ return when {
+ progress < 0f || bouncingContent == fromContent -> fromOverscrollSpec
+ progress > 1f || bouncingContent == toContent -> toOverscrollSpec
+ else -> null
+ }
+ }
+
+ /**
+ * An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden
+ * jump of values when this transitions interrupts another one.
+ */
+ private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null
+
+ /** The map of active links that connects this transition to other transitions. */
+ internal val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()
+
+ init {
+ check(fromContent != toContent)
+ check(
+ replacedTransition == null ||
+ (replacedTransition.fromContent == fromContent &&
+ replacedTransition.toContent == toContent)
+ )
+ }
+
+ /**
+ * Whether we are transitioning. If [from] or [to] is empty, we will also check that they
+ * match the contents we are animating from and/or to.
+ */
+ fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean {
+ return (from == null || fromContent == from) && (to == null || toContent == to)
+ }
+
+ /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */
+ fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean {
+ return isTransitioning(from = content, to = other) ||
+ isTransitioning(from = other, to = content)
+ }
+
+ /** Whether we are transitioning from or to [content]. */
+ fun isTransitioningFromOrTo(content: ContentKey): Boolean {
+ return fromContent == content || toContent == content
+ }
+
+ /**
+ * Force this transition to finish and animate to an [Idle] state.
+ *
+ * Important: Once this is called, the effective state of the transition should remain
+ * unchanged. For instance, in the case of a [TransitionState.Transition], its
+ * [currentScene][TransitionState.Transition.currentScene] should never change once [finish]
+ * is called.
+ *
+ * @return the [Job] that animates to the idle state. It can be used to wait until the
+ * animation is complete or cancel it to snap the animation. Calling [finish] multiple
+ * times will return the same [Job].
+ */
+ internal abstract fun finish(): Job
+
+ internal fun updateOverscrollSpecs(
+ fromSpec: OverscrollSpecImpl?,
+ toSpec: OverscrollSpecImpl?,
+ ) {
+ fromOverscrollSpec = fromSpec
+ toOverscrollSpec = toSpec
+ }
+
+ /** Returns if the [progress] value of this transition can go beyond range `[0; 1]` */
+ internal fun isWithinProgressRange(progress: Float): Boolean {
+ // If the properties are missing we assume that every [Transition] can overscroll
+ if (this !is HasOverscrollProperties) return true
+ // [OverscrollSpec] for the current scene, even if it hasn't started overscrolling yet.
+ val specForCurrentScene =
+ when {
+ progress <= 0f -> fromOverscrollSpec
+ progress >= 1f -> toOverscrollSpec
+ else -> null
+ } ?: return true
+
+ return specForCurrentScene.transformationSpec.transformations.isNotEmpty()
+ }
+
+ internal open fun interruptionProgress(
+ layoutImpl: SceneTransitionLayoutImpl,
+ ): Float {
+ if (!layoutImpl.state.enableInterruptions) {
+ return 0f
+ }
+
+ if (replacedTransition != null) {
+ return replacedTransition.interruptionProgress(layoutImpl)
+ }
+
+ fun create(): Animatable<Float, AnimationVector1D> {
+ val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold)
+ layoutImpl.coroutineScope.launch {
+ val swipeSpec = layoutImpl.state.transitions.defaultSwipeSpec
+ val progressSpec =
+ spring(
+ stiffness = swipeSpec.stiffness,
+ dampingRatio = swipeSpec.dampingRatio,
+ visibilityThreshold = ProgressVisibilityThreshold,
+ )
+ animatable.animateTo(0f, progressSpec)
+ }
+
+ return animatable
+ }
+
+ val animatable = interruptionDecay ?: create().also { interruptionDecay = it }
+ return animatable.value
+ }
+ }
+
+ interface HasOverscrollProperties {
+ /**
+ * The position of the [Transition.toContent].
+ *
+ * Used to understand the direction of the overscroll.
+ */
+ val isUpOrLeft: Boolean
+
+ /**
+ * The relative orientation between [Transition.fromContent] and [Transition.toContent].
+ *
+ * Used to understand the orientation of the overscroll.
+ */
+ val orientation: Orientation
+
+ /**
+ * Scope which can be used in the Overscroll DSL to define a transformation based on the
+ * distance between [Transition.fromContent] and [Transition.toContent].
+ */
+ val overscrollScope: OverscrollScope
+
+ /**
+ * The content (scene or overlay) around which the transition is currently bouncing. When
+ * not `null`, this transition is currently oscillating around this content and will soon
+ * settle to that content.
+ */
+ val bouncingContent: ContentKey?
+
+ companion object {
+ const val DistanceUnspecified = 0f
+ }
+ }
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
index 538ce79..c5a3067c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -22,7 +22,7 @@
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
-import com.android.compose.animation.scene.content.state.ContentState
+import com.android.compose.animation.scene.content.state.TransitionState
/** Anchor the size of an element to the size of another element. */
internal class AnchoredSize(
@@ -36,7 +36,7 @@
content: ContentKey,
element: Element,
stateInContent: Element.State,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
value: IntSize,
): IntSize {
fun anchorSizeIn(content: ContentKey): IntSize {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
index 258f541..05878c2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -23,7 +23,7 @@
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
-import com.android.compose.animation.scene.content.state.ContentState
+import com.android.compose.animation.scene.content.state.TransitionState
/** Anchor the translation of an element to another element. */
internal class AnchoredTranslate(
@@ -35,7 +35,7 @@
content: ContentKey,
element: Element,
stateInContent: Element.State,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
value: Offset,
): Offset {
fun throwException(content: ContentKey?): Nothing {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
index be8dac21..7f86479 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
@@ -22,7 +22,7 @@
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.Scale
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
-import com.android.compose.animation.scene.content.state.ContentState
+import com.android.compose.animation.scene.content.state.TransitionState
/**
* Scales the draw size of an element. Note this will only scale the draw inside of an element,
@@ -40,7 +40,7 @@
content: ContentKey,
element: Element,
stateInContent: Element.State,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
value: Scale,
): Scale {
return Scale(scaleX, scaleY, pivot)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index d72e43a..a32c7dd 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -22,7 +22,7 @@
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
-import com.android.compose.animation.scene.content.state.ContentState
+import com.android.compose.animation.scene.content.state.TransitionState
/** Translate an element from an edge of the layout. */
internal class EdgeTranslate(
@@ -35,7 +35,7 @@
content: ContentKey,
element: Element,
stateInContent: Element.State,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
value: Offset
): Offset {
val sceneSize = layoutImpl.content(content).targetSize
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
index 92ae30f8..4528eef 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -20,7 +20,7 @@
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
-import com.android.compose.animation.scene.content.state.ContentState
+import com.android.compose.animation.scene.content.state.TransitionState
/** Fade an element in or out. */
internal class Fade(
@@ -31,7 +31,7 @@
content: ContentKey,
element: Element,
stateInContent: Element.State,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
value: Float
): Float {
// Return the alpha value of [element] either when it starts fading in or when it finished
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
index e8515dc..5f3fdaf 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -21,7 +21,7 @@
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
-import com.android.compose.animation.scene.content.state.ContentState
+import com.android.compose.animation.scene.content.state.TransitionState
import kotlin.math.roundToInt
/**
@@ -38,7 +38,7 @@
content: ContentKey,
element: Element,
stateInContent: Element.State,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
value: IntSize,
): IntSize {
return IntSize(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index eda8ede..505ad04 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -25,7 +25,7 @@
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
-import com.android.compose.animation.scene.content.state.ContentState
+import com.android.compose.animation.scene.content.state.TransitionState
/** A transformation applied to one or more elements during a transition. */
sealed interface Transformation {
@@ -66,7 +66,7 @@
content: ContentKey,
element: Element,
stateInContent: Element.State,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
value: T,
): T
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
index fab4ced..59bca50 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -24,7 +24,7 @@
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.OverscrollScope
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
-import com.android.compose.animation.scene.content.state.ContentState
+import com.android.compose.animation.scene.content.state.TransitionState
internal class Translate(
override val matcher: ElementMatcher,
@@ -36,7 +36,7 @@
content: ContentKey,
element: Element,
stateInContent: Element.State,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
value: Offset,
): Offset {
return with(layoutImpl.density) {
@@ -58,13 +58,13 @@
content: ContentKey,
element: Element,
stateInContent: Element.State,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
value: Offset,
): Offset {
// As this object is created by OverscrollBuilderImpl and we retrieve the current
// OverscrollSpec only when the transition implements HasOverscrollProperties, we can assume
// that this method was invoked after performing this check.
- val overscrollProperties = transition as ContentState.HasOverscrollProperties
+ val overscrollProperties = transition as TransitionState.HasOverscrollProperties
return Offset(
x = value.x + overscrollProperties.overscrollScope.x(),
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
index 23bcf10..89b0040 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
@@ -23,11 +23,11 @@
/** A linked transition which is driven by a [originalTransition]. */
internal class LinkedTransition(
- private val originalTransition: TransitionState.Transition,
+ private val originalTransition: TransitionState.Transition.ChangeCurrentScene,
fromScene: SceneKey,
toScene: SceneKey,
override val key: TransitionKey? = null,
-) : TransitionState.Transition(fromScene, toScene) {
+) : TransitionState.Transition.ChangeCurrentScene(fromScene, toScene) {
override val currentScene: SceneKey
get() {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
index c0c40dd..c29bf21 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
@@ -49,7 +49,9 @@
error("From and To can't be the same")
}
- internal fun isMatchingLink(transition: TransitionState.Transition): Boolean {
+ internal fun isMatchingLink(
+ transition: TransitionState.Transition.ChangeCurrentScene,
+ ): Boolean {
return (sourceFrom == null || sourceFrom == transition.fromScene) &&
(sourceTo == null || sourceTo == transition.toScene)
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
index 01895c9..8ebb42a 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
@@ -168,10 +168,7 @@
assertThat(lastValueInTo).isEqualTo(expectedValues)
}
- after {
- assertThat(lastValueInFrom).isEqualTo(toValues)
- assertThat(lastValueInTo).isEqualTo(toValues)
- }
+ after { assertThat(lastValueInTo).isEqualTo(toValues) }
}
}
@@ -229,10 +226,7 @@
assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.75f))
}
- after {
- assertThat(lastValueInFrom).isEqualTo(toValues)
- assertThat(lastValueInTo).isEqualTo(toValues)
- }
+ after { assertThat(lastValueInTo).isEqualTo(toValues) }
}
}
@@ -288,10 +282,7 @@
assertThat(lastValueInTo).isEqualTo(expectedValues)
}
- after {
- assertThat(lastValueInFrom).isEqualTo(toValues)
- assertThat(lastValueInTo).isEqualTo(toValues)
- }
+ after { assertThat(lastValueInTo).isEqualTo(toValues) }
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 6360f85..25be3f9 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -65,19 +65,19 @@
var layoutDirection = LayoutDirection.Rtl
set(value) {
field = value
- layoutImpl.updateScenes(scenesBuilder, layoutDirection)
+ layoutImpl.updateContents(scenesBuilder, layoutDirection)
}
var mutableUserActionsA = mapOf(Swipe.Up to SceneB, Swipe.Down to SceneC)
set(value) {
field = value
- layoutImpl.updateScenes(scenesBuilder, layoutDirection)
+ layoutImpl.updateContents(scenesBuilder, layoutDirection)
}
var mutableUserActionsB = mapOf(Swipe.Up to SceneC, Swipe.Down to SceneA)
set(value) {
field = value
- layoutImpl.updateScenes(scenesBuilder, layoutDirection)
+ layoutImpl.updateContents(scenesBuilder, layoutDirection)
}
private val scenesBuilder: SceneTransitionLayoutScope.() -> Unit = {
@@ -182,7 +182,7 @@
progress: Float? = null,
isUserInputOngoing: Boolean? = null
): Transition {
- val transition = assertThat(transitionState).isTransition()
+ val transition = assertThat(transitionState).isSceneTransition()
currentScene?.let { assertThat(transition).hasCurrentScene(it) }
fromScene?.let { assertThat(transition).hasFromScene(it) }
toScene?.let { assertThat(transition).hasToScene(it) }
@@ -1075,7 +1075,7 @@
val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
val dragController = onDragStarted(startedPosition = middle, overSlop = up(0.5f))
- val transition = assertThat(transitionState).isTransition()
+ val transition = assertThat(transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(transition).hasProgress(0.5f)
@@ -1101,7 +1101,7 @@
val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
val dragController = onDragStarted(startedPosition = middle, overSlop = down(0.5f))
- val transition = assertThat(transitionState).isTransition()
+ val transition = assertThat(transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneC)
assertThat(transition).hasProgress(0.5f)
@@ -1127,7 +1127,7 @@
val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
val dragController = onDragStarted(startedPosition = middle, overSlop = up(1.5f))
- val transition = assertThat(transitionState).isTransition()
+ val transition = assertThat(transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(transition).hasProgress(1.5f)
@@ -1154,7 +1154,7 @@
val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
val dragController = onDragStarted(startedPosition = middle, overSlop = down(1.5f))
- val transition = assertThat(transitionState).isTransition()
+ val transition = assertThat(transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneC)
assertThat(transition).hasProgress(1.5f)
@@ -1182,7 +1182,7 @@
val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
val dragController = onDragStarted(startedPosition = middle, overSlop = down(1f))
- val transition = assertThat(transitionState).isTransition()
+ val transition = assertThat(transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(transition).hasProgress(-1f)
@@ -1210,7 +1210,7 @@
val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
val dragController = onDragStarted(startedPosition = middle, overSlop = up(1f))
- val transition = assertThat(transitionState).isTransition()
+ val transition = assertThat(transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneC)
assertThat(transition).hasProgress(-1f)
@@ -1267,12 +1267,12 @@
@Test
fun interceptingTransitionReplacesCurrentTransition() = runGestureTest {
val controller = onDragStarted(overSlop = up(fractionOfScreen = 0.5f))
- val transition = assertThat(layoutState.transitionState).isTransition()
+ val transition = assertThat(layoutState.transitionState).isSceneTransition()
controller.onDragStopped(velocity = 0f)
// Intercept the transition.
onDragStartedImmediately()
- val newTransition = assertThat(layoutState.transitionState).isTransition()
+ val newTransition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(newTransition).isNotSameInstanceAs(transition)
assertThat(newTransition.replacedTransition).isSameInstanceAs(transition)
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 20b9b49..682fe95 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -735,7 +735,7 @@
val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag)
fooElement.assertTopPositionInRootIsEqualTo(0.dp)
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).isNotNull()
assertThat(transition).hasProgress(0.5f)
assertThat(animatedFloat).isEqualTo(50f)
@@ -822,7 +822,7 @@
moveBy(Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
}
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasOverscrollSpec()
assertThat(transition).hasProgress(-0.5f)
fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f)
@@ -905,7 +905,7 @@
}
}
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasProgress(0.5f)
fooElement.assertTopPositionInRootIsEqualTo(translateY * 0.5f)
}
@@ -939,7 +939,7 @@
moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
}
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(animatedFloat).isEqualTo(100f)
// Scroll 150% (100% scroll + 50% overscroll)
@@ -992,7 +992,7 @@
moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000)
}
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(animatedFloat).isEqualTo(100f)
// Scroll 200% (100% scroll + 100% overscroll)
@@ -1039,7 +1039,7 @@
moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000)
}
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(animatedFloat).isEqualTo(100f)
// Scroll 200% (100% scroll + 100% overscroll)
@@ -1083,7 +1083,7 @@
moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000)
}
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(animatedFloat).isEqualTo(100f)
// Scroll 200% (100% scroll + 100% overscroll)
@@ -1143,7 +1143,7 @@
moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
}
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
// Scroll 150% (100% scroll + 50% overscroll)
assertThat(transition).hasProgress(1.5f)
@@ -1160,7 +1160,7 @@
assertThat(transition.progress).isLessThan(1f)
assertThat(transition).hasOverscrollSpec()
- assertThat(transition).hasBouncingScene(transition.toScene)
+ assertThat(transition).hasBouncingContent(transition.toContent)
assertThat(animatedFloat).isEqualTo(100f)
}
@@ -1243,13 +1243,15 @@
val transitions = state.currentTransitions
assertThat(transitions).hasSize(2)
- assertThat(transitions[0]).hasFromScene(SceneA)
- assertThat(transitions[0]).hasToScene(SceneB)
- assertThat(transitions[0]).hasProgress(0f)
+ val firstTransition = assertThat(transitions[0]).isSceneTransition()
+ assertThat(firstTransition).hasFromScene(SceneA)
+ assertThat(firstTransition).hasToScene(SceneB)
+ assertThat(firstTransition).hasProgress(0f)
- assertThat(transitions[1]).hasFromScene(SceneB)
- assertThat(transitions[1]).hasToScene(SceneC)
- assertThat(transitions[1]).hasProgress(0f)
+ val secondTransition = assertThat(transitions[1]).isSceneTransition()
+ assertThat(secondTransition).hasFromScene(SceneB)
+ assertThat(secondTransition).hasToScene(SceneC)
+ assertThat(secondTransition).hasProgress(0f)
// First frame: both are at x = 0dp. For the whole transition, Foo is at y = 0dp and Bar is
// at y = layoutSize - elementSoze = 100dp.
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
index ca72181..f4e60a2 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
@@ -69,7 +69,7 @@
interruptionHandler =
object : InterruptionHandler {
override fun onInterruption(
- interrupted: TransitionState.Transition,
+ interrupted: TransitionState.Transition.ChangeCurrentScene,
newTargetScene: SceneKey
): InterruptionResult {
return InterruptionResult(
@@ -104,7 +104,7 @@
interruptionHandler =
object : InterruptionHandler {
override fun onInterruption(
- interrupted: TransitionState.Transition,
+ interrupted: TransitionState.Transition.ChangeCurrentScene,
newTargetScene: SceneKey
): InterruptionResult {
return InterruptionResult(
@@ -198,7 +198,7 @@
companion object {
val FromToCurrentTriple =
Correspondence.transforming(
- { transition: TransitionState.Transition? ->
+ { transition: TransitionState.Transition.ChangeCurrentScene? ->
Triple(transition?.fromScene, transition?.toScene, transition?.currentScene)
},
"(from, to, current) triple"
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
index 520e759..a549d03 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
@@ -45,7 +45,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
-import com.android.compose.animation.scene.content.state.ContentState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.test.assertSizeIsEqualTo
@@ -107,7 +106,7 @@
rule
.onNode(
hasText("count: 3") and
- hasParent(isElement(TestElements.Foo, scene = SceneA))
+ hasParent(isElement(TestElements.Foo, content = SceneA))
)
.assertExists()
.assertIsNotDisplayed()
@@ -115,7 +114,7 @@
rule
.onNode(
hasText("count: 0") and
- hasParent(isElement(TestElements.Foo, scene = SceneB))
+ hasParent(isElement(TestElements.Foo, content = SceneB))
)
.assertIsDisplayed()
.assertSizeIsEqualTo(75.dp, 75.dp)
@@ -160,11 +159,11 @@
override fun contentDuringTransition(
element: ElementKey,
- transition: ContentState.Transition<*>,
+ transition: TransitionState.Transition,
fromContentZIndex: Float,
toContentZIndex: Float
): ContentKey {
- transition as TransitionState.Transition
+ transition as TransitionState.Transition.ChangeCurrentScene
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(fromContentZIndex).isEqualTo(0)
@@ -214,7 +213,7 @@
rule
.onNode(
hasText("count: 3") and
- hasParent(isElement(TestElements.Foo, scene = SceneA))
+ hasParent(isElement(TestElements.Foo, content = SceneA))
)
.assertIsDisplayed()
.assertSizeIsEqualTo(75.dp, 75.dp)
@@ -235,7 +234,7 @@
rule
.onNode(
hasText("count: 3") and
- hasParent(isElement(TestElements.Foo, scene = SceneB))
+ hasParent(isElement(TestElements.Foo, content = SceneB))
)
.assertIsDisplayed()
@@ -325,7 +324,7 @@
fun movableElementScopeExtendsBoxScope() {
val key = MovableElementKey("Foo", contents = setOf(SceneA))
rule.setContent {
- TestContentScope {
+ TestContentScope(currentScene = SceneA) {
MovableElement(key, Modifier.size(200.dp)) {
content {
Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd))
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
index ccefe3d..d58a0a3 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
@@ -125,7 +125,7 @@
scrollUp(percent = 0.5f)
// STL will start a transition with the remaining scroll
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasProgress(0.5f)
scrollUp(percent = 1f)
@@ -156,7 +156,7 @@
// Start a new gesture
pointerDownAndScrollTouchSlop()
scrollUp(percent = 0.5f)
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasProgress(0.5f)
pointerUp()
@@ -181,7 +181,7 @@
// Reach the end of the scrollable element
canScroll = false
scrollUp(percent = 0.5f)
- val transition1 = assertThat(state.transitionState).isTransition()
+ val transition1 = assertThat(state.transitionState).isSceneTransition()
assertThat(transition1).hasProgress(0.5f)
pointerUp()
@@ -192,7 +192,7 @@
// Start a new gesture
pointerDownAndScrollTouchSlop()
scrollUp(percent = 0.5f)
- val transition2 = assertThat(state.transitionState).isTransition()
+ val transition2 = assertThat(state.transitionState).isSceneTransition()
assertThat(transition2).hasProgress(0.5f)
pointerUp()
@@ -215,7 +215,7 @@
// Reach the end of the scrollable element
canScroll = false
scrollUp(percent = 0.5f)
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasProgress(0.5f)
pointerUp()
@@ -244,7 +244,7 @@
scrollUp(percent = 0.5f)
// EdgeAlways always consume the remaining scroll, EdgeNoPreview does not.
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasProgress(0.5f)
}
@@ -278,7 +278,7 @@
scrollUp(percent = 0.2f)
// STL can only start the transition if it has reset the amount of scroll consumed.
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasProgress(0.2f)
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
new file mode 100644
index 0000000..d4391e0
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
@@ -0,0 +1,319 @@
+/*
+ * 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.compose.animation.scene
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestOverlays.OverlayA
+import com.android.compose.animation.scene.TestOverlays.OverlayB
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.test.assertSizeIsEqualTo
+import kotlinx.coroutines.CoroutineScope
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class OverlayTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Composable
+ private fun ContentScope.Foo() {
+ Box(Modifier.element(TestElements.Foo).size(100.dp))
+ }
+
+ @Test
+ fun showThenHideOverlay() {
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ lateinit var coroutineScope: CoroutineScope
+ rule.setContent {
+ coroutineScope = rememberCoroutineScope()
+ SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
+ overlay(OverlayA) { Foo() }
+ }
+ }
+
+ // Initial state: overlay A is not shown, so Foo is displayed at the top left in scene A.
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertIsDisplayed()
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+
+ // Show overlay A: Foo is now centered on screen and placed in overlay A. It is not placed
+ // in scene A.
+ rule.runOnUiThread { state.showOverlay(OverlayA, coroutineScope) }
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+
+ // Hide overlay A: back to initial state, top-left in scene A.
+ rule.runOnUiThread { state.hideOverlay(OverlayA, coroutineScope) }
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertIsDisplayed()
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+ }
+
+ @Test
+ fun multipleOverlays() {
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ lateinit var coroutineScope: CoroutineScope
+ rule.setContent {
+ coroutineScope = rememberCoroutineScope()
+ SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
+ overlay(OverlayA) { Foo() }
+ overlay(OverlayB) { Foo() }
+ }
+ }
+
+ // Initial state.
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertIsDisplayed()
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+ rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist()
+
+ // Show overlay A.
+ rule.runOnUiThread { state.showOverlay(OverlayA, coroutineScope) }
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+ rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist()
+
+ // Replace overlay A by overlay B.
+ rule.runOnUiThread { state.replaceOverlay(OverlayA, OverlayB, coroutineScope) }
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayB))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+
+ // Show overlay A: Foo is still placed in B because it has a higher zIndex, but it now
+ // exists in A as well.
+ rule.runOnUiThread { state.showOverlay(OverlayA, coroutineScope) }
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayB))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+
+ // Hide overlay B.
+ rule.runOnUiThread { state.hideOverlay(OverlayB, coroutineScope) }
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+ rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist()
+
+ // Hide overlay A.
+ rule.runOnUiThread { state.hideOverlay(OverlayA, coroutineScope) }
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertIsDisplayed()
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+ rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist()
+ }
+
+ @Test
+ fun movableElement() {
+ val key = MovableElementKey("MovableBar", contents = setOf(SceneA, OverlayA, OverlayB))
+ val elementChildTag = "elementChildTag"
+
+ fun elementChild(content: ContentKey) = hasTestTag(elementChildTag) and inContent(content)
+
+ @Composable
+ fun ContentScope.MovableBar() {
+ MovableElement(key, Modifier) {
+ content { Box(Modifier.testTag(elementChildTag).size(100.dp)) }
+ }
+ }
+
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ lateinit var coroutineScope: CoroutineScope
+ rule.setContent {
+ coroutineScope = rememberCoroutineScope()
+ SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ scene(SceneA) { Box(Modifier.fillMaxSize()) { MovableBar() } }
+ overlay(OverlayA) { MovableBar() }
+ overlay(OverlayB) { MovableBar() }
+ }
+ }
+
+ // Initial state.
+ rule
+ .onNode(elementChild(content = SceneA))
+ .assertIsDisplayed()
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ rule.onNode(elementChild(content = OverlayA)).assertDoesNotExist()
+ rule.onNode(elementChild(content = OverlayB)).assertDoesNotExist()
+
+ // Show overlay A: movable element child only exists (is only composed) in overlay A.
+ rule.runOnUiThread { state.showOverlay(OverlayA, coroutineScope) }
+ rule.onNode(elementChild(content = SceneA)).assertDoesNotExist()
+ rule
+ .onNode(elementChild(content = OverlayA))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+ rule.onNode(elementChild(content = OverlayB)).assertDoesNotExist()
+
+ // Replace overlay A by overlay B: element child is only in overlay B.
+ rule.runOnUiThread { state.replaceOverlay(OverlayA, OverlayB, coroutineScope) }
+ rule.onNode(elementChild(content = SceneA)).assertDoesNotExist()
+ rule.onNode(elementChild(content = OverlayA)).assertDoesNotExist()
+ rule
+ .onNode(elementChild(content = OverlayB))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+
+ // Show overlay A: element child still only exists in overlay B because it has a higher
+ // zIndex.
+ rule.runOnUiThread { state.showOverlay(OverlayA, coroutineScope) }
+ rule.onNode(elementChild(content = SceneA)).assertDoesNotExist()
+ rule.onNode(elementChild(content = OverlayA)).assertDoesNotExist()
+ rule
+ .onNode(elementChild(content = OverlayB))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+
+ // Hide overlay B: element child is in overlay A.
+ rule.runOnUiThread { state.hideOverlay(OverlayB, coroutineScope) }
+ rule.onNode(elementChild(content = SceneA)).assertDoesNotExist()
+ rule
+ .onNode(elementChild(content = OverlayA))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+ rule.onNode(elementChild(content = OverlayB)).assertDoesNotExist()
+
+ // Hide overlay A: element child is in scene A.
+ rule.runOnUiThread { state.hideOverlay(OverlayA, coroutineScope) }
+ rule
+ .onNode(elementChild(content = SceneA))
+ .assertIsDisplayed()
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ rule.onNode(elementChild(content = OverlayA)).assertDoesNotExist()
+ rule.onNode(elementChild(content = OverlayB)).assertDoesNotExist()
+ }
+
+ @Test
+ fun overlayAlignment() {
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(SceneA, initialOverlays = setOf(OverlayA))
+ }
+ var alignment by mutableStateOf(Alignment.Center)
+ rule.setContent {
+ SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
+ overlay(OverlayA, alignment) { Foo() }
+ }
+ }
+
+ // Initial state: 100x100dp centered in 200x200dp layout.
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+
+ // BottomStart.
+ alignment = Alignment.BottomStart
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 100.dp)
+
+ // TopEnd.
+ alignment = Alignment.TopEnd
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(100.dp)
+ .assertPositionInRootIsEqualTo(100.dp, 0.dp)
+ }
+
+ @Test
+ fun overlayMaxSizeIsCurrentSceneSize() {
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(SceneA, initialOverlays = setOf(OverlayA))
+ }
+
+ val contentTag = "overlayContent"
+ rule.setContent {
+ SceneTransitionLayout(state) {
+ scene(SceneA) { Box(Modifier.size(100.dp)) { Foo() } }
+ overlay(OverlayA) { Box(Modifier.testTag(contentTag).fillMaxSize()) }
+ }
+ }
+
+ // Max overlay size is the size of the layout without overlays, not the (max) possible size
+ // of the layout.
+ rule.onNodeWithTag(contentTag).assertSizeIsEqualTo(100.dp)
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
index 0eaecb0..00c7588 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
@@ -76,7 +76,7 @@
dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f))
}
- val transition = assertThat(layoutState.transitionState).isTransition()
+ val transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(transition).hasProgress(0.4f)
@@ -124,7 +124,7 @@
dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f))
}
- val transition = assertThat(layoutState.transitionState).isTransition()
+ val transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(transition).hasPreviewProgress(0.4f)
@@ -178,13 +178,13 @@
val dispatcher = rule.activity.onBackPressedDispatcher
rule.runOnUiThread { dispatcher.dispatchOnBackStarted(backEvent()) }
- val predictiveTransition = assertThat(layoutState.transitionState).isTransition()
+ val predictiveTransition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(predictiveTransition).hasFromScene(SceneA)
assertThat(predictiveTransition).hasToScene(SceneB)
// Start a new transition to C.
rule.runOnUiThread { layoutState.setTargetScene(SceneC, coroutineScope) }
- val newTransition = assertThat(layoutState.transitionState).isTransition()
+ val newTransition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(newTransition).hasFromScene(SceneA)
assertThat(newTransition).hasToScene(SceneC)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index c8ac580..3422a8e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -478,7 +478,7 @@
overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
}
)
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasNoOverscrollSpec()
// overscroll for SceneA is NOT defined
@@ -510,7 +510,7 @@
}
)
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasNoOverscrollSpec()
// overscroll for SceneA is defined
@@ -539,7 +539,7 @@
sceneTransitions = transitions {}
)
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasNoOverscrollSpec()
// overscroll for SceneA is NOT defined
@@ -642,7 +642,7 @@
// Transition to B.
state.setTargetScene(SceneB, coroutineScope = this)
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasCurrentScene(SceneB)
// Snap to C.
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 84bcc28f..b8e13da 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -179,7 +179,7 @@
// Change the current scene.
currentScene = SceneB
- val transition = assertThat(layoutState.transitionState).isTransition()
+ val transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(transition).hasProgress(0f)
@@ -241,7 +241,7 @@
// 100.dp. We pause at the middle of the transition, so it should now be 75.dp given that we
// use a linear interpolator. Foo was at (x = layoutSize - 50dp, y = 0) in SceneA and is
// going to (x = 0, y = 0), so the offset should now be half what it was.
- var transition = assertThat(layoutState.transitionState).isTransition()
+ var transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasProgress(0.5f)
sharedFoo.assertWidthIsEqualTo(75.dp)
sharedFoo.assertHeightIsEqualTo(75.dp)
@@ -269,7 +269,7 @@
val expectedSize = 100.dp + (150.dp - 100.dp) * interpolatedProgress
sharedFoo = rule.onNode(isElement(TestElements.Foo, SceneC))
- transition = assertThat(layoutState.transitionState).isTransition()
+ transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasProgress(interpolatedProgress)
sharedFoo.assertWidthIsEqualTo(expectedSize)
sharedFoo.assertHeightIsEqualTo(expectedSize)
@@ -399,7 +399,7 @@
rule.mainClock.advanceTimeBy(duration / 2)
rule.waitForIdle()
- var transition = assertThat(state.transitionState).isTransition()
+ var transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasProgress(0.5f)
// A and B are composed.
@@ -412,7 +412,7 @@
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
- transition = assertThat(state.transitionState).isTransition()
+ transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasProgress(0f)
// A, B and C are composed.
@@ -500,4 +500,19 @@
assertThat(keyInB).isEqualTo(SceneB)
assertThat(keyInC).isEqualTo(SceneC)
}
+
+ @Test
+ fun overlaysMapIsNotAllocatedWhenNoOverlayIsDefined() {
+ lateinit var layoutImpl: SceneTransitionLayoutImpl
+ rule.setContent {
+ SceneTransitionLayoutForTesting(
+ remember { MutableSceneTransitionLayoutState(SceneA) },
+ onLayoutImpl = { layoutImpl = it },
+ ) {
+ scene(SceneA) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ assertThat(layoutImpl.overlaysOrNullForTest()).isNull()
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 04a9380..06799bc 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -155,7 +155,7 @@
// We should be at a progress = 55dp / LayoutWidth given that we use the layout size in
// the gesture axis as swipe distance.
- var transition = assertThat(layoutState.transitionState).isTransition()
+ var transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(transition).hasCurrentScene(SceneA)
@@ -165,7 +165,7 @@
// Release the finger. We should now be animating back to A (currentScene = SceneA) given
// that 55dp < positional threshold.
rule.onRoot().performTouchInput { up() }
- transition = assertThat(layoutState.transitionState).isTransition()
+ transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(transition).hasCurrentScene(SceneA)
@@ -185,7 +185,7 @@
}
// Drag is in progress, so currentScene = SceneA and progress = 56dp / LayoutHeight
- transition = assertThat(layoutState.transitionState).isTransition()
+ transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(TestScenes.SceneC)
assertThat(transition).hasCurrentScene(SceneA)
@@ -195,7 +195,7 @@
// Release the finger. We should now be animating to C (currentScene = SceneC) given
// that 56dp >= positional threshold.
rule.onRoot().performTouchInput { up() }
- transition = assertThat(layoutState.transitionState).isTransition()
+ transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(TestScenes.SceneC)
assertThat(transition).hasCurrentScene(TestScenes.SceneC)
@@ -236,7 +236,7 @@
// We should be animating back to A (currentScene = SceneA) given that 124 dp/s < velocity
// threshold.
- var transition = assertThat(layoutState.transitionState).isTransition()
+ var transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(transition).hasCurrentScene(SceneA)
@@ -260,7 +260,7 @@
}
// We should be animating to C (currentScene = SceneC).
- transition = assertThat(layoutState.transitionState).isTransition()
+ transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(TestScenes.SceneC)
assertThat(transition).hasCurrentScene(TestScenes.SceneC)
@@ -297,7 +297,7 @@
}
// We are transitioning to B because we used 2 fingers.
- val transition = assertThat(layoutState.transitionState).isTransition()
+ val transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(TestScenes.SceneC)
assertThat(transition).hasToScene(SceneB)
@@ -332,7 +332,7 @@
}
// We are transitioning to B (and not A) because we started from the top edge.
- var transition = assertThat(layoutState.transitionState).isTransition()
+ var transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(TestScenes.SceneC)
assertThat(transition).hasToScene(SceneB)
@@ -350,7 +350,7 @@
}
// We are transitioning to B (and not A) because we started from the left edge.
- transition = assertThat(layoutState.transitionState).isTransition()
+ transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(TestScenes.SceneC)
assertThat(transition).hasToScene(SceneB)
@@ -406,7 +406,7 @@
}
// We should be at 50%
- val transition = assertThat(layoutState.transitionState).isTransition()
+ val transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).isNotNull()
assertThat(transition).hasProgress(0.5f)
}
@@ -427,7 +427,7 @@
}
// We should still correctly compute that we are swiping down to scene C.
- var transition = assertThat(layoutState.transitionState).isTransition()
+ var transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasToScene(TestScenes.SceneC)
// Release the finger, animating back to scene A.
@@ -443,7 +443,7 @@
}
// We should still correctly compute that we are swiping up to scene B.
- transition = assertThat(layoutState.transitionState).isTransition()
+ transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasToScene(SceneB)
// Release the finger, animating back to scene A.
@@ -459,7 +459,7 @@
}
// We should still correctly compute that we are swiping down to scene B.
- transition = assertThat(layoutState.transitionState).isTransition()
+ transition = assertThat(layoutState.transitionState).isSceneTransition()
assertThat(transition).hasToScene(SceneB)
}
@@ -597,7 +597,7 @@
}
rule.waitForIdle()
- val transition = assertThat(state.transitionState).isTransition()
+ val transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(transition).hasProgress(0.5f, tolerance = 0.01f)
@@ -686,7 +686,7 @@
}
// Scene B should come from the right (end) edge.
- var transition = assertThat(state.transitionState).isTransition()
+ var transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
rule
@@ -707,7 +707,7 @@
}
// Scene C should come from the left (start) edge.
- transition = assertThat(state.transitionState).isTransition()
+ transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneC)
rule
@@ -761,7 +761,7 @@
}
// Scene C should come from the right (start) edge.
- var transition = assertThat(state.transitionState).isTransition()
+ var transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneC)
rule
@@ -782,7 +782,7 @@
}
// Scene C should come from the left (end) edge.
- transition = assertThat(state.transitionState).isTransition()
+ transition = assertThat(state.transitionState).isSceneTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
rule
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
index e4e4108..1f7fe37 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
@@ -17,7 +17,6 @@
package com.android.compose.animation.scene
import androidx.compose.foundation.gestures.Orientation
-import com.android.compose.animation.scene.content.state.ContentState
import com.android.compose.animation.scene.content.state.TransitionState
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -43,10 +42,10 @@
orientation: Orientation = Orientation.Horizontal,
onFinish: ((TransitionState.Transition) -> Job)? = null,
replacedTransition: TransitionState.Transition? = null,
-): TransitionState.Transition {
+): TransitionState.Transition.ChangeCurrentScene {
return object :
- TransitionState.Transition(from, to, replacedTransition),
- ContentState.HasOverscrollProperties {
+ TransitionState.Transition.ChangeCurrentScene(from, to, replacedTransition),
+ TransitionState.HasOverscrollProperties {
override val currentScene: SceneKey
get() = current()
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
index a12ab78..a98bd76 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
@@ -16,9 +16,9 @@
package com.android.compose.animation.scene.subjects
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.OverscrollSpec
import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.content.state.ContentState
import com.android.compose.animation.scene.content.state.TransitionState
import com.google.common.truth.Fact.simpleFact
import com.google.common.truth.FailureMetadata
@@ -31,9 +31,9 @@
return Truth.assertAbout(TransitionStateSubject.transitionStates()).that(state)
}
-/** Assert on a [TransitionState.Transition]. */
-fun assertThat(transitions: TransitionState.Transition): TransitionSubject {
- return Truth.assertAbout(TransitionSubject.transitions()).that(transitions)
+/** Assert on a [TransitionState.Transition.ChangeCurrentScene]. */
+fun assertThat(transition: TransitionState.Transition.ChangeCurrentScene): SceneTransitionSubject {
+ return Truth.assertAbout(SceneTransitionSubject.transitions()).that(transition)
}
class TransitionStateSubject
@@ -53,12 +53,14 @@
return actual as TransitionState.Idle
}
- fun isTransition(): TransitionState.Transition {
- if (actual !is TransitionState.Transition) {
- failWithActual(simpleFact("expected to be TransitionState.Transition"))
+ fun isSceneTransition(): TransitionState.Transition.ChangeCurrentScene {
+ if (actual !is TransitionState.Transition.ChangeCurrentScene) {
+ failWithActual(
+ simpleFact("expected to be TransitionState.Transition.ChangeCurrentScene")
+ )
}
- return actual as TransitionState.Transition
+ return actual as TransitionState.Transition.ChangeCurrentScene
}
companion object {
@@ -68,10 +70,10 @@
}
}
-class TransitionSubject
+class SceneTransitionSubject
private constructor(
metadata: FailureMetadata,
- private val actual: TransitionState.Transition,
+ private val actual: TransitionState.Transition.ChangeCurrentScene,
) : Subject(metadata, actual) {
fun hasCurrentScene(sceneKey: SceneKey) {
check("currentScene").that(actual.currentScene).isEqualTo(sceneKey)
@@ -132,19 +134,21 @@
check("currentOverscrollSpec").that(actual.currentOverscrollSpec).isNull()
}
- fun hasBouncingScene(scene: SceneKey) {
- if (actual !is ContentState.HasOverscrollProperties) {
+ fun hasBouncingContent(content: ContentKey) {
+ val actual = actual
+ if (actual !is TransitionState.HasOverscrollProperties) {
failWithActual(simpleFact("expected to be ContentState.HasOverscrollProperties"))
}
check("bouncingContent")
- .that((actual as ContentState.HasOverscrollProperties).bouncingContent)
- .isEqualTo(scene)
+ .that((actual as TransitionState.HasOverscrollProperties).bouncingContent)
+ .isEqualTo(content)
}
companion object {
- fun transitions() = Factory { metadata, actual: TransitionState.Transition ->
- TransitionSubject(metadata, actual)
- }
+ fun transitions() =
+ Factory { metadata, actual: TransitionState.Transition.ChangeCurrentScene ->
+ SceneTransitionSubject(metadata, actual)
+ }
}
}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
index 00adefb..5cccfb1 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
@@ -26,9 +26,9 @@
@Composable
fun TestContentScope(
modifier: Modifier = Modifier,
+ currentScene: SceneKey = remember { SceneKey("current") },
content: @Composable ContentScope.() -> Unit,
) {
- val currentScene = remember { SceneKey("current") }
val state = remember { MutableSceneTransitionLayoutState(currentScene) }
SceneTransitionLayout(state, modifier) { scene(currentScene, content = content) }
}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
index 6d063a0..22450d3 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
@@ -20,11 +20,16 @@
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasTestTag
-/** A [SemanticsMatcher] that matches [element], optionally restricted to scene [scene]. */
-fun isElement(element: ElementKey, scene: SceneKey? = null): SemanticsMatcher {
- return if (scene == null) {
+/** A [SemanticsMatcher] that matches [element], optionally restricted to content [content]. */
+fun isElement(element: ElementKey, content: ContentKey? = null): SemanticsMatcher {
+ return if (content == null) {
hasTestTag(element.testTag)
} else {
- hasTestTag(element.testTag) and hasAnyAncestor(hasTestTag(scene.testTag))
+ hasTestTag(element.testTag) and inContent(content)
}
}
+
+/** A [SemanticsMatcher] that matches anything inside [content]. */
+fun inContent(content: ContentKey): SemanticsMatcher {
+ return hasAnyAncestor(hasTestTag(content.testTag))
+}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt
index b83705a..f39dd67 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt
@@ -21,7 +21,7 @@
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
-/** Scenes keys that can be reused by tests. */
+/** Scene keys that can be reused by tests. */
object TestScenes {
val SceneA = SceneKey("SceneA")
val SceneB = SceneKey("SceneB")
@@ -29,6 +29,12 @@
val SceneD = SceneKey("SceneD")
}
+/** Overlay keys that can be reused by tests. */
+object TestOverlays {
+ val OverlayA = OverlayKey("OverlayA")
+ val OverlayB = OverlayKey("OverlayB")
+}
+
/** Element keys that can be reused by tests. */
object TestElements {
val Foo = ElementKey("Foo")
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewControllerTest.java
index 201ed00..43db5a7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewControllerTest.java
@@ -148,6 +148,7 @@
mKosmos.getWifiInteractor(),
mKosmos.getCommunalSceneInteractor(),
mLogBuffer);
+ mController.onInit();
}
@Test
@@ -517,6 +518,15 @@
verify(mDreamOverlayStateController).setDreamOverlayStatusBarVisible(false);
}
+ @Test
+ public void testStatusBarWindowStateControllerListenerLifecycle() {
+ ArgumentCaptor<StatusBarWindowStateListener> listenerCaptor =
+ ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
+ verify(mStatusBarWindowStateController).addListener(listenerCaptor.capture());
+ mController.destroy();
+ verify(mStatusBarWindowStateController).removeListener(eq(listenerCaptor.getValue()));
+ }
+
private StatusBarWindowStateListener updateStatusBarWindowState(boolean show) {
when(mStatusBarWindowStateController.windowIsShowing()).thenReturn(show);
final ArgumentCaptor<StatusBarWindowStateListener>
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 752c93e..b7d99d2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -79,6 +79,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.app.viewcapture.ViewCapture;
import com.android.internal.R;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.widget.LockPatternUtils;
@@ -96,6 +97,8 @@
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
+import dagger.Lazy;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -165,6 +168,8 @@
private PromptViewModel mPromptViewModel;
@Mock
private UdfpsUtils mUdfpsUtils;
+ @Mock
+ private Lazy<ViewCapture> mLazyViewCapture;
@Captor
private ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mFpAuthenticatorsRegisteredCaptor;
@@ -1060,7 +1065,8 @@
mWakefulnessLifecycle, mUserManager, mLockPatternUtils, () -> mUdfpsLogger,
() -> mLogContextInteractor, () -> mPromptSelectionInteractor,
() -> mCredentialViewModel, () -> mPromptViewModel, mInteractionJankMonitor,
- mHandler, mBackgroundExecutor, mUdfpsUtils, mVibratorHelper);
+ mHandler, mBackgroundExecutor, mUdfpsUtils, mVibratorHelper,
+ mLazyViewCapture);
}
@Override
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
index cfe0bec..65c9b72 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
@@ -17,6 +17,8 @@
package com.android.systemui.bouncer.domain.interactor
import android.content.pm.UserInfo
+import android.hardware.biometrics.BiometricFaceConstants
+import android.hardware.biometrics.BiometricSourceType
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -25,6 +27,7 @@
import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Pattern
import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FaceSensorInfo
@@ -36,7 +39,6 @@
import com.android.systemui.bouncer.shared.model.BouncerMessageModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceEntryBiometricsAllowedInteractor
-import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
import com.android.systemui.flags.SystemPropertiesHelper
import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
@@ -58,7 +60,9 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -77,6 +81,8 @@
@Mock private lateinit var securityModel: KeyguardSecurityModel
@Mock private lateinit var countDownTimerUtil: CountDownTimerUtil
@Mock private lateinit var systemPropertiesHelper: SystemPropertiesHelper
+ @Captor
+ private lateinit var keyguardUpdateMonitorCaptor: ArgumentCaptor<KeyguardUpdateMonitorCallback>
private lateinit var underTest: BouncerMessageInteractor
@@ -107,8 +113,6 @@
systemPropertiesHelper = systemPropertiesHelper,
primaryBouncerInteractor = kosmos.primaryBouncerInteractor,
facePropertyRepository = kosmos.fakeFacePropertyRepository,
- deviceEntryFingerprintAuthInteractor = kosmos.deviceEntryFingerprintAuthInteractor,
- faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository,
securityModel = securityModel,
deviceEntryBiometricsAllowedInteractor =
kosmos.deviceEntryBiometricsAllowedInteractor,
@@ -207,6 +211,52 @@
}
@Test
+ fun resetMessageBackToDefault_faceAuthRestarts() =
+ testScope.runTest {
+ init()
+ captureKeyguardUpdateMonitorCallback()
+ val bouncerMessage by collectLastValue(underTest.bouncerMessage)
+
+ underTest.setFaceAcquisitionMessage("not empty")
+
+ assertThat(primaryResMessage(bouncerMessage))
+ .isEqualTo("Unlock with PIN or fingerprint")
+ assertThat(bouncerMessage?.secondaryMessage?.message).isEqualTo("not empty")
+
+ keyguardUpdateMonitorCaptor.value.onBiometricAcquired(
+ BiometricSourceType.FACE,
+ BiometricFaceConstants.FACE_ACQUIRED_START
+ )
+
+ assertThat(primaryResMessage(bouncerMessage))
+ .isEqualTo("Unlock with PIN or fingerprint")
+ assertThat(bouncerMessage?.secondaryMessage?.message).isNull()
+ }
+
+ @Test
+ fun faceRestartDoesNotResetFingerprintMessage() =
+ testScope.runTest {
+ init()
+ captureKeyguardUpdateMonitorCallback()
+ val bouncerMessage by collectLastValue(underTest.bouncerMessage)
+
+ underTest.setFingerprintAcquisitionMessage("not empty")
+
+ assertThat(primaryResMessage(bouncerMessage))
+ .isEqualTo("Unlock with PIN or fingerprint")
+ assertThat(bouncerMessage?.secondaryMessage?.message).isEqualTo("not empty")
+
+ keyguardUpdateMonitorCaptor.value.onBiometricAcquired(
+ BiometricSourceType.FACE,
+ BiometricFaceConstants.FACE_ACQUIRED_START
+ )
+
+ assertThat(primaryResMessage(bouncerMessage))
+ .isEqualTo("Unlock with PIN or fingerprint")
+ assertThat(bouncerMessage?.secondaryMessage?.message).isEqualTo("not empty")
+ }
+
+ @Test
fun setFingerprintMessage_propagateValue() =
testScope.runTest {
init()
@@ -284,6 +334,32 @@
}
@Test
+ fun faceLockoutThenFaceFailure_doesNotUpdateMessage() =
+ testScope.runTest {
+ init()
+ captureKeyguardUpdateMonitorCallback()
+ val bouncerMessage by collectLastValue(underTest.bouncerMessage)
+
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(primaryResMessage(bouncerMessage))
+ .isEqualTo("Unlock with PIN or fingerprint")
+ assertThat(secondaryResMessage(bouncerMessage))
+ .isEqualTo("Can’t unlock with face. Too many attempts.")
+
+ // WHEN face failure comes in during lockout
+ keyguardUpdateMonitorCaptor.value.onBiometricAuthFailed(BiometricSourceType.FACE)
+
+ // THEN lockout message does NOT update to face failure message
+ assertThat(primaryResMessage(bouncerMessage))
+ .isEqualTo("Unlock with PIN or fingerprint")
+ assertThat(secondaryResMessage(bouncerMessage))
+ .isEqualTo("Can’t unlock with face. Too many attempts.")
+ }
+
+ @Test
fun onFaceLockoutStateChange_whenFaceIsNotEnrolled_isANoop() =
testScope.runTest {
init()
@@ -576,6 +652,10 @@
}
}
+ private fun captureKeyguardUpdateMonitorCallback() {
+ verify(updateMonitor).registerCallback(keyguardUpdateMonitorCaptor.capture())
+ }
+
companion object {
private const val PRIMARY_USER_ID = 0
private val PRIMARY_USER =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
index ad2c42f..eba395b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
@@ -115,21 +115,21 @@
.addWidget(
widgetId = 0,
componentName = defaultWidgets[0],
- priority = 3,
+ rank = 0,
userSerialNumber = 0,
)
verify(communalWidgetDao)
.addWidget(
widgetId = 1,
componentName = defaultWidgets[1],
- priority = 2,
+ rank = 1,
userSerialNumber = 0,
)
verify(communalWidgetDao)
.addWidget(
widgetId = 2,
componentName = defaultWidgets[2],
- priority = 1,
+ rank = 2,
userSerialNumber = 0,
)
}
@@ -150,7 +150,7 @@
.addWidget(
widgetId = anyInt(),
componentName = any(),
- priority = anyInt(),
+ rank = anyInt(),
userSerialNumber = anyInt(),
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index ca81838..980a5ec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -154,7 +154,7 @@
CommunalWidgetContentModel.Available(
appWidgetId = communalWidgetItemEntry.widgetId,
providerInfo = providerInfoA,
- priority = communalItemRankEntry.rank,
+ rank = communalItemRankEntry.rank,
)
)
@@ -190,12 +190,12 @@
CommunalWidgetContentModel.Available(
appWidgetId = 1,
providerInfo = providerInfoA,
- priority = 1,
+ rank = 1,
),
CommunalWidgetContentModel.Available(
appWidgetId = 2,
providerInfo = providerInfoB,
- priority = 2,
+ rank = 2,
),
)
}
@@ -225,12 +225,12 @@
CommunalWidgetContentModel.Available(
appWidgetId = 1,
providerInfo = providerInfoA,
- priority = 1,
+ rank = 1,
),
CommunalWidgetContentModel.Available(
appWidgetId = 2,
providerInfo = providerInfoB,
- priority = 2,
+ rank = 2,
),
)
@@ -248,12 +248,12 @@
appWidgetId = 1,
// Verify that provider info updated
providerInfo = providerInfoC,
- priority = 1,
+ rank = 1,
),
CommunalWidgetContentModel.Available(
appWidgetId = 2,
providerInfo = providerInfoB,
- priority = 2,
+ rank = 2,
),
)
}
@@ -263,7 +263,7 @@
testScope.runTest {
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
- val priority = 1
+ val rank = 1
whenever(communalWidgetHost.getAppWidgetInfo(id))
.thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION)
whenever(
@@ -273,12 +273,11 @@
)
)
.thenReturn(id)
- underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorSuccess)
+ underTest.addWidget(provider, mainUser, rank, kosmos.widgetConfiguratorSuccess)
runCurrent()
verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser)
- verify(communalWidgetDao)
- .addWidget(id, provider, priority, testUserSerialNumber(mainUser))
+ verify(communalWidgetDao).addWidget(id, provider, rank, testUserSerialNumber(mainUser))
// Verify backup requested
verify(backupManager).dataChanged()
@@ -289,7 +288,7 @@
testScope.runTest {
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
- val priority = 1
+ val rank = 1
whenever(communalWidgetHost.getAppWidgetInfo(id))
.thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION)
whenever(
@@ -299,7 +298,7 @@
)
)
.thenReturn(id)
- underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorFail)
+ underTest.addWidget(provider, mainUser, rank, kosmos.widgetConfiguratorFail)
runCurrent()
verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser)
@@ -316,7 +315,7 @@
testScope.runTest {
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
- val priority = 1
+ val rank = 1
whenever(communalWidgetHost.getAppWidgetInfo(id))
.thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION)
whenever(
@@ -326,7 +325,7 @@
)
)
.thenReturn(id)
- underTest.addWidget(provider, mainUser, priority) {
+ underTest.addWidget(provider, mainUser, rank) {
throw IllegalStateException("some error")
}
runCurrent()
@@ -345,7 +344,7 @@
testScope.runTest {
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
- val priority = 1
+ val rank = 1
whenever(communalWidgetHost.getAppWidgetInfo(id))
.thenReturn(PROVIDER_INFO_CONFIGURATION_OPTIONAL)
whenever(
@@ -355,12 +354,11 @@
)
)
.thenReturn(id)
- underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorFail)
+ underTest.addWidget(provider, mainUser, rank, kosmos.widgetConfiguratorFail)
runCurrent()
verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser)
- verify(communalWidgetDao)
- .addWidget(id, provider, priority, testUserSerialNumber(mainUser))
+ verify(communalWidgetDao).addWidget(id, provider, rank, testUserSerialNumber(mainUser))
// Verify backup requested
verify(backupManager).dataChanged()
@@ -399,11 +397,11 @@
@Test
fun reorderWidgets_queryDb() =
testScope.runTest {
- val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3)
- underTest.updateWidgetOrder(widgetIdToPriorityMap)
+ val widgetIdToRankMap = mapOf(104 to 1, 103 to 2, 101 to 3)
+ underTest.updateWidgetOrder(widgetIdToRankMap)
runCurrent()
- verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap)
+ verify(communalWidgetDao).updateWidgetOrder(widgetIdToRankMap)
// Verify backup requested
verify(backupManager).dataChanged()
@@ -691,11 +689,11 @@
CommunalWidgetContentModel.Available(
appWidgetId = 1,
providerInfo = providerInfoA,
- priority = 1,
+ rank = 1,
),
CommunalWidgetContentModel.Pending(
appWidgetId = 2,
- priority = 2,
+ rank = 2,
componentName = ComponentName("pk_2", "cls_2"),
icon = fakeIcon,
user = mainUser,
@@ -730,7 +728,7 @@
.containsExactly(
CommunalWidgetContentModel.Pending(
appWidgetId = 1,
- priority = 1,
+ rank = 1,
componentName = ComponentName("pk_1", "cls_1"),
icon = fakeIcon,
user = mainUser,
@@ -750,7 +748,7 @@
CommunalWidgetContentModel.Available(
appWidgetId = 1,
providerInfo = providerInfoA,
- priority = 1,
+ rank = 1,
),
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
index c5518b0..2ba4bf9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
@@ -36,6 +36,7 @@
import com.android.systemui.keyguard.data.repository.realKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
+import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
@@ -834,4 +835,86 @@
)
)
}
+
+ /**
+ * KTF: LOCKSCREEN -> ALTERNATE_BOUNCER starts but then STL: GLANCEABLE_HUB -> BLANK interrupts.
+ *
+ * Verifies that we correctly cancel the previous KTF state before starting the glanceable hub
+ * transition.
+ */
+ @Test
+ fun transition_to_blank_after_ktf_started_another_transition() =
+ testScope.runTest {
+ // Another transition has already started to the alternate bouncer.
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ from = LOCKSCREEN,
+ to = ALTERNATE_BOUNCER,
+ animator = null,
+ ownerName = "external",
+ modeOnCanceled = TransitionModeOnCanceled.RESET
+ ),
+ )
+
+ val allSteps by collectValues(keyguardTransitionRepository.transitions)
+ // Keep track of existing size to drop any pre-existing steps that we don't
+ // care about.
+ val numToDrop = allSteps.size
+
+ sceneTransitions.value = hubToBlank
+ runCurrent()
+ progress.emit(0.4f)
+ runCurrent()
+ // We land on blank.
+ sceneTransitions.value = Idle(CommunalScenes.Blank)
+
+ // We should cancel the previous ALTERNATE_BOUNCER transition and transition back
+ // to the GLANCEABLE_HUB before we can transition away from it.
+ assertThat(allSteps.drop(numToDrop))
+ .containsExactly(
+ TransitionStep(
+ from = LOCKSCREEN,
+ to = ALTERNATE_BOUNCER,
+ transitionState = CANCELED,
+ value = 0f,
+ ownerName = "external",
+ ),
+ TransitionStep(
+ from = ALTERNATE_BOUNCER,
+ to = GLANCEABLE_HUB,
+ transitionState = STARTED,
+ value = 1f,
+ ownerName = ownerName,
+ ),
+ TransitionStep(
+ from = ALTERNATE_BOUNCER,
+ to = GLANCEABLE_HUB,
+ transitionState = FINISHED,
+ value = 1f,
+ ownerName = ownerName,
+ ),
+ TransitionStep(
+ from = GLANCEABLE_HUB,
+ to = LOCKSCREEN,
+ transitionState = STARTED,
+ value = 0f,
+ ownerName = ownerName,
+ ),
+ TransitionStep(
+ from = GLANCEABLE_HUB,
+ to = LOCKSCREEN,
+ transitionState = RUNNING,
+ value = 0.4f,
+ ownerName = ownerName,
+ ),
+ TransitionStep(
+ from = GLANCEABLE_HUB,
+ to = LOCKSCREEN,
+ transitionState = FINISHED,
+ value = 1f,
+ ownerName = ownerName,
+ ),
+ )
+ .inOrder()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 57ce9de..8218178 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -150,8 +150,8 @@
tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
// Widgets available.
- widgetRepository.addWidget(appWidgetId = 0, priority = 30)
- widgetRepository.addWidget(appWidgetId = 1, priority = 20)
+ widgetRepository.addWidget(appWidgetId = 0, rank = 30)
+ widgetRepository.addWidget(appWidgetId = 1, rank = 20)
// Smartspace available.
smartspaceRepository.setTimers(
@@ -212,8 +212,8 @@
tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
// Widgets available.
- widgetRepository.addWidget(appWidgetId = 0, priority = 30)
- widgetRepository.addWidget(appWidgetId = 1, priority = 20)
+ widgetRepository.addWidget(appWidgetId = 0, rank = 30)
+ widgetRepository.addWidget(appWidgetId = 1, rank = 20)
val communalContent by collectLastValue(underTest.communalContent)
@@ -227,7 +227,7 @@
underTest.onDeleteWidget(
id = 0,
componentName = ComponentName("test_package", "test_class"),
- priority = 30,
+ rank = 30,
)
// Only one widget and CTA tile remain.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index cc945d6..fb151a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -101,7 +101,6 @@
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.spy
-import org.mockito.kotlin.times
import org.mockito.kotlin.whenever
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
@@ -213,8 +212,8 @@
tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
// Widgets available.
- widgetRepository.addWidget(appWidgetId = 0, priority = 30)
- widgetRepository.addWidget(appWidgetId = 1, priority = 20)
+ widgetRepository.addWidget(appWidgetId = 0, rank = 30)
+ widgetRepository.addWidget(appWidgetId = 1, rank = 20)
// Smartspace available.
smartspaceRepository.setTimers(
@@ -303,7 +302,7 @@
testScope.runTest {
tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
- widgetRepository.addWidget(appWidgetId = 1, priority = 1)
+ widgetRepository.addWidget(appWidgetId = 1, rank = 1)
mediaRepository.mediaInactive()
smartspaceRepository.setTimers(emptyList())
@@ -660,8 +659,8 @@
)
// Widgets available
- widgetRepository.addWidget(appWidgetId = 0, priority = 30)
- widgetRepository.addWidget(appWidgetId = 1, priority = 20)
+ widgetRepository.addWidget(appWidgetId = 0, rank = 30)
+ widgetRepository.addWidget(appWidgetId = 1, rank = 20)
// Then hub shows widgets and the CTA tile
assertThat(communalContent).hasSize(3)
@@ -716,8 +715,8 @@
)
// And widgets available
- widgetRepository.addWidget(appWidgetId = 0, priority = 30)
- widgetRepository.addWidget(appWidgetId = 1, priority = 20)
+ widgetRepository.addWidget(appWidgetId = 0, rank = 30)
+ widgetRepository.addWidget(appWidgetId = 1, rank = 20)
// Then emits widgets and the CTA tile
assertThat(communalContent).hasSize(3)
@@ -770,7 +769,7 @@
@Test
fun onTapWidget_logEvent() {
- underTest.onTapWidget(ComponentName("test_pkg", "test_cls"), priority = 10)
+ underTest.onTapWidget(ComponentName("test_pkg", "test_cls"), rank = 10)
verify(metricsLogger).logTapWidget("test_pkg/test_cls", rank = 10)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt
new file mode 100644
index 0000000..3ba8625
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.communal.widgets
+
+import android.app.Activity
+import android.app.Application.ActivityLifecycleCallbacks
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class EditWidgetsActivityControllerTest : SysuiTestCase() {
+ @Test
+ fun activityLifecycle_finishedWhenNotWaitingForResult() {
+ val activity = mock<Activity>()
+ val controller = EditWidgetsActivity.ActivityControllerImpl(activity)
+
+ val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>()
+ verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture())
+
+ controller.setActivityFullyVisible(true)
+ callbackCapture.lastValue.onActivityStopped(activity)
+
+ verify(activity).finish()
+ }
+
+ @Test
+ fun activityLifecycle_notFinishedWhenOnStartCalledAfterOnStop() {
+ val activity = mock<Activity>()
+
+ val controller = EditWidgetsActivity.ActivityControllerImpl(activity)
+
+ val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>()
+ verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture())
+
+ controller.setActivityFullyVisible(false)
+ callbackCapture.lastValue.onActivityStopped(activity)
+ callbackCapture.lastValue.onActivityStarted(activity)
+
+ verify(activity, never()).finish()
+ }
+
+ @Test
+ fun activityLifecycle_notFinishedDuringConfigurationChange() {
+ val activity = mock<Activity>()
+
+ val controller = EditWidgetsActivity.ActivityControllerImpl(activity)
+
+ val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>()
+ verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture())
+
+ controller.setActivityFullyVisible(true)
+ whenever(activity.isChangingConfigurations).thenReturn(true)
+ callbackCapture.lastValue.onActivityStopped(activity)
+ callbackCapture.lastValue.onActivityStarted(activity)
+
+ verify(activity, never()).finish()
+ }
+
+ @Test
+ fun activityLifecycle_notFinishedWhenWaitingForResult() {
+ val activity = mock<Activity>()
+ val controller = EditWidgetsActivity.ActivityControllerImpl(activity)
+
+ val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>()
+ verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture())
+
+ controller.onWaitingForResult(true)
+ callbackCapture.lastValue.onActivityStopped(activity)
+
+ verify(activity, never()).finish()
+ }
+
+ @Test
+ fun activityLifecycle_finishedAfterResultReturned() {
+ val activity = mock<Activity>()
+ val controller = EditWidgetsActivity.ActivityControllerImpl(activity)
+
+ val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>()
+ verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture())
+
+ controller.onWaitingForResult(true)
+ controller.onWaitingForResult(false)
+ controller.setActivityFullyVisible(true)
+ callbackCapture.lastValue.onActivityStopped(activity)
+
+ verify(activity).finish()
+ }
+
+ @Test
+ fun activityLifecycle_statePreservedThroughInstanceSave() {
+ val activity = mock<Activity>()
+ val bundle = Bundle(1)
+
+ run {
+ val controller = EditWidgetsActivity.ActivityControllerImpl(activity)
+ val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>()
+ verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture())
+
+ controller.onWaitingForResult(true)
+ callbackCapture.lastValue.onActivitySaveInstanceState(activity, bundle)
+ }
+
+ clearInvocations(activity)
+
+ run {
+ val controller = EditWidgetsActivity.ActivityControllerImpl(activity)
+ val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>()
+ verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture())
+
+ callbackCapture.lastValue.onActivityCreated(activity, bundle)
+ callbackCapture.lastValue.onActivityStopped(activity)
+
+ verify(activity, never()).finish()
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 6412276..3895595 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -62,6 +62,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -324,4 +325,13 @@
// enabled.
mController.onViewAttached();
}
+
+ @Test
+ public void destroy_cleansUpState() {
+ mController.destroy();
+ verify(mStateController).removeCallback(any());
+ verify(mAmbientStatusBarViewController).destroy();
+ verify(mComplicationHostViewController).destroy();
+ verify(mLowLightTransitionCoordinator).setLowLightEnterListener(ArgumentMatchers.isNull());
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index 89ec3cf..29aa89c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -68,7 +68,6 @@
import com.android.systemui.testKosmos
import com.android.systemui.touch.TouchInsetManager
import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -89,7 +88,9 @@
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -115,8 +116,6 @@
@Mock lateinit var mComplicationComponentFactory: ComplicationComponent.Factory
- @Mock lateinit var mComplicationComponent: ComplicationComponent
-
@Mock lateinit var mComplicationHostViewController: ComplicationHostViewController
@Mock lateinit var mComplicationVisibilityController: ComplicationLayoutEngine
@@ -125,20 +124,12 @@
lateinit var mDreamComplicationComponentFactory:
com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory
- @Mock
- lateinit var mDreamComplicationComponent:
- com.android.systemui.dreams.complication.dagger.ComplicationComponent
-
@Mock lateinit var mHideComplicationTouchHandler: HideComplicationTouchHandler
@Mock lateinit var mDreamOverlayComponentFactory: DreamOverlayComponent.Factory
- @Mock lateinit var mDreamOverlayComponent: DreamOverlayComponent
-
@Mock lateinit var mAmbientTouchComponentFactory: AmbientTouchComponent.Factory
- @Mock lateinit var mAmbientTouchComponent: AmbientTouchComponent
-
@Mock lateinit var mDreamOverlayContainerView: DreamOverlayContainerView
@Mock lateinit var mDreamOverlayContainerViewController: DreamOverlayContainerViewController
@@ -170,10 +161,83 @@
private lateinit var communalRepository: FakeCommunalSceneRepository
private var viewCaptureSpy = spy(ViewCaptureFactory.getInstance(context))
private lateinit var gestureInteractor: GestureInteractor
+ private lateinit var environmentComponents: EnvironmentComponents
@Captor var mViewCaptor: ArgumentCaptor<View>? = null
private lateinit var mService: DreamOverlayService
+ private class EnvironmentComponents(
+ val dreamsComplicationComponent:
+ com.android.systemui.dreams.complication.dagger.ComplicationComponent,
+ val dreamOverlayComponent: DreamOverlayComponent,
+ val complicationComponent: ComplicationComponent,
+ val ambientTouchComponent: AmbientTouchComponent,
+ ) {
+ fun clearInvocations() {
+ clearInvocations(
+ dreamsComplicationComponent,
+ dreamOverlayComponent,
+ complicationComponent,
+ ambientTouchComponent
+ )
+ }
+
+ fun verifyNoMoreInteractions() {
+ Mockito.verifyNoMoreInteractions(
+ dreamsComplicationComponent,
+ dreamOverlayComponent,
+ complicationComponent,
+ ambientTouchComponent
+ )
+ }
+ }
+
+ private fun setupComponentFactories(
+ dreamComplicationComponentFactory:
+ com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory,
+ dreamOverlayComponentFactory: DreamOverlayComponent.Factory,
+ complicationComponentFactory: ComplicationComponent.Factory,
+ ambientTouchComponentFactory: AmbientTouchComponent.Factory
+ ): EnvironmentComponents {
+ val dreamOverlayComponent = mock<DreamOverlayComponent>()
+ whenever(dreamOverlayComponent.getDreamOverlayContainerViewController())
+ .thenReturn(mDreamOverlayContainerViewController)
+
+ val complicationComponent = mock<ComplicationComponent>()
+ whenever(complicationComponent.getComplicationHostViewController())
+ .thenReturn(mComplicationHostViewController)
+ whenever(mLifecycleOwner.registry).thenReturn(lifecycleRegistry)
+
+ mCommunalInteractor = Mockito.spy(kosmos.communalInteractor)
+
+ whenever(complicationComponentFactory.create(any(), any(), any(), any()))
+ .thenReturn(complicationComponent)
+ whenever(complicationComponent.getVisibilityController())
+ .thenReturn(mComplicationVisibilityController)
+
+ val dreamComplicationComponent =
+ mock<com.android.systemui.dreams.complication.dagger.ComplicationComponent>()
+ whenever(dreamComplicationComponent.getHideComplicationTouchHandler())
+ .thenReturn(mHideComplicationTouchHandler)
+ whenever(dreamComplicationComponentFactory.create(any(), any()))
+ .thenReturn(dreamComplicationComponent)
+
+ whenever(dreamOverlayComponentFactory.create(any(), any(), any()))
+ .thenReturn(dreamOverlayComponent)
+
+ val ambientTouchComponent = mock<AmbientTouchComponent>()
+ whenever(ambientTouchComponentFactory.create(any(), any()))
+ .thenReturn(ambientTouchComponent)
+ whenever(ambientTouchComponent.getTouchMonitor()).thenReturn(mTouchMonitor)
+
+ return EnvironmentComponents(
+ dreamComplicationComponent,
+ dreamOverlayComponent,
+ complicationComponent,
+ ambientTouchComponent
+ )
+ }
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
@@ -183,27 +247,14 @@
communalRepository = kosmos.fakeCommunalSceneRepository
gestureInteractor = spy(kosmos.gestureInteractor)
- whenever(mDreamOverlayComponent.getDreamOverlayContainerViewController())
- .thenReturn(mDreamOverlayContainerViewController)
- whenever(mComplicationComponent.getComplicationHostViewController())
- .thenReturn(mComplicationHostViewController)
- whenever(mLifecycleOwner.registry).thenReturn(lifecycleRegistry)
+ environmentComponents =
+ setupComponentFactories(
+ mDreamComplicationComponentFactory,
+ mDreamOverlayComponentFactory,
+ mComplicationComponentFactory,
+ mAmbientTouchComponentFactory
+ )
- mCommunalInteractor = Mockito.spy(kosmos.communalInteractor)
-
- whenever(mComplicationComponentFactory.create(any(), any(), any(), any()))
- .thenReturn(mComplicationComponent)
- whenever(mComplicationComponent.getVisibilityController())
- .thenReturn(mComplicationVisibilityController)
- whenever(mDreamComplicationComponent.getHideComplicationTouchHandler())
- .thenReturn(mHideComplicationTouchHandler)
- whenever(mDreamComplicationComponentFactory.create(any(), any()))
- .thenReturn(mDreamComplicationComponent)
- whenever(mDreamOverlayComponentFactory.create(any(), any(), any()))
- .thenReturn(mDreamOverlayComponent)
- whenever(mAmbientTouchComponentFactory.create(any(), any()))
- .thenReturn(mAmbientTouchComponent)
- whenever(mAmbientTouchComponent.getTouchMonitor()).thenReturn(mTouchMonitor)
whenever(mDreamOverlayContainerViewController.containerView)
.thenReturn(mDreamOverlayContainerView)
whenever(mScrimManager.getCurrentController()).thenReturn(mScrimController)
@@ -570,9 +621,8 @@
// Assert that the overlay is not showing complications.
assertThat(mService.shouldShowComplications()).isFalse()
- Mockito.clearInvocations(mDreamOverlayComponent)
- Mockito.clearInvocations(mAmbientTouchComponent)
- Mockito.clearInvocations(mWindowManager)
+ environmentComponents.clearInvocations()
+ clearInvocations(mWindowManager)
// New dream starting with dream complications showing. Note that when a new dream is
// binding to the dream overlay service, it receives the same instance of IBinder as the
@@ -594,8 +644,11 @@
// Verify that new instances of overlay container view controller and overlay touch monitor
// are created.
- verify(mDreamOverlayComponent).getDreamOverlayContainerViewController()
- verify(mAmbientTouchComponent).getTouchMonitor()
+ verify(environmentComponents.dreamOverlayComponent).getDreamOverlayContainerViewController()
+ verify(environmentComponents.ambientTouchComponent).getTouchMonitor()
+
+ // Verify DreamOverlayContainerViewController is destroyed.
+ verify(mDreamOverlayContainerViewController).destroy()
}
@Test
@@ -1002,6 +1055,34 @@
.isEqualTo(ComponentName.unflattenFromString(DREAM_COMPONENT)?.packageName)
}
+ @Test
+ fun testComponentsRecreatedBetweenDreams() {
+ clearInvocations(
+ mDreamComplicationComponentFactory,
+ mDreamOverlayComponentFactory,
+ mComplicationComponentFactory,
+ mAmbientTouchComponentFactory
+ )
+
+ mService.onEndDream()
+
+ setupComponentFactories(
+ mDreamComplicationComponentFactory,
+ mDreamOverlayComponentFactory,
+ mComplicationComponentFactory,
+ mAmbientTouchComponentFactory
+ )
+
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ false /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+ environmentComponents.verifyNoMoreInteractions()
+ }
+
internal class FakeLifecycleRegistry(provider: LifecycleOwner) : LifecycleRegistry(provider) {
val mLifecycles: MutableList<State> = ArrayList()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
index f82beff..50b727c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
@@ -23,6 +23,7 @@
import com.android.systemui.SysuiTestableContext
import com.android.systemui.contextualeducation.GestureType.BACK
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.education.data.model.EduDeviceConnectionTime
import com.android.systemui.education.data.model.GestureEduModel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
@@ -105,6 +106,19 @@
assertThat(model).isEqualTo(newModel)
}
+ @Test
+ fun eduDeviceConnectionTimeDataChangedOnUpdate() =
+ testScope.runTest {
+ val newModel =
+ EduDeviceConnectionTime(
+ keyboardFirstConnectionTime = kosmos.fakeEduClock.instant(),
+ touchpadFirstConnectionTime = kosmos.fakeEduClock.instant(),
+ )
+ underTest.updateEduDeviceConnectionTime { newModel }
+ val model by collectLastValue(underTest.readEduDeviceConnectionTime())
+ assertThat(model).isEqualTo(newModel)
+ }
+
/** Test context which allows overriding getFilesDir path */
private class TestContext(context: Context, private val folder: File) :
SysuiTestableContext(context) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
index 23f923a..3aed79f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.education.domain.interactor
+import android.content.pm.UserInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -26,10 +27,15 @@
import com.android.systemui.education.data.repository.contextualEducationRepository
import com.android.systemui.education.data.repository.fakeEduClock
import com.android.systemui.education.shared.model.EducationUiType
+import com.android.systemui.keyboard.data.repository.keyboardRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
+import com.android.systemui.touchpad.data.repository.touchpadRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -38,16 +44,23 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val contextualEduInteractor = kosmos.contextualEducationInteractor
+ private val touchpadRepository = kosmos.touchpadRepository
+ private val keyboardRepository = kosmos.keyboardRepository
+ private val userRepository = kosmos.fakeUserRepository
+
private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor
private val eduClock = kosmos.fakeEduClock
@Before
fun setup() {
underTest.start()
+ contextualEduInteractor.start()
+ userRepository.setUserInfos(USER_INFOS)
}
@Test
@@ -67,7 +80,6 @@
}
@Test
- @kotlinx.coroutines.ExperimentalCoroutinesApi
fun newEducationNotificationOn2ndEducation() =
testScope.runTest {
val model by collectLastValue(underTest.educationTriggered)
@@ -115,10 +127,103 @@
)
}
+ @Test
+ fun newTouchpadConnectionTimeOnFirstTouchpadConnected() =
+ testScope.runTest {
+ setIsAnyTouchpadConnected(true)
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.touchpadFirstConnectionTime).isEqualTo(eduClock.instant())
+ }
+
+ @Test
+ fun unchangedTouchpadConnectionTimeOnSecondConnection() =
+ testScope.runTest {
+ val firstConnectionTime = eduClock.instant()
+ setIsAnyTouchpadConnected(true)
+ setIsAnyTouchpadConnected(false)
+
+ eduClock.offset(1.hours)
+ setIsAnyTouchpadConnected(true)
+
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.touchpadFirstConnectionTime).isEqualTo(firstConnectionTime)
+ }
+
+ @Test
+ fun newTouchpadConnectionTimeOnUserChanged() =
+ testScope.runTest {
+ // Touchpad connected for user 0
+ setIsAnyTouchpadConnected(true)
+
+ // Change user
+ eduClock.offset(1.hours)
+ val newUserFirstConnectionTime = eduClock.instant()
+ userRepository.setSelectedUserInfo(USER_INFOS[0])
+ runCurrent()
+
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.touchpadFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
+ }
+
+ @Test
+ fun newKeyboardConnectionTimeOnKeyboardConnected() =
+ testScope.runTest {
+ setIsAnyKeyboardConnected(true)
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.keyboardFirstConnectionTime).isEqualTo(eduClock.instant())
+ }
+
+ @Test
+ fun unchangedKeyboardConnectionTimeOnSecondConnection() =
+ testScope.runTest {
+ val firstConnectionTime = eduClock.instant()
+ setIsAnyKeyboardConnected(true)
+ setIsAnyKeyboardConnected(false)
+
+ eduClock.offset(1.hours)
+ setIsAnyKeyboardConnected(true)
+
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.keyboardFirstConnectionTime).isEqualTo(firstConnectionTime)
+ }
+
+ @Test
+ fun newKeyboardConnectionTimeOnUserChanged() =
+ testScope.runTest {
+ // Keyboard connected for user 0
+ setIsAnyKeyboardConnected(true)
+
+ // Change user
+ eduClock.offset(1.hours)
+ val newUserFirstConnectionTime = eduClock.instant()
+ userRepository.setSelectedUserInfo(USER_INFOS[0])
+ runCurrent()
+
+ val model = contextualEduInteractor.getEduDeviceConnectionTime()
+ assertThat(model.keyboardFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
+ }
+
private suspend fun triggerMaxEducationSignals(gestureType: GestureType) {
// Increment max number of signal to try triggering education
for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
contextualEduInteractor.incrementSignalCount(gestureType)
}
}
+
+ private fun TestScope.setIsAnyTouchpadConnected(isConnected: Boolean) {
+ touchpadRepository.setIsAnyTouchpadConnected(isConnected)
+ runCurrent()
+ }
+
+ private fun TestScope.setIsAnyKeyboardConnected(isConnected: Boolean) {
+ keyboardRepository.setIsAnyKeyboardConnected(isConnected)
+ runCurrent()
+ }
+
+ companion object {
+ private val USER_INFOS =
+ listOf(
+ UserInfo(101, "Second User", 0),
+ )
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
index 3a28471..9bcc19d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
@@ -369,4 +369,18 @@
invokeOnCallback { it.onStrongAuthStateChanged(0) }
assertThat(shouldUpdateIndicatorVisibility).isTrue()
}
+
+ @Test
+ fun isLockedOut_initialStateFalse() =
+ testScope.runTest {
+ whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false)
+ assertThat(underTest.isLockedOut.value).isEqualTo(false)
+ }
+
+ @Test
+ fun isLockedOut_initialStateTrue() =
+ testScope.runTest {
+ whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(true)
+ assertThat(underTest.isLockedOut.value).isEqualTo(true)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index 783e3b5..ee4a0d2d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -150,13 +150,13 @@
@Test
@EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
@DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
- fun testTransitionToLockscreen_onPowerButtonPress_canDream_glanceableHubAvailable() =
+ fun testTransitionToLockscreen_onWake_canDream_glanceableHubAvailable() =
testScope.runTest {
whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
kosmos.setCommunalAvailable(true)
runCurrent()
- powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON)
+ powerInteractor.setAwakeForTest()
runCurrent()
// If dreaming is possible and communal is available, then we should transition to
@@ -170,14 +170,14 @@
@Test
@EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR, FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
- fun testTransitionToLockscreen_onPowerButtonPress_canDream_ktfRefactor() =
+ fun testTransitionToLockscreen_onWake_canDream_ktfRefactor() =
testScope.runTest {
whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
kosmos.setCommunalAvailable(true)
runCurrent()
clearInvocations(kosmos.fakeCommunalSceneRepository)
- powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON)
+ powerInteractor.setAwakeForTest()
runCurrent()
// If dreaming is possible and communal is available, then we should transition to
@@ -188,13 +188,13 @@
@Test
@EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
- fun testTransitionToLockscreen_onPowerButtonPress_canNotDream_glanceableHubAvailable() =
+ fun testTransitionToLockscreen_onWake_canNotDream_glanceableHubAvailable() =
testScope.runTest {
whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(false)
kosmos.setCommunalAvailable(true)
runCurrent()
- powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON)
+ powerInteractor.setAwakeForTest()
runCurrent()
// If dreaming is NOT possible but communal is available, then we should transition to
@@ -208,13 +208,13 @@
@Test
@EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
- fun testTransitionToLockscreen_onPowerButtonPress_canNDream_glanceableHubNotAvailable() =
+ fun testTransitionToLockscreen_onWake_canNDream_glanceableHubNotAvailable() =
testScope.runTest {
whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
kosmos.setCommunalAvailable(false)
runCurrent()
- powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON)
+ powerInteractor.setAwakeForTest()
runCurrent()
// If dreaming is possible but communal is NOT available, then we should transition to
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 1bc2e24..5a6f2be 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -1968,47 +1968,6 @@
@Test
@DisableSceneContainer
- @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
- fun glanceableHubToLockscreen_communalKtfRefactor() =
- testScope.runTest {
- // GIVEN a prior transition has run to GLANCEABLE_HUB
- communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
- runCurrent()
- clearInvocations(transitionRepository)
-
- // WHEN a transition away from glanceable hub starts
- val currentScene = CommunalScenes.Communal
- val targetScene = CommunalScenes.Blank
-
- val progress = MutableStateFlow(0f)
- val transitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Transition(
- fromScene = currentScene,
- toScene = targetScene,
- currentScene = flowOf(targetScene),
- progress = progress,
- isInitiatedByUserInput = false,
- isUserInputOngoing = flowOf(false),
- )
- )
- communalSceneInteractor.setTransitionState(transitionState)
- progress.value = .1f
- runCurrent()
-
- assertThat(transitionRepository)
- .startedTransition(
- ownerName = CommunalSceneTransitionInteractor::class.simpleName,
- from = KeyguardState.GLANCEABLE_HUB,
- to = KeyguardState.LOCKSCREEN,
- animatorAssertion = { it.isNull() }, // transition should be manually animated
- )
-
- coroutineContext.cancelChildren()
- }
-
- @Test
- @DisableSceneContainer
@DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun glanceableHubToDozing() =
testScope.runTest {
@@ -2260,54 +2219,6 @@
}
@Test
- @DisableSceneContainer
- @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
- fun glanceableHubToDreaming_communalKtfRefactor() =
- testScope.runTest {
- // GIVEN that we are dreaming and not dozing
- powerInteractor.setAwakeForTest()
- keyguardRepository.setDreaming(true)
- keyguardRepository.setDreamingWithOverlay(true)
- keyguardRepository.setDozeTransitionModel(
- DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
- )
- advanceTimeBy(600L)
-
- // GIVEN a prior transition has run to GLANCEABLE_HUB
- communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
- runCurrent()
- clearInvocations(transitionRepository)
-
- // WHEN a transition away from glanceable hub starts
- val currentScene = CommunalScenes.Communal
- val targetScene = CommunalScenes.Blank
-
- val transitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Transition(
- fromScene = currentScene,
- toScene = targetScene,
- currentScene = flowOf(targetScene),
- progress = flowOf(0f, 0.1f),
- isInitiatedByUserInput = false,
- isUserInputOngoing = flowOf(false),
- )
- )
- communalSceneInteractor.setTransitionState(transitionState)
- runCurrent()
-
- assertThat(transitionRepository)
- .startedTransition(
- ownerName = CommunalSceneTransitionInteractor::class.simpleName,
- from = KeyguardState.GLANCEABLE_HUB,
- to = KeyguardState.DREAMING,
- animatorAssertion = { it.isNull() }, // transition should be manually animated
- )
-
- coroutineContext.cancelChildren()
- }
-
- @Test
@BrokenWithSceneContainer(339465026)
@EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun glanceableHubToOccludedDoesNotTriggerWhenDreamStateChanges_communalKtfRefactor() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 6dbe94b..3e1f4f6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -360,6 +360,25 @@
}
@Test
+ fun alpha_transitionBetweenHubAndDream_isZero() =
+ testScope.runTest {
+ val alpha by collectLastValue(underTest.alpha(viewState))
+
+ // Default value check
+ assertThat(alpha).isEqualTo(1f)
+
+ // Start transitioning between DREAM and HUB but don't finish.
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.DREAMING,
+ to = KeyguardState.GLANCEABLE_HUB,
+ testScope = testScope,
+ throughTransitionState = TransitionState.STARTED,
+ )
+
+ assertThat(alpha).isEqualTo(0f)
+ }
+
+ @Test
@EnableSceneContainer
fun alpha_transitionToHub_isZero_scene_container() =
testScope.runTest {
@@ -485,6 +504,45 @@
}
@Test
+ @DisableSceneContainer
+ fun alphaFromShadeExpansion_doesNotEmitWhenLockscreenToDreamTransitionRunning() =
+ testScope.runTest {
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ testScope,
+ )
+
+ val alpha by collectLastValue(underTest.alpha(viewState))
+ shadeTestUtil.setQsExpansion(0f)
+
+ assertThat(alpha).isEqualTo(1f)
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DREAMING,
+ transitionState = TransitionState.STARTED,
+ value = 0f,
+ ),
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DREAMING,
+ transitionState = TransitionState.RUNNING,
+ value = 0.1f,
+ ),
+ ),
+ testScope,
+ )
+
+ val alphaBeforeExpansion = alpha
+ shadeTestUtil.setQsExpansion(0.5f)
+ // Alpha should remain unchanged instead of being affected by expansion.
+ assertThat(alpha).isEqualTo(alphaBeforeExpansion)
+ }
+
+ @Test
fun alpha_shadeClosedOverLockscreen_isOne() =
testScope.runTest {
val alpha by collectLastValue(underTest.alpha(viewState))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
index 5999265..768fbca 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
@@ -31,6 +31,8 @@
import com.android.systemui.qs.fgsManagerController
import com.android.systemui.res.R
import com.android.systemui.shade.largeScreenHeaderHelper
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.sysuiStatusBarStateController
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
@@ -140,6 +142,42 @@
}
}
+ @Test
+ fun statusBarState_followsController() =
+ with(kosmos) {
+ testScope.testWithinLifecycle {
+ val statusBarState by collectLastValue(underTest.statusBarState)
+ runCurrent()
+
+ sysuiStatusBarStateController.setState(StatusBarState.SHADE)
+ assertThat(statusBarState).isEqualTo(StatusBarState.SHADE)
+
+ sysuiStatusBarStateController.setState(StatusBarState.KEYGUARD)
+ assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
+
+ sysuiStatusBarStateController.setState(StatusBarState.SHADE_LOCKED)
+ assertThat(statusBarState).isEqualTo(StatusBarState.SHADE_LOCKED)
+ }
+ }
+
+ @Test
+ fun statusBarState_changesEarlyIfUpcomingStateIsKeyguard() =
+ with(kosmos) {
+ testScope.testWithinLifecycle {
+ val statusBarState by collectLastValue(underTest.statusBarState)
+
+ sysuiStatusBarStateController.setState(StatusBarState.SHADE)
+ sysuiStatusBarStateController.setUpcomingState(StatusBarState.SHADE_LOCKED)
+ assertThat(statusBarState).isEqualTo(StatusBarState.SHADE)
+
+ sysuiStatusBarStateController.setUpcomingState(StatusBarState.KEYGUARD)
+ assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
+
+ sysuiStatusBarStateController.setUpcomingState(StatusBarState.SHADE)
+ assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
+ }
+ }
+
private inline fun TestScope.testWithinLifecycle(
crossinline block: suspend TestScope.() -> TestResult
): TestResult {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
index 94fa9b9..e0a53f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
@@ -31,9 +31,8 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
-import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
-import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -44,7 +43,6 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -55,12 +53,7 @@
private val dispatcher = kosmos.testDispatcher
private val zenModeRepository = kosmos.fakeZenModeRepository
- private val underTest =
- ModesTileDataInteractor(
- context,
- ZenModeInteractor(zenModeRepository, mock<NotificationSettingsRepository>()),
- dispatcher
- )
+ private val underTest = ModesTileDataInteractor(context, kosmos.zenModeInteractor, dispatcher)
@Before
fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index d472d98..22913f1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -255,7 +255,7 @@
runCurrent()
clearInvocations(qsImpl!!)
- underTest.setState(QSSceneAdapter.State.Expanding(progress))
+ underTest.setState(QSSceneAdapter.State.Expanding { progress })
with(qsImpl!!) {
verify(this).setQsVisible(true)
verify(this, never())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
index 63ce67c..41b5988 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
@@ -20,7 +20,12 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.ui.adapter.ExpandingSubject.Companion.assertThatExpanding
import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth.assertAbout
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -32,33 +37,59 @@
@Test
fun expanding_squishiness1() {
- assertThat(QSSceneAdapter.State.Expanding(0.3f).squishiness()).isEqualTo(1f)
+ assertThat(QSSceneAdapter.State.Expanding { 0.3f }.squishiness()).isEqualTo(1f)
}
@Test
fun expandingSpecialValues() {
- assertThat(QSSceneAdapter.State.QQS).isEqualTo(QSSceneAdapter.State.Expanding(0f))
- assertThat(QSSceneAdapter.State.QS).isEqualTo(QSSceneAdapter.State.Expanding(1f))
+ assertThatExpanding(QSSceneAdapter.State.QQS)
+ .isEqualTo(QSSceneAdapter.State.Expanding { 0f })
+ assertThatExpanding(QSSceneAdapter.State.QS)
+ .isEqualTo(QSSceneAdapter.State.Expanding { 1f })
}
@Test
fun collapsing() {
val collapsingProgress = 0.3f
- assertThat(Collapsing(collapsingProgress))
- .isEqualTo(QSSceneAdapter.State.Expanding(1 - collapsingProgress))
+ assertThatExpanding(Collapsing { collapsingProgress })
+ .isEqualTo(QSSceneAdapter.State.Expanding { 1 - collapsingProgress })
}
@Test
fun unsquishingQQS_expansionSameAsQQS() {
val squishiness = 0.6f
- assertThat(QSSceneAdapter.State.UnsquishingQQS { squishiness }.expansion)
- .isEqualTo(QSSceneAdapter.State.QQS.expansion)
+ assertThat(QSSceneAdapter.State.UnsquishingQQS { squishiness }.expansion())
+ .isEqualTo(QSSceneAdapter.State.QQS.expansion())
}
@Test
fun unsquishingQS_expansionSameAsQS() {
val squishiness = 0.6f
- assertThat(QSSceneAdapter.State.UnsquishingQS { squishiness }.expansion)
- .isEqualTo(QSSceneAdapter.State.QS.expansion)
+ assertThat(QSSceneAdapter.State.UnsquishingQS { squishiness }.expansion())
+ .isEqualTo(QSSceneAdapter.State.QS.expansion())
+ }
+}
+
+private class ExpandingSubject(
+ metadata: FailureMetadata,
+ private val actual: QSSceneAdapter.State.Expanding?
+) : Subject(metadata, actual) {
+ fun isEqualTo(expected: QSSceneAdapter.State.Expanding) {
+ isNotNull()
+ check("expansion()")
+ .that(actual?.expansion?.invoke())
+ .isEqualTo(expected.expansion.invoke())
+ }
+
+ companion object {
+ fun expanding(): Factory<ExpandingSubject, QSSceneAdapter.State.Expanding> {
+ return Factory { metadata: FailureMetadata, actual: QSSceneAdapter.State.Expanding? ->
+ ExpandingSubject(metadata, actual)
+ }
+ }
+
+ fun assertThatExpanding(actual: QSSceneAdapter.State.Expanding): ExpandingSubject {
+ return assertAbout(expanding()).that(actual)
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index c15a4e5..c633816 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -58,6 +58,7 @@
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
@@ -133,6 +134,7 @@
sceneInteractor = sceneInteractor,
falsingInteractor = kosmos.falsingInteractor,
powerInteractor = kosmos.powerInteractor,
+ logger = kosmos.sceneLogger,
motionEventHandlerReceiver = {},
)
.apply { setTransitionState(transitionState) }
@@ -493,7 +495,9 @@
private fun getCurrentSceneInUi(): SceneKey {
return when (val state = transitionState.value) {
is ObservableTransitionState.Idle -> state.currentScene
- is ObservableTransitionState.Transition -> state.fromScene
+ is ObservableTransitionState.Transition.ChangeCurrentScene -> state.fromScene
+ is ObservableTransitionState.Transition.ShowOrHideOverlay -> state.currentScene
+ is ObservableTransitionState.Transition.ReplaceOverlay -> state.currentScene
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 64a13de..8f8d2e2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -1682,6 +1682,7 @@
underTest.start()
// run all pending dismiss succeeded/cancelled calls from setup:
+ runCurrent()
kosmos.fakeExecutor.runAllReady()
val dismissCallback: IKeyguardDismissCallback = mock()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index b315783..f856c55 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -33,6 +33,7 @@
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.sceneContainerConfig
import com.android.systemui.scene.sceneKeys
+import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.testKosmos
@@ -72,6 +73,7 @@
sceneInteractor = sceneInteractor,
falsingInteractor = kosmos.falsingInteractor,
powerInteractor = kosmos.powerInteractor,
+ logger = kosmos.sceneLogger,
motionEventHandlerReceiver = { motionEventHandler ->
this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
},
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt
index eac86e5..ce9b3be 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt
@@ -23,7 +23,6 @@
import android.os.Handler
import android.testing.TestableLooper
import android.view.View
-import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -46,6 +45,8 @@
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.never
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -67,12 +68,9 @@
@Mock private lateinit var session: SmartspaceSession
- private lateinit var controller: CommunalSmartspaceController
+ private val preconditionListenerCaptor = argumentCaptor<SmartspacePrecondition.Listener>()
- // TODO(b/272811280): Remove usage of real view
- private val fakeParent by lazy {
- FrameLayout(context)
- }
+ private lateinit var controller: CommunalSmartspaceController
/**
* A class which implements SmartspaceView and extends View. This is mocked to provide the right
@@ -155,6 +153,26 @@
verify(session).close()
}
+ /** Ensures smartspace session begins when precondition is met if there is any listener. */
+ @Test
+ fun testConnectOnPreconditionMet() {
+ // Precondition not met
+ `when`(precondition.conditionsMet()).thenReturn(false)
+ controller.addListener(listener)
+
+ // Verify session not created because precondition not met
+ verify(smartspaceManager, never()).createSmartspaceSession(any())
+
+ // Precondition met
+ `when`(precondition.conditionsMet()).thenReturn(true)
+ verify(precondition).addListener(preconditionListenerCaptor.capture())
+ val preconditionListener = preconditionListenerCaptor.firstValue
+ preconditionListener.onCriteriaChanged()
+
+ // Verify session created
+ verify(smartspaceManager).createSmartspaceSession(any())
+ }
+
/**
* Ensures session is closed and weather plugin unregisters the notifier when weather smartspace
* view is detached.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 11504aa..20d3a7b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.policy.domain.interactor
+import android.app.AutomaticZenRule
import android.app.NotificationManager.Policy
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION
@@ -217,4 +218,35 @@
assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id))
.isEqualTo(Duration.ofMinutes(60))
}
+
+ @Test
+ fun mainActiveMode_returnsMainActiveMode() =
+ testScope.runTest {
+ val mainActiveMode by collectLastValue(underTest.mainActiveMode)
+
+ zenModeRepository.addMode(id = "Bedtime", type = AutomaticZenRule.TYPE_BEDTIME)
+ zenModeRepository.addMode(id = "Other", type = AutomaticZenRule.TYPE_OTHER)
+
+ runCurrent()
+ assertThat(mainActiveMode).isNull()
+
+ zenModeRepository.activateMode("Other")
+ runCurrent()
+ assertThat(mainActiveMode).isNotNull()
+ assertThat(mainActiveMode!!.id).isEqualTo("Other")
+
+ zenModeRepository.activateMode("Bedtime")
+ runCurrent()
+ assertThat(mainActiveMode).isNotNull()
+ assertThat(mainActiveMode!!.id).isEqualTo("Bedtime")
+
+ zenModeRepository.deactivateMode("Other")
+ runCurrent()
+ assertThat(mainActiveMode).isNotNull()
+ assertThat(mainActiveMode!!.id).isEqualTo("Bedtime")
+
+ zenModeRepository.deactivateMode("Bedtime")
+ runCurrent()
+ assertThat(mainActiveMode).isNull()
+ }
}
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 18b7073..fd943d0 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1869,6 +1869,8 @@
<!-- Text displayed indicating that the user is connected to a satellite signal. -->
<string name="satellite_connected_carrier_text">Satellite SOS</string>
+ <!-- Text displayed indicating that the user might be able to use satellite SOS. -->
+ <string name="satellite_emergency_only_carrier_text">Emergency calls or SOS</string>
<!-- Accessibility label for managed profile icon (not shown on screen) [CHAR LIMIT=NONE] -->
<string name="accessibility_managed_profile">Work profile</string>
diff --git a/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/3.json b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/3.json
new file mode 100644
index 0000000..fe996b7
--- /dev/null
+++ b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/3.json
@@ -0,0 +1,81 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "02e2da2d36e6955200edd5fb49e63c72",
+ "entities": [
+ {
+ "tableName": "communal_widget_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `widget_id` INTEGER NOT NULL, `component_name` TEXT NOT NULL, `item_id` INTEGER NOT NULL, `user_serial_number` INTEGER NOT NULL DEFAULT -1)",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "widgetId",
+ "columnName": "widget_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "componentName",
+ "columnName": "component_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "itemId",
+ "columnName": "item_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userSerialNumber",
+ "columnName": "user_serial_number",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "uid"
+ ]
+ }
+ },
+ {
+ "tableName": "communal_item_rank_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rank` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rank",
+ "columnName": "rank",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "uid"
+ ]
+ }
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '02e2da2d36e6955200edd5fb49e63c72')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index bf905db..7efe2dd 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -344,7 +344,7 @@
R.dimen.keyguard_security_container_padding_top), getPaddingRight(),
getPaddingBottom());
setBackgroundColor(Utils.getColorAttrDefaultColor(getContext(),
- com.android.internal.R.attr.materialColorSurface));
+ com.android.internal.R.attr.materialColorSurfaceDim));
}
void onResume(SecurityMode securityMode, boolean faceAuthEnabled) {
@@ -808,7 +808,7 @@
void reloadColors() {
mViewMode.reloadColors();
setBackgroundColor(Utils.getColorAttrDefaultColor(getContext(),
- com.android.internal.R.attr.materialColorSurface));
+ com.android.internal.R.attr.materialColorSurfaceDim));
}
/** Handles density or font scale changes. */
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewController.java
index abdc333..04595a2 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewController.java
@@ -48,6 +48,7 @@
import com.android.systemui.statusbar.policy.NextAlarmController;
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
import com.android.systemui.util.ViewController;
import com.android.systemui.util.time.DateFormatUtil;
@@ -127,6 +128,9 @@
private final DreamOverlayStatusBarItemsProvider.Callback mStatusBarItemsProviderCallback =
this::onStatusBarItemsChanged;
+ private final StatusBarWindowStateListener mStatusBarWindowStateListener =
+ this::onSystemStatusBarStateChanged;
+
@Inject
public AmbientStatusBarViewController(
AmbientStatusBarView view,
@@ -161,10 +165,22 @@
mWifiInteractor = wifiInteractor;
mCommunalSceneInteractor = communalSceneInteractor;
mLogger = new DreamLogger(logBuffer, TAG);
+ }
+
+ @Override
+ protected void onInit() {
+ super.onInit();
// Register to receive show/hide updates for the system status bar. Our custom status bar
// needs to hide when the system status bar is showing to ovoid overlapping status bars.
- statusBarWindowStateController.addListener(this::onSystemStatusBarStateChanged);
+ mStatusBarWindowStateController.addListener(mStatusBarWindowStateListener);
+ }
+
+ @Override
+ public void destroy() {
+ mStatusBarWindowStateController.removeListener(mStatusBarWindowStateListener);
+
+ super.destroy();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java
index 190bc15..d27e72a 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java
@@ -122,4 +122,9 @@
* @param session
*/
void onSessionStart(TouchSession session);
+
+ /**
+ * Called when the handler is being torn down.
+ */
+ default void onDestroy() {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
index efa55e9..1be6f9e 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
@@ -581,6 +581,10 @@
mBoundsFlow.cancel(new CancellationException());
}
+ for (TouchHandler handler : mHandlers) {
+ handler.onDestroy();
+ }
+
mInitialized = false;
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 723587e..970fdea 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -20,6 +20,7 @@
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION;
+import static com.android.systemui.Flags.enableViewCaptureTracing;
import android.animation.Animator;
import android.annotation.IntDef;
@@ -56,6 +57,8 @@
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.app.animation.Interpolators;
+import com.android.app.viewcapture.ViewCapture;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.widget.LockPatternUtils;
@@ -75,6 +78,8 @@
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.util.concurrency.DelayableExecutor;
+import kotlin.Lazy;
+
import kotlinx.coroutines.CoroutineScope;
import java.io.PrintWriter;
@@ -124,7 +129,7 @@
private final Config mConfig;
private final int mEffectiveUserId;
private final IBinder mWindowToken = new Binder();
- private final WindowManager mWindowManager;
+ private final ViewCaptureAwareWindowManager mWindowManager;
private final Interpolator mLinearOutSlowIn;
private final LockPatternUtils mLockPatternUtils;
private final WakefulnessLifecycle mWakefulnessLifecycle;
@@ -286,13 +291,16 @@
@NonNull PromptViewModel promptViewModel,
@NonNull Provider<CredentialViewModel> credentialViewModelProvider,
@NonNull @Background DelayableExecutor bgExecutor,
- @NonNull VibratorHelper vibratorHelper) {
+ @NonNull VibratorHelper vibratorHelper,
+ Lazy<ViewCapture> lazyViewCapture) {
super(config.mContext);
mConfig = config;
mLockPatternUtils = lockPatternUtils;
mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId);
- mWindowManager = mContext.getSystemService(WindowManager.class);
+ WindowManager wm = getContext().getSystemService(WindowManager.class);
+ mWindowManager = new ViewCaptureAwareWindowManager(wm, lazyViewCapture,
+ enableViewCaptureTracing());
mWakefulnessLifecycle = wakefulnessLifecycle;
mApplicationCoroutineScope = applicationCoroutineScope;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 037f5b7..097ab72 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -20,6 +20,8 @@
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
+import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityTaskManager;
@@ -61,6 +63,7 @@
import android.view.MotionEvent;
import android.view.WindowManager;
+import com.android.app.viewcapture.ViewCapture;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
@@ -181,6 +184,8 @@
private final DisplayInfo mCachedDisplayInfo = new DisplayInfo();
@NonNull private final VibratorHelper mVibratorHelper;
+ private final kotlin.Lazy<ViewCapture> mLazyViewCapture;
+
@VisibleForTesting
final TaskStackListener mTaskStackListener = new TaskStackListener() {
@Override
@@ -736,7 +741,8 @@
@Main Handler handler,
@Background DelayableExecutor bgExecutor,
@NonNull UdfpsUtils udfpsUtils,
- @NonNull VibratorHelper vibratorHelper) {
+ @NonNull VibratorHelper vibratorHelper,
+ Lazy<ViewCapture> daggerLazyViewCapture) {
mContext = context;
mExecution = execution;
mUserManager = userManager;
@@ -785,6 +791,8 @@
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
context.registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED_UNAUDITED);
mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
+
+ mLazyViewCapture = toKotlinLazy(daggerLazyViewCapture);
}
// TODO(b/229290039): UDFPS controller should manage its dimensions on its own. Remove this.
@@ -1318,7 +1326,8 @@
return new AuthContainerView(config, mApplicationCoroutineScope, mFpProps, mFaceProps,
wakefulnessLifecycle, userManager, lockPatternUtils,
mInteractionJankMonitor, mPromptSelectorInteractor, viewModel,
- mCredentialViewModelProvider, bgExecutor, mVibratorHelper);
+ mCredentialViewModelProvider, bgExecutor, mVibratorHelper,
+ mLazyViewCapture);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt
index 094dc0a..e939f37 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.bouncer.data.repository
+import android.hardware.biometrics.BiometricSourceType
import com.android.systemui.bouncer.shared.model.BouncerMessageModel
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
@@ -26,7 +27,12 @@
interface BouncerMessageRepository {
val bouncerMessage: Flow<BouncerMessageModel>
- fun setMessage(message: BouncerMessageModel)
+ fun setMessage(message: BouncerMessageModel, source: BiometricSourceType? = null)
+
+ /**
+ * Return the source of the current [bouncerMessage] if it was set from a biometric. Else, null.
+ */
+ fun getMessageSource(): BiometricSourceType?
}
@SysUISingleton
@@ -34,8 +40,14 @@
private val _bouncerMessage = MutableStateFlow(BouncerMessageModel())
override val bouncerMessage: Flow<BouncerMessageModel> = _bouncerMessage
+ private var messageSource: BiometricSourceType? = null
- override fun setMessage(message: BouncerMessageModel) {
+ override fun setMessage(message: BouncerMessageModel, source: BiometricSourceType?) {
_bouncerMessage.value = message
+ messageSource = source
+ }
+
+ override fun getMessageSource(): BiometricSourceType? {
+ return messageSource
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
index 8e12646..d125c36 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.bouncer.domain.interactor
+import android.hardware.biometrics.BiometricFaceConstants
import android.hardware.biometrics.BiometricSourceType
import android.os.CountDownTimer
import com.android.keyguard.KeyguardSecurityModel
@@ -32,9 +33,7 @@
import com.android.systemui.bouncer.shared.model.Message
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryBiometricsAllowedInteractor
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
import com.android.systemui.flags.SystemPropertiesHelper
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.TrustRepository
@@ -74,8 +73,6 @@
primaryBouncerInteractor: PrimaryBouncerInteractor,
@Application private val applicationScope: CoroutineScope,
private val facePropertyRepository: FacePropertyRepository,
- private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
- faceAuthRepository: DeviceEntryFaceAuthRepository,
private val securityModel: KeyguardSecurityModel,
deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor,
) {
@@ -96,6 +93,17 @@
private val kumCallback =
object : KeyguardUpdateMonitorCallback() {
override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) {
+ // Only show the biometric failure messages if the biometric is NOT locked out.
+ // If the biometric is locked out, rely on the lock out message to show
+ // the lockout message & don't override it with the failure message.
+ if (
+ (biometricSourceType == BiometricSourceType.FACE &&
+ deviceEntryBiometricsAllowedInteractor.isFaceLockedOut.value) ||
+ (biometricSourceType == BiometricSourceType.FINGERPRINT &&
+ deviceEntryBiometricsAllowedInteractor.isFingerprintLockedOut.value)
+ ) {
+ return
+ }
repository.setMessage(
when (biometricSourceType) {
BiometricSourceType.FINGERPRINT ->
@@ -115,7 +123,8 @@
isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
- }
+ },
+ biometricSourceType,
)
}
@@ -123,7 +132,12 @@
biometricSourceType: BiometricSourceType?,
acquireInfo: Int
) {
- super.onBiometricAcquired(biometricSourceType, acquireInfo)
+ if (
+ repository.getMessageSource() == BiometricSourceType.FACE &&
+ acquireInfo == BiometricFaceConstants.FACE_ACQUIRED_START
+ ) {
+ repository.setMessage(defaultMessage)
+ }
}
override fun onBiometricAuthenticated(
@@ -131,7 +145,7 @@
biometricSourceType: BiometricSourceType?,
isStrongBiometric: Boolean
) {
- repository.setMessage(defaultMessage)
+ repository.setMessage(defaultMessage, biometricSourceType)
}
}
@@ -152,8 +166,8 @@
biometricSettingsRepository.authenticationFlags,
trustRepository.isCurrentUserTrustManaged,
isAnyBiometricsEnabledAndEnrolled,
- deviceEntryFingerprintAuthInteractor.isLockedOut,
- faceAuthRepository.isLockedOut,
+ deviceEntryBiometricsAllowedInteractor.isFingerprintLockedOut,
+ deviceEntryBiometricsAllowedInteractor.isFaceLockedOut,
isFingerprintAuthCurrentlyAllowedOnBouncer,
::Septuple
)
@@ -291,7 +305,8 @@
currentSecurityMode,
value,
isFingerprintAuthCurrentlyAllowedOnBouncer.value
- )
+ ),
+ BiometricSourceType.FINGERPRINT,
)
}
@@ -302,7 +317,8 @@
currentSecurityMode,
value,
isFingerprintAuthCurrentlyAllowedOnBouncer.value
- )
+ ),
+ BiometricSourceType.FACE,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index 040af90..aabfbd1 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -20,6 +20,7 @@
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS;
import static com.android.systemui.Flags.clipboardImageTimeout;
+import static com.android.systemui.Flags.clipboardSharedTransitions;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER;
@@ -33,7 +34,6 @@
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
-import static com.android.systemui.flags.Flags.CLIPBOARD_SHARED_TRANSITIONS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -207,7 +207,7 @@
mClipboardUtils = clipboardUtils;
mBgExecutor = bgExecutor;
- if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+ if (clipboardSharedTransitions()) {
mView.setCallbacks(this);
} else {
mView.setCallbacks(mClipboardCallbacks);
@@ -220,7 +220,7 @@
});
mTimeoutHandler.setOnTimeoutRunnable(() -> {
- if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+ if (clipboardSharedTransitions()) {
finish(CLIPBOARD_OVERLAY_TIMED_OUT);
} else {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT);
@@ -232,7 +232,7 @@
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
- if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+ if (clipboardSharedTransitions()) {
finish(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
} else {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
@@ -248,7 +248,7 @@
@Override
public void onReceive(Context context, Intent intent) {
if (SCREENSHOT_ACTION.equals(intent.getAction())) {
- if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+ if (clipboardSharedTransitions()) {
finish(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
} else {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
@@ -481,7 +481,7 @@
remoteAction.ifPresent(action -> {
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN);
mView.post(() -> mView.setActionChip(action, () -> {
- if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+ if (clipboardSharedTransitions()) {
finish(CLIPBOARD_OVERLAY_ACTION_TAPPED);
} else {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
@@ -528,7 +528,7 @@
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
if (!mView.isInTouchRegion(
(int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
- if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+ if (clipboardSharedTransitions()) {
finish(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
} else {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
@@ -575,6 +575,10 @@
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
+ // check again after animation to see if we should still be minimized
+ if (mIsMinimized && !shouldShowMinimized(mWindow.getWindowInsets())) {
+ animateFromMinimized();
+ }
if (mOnUiUpdate != null) {
mOnUiUpdate.run();
}
@@ -690,14 +694,14 @@
@Override
public void onDismissButtonTapped() {
- if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+ if (clipboardSharedTransitions()) {
finish(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
}
}
@Override
public void onRemoteCopyButtonTapped() {
- if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+ if (clipboardSharedTransitions()) {
finish(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED,
IntentCreator.getRemoteCopyIntent(mClipboardModel.getClipData(), mContext));
}
@@ -705,7 +709,7 @@
@Override
public void onShareButtonTapped() {
- if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+ if (clipboardSharedTransitions()) {
if (mClipboardModel.getType() != ClipboardModel.Type.OTHER) {
finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED,
IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
@@ -715,7 +719,7 @@
@Override
public void onPreviewTapped() {
- if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+ if (clipboardSharedTransitions()) {
switch (mClipboardModel.getType()) {
case TEXT:
finish(CLIPBOARD_OVERLAY_EDIT_TAPPED,
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
index dff6352..8f1854f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
@@ -26,7 +26,7 @@
import androidx.sqlite.db.SupportSQLiteDatabase
import com.android.systemui.res.R
-@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 2)
+@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 3)
abstract class CommunalDatabase : RoomDatabase() {
abstract fun communalWidgetDao(): CommunalWidgetDao
@@ -55,7 +55,7 @@
context.resources.getString(R.string.config_communalDatabase)
)
.also { builder ->
- builder.addMigrations(MIGRATION_1_2)
+ builder.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
builder.fallbackToDestructiveMigration(dropAllTables = true)
callback?.let { callback -> builder.addCallback(callback) }
}
@@ -87,5 +87,21 @@
)
}
}
+
+ /**
+ * This migration reverses the ranks. For example, if the ranks are 2, 1, 0, then after the
+ * migration they will be 0, 1, 2.
+ */
+ @VisibleForTesting
+ val MIGRATION_2_3 =
+ object : Migration(2, 3) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ Log.i(TAG, "Migrating from version 2 to 3")
+ db.execSQL(
+ "UPDATE communal_item_rank_table " +
+ "SET rank = (SELECT MAX(rank) FROM communal_item_rank_table) - rank"
+ )
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
index 933a25a..93b86bd 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -97,7 +97,7 @@
.addWidget(
widgetId = id,
componentName = name,
- priority = defaultWidgets.size - index,
+ rank = index,
userSerialNumber = userSerialNumber,
)
}
@@ -132,10 +132,17 @@
@Query(
"SELECT * FROM communal_widget_table JOIN communal_item_rank_table " +
"ON communal_item_rank_table.uid = communal_widget_table.item_id " +
- "ORDER BY communal_item_rank_table.rank DESC"
+ "ORDER BY communal_item_rank_table.rank ASC"
)
fun getWidgets(): Flow<Map<CommunalItemRank, CommunalWidgetItem>>
+ @Query(
+ "SELECT * FROM communal_widget_table JOIN communal_item_rank_table " +
+ "ON communal_item_rank_table.uid = communal_widget_table.item_id " +
+ "ORDER BY communal_item_rank_table.rank ASC"
+ )
+ fun getWidgetsNow(): Map<CommunalItemRank, CommunalWidgetItem>
+
@Query("SELECT * FROM communal_widget_table WHERE widget_id = :id")
fun getWidgetByIdNow(id: Int): CommunalWidgetItem?
@@ -167,11 +174,11 @@
@Query("DELETE FROM communal_item_rank_table") fun clearCommunalItemRankTable()
@Transaction
- fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {
- widgetIdToPriorityMap.forEach { (id, priority) ->
+ fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) {
+ widgetIdToRankMap.forEach { (id, rank) ->
val widget = getWidgetByIdNow(id)
if (widget != null) {
- updateItemRank(widget.itemId, priority)
+ updateItemRank(widget.itemId, rank)
}
}
}
@@ -180,13 +187,13 @@
fun addWidget(
widgetId: Int,
provider: ComponentName,
- priority: Int,
+ rank: Int? = null,
userSerialNumber: Int,
): Long {
return addWidget(
widgetId = widgetId,
componentName = provider.flattenToString(),
- priority = priority,
+ rank = rank,
userSerialNumber = userSerialNumber,
)
}
@@ -195,13 +202,27 @@
fun addWidget(
widgetId: Int,
componentName: String,
- priority: Int,
+ rank: Int? = null,
userSerialNumber: Int,
): Long {
+ val widgets = getWidgetsNow()
+
+ // If rank is not specified, rank it last by finding the current maximum rank and increment
+ // by 1. If the new widget is the first widget, set the rank to 0.
+ val newRank = rank ?: widgets.keys.maxOfOrNull { it.rank + 1 } ?: 0
+
+ // Shift widgets after [rank], unless widget is added at the end.
+ if (rank != null) {
+ widgets.forEach { (rankEntry, widgetEntry) ->
+ if (rankEntry.rank < newRank) return@forEach
+ updateItemRank(widgetEntry.itemId, rankEntry.rank + 1)
+ }
+ }
+
return insertWidget(
widgetId = widgetId,
componentName = componentName,
- itemId = insertItemRank(priority),
+ itemId = insertItemRank(newRank),
userSerialNumber = userSerialNumber,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index ad0bfc7..6cdd9ff 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -57,12 +57,17 @@
/** A flow of information about active communal widgets stored in database. */
val communalWidgets: Flow<List<CommunalWidgetContentModel>>
- /** Add a widget at the specified position in the app widget service and the database. */
+ /**
+ * Add a widget in the app widget service and the database.
+ *
+ * @param rank The rank of the widget determines its position in the grid. 0 is first place, 1
+ * is second, etc. If rank is not specified, widget is added at the end.
+ */
fun addWidget(
provider: ComponentName,
user: UserHandle,
- priority: Int,
- configurator: WidgetConfigurator? = null
+ rank: Int?,
+ configurator: WidgetConfigurator? = null,
) {}
/**
@@ -75,9 +80,9 @@
/**
* Update the order of widgets in the database.
*
- * @param widgetIdToPriorityMap mapping of the widget ids to the priority of the widget.
+ * @param widgetIdToRankMap mapping of the widget ids to the rank of the widget.
*/
- fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {}
+ fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) {}
/**
* Restores the database by reading a state file from disk and updating the widget ids according
@@ -121,7 +126,7 @@
CommunalWidgetEntry(
appWidgetId = widget.widgetId,
componentName = widget.componentName,
- priority = rank.rank,
+ rank = rank.rank,
providerInfo = providers[widget.widgetId]
)
}
@@ -151,8 +156,8 @@
override fun addWidget(
provider: ComponentName,
user: UserHandle,
- priority: Int,
- configurator: WidgetConfigurator?
+ rank: Int?,
+ configurator: WidgetConfigurator?,
) {
bgScope.launch {
val id = communalWidgetHost.allocateIdAndBindWidget(provider, user)
@@ -190,14 +195,14 @@
communalWidgetDao.addWidget(
widgetId = id,
provider = provider,
- priority = priority,
+ rank = rank,
userSerialNumber = userManager.getUserSerialNumber(user.identifier),
)
backupManager.dataChanged()
} else {
appWidgetHost.deleteAppWidgetId(id)
}
- logger.i("Added widget ${provider.flattenToString()} at position $priority.")
+ logger.i("Added widget ${provider.flattenToString()} at position $rank.")
}
}
@@ -211,11 +216,11 @@
}
}
- override fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {
+ override fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) {
bgScope.launch {
- communalWidgetDao.updateWidgetOrder(widgetIdToPriorityMap)
+ communalWidgetDao.updateWidgetOrder(widgetIdToRankMap)
logger.i({ "Updated the order of widget list with ids: $str1." }) {
- str1 = widgetIdToPriorityMap.toString()
+ str1 = widgetIdToRankMap.toString()
}
backupManager.dataChanged()
}
@@ -342,7 +347,7 @@
addWidget(
provider = ComponentName.unflattenFromString(widget.componentName)!!,
user = newUser,
- priority = widget.rank,
+ rank = widget.rank,
)
}
@@ -377,7 +382,7 @@
return CommunalWidgetContentModel.Available(
appWidgetId = entry.appWidgetId,
providerInfo = entry.providerInfo!!,
- priority = entry.priority,
+ rank = entry.rank,
)
}
@@ -394,7 +399,7 @@
return CommunalWidgetContentModel.Available(
appWidgetId = entry.appWidgetId,
providerInfo = entry.providerInfo!!,
- priority = entry.priority,
+ rank = entry.rank,
)
}
@@ -403,7 +408,7 @@
return if (componentName != null && session != null) {
CommunalWidgetContentModel.Pending(
appWidgetId = entry.appWidgetId,
- priority = entry.priority,
+ rank = entry.rank,
componentName = componentName,
icon = session.icon,
user = session.user,
@@ -416,7 +421,7 @@
private data class CommunalWidgetEntry(
val appWidgetId: Int,
val componentName: String,
- val priority: Int,
+ val rank: Int,
var providerInfo: AppWidgetProviderInfo? = null,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 7181b15..2aa6c19 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -367,13 +367,16 @@
/** Dismiss the CTA tile from the hub in view mode. */
suspend fun dismissCtaTile() = communalPrefsInteractor.setCtaDismissed()
- /** Add a widget at the specified position. */
+ /**
+ * Add a widget at the specified rank. If rank is not provided, the widget will be added at the
+ * end.
+ */
fun addWidget(
componentName: ComponentName,
user: UserHandle,
- priority: Int,
+ rank: Int? = null,
configurator: WidgetConfigurator?,
- ) = widgetRepository.addWidget(componentName, user, priority, configurator)
+ ) = widgetRepository.addWidget(componentName, user, rank, configurator)
/**
* Delete a widget by id. Called when user deletes a widget from the hub or a widget is
@@ -384,10 +387,10 @@
/**
* Reorder the widgets.
*
- * @param widgetIdToPriorityMap mapping of the widget ids to their new priorities.
+ * @param widgetIdToRankMap mapping of the widget ids to their new priorities.
*/
- fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) =
- widgetRepository.updateWidgetOrder(widgetIdToPriorityMap)
+ fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) =
+ widgetRepository.updateWidgetOrder(widgetIdToRankMap)
/** Request to unpause work profile that is currently in quiet mode. */
fun unpauseWorkProfile() {
@@ -440,7 +443,7 @@
is CommunalWidgetContentModel.Available -> {
WidgetContent.Widget(
appWidgetId = widget.appWidgetId,
- priority = widget.priority,
+ rank = widget.rank,
providerInfo = widget.providerInfo,
appWidgetHost = appWidgetHost,
inQuietMode = isQuietModeEnabled(widget.providerInfo.profile)
@@ -449,7 +452,7 @@
is CommunalWidgetContentModel.Pending -> {
WidgetContent.PendingWidget(
appWidgetId = widget.appWidgetId,
- priority = widget.priority,
+ rank = widget.rank,
componentName = widget.componentName,
icon = widget.icon,
)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
index 6343752..5bbb46d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
@@ -223,19 +223,15 @@
// KTF for this case and just collect the new progress instead.
collectProgress(transition)
} else if (transition.toScene == CommunalScenes.Communal) {
- if (currentTransitionId != null) {
- if (currentToState == KeyguardState.GLANCEABLE_HUB) {
- transitionKtfTo(transitionInteractor.getStartedFromState())
- }
+ if (currentToState == KeyguardState.GLANCEABLE_HUB) {
+ transitionKtfTo(transitionInteractor.getStartedFromState())
}
startTransitionToGlanceableHub()
collectProgress(transition)
} else if (transition.toScene == CommunalScenes.Blank) {
- if (currentTransitionId != null) {
- // Another transition started before this one is completed. Transition to the
- // GLANCEABLE_HUB state so that we can properly transition away from it.
- transitionKtfTo(KeyguardState.GLANCEABLE_HUB)
- }
+ // Another transition started before this one is completed. Transition to the
+ // GLANCEABLE_HUB state so that we can properly transition away from it.
+ transitionKtfTo(KeyguardState.GLANCEABLE_HUB)
startTransitionFromGlanceableHub()
collectProgress(transition)
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
index 73c6ce3..4c821d4 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
@@ -47,12 +47,12 @@
sealed interface WidgetContent : CommunalContentModel {
val appWidgetId: Int
- val priority: Int
+ val rank: Int
val componentName: ComponentName
data class Widget(
override val appWidgetId: Int,
- override val priority: Int,
+ override val rank: Int,
val providerInfo: AppWidgetProviderInfo,
val appWidgetHost: CommunalAppWidgetHost,
val inQuietMode: Boolean,
@@ -71,7 +71,7 @@
data class DisabledWidget(
override val appWidgetId: Int,
- override val priority: Int,
+ override val rank: Int,
val providerInfo: AppWidgetProviderInfo
) : WidgetContent {
override val key = KEY.disabledWidget(appWidgetId)
@@ -85,7 +85,7 @@
data class PendingWidget(
override val appWidgetId: Int,
- override val priority: Int,
+ override val rank: Int,
override val componentName: ComponentName,
val icon: Bitmap? = null,
) : WidgetContent {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalMetricsLogger.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalMetricsLogger.kt
index 9ce8cf7..7cfad60 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalMetricsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalMetricsLogger.kt
@@ -31,7 +31,7 @@
private val statsLogProxy: StatsLogProxy,
) {
/** Logs an add widget event for metrics. No-op if widget is not loggable. */
- fun logAddWidget(componentName: String, rank: Int) {
+ fun logAddWidget(componentName: String, rank: Int?) {
if (!componentName.isLoggable()) {
return
}
@@ -39,7 +39,7 @@
statsLogProxy.writeCommunalHubWidgetEventReported(
SysUiStatsLog.COMMUNAL_HUB_WIDGET_EVENT_REPORTED__ACTION__ADD,
componentName,
- rank,
+ rank ?: -1,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
index 7cddb72..63b1a14 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
@@ -24,19 +24,19 @@
/** Encapsulates data for a communal widget. */
sealed interface CommunalWidgetContentModel {
val appWidgetId: Int
- val priority: Int
+ val rank: Int
/** Widget is ready to display */
data class Available(
override val appWidgetId: Int,
val providerInfo: AppWidgetProviderInfo,
- override val priority: Int,
+ override val rank: Int,
) : CommunalWidgetContentModel
/** Widget is pending installation */
data class Pending(
override val appWidgetId: Int,
- override val priority: Int,
+ override val rank: Int,
val componentName: ComponentName,
val icon: Bitmap?,
val user: UserHandle,
diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
index 80db535..012c844 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
@@ -65,6 +65,12 @@
var preconditionListener =
object : SmartspacePrecondition.Listener {
override fun onCriteriaChanged() {
+ if (session == null && hasActiveSessionListeners()) {
+ Log.d(TAG, "Precondition criteria changed. Attempting to connect session.")
+ connectSession()
+ return
+ }
+
reloadSmartspace()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index b822133..6be94a7 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -153,7 +153,7 @@
open fun onAddWidget(
componentName: ComponentName,
user: UserHandle,
- priority: Int,
+ rank: Int? = null,
configurator: WidgetConfigurator? = null,
) {}
@@ -161,23 +161,23 @@
open fun onDeleteWidget(
id: Int,
componentName: ComponentName,
- priority: Int,
+ rank: Int,
) {}
/** Called as the UI detects a tap event on the widget. */
open fun onTapWidget(
componentName: ComponentName,
- priority: Int,
+ rank: Int,
) {}
/**
* Called as the UI requests reordering widgets.
*
- * @param widgetIdToPriorityMap mapping of the widget ids to its priority. When re-ordering to
- * add a new item in the middle, provide the priorities of existing widgets as if the new item
- * existed, and then, call [onAddWidget] to add the new item at intended order.
+ * @param widgetIdToRankMap mapping of the widget ids to its rank. When re-ordering to add a new
+ * item in the middle, provide the priorities of existing widgets as if the new item existed,
+ * and then, call [onAddWidget] to add the new item at intended order.
*/
- open fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) {}
+ open fun onReorderWidgets(widgetIdToRankMap: Map<Int, Int>) {}
/** Called as the UI requests opening the widget editor with an optional preselected widget. */
open fun onOpenWidgetEditor(
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 1a86c71..16788d1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -125,24 +125,24 @@
override fun onAddWidget(
componentName: ComponentName,
user: UserHandle,
- priority: Int,
+ rank: Int?,
configurator: WidgetConfigurator?
) {
- communalInteractor.addWidget(componentName, user, priority, configurator)
- metricsLogger.logAddWidget(componentName.flattenToString(), priority)
+ communalInteractor.addWidget(componentName, user, rank, configurator)
+ metricsLogger.logAddWidget(componentName.flattenToString(), rank)
}
override fun onDeleteWidget(
id: Int,
componentName: ComponentName,
- priority: Int,
+ rank: Int,
) {
communalInteractor.deleteWidget(id)
- metricsLogger.logRemoveWidget(componentName.flattenToString(), priority)
+ metricsLogger.logRemoveWidget(componentName.flattenToString(), rank)
}
- override fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) =
- communalInteractor.updateWidgetOrder(widgetIdToPriorityMap)
+ override fun onReorderWidgets(widgetIdToRankMap: Map<Int, Int>) =
+ communalInteractor.updateWidgetOrder(widgetIdToRankMap)
override fun onReorderWidgetStart() {
// Clear selection status
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index c0a18f2..4c762dc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -272,8 +272,8 @@
}
}
- override fun onTapWidget(componentName: ComponentName, priority: Int) {
- metricsLogger.logTapWidget(componentName.flattenToString(), priority)
+ override fun onTapWidget(componentName: ComponentName, rank: Int) {
+ metricsLogger.logTapWidget(componentName.flattenToString(), rank)
}
fun onClick() {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index b421e59..93c3a63 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -16,7 +16,10 @@
package com.android.systemui.communal.widgets
+import android.app.Activity
+import android.app.Application.ActivityLifecycleCallbacks
import android.content.Intent
+import android.content.IntentSender
import android.os.Bundle
import android.os.RemoteException
import android.util.Log
@@ -34,6 +37,7 @@
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.compose.theme.PlatformTheme
import com.android.internal.logging.UiEventLogger
+import com.android.systemui.Flags.communalEditWidgetsActivityFinishFix
import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.shared.model.CommunalTransitionKeys
@@ -68,12 +72,106 @@
const val EXTRA_OPEN_WIDGET_PICKER_ON_START = "open_widget_picker_on_start"
}
+ /**
+ * [ActivityController] handles closing the activity in the case it is backgrounded without
+ * waiting for an activity result
+ */
+ interface ActivityController {
+ /**
+ * Invoked when waiting for an activity result changes, either initiating such wait or
+ * finishing due to the return of a result.
+ */
+ fun onWaitingForResult(waitingForResult: Boolean) {}
+
+ /** Set the visibility of the activity under control. */
+ fun setActivityFullyVisible(fullyVisible: Boolean) {}
+ }
+
+ /**
+ * A nop ActivityController to be use when the communalEditWidgetsActivityFinishFix flag is
+ * false.
+ */
+ class NopActivityController : ActivityController
+
+ /**
+ * A functional ActivityController to be used when the communalEditWidgetsActivityFinishFix flag
+ * is true.
+ */
+ class ActivityControllerImpl(activity: Activity) : ActivityController {
+ companion object {
+ private const val STATE_EXTRA_IS_WAITING_FOR_RESULT = "extra_is_waiting_for_result"
+ }
+
+ private var waitingForResult = false
+ private var activityFullyVisible = false
+
+ init {
+ activity.registerActivityLifecycleCallbacks(
+ object : ActivityLifecycleCallbacks {
+ override fun onActivityCreated(
+ activity: Activity,
+ savedInstanceState: Bundle?
+ ) {
+ waitingForResult =
+ savedInstanceState?.getBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT)
+ ?: false
+ }
+
+ override fun onActivityStarted(activity: Activity) {
+ // Nothing to implement.
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+ // Nothing to implement.
+ }
+
+ override fun onActivityPaused(activity: Activity) {
+ // Nothing to implement.
+ }
+
+ override fun onActivityStopped(activity: Activity) {
+ // If we're not backgrounded due to waiting for a result (either widget
+ // selection or configuration), and we are fully visible, then finish the
+ // activity.
+ if (
+ !waitingForResult &&
+ activityFullyVisible &&
+ !activity.isChangingConfigurations
+ ) {
+ activity.finish()
+ }
+ }
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
+ outState.putBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT, waitingForResult)
+ }
+
+ override fun onActivityDestroyed(activity: Activity) {
+ // Nothing to implement.
+ }
+ }
+ )
+ }
+
+ override fun onWaitingForResult(waitingForResult: Boolean) {
+ this.waitingForResult = waitingForResult
+ }
+
+ override fun setActivityFullyVisible(fullyVisible: Boolean) {
+ activityFullyVisible = fullyVisible
+ }
+ }
+
private val logger = Logger(logBuffer, "EditWidgetsActivity")
private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) }
private var shouldOpenWidgetPickerOnStart = false
+ private val activityController: ActivityController =
+ if (communalEditWidgetsActivityFinishFix()) ActivityControllerImpl(this)
+ else NopActivityController()
+
private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> =
registerForActivityResult(StartActivityForResult()) { result ->
when (result.resultCode) {
@@ -89,11 +187,11 @@
if (!isPendingWidgetDrag) {
val (componentName, user) = getWidgetExtraFromIntent(intent)
if (componentName != null && user != null) {
+ // Add widget at the end.
communalViewModel.onAddWidget(
componentName,
user,
- 0,
- widgetConfigurator
+ configurator = widgetConfigurator,
)
} else {
run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
@@ -111,8 +209,10 @@
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
listenForTransitionAndChangeScene()
+ activityController.setActivityFullyVisible(false)
communalViewModel.setEditModeOpen(true)
val windowInsetsController = window.decorView.windowInsetsController
@@ -159,6 +259,9 @@
communalViewModel.currentScene.first { it == CommunalScenes.Blank }
communalViewModel.setEditModeState(EditModeState.SHOWING)
+ // Inform the ActivityController that we are now fully visible.
+ activityController.setActivityFullyVisible(true)
+
// Show the widget picker, if necessary, after the edit activity has animated in.
// Waiting until after the activity has appeared avoids transitions issues.
if (shouldOpenWidgetPickerOnStart) {
@@ -198,7 +301,34 @@
}
}
+ override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
+ activityController.onWaitingForResult(true)
+ super.startActivityForResult(intent, requestCode, options)
+ }
+
+ override fun startIntentSenderForResult(
+ intent: IntentSender,
+ requestCode: Int,
+ fillInIntent: Intent?,
+ flagsMask: Int,
+ flagsValues: Int,
+ extraFlags: Int,
+ options: Bundle?
+ ) {
+ activityController.onWaitingForResult(true)
+ super.startIntentSenderForResult(
+ intent,
+ requestCode,
+ fillInIntent,
+ flagsMask,
+ flagsValues,
+ extraFlags,
+ options
+ )
+ }
+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ activityController.onWaitingForResult(false)
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == WidgetConfigurationController.REQUEST_CODE) {
widgetConfigurator.setConfigurationResult(resultCode)
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricsAllowedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricsAllowedInteractor.kt
index 79b176c..7aee12f 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricsAllowedInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricsAllowedInteractor.kt
@@ -22,6 +22,7 @@
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -44,18 +45,30 @@
biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor,
facePropertyRepository: FacePropertyRepository,
) {
+ /**
+ * Whether face is locked out due to too many failed face attempts. This currently includes
+ * whether face is not allowed based on other biometric lockouts; however does not include if
+ * face isn't allowed due to other strong or primary authentication requirements.
+ */
+ val isFaceLockedOut: StateFlow<Boolean> = deviceEntryFaceAuthInteractor.isLockedOut
private val isStrongFaceAuth: Flow<Boolean> =
facePropertyRepository.sensorInfo.map { it?.strength == SensorStrength.STRONG }
private val isStrongFaceAuthLockedOut: Flow<Boolean> =
- combine(isStrongFaceAuth, deviceEntryFaceAuthInteractor.isLockedOut) {
- isStrongFaceAuth,
- isFaceAuthLockedOut ->
+ combine(isStrongFaceAuth, isFaceLockedOut) { isStrongFaceAuth, isFaceAuthLockedOut ->
isStrongFaceAuth && isFaceAuthLockedOut
}
/**
+ * Whether fingerprint is locked out due to too many failed fingerprint attempts. This does NOT
+ * include whether fingerprint is not allowed based on other biometric lockouts nor if
+ * fingerprint isn't allowed due to other strong or primary authentication requirements.
+ */
+ val isFingerprintLockedOut: StateFlow<Boolean> =
+ deviceEntryFingerprintAuthInteractor.isLockedOut
+
+ /**
* Whether fingerprint authentication is currently allowed for the user. This is true if the
* user has fingerprint auth enabled, enrolled, it is not disabled by any security timeouts by
* [com.android.systemui.keyguard.shared.model.AuthenticationFlags], not locked out due to too
@@ -64,7 +77,7 @@
*/
val isFingerprintAuthCurrentlyAllowed: Flow<Boolean> =
combine(
- deviceEntryFingerprintAuthInteractor.isLockedOut,
+ isFingerprintLockedOut,
biometricSettingsInteractor.fingerprintAuthCurrentlyAllowed,
isStrongFaceAuthLockedOut,
) { fpLockedOut, fpAllowedBySettings, strongAuthFaceAuthLockedOut ->
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
index 969f53f..5c058fe 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
@@ -54,7 +54,7 @@
val authenticationStatus: Flow<FingerprintAuthenticationStatus> =
repository.authenticationStatus
- val isLockedOut: Flow<Boolean> = repository.isLockedOut
+ val isLockedOut: StateFlow<Boolean> = repository.isLockedOut
val fingerprintFailure: Flow<FailFingerprintAuthenticationStatus> =
repository.authenticationStatus.filterIsInstance<FailFingerprintAuthenticationStatus>()
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index b45ebd8..24ac542 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -44,6 +44,7 @@
import com.android.systemui.statusbar.CrossFadeHelper
import javax.inject.Inject
import javax.inject.Named
+import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.launch
/** Controller for dream overlay animations. */
@@ -84,51 +85,62 @@
private var mCurrentBlurRadius: Float = 0f
+ private var mLifecycleFlowHandle: DisposableHandle? = null
+
fun init(view: View) {
this.view = view
- view.repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- launch {
- dreamViewModel.dreamOverlayTranslationY.collect { px ->
- ComplicationLayoutParams.iteratePositions(
- { position: Int -> setElementsTranslationYAtPosition(px, position) },
- POSITION_TOP or POSITION_BOTTOM
- )
+ mLifecycleFlowHandle =
+ view.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ launch {
+ dreamViewModel.dreamOverlayTranslationY.collect { px ->
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsTranslationYAtPosition(px, position)
+ },
+ POSITION_TOP or POSITION_BOTTOM
+ )
+ }
}
- }
- launch {
- dreamViewModel.dreamOverlayTranslationX.collect { px ->
- ComplicationLayoutParams.iteratePositions(
- { position: Int -> setElementsTranslationXAtPosition(px, position) },
- POSITION_TOP or POSITION_BOTTOM
- )
+ launch {
+ dreamViewModel.dreamOverlayTranslationX.collect { px ->
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsTranslationXAtPosition(px, position)
+ },
+ POSITION_TOP or POSITION_BOTTOM
+ )
+ }
}
- }
- launch {
- dreamViewModel.dreamOverlayAlpha.collect { alpha ->
- ComplicationLayoutParams.iteratePositions(
- { position: Int ->
- setElementsAlphaAtPosition(
- alpha = alpha,
- position = position,
- fadingOut = true,
- )
- },
- POSITION_TOP or POSITION_BOTTOM
- )
+ launch {
+ dreamViewModel.dreamOverlayAlpha.collect { alpha ->
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsAlphaAtPosition(
+ alpha = alpha,
+ position = position,
+ fadingOut = true,
+ )
+ },
+ POSITION_TOP or POSITION_BOTTOM
+ )
+ }
}
- }
- launch {
- dreamViewModel.transitionEnded.collect { _ ->
- mOverlayStateController.setExitAnimationsRunning(false)
+ launch {
+ dreamViewModel.transitionEnded.collect { _ ->
+ mOverlayStateController.setExitAnimationsRunning(false)
+ }
}
}
}
- }
+ }
+
+ fun destroy() {
+ mLifecycleFlowHandle?.dispose()
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 76c7d23..bf6d266 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -59,6 +59,7 @@
import com.android.systemui.util.ViewController;
import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.DisposableHandle;
import kotlinx.coroutines.flow.FlowKt;
import java.util.Arrays;
@@ -185,6 +186,8 @@
}
};
+ private DisposableHandle mFlowHandle;
+
@Inject
public DreamOverlayContainerViewController(
DreamOverlayContainerView containerView,
@@ -252,6 +255,17 @@
}
@Override
+ public void destroy() {
+ mStateController.removeCallback(mDreamOverlayStateCallback);
+ mStatusBarViewController.destroy();
+ mComplicationHostViewController.destroy();
+ mDreamOverlayAnimationsController.destroy();
+ mLowLightTransitionCoordinator.setLowLightEnterListener(null);
+
+ super.destroy();
+ }
+
+ @Override
protected void onViewAttached() {
mWakingUpFromSwipe = false;
mJitterStartTimeMillis = System.currentTimeMillis();
@@ -263,7 +277,7 @@
emptyRegion.recycle();
if (dreamHandlesBeingObscured()) {
- collectFlow(
+ mFlowHandle = collectFlow(
mView,
FlowKt.distinctUntilChanged(combineFlows(
mKeyguardTransitionInteractor.isFinishedIn(
@@ -295,6 +309,10 @@
@Override
protected void onViewDetached() {
+ if (mFlowHandle != null) {
+ mFlowHandle.dispose();
+ mFlowHandle = null;
+ }
mHandler.removeCallbacksAndMessages(null);
mPrimaryBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback);
mBouncerlessScrimController.removeCallback(mBouncerlessExpansionCallback);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 7a9537b..4c22763 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -60,7 +60,6 @@
import com.android.systemui.communal.domain.interactor.CommunalInteractor;
import com.android.systemui.communal.shared.log.CommunalUiEvent;
import com.android.systemui.communal.shared.model.CommunalScenes;
-import com.android.systemui.complication.Complication;
import com.android.systemui.complication.dagger.ComplicationComponent;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
@@ -70,8 +69,12 @@
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
+import kotlinx.coroutines.Job;
+
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import javax.inject.Inject;
@@ -129,17 +132,21 @@
*/
private boolean mBouncerShowing = false;
- private final ComplicationComponent mComplicationComponent;
+ private final com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory
+ mDreamComplicationComponentFactory;
+ private final ComplicationComponent.Factory mComplicationComponentFactory;
+ private final DreamOverlayComponent.Factory mDreamOverlayComponentFactory;
+ private final AmbientTouchComponent.Factory mAmbientTouchComponentFactory;
- private final AmbientTouchComponent mAmbientTouchComponent;
+ private final TouchInsetManager mTouchInsetManager;
+ private final LifecycleOwner mLifecycleOwner;
- private final com.android.systemui.dreams.complication.dagger.ComplicationComponent
- mDreamComplicationComponent;
- private final DreamOverlayComponent mDreamOverlayComponent;
private ComponentName mCurrentBlockedGestureDreamActivityComponent;
+ private final ArrayList<Job> mFlows = new ArrayList<>();
+
/**
* This {@link LifecycleRegistry} controls when dream overlay functionality, like touch
* handling, should be active. It will automatically be paused when the dream overlay is hidden
@@ -285,36 +292,27 @@
mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
mStateController = stateController;
mUiEventLogger = uiEventLogger;
+ mComplicationComponentFactory = complicationComponentFactory;
+ mDreamComplicationComponentFactory = dreamComplicationComponentFactory;
mDreamOverlayCallbackController = dreamOverlayCallbackController;
mWindowTitle = windowTitle;
mCommunalInteractor = communalInteractor;
mSystemDialogsCloser = systemDialogsCloser;
mGestureInteractor = gestureInteractor;
-
- final ViewModelStore viewModelStore = new ViewModelStore();
- final Complication.Host host =
- () -> mExecutor.execute(DreamOverlayService.this::requestExit);
-
- mComplicationComponent = complicationComponentFactory.create(lifecycleOwner, host,
- viewModelStore, touchInsetManager);
- mDreamComplicationComponent = dreamComplicationComponentFactory.create(
- mComplicationComponent.getVisibilityController(), touchInsetManager);
- mDreamOverlayComponent = dreamOverlayComponentFactory.create(lifecycleOwner,
- mComplicationComponent.getComplicationHostViewController(), touchInsetManager);
- mAmbientTouchComponent = ambientTouchComponentFactory.create(lifecycleOwner,
- new HashSet<>(Arrays.asList(
- mDreamComplicationComponent.getHideComplicationTouchHandler(),
- mDreamOverlayComponent.getCommunalTouchHandler())));
+ mDreamOverlayComponentFactory = dreamOverlayComponentFactory;
+ mAmbientTouchComponentFactory = ambientTouchComponentFactory;
+ mTouchInsetManager = touchInsetManager;
+ mLifecycleOwner = lifecycleOwner;
mLifecycleRegistry = lifecycleOwner.getRegistry();
mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED));
- collectFlow(getLifecycle(), mCommunalInteractor.isCommunalAvailable(),
- mIsCommunalAvailableCallback);
- collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(),
- mCommunalVisibleConsumer);
- collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
- mBouncerShowingConsumer);
+ mFlows.add(collectFlow(getLifecycle(), mCommunalInteractor.isCommunalAvailable(),
+ mIsCommunalAvailableCallback));
+ mFlows.add(collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(),
+ mCommunalVisibleConsumer));
+ mFlows.add(collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
+ mBouncerShowingConsumer));
}
@NonNull
@@ -339,6 +337,11 @@
public void onDestroy() {
mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback);
+ for (Job job : mFlows) {
+ job.cancel(new CancellationException());
+ }
+ mFlows.clear();
+
mExecutor.execute(() -> {
setLifecycleStateLocked(Lifecycle.State.DESTROYED);
@@ -353,6 +356,23 @@
@Override
public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
+ final ComplicationComponent complicationComponent = mComplicationComponentFactory.create(
+ mLifecycleOwner,
+ () -> mExecutor.execute(DreamOverlayService.this::requestExit),
+ new ViewModelStore(), mTouchInsetManager);
+ final com.android.systemui.dreams.complication.dagger.ComplicationComponent
+ dreamComplicationComponent = mDreamComplicationComponentFactory.create(
+ complicationComponent.getVisibilityController(), mTouchInsetManager);
+
+ final DreamOverlayComponent dreamOverlayComponent = mDreamOverlayComponentFactory.create(
+ mLifecycleOwner, complicationComponent.getComplicationHostViewController(),
+ mTouchInsetManager);
+ final AmbientTouchComponent ambientTouchComponent = mAmbientTouchComponentFactory.create(
+ mLifecycleOwner,
+ new HashSet<>(Arrays.asList(
+ dreamComplicationComponent.getHideComplicationTouchHandler(),
+ dreamOverlayComponent.getCommunalTouchHandler())));
+
setLifecycleStateLocked(Lifecycle.State.STARTED);
mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
@@ -371,8 +391,8 @@
}
mDreamOverlayContainerViewController =
- mDreamOverlayComponent.getDreamOverlayContainerViewController();
- mTouchMonitor = mAmbientTouchComponent.getTouchMonitor();
+ dreamOverlayComponent.getDreamOverlayContainerViewController();
+ mTouchMonitor = ambientTouchComponent.getTouchMonitor();
mTouchMonitor.init();
mStateController.setShouldShowComplications(shouldShowComplications());
@@ -541,6 +561,10 @@
}
private void removeContainerViewFromParentLocked() {
+ if (mDreamOverlayContainerViewController == null) {
+ return;
+ }
+
View containerView = mDreamOverlayContainerViewController.getContainerView();
if (containerView == null) {
return;
@@ -559,8 +583,13 @@
return;
}
+ // This ensures the container view of the current dream is removed before
+ // the controller is potentially reset.
+ removeContainerViewFromParentLocked();
+
if (mStarted && mWindow != null) {
try {
+ mWindow.clearContentView();
mWindowManager.removeView(mWindow.getDecorView());
} catch (IllegalArgumentException e) {
Log.e(TAG, "Error removing decor view when resetting overlay", e);
@@ -571,7 +600,10 @@
mStateController.setLowLightActive(false);
mStateController.setEntryAnimationsFinished(false);
- mDreamOverlayContainerViewController = null;
+ if (mDreamOverlayContainerViewController != null) {
+ mDreamOverlayContainerViewController.destroy();
+ mDreamOverlayContainerViewController = null;
+ }
if (mTouchMonitor != null) {
mTouchMonitor.destroy();
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
index ee7b6f5..5ba780f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
@@ -33,7 +33,11 @@
import com.android.systemui.dreams.touch.dagger.CommunalTouchModule;
import com.android.systemui.statusbar.phone.CentralSurfaces;
+import kotlinx.coroutines.Job;
+
+import java.util.ArrayList;
import java.util.Optional;
+import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import javax.inject.Inject;
@@ -49,6 +53,8 @@
private final ConfigurationInteractor mConfigurationInteractor;
private Boolean mIsEnabled = false;
+ private ArrayList<Job> mFlows = new ArrayList<>();
+
private int mLayoutDirection = LayoutDirection.LTR;
@VisibleForTesting
@@ -70,17 +76,17 @@
mCommunalInteractor = communalInteractor;
mConfigurationInteractor = configurationInteractor;
- collectFlow(
+ mFlows.add(collectFlow(
mLifecycle,
mCommunalInteractor.isCommunalAvailable(),
mIsCommunalAvailableCallback
- );
+ ));
- collectFlow(
+ mFlows.add(collectFlow(
mLifecycle,
mConfigurationInteractor.getLayoutDirection(),
mLayoutDirectionCallback
- );
+ ));
}
@Override
@@ -140,4 +146,13 @@
}
});
}
+
+ @Override
+ public void onDestroy() {
+ for (Job job : mFlows) {
+ job.cancel(new CancellationException());
+ }
+ mFlows.clear();
+ TouchHandler.super.onDestroy();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/model/EduDeviceConnectionTime.kt b/packages/SystemUI/src/com/android/systemui/education/data/model/EduDeviceConnectionTime.kt
new file mode 100644
index 0000000..8682848
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/data/model/EduDeviceConnectionTime.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 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.education.data.model
+
+import java.time.Instant
+
+data class EduDeviceConnectionTime(
+ val keyboardFirstConnectionTime: Instant? = null,
+ val touchpadFirstConnectionTime: Instant? = null
+)
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
index 4fd79d7..01f838f 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
@@ -29,6 +29,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope
+import com.android.systemui.education.data.model.EduDeviceConnectionTime
import com.android.systemui.education.data.model.GestureEduModel
import java.time.Instant
import javax.inject.Inject
@@ -53,10 +54,16 @@
fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel>
+ fun readEduDeviceConnectionTime(): Flow<EduDeviceConnectionTime>
+
suspend fun updateGestureEduModel(
gestureType: GestureType,
transform: (GestureEduModel) -> GestureEduModel
)
+
+ suspend fun updateEduDeviceConnectionTime(
+ transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime
+ )
}
/**
@@ -76,6 +83,8 @@
const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME"
const val USAGE_SESSION_START_TIME_SUFFIX = "_USAGE_SESSION_START_TIME"
const val LAST_EDUCATION_TIME_SUFFIX = "_LAST_EDUCATION_TIME"
+ const val KEYBOARD_FIRST_CONNECTION_TIME = "KEYBOARD_FIRST_CONNECTION_TIME"
+ const val TOUCHPAD_FIRST_CONNECTION_TIME = "TOUCHPAD_FIRST_CONNECTION_TIME"
const val DATASTORE_DIR = "education/USER%s_ContextualEducation"
}
@@ -158,6 +167,37 @@
}
}
+ override fun readEduDeviceConnectionTime(): Flow<EduDeviceConnectionTime> =
+ prefData.map { preferences -> getEduDeviceConnectionTime(preferences) }
+
+ override suspend fun updateEduDeviceConnectionTime(
+ transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime
+ ) {
+ datastore.filterNotNull().first().edit { preferences ->
+ val currentModel = getEduDeviceConnectionTime(preferences)
+ val updatedModel = transform(currentModel)
+ setInstant(
+ preferences,
+ updatedModel.keyboardFirstConnectionTime,
+ getKeyboardFirstConnectionTimeKey()
+ )
+ setInstant(
+ preferences,
+ updatedModel.touchpadFirstConnectionTime,
+ getTouchpadFirstConnectionTimeKey()
+ )
+ }
+ }
+
+ private fun getEduDeviceConnectionTime(preferences: Preferences): EduDeviceConnectionTime {
+ return EduDeviceConnectionTime(
+ keyboardFirstConnectionTime =
+ preferences[getKeyboardFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) },
+ touchpadFirstConnectionTime =
+ preferences[getTouchpadFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) }
+ )
+ }
+
private fun getSignalCountKey(gestureType: GestureType): Preferences.Key<Int> =
intPreferencesKey(gestureType.name + SIGNAL_COUNT_SUFFIX)
@@ -173,6 +213,12 @@
private fun getLastEducationTimeKey(gestureType: GestureType): Preferences.Key<Long> =
longPreferencesKey(gestureType.name + LAST_EDUCATION_TIME_SUFFIX)
+ private fun getKeyboardFirstConnectionTimeKey(): Preferences.Key<Long> =
+ longPreferencesKey(KEYBOARD_FIRST_CONNECTION_TIME)
+
+ private fun getTouchpadFirstConnectionTimeKey(): Preferences.Key<Long> =
+ longPreferencesKey(TOUCHPAD_FIRST_CONNECTION_TIME)
+
private fun setInstant(
preferences: MutablePreferences,
instant: Instant?,
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
index db5c386..10be26e 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
@@ -22,6 +22,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.education.dagger.ContextualEducationModule.EduClock
+import com.android.systemui.education.data.model.EduDeviceConnectionTime
import com.android.systemui.education.data.model.GestureEduModel
import com.android.systemui.education.data.repository.ContextualEducationRepository
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
@@ -32,6 +33,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
@@ -67,6 +69,10 @@
.flowOn(backgroundDispatcher)
}
+ suspend fun getEduDeviceConnectionTime(): EduDeviceConnectionTime {
+ return repository.readEduDeviceConnectionTime().first()
+ }
+
suspend fun incrementSignalCount(gestureType: GestureType) {
repository.updateGestureEduModel(gestureType) {
it.copy(
@@ -100,4 +106,16 @@
it.copy(usageSessionStartTime = clock.instant(), signalCount = 1)
}
}
+
+ suspend fun updateKeyboardFirstConnectionTime() {
+ repository.updateEduDeviceConnectionTime {
+ it.copy(keyboardFirstConnectionTime = clock.instant())
+ }
+ }
+
+ suspend fun updateTouchpadFirstConnectionTime() {
+ repository.updateEduDeviceConnectionTime {
+ it.copy(touchpadFirstConnectionTime = clock.instant())
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
index ad3335b..e88349b2 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
@@ -24,6 +24,7 @@
import com.android.systemui.education.data.model.GestureEduModel
import com.android.systemui.education.shared.model.EducationInfo
import com.android.systemui.education.shared.model.EducationUiType
+import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository
import java.time.Clock
import javax.inject.Inject
import kotlin.time.Duration.Companion.hours
@@ -39,6 +40,7 @@
constructor(
@Background private val backgroundScope: CoroutineScope,
private val contextualEducationInteractor: ContextualEducationInteractor,
+ private val userInputDeviceRepository: UserInputDeviceRepository,
@EduClock private val clock: Clock,
) : CoreStartable {
@@ -61,6 +63,32 @@
}
}
}
+
+ backgroundScope.launch {
+ userInputDeviceRepository.isAnyTouchpadConnectedForUser.collect {
+ if (
+ it.isConnected &&
+ contextualEducationInteractor
+ .getEduDeviceConnectionTime()
+ .touchpadFirstConnectionTime == null
+ ) {
+ contextualEducationInteractor.updateTouchpadFirstConnectionTime()
+ }
+ }
+ }
+
+ backgroundScope.launch {
+ userInputDeviceRepository.isAnyKeyboardConnectedForUser.collect {
+ if (
+ it.isConnected &&
+ contextualEducationInteractor
+ .getEduDeviceConnectionTime()
+ .keyboardFirstConnectionTime == null
+ ) {
+ contextualEducationInteractor.updateKeyboardFirstConnectionTime()
+ }
+ }
+ }
}
private fun isEducationNeeded(model: GestureEduModel): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e5f3a57..4d75d66 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -337,10 +337,6 @@
// TODO(b/278714186) Tracking Bug
@JvmField
val CLIPBOARD_IMAGE_TIMEOUT = unreleasedFlag("clipboard_image_timeout", teamfood = true)
- // TODO(b/279405451): Tracking Bug
- @JvmField
- val CLIPBOARD_SHARED_TRANSITIONS =
- unreleasedFlag("clipboard_shared_transitions", teamfood = true)
// 1900
@JvmField val NOTE_TASKS = releasedFlag("keycode_flag")
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
index 6bc640d..1aa5ee0 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
@@ -20,7 +20,6 @@
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -97,7 +96,6 @@
val secondaryFixedDim = LocalAndroidColorScheme.current.secondaryFixedDim
val onSecondaryFixed = LocalAndroidColorScheme.current.onSecondaryFixed
val onSecondaryFixedVariant = LocalAndroidColorScheme.current.onSecondaryFixedVariant
- val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer
val dynamicProperties =
rememberLottieDynamicProperties(
rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim),
@@ -106,11 +104,10 @@
rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant)
)
val screenColors =
- remember(surfaceContainer, dynamicProperties) {
+ remember(dynamicProperties) {
TutorialScreenConfig.Colors(
background = onSecondaryFixed,
- successBackground = surfaceContainer,
- title = primaryFixedDim,
+ title = secondaryFixedDim,
animationColors = dynamicProperties,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
index c50b7dc..b271356 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
@@ -24,13 +24,13 @@
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -46,7 +46,6 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource
@@ -60,6 +59,7 @@
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.compose.rememberLottieDynamicProperty
+import com.android.compose.modifiers.background
import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.FINISHED
import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.IN_PROGRESS
import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NOT_STARTED
@@ -76,19 +76,11 @@
onDoneButtonClicked: () -> Unit,
config: TutorialScreenConfig
) {
- val animatedColor by
- animateColorAsState(
- targetValue =
- if (actionState == FINISHED) config.colors.successBackground
- else config.colors.background,
- animationSpec = tween(durationMillis = 150, easing = LinearEasing),
- label = "backgroundColor"
- )
Column(
verticalArrangement = Arrangement.Center,
modifier =
Modifier.fillMaxSize()
- .drawBehind { drawRect(animatedColor) }
+ .background(config.colors.background)
.padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp)
) {
Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt
index 0406bb9..55e5f2d 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt
@@ -29,7 +29,6 @@
data class Colors(
val background: Color,
- val successBackground: Color,
val title: Color,
val animationColors: LottieDynamicProperties
)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
index b1589da..e68d799 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
@@ -39,7 +39,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.buffer
@@ -53,7 +53,7 @@
/** Encapsulates state about device entry fingerprint auth mechanism. */
interface DeviceEntryFingerprintAuthRepository {
/** Whether the device entry fingerprint auth is locked out. */
- val isLockedOut: Flow<Boolean>
+ val isLockedOut: StateFlow<Boolean>
/**
* Whether the fingerprint sensor is currently listening, this doesn't mean that the user is
@@ -127,7 +127,7 @@
else if (authController.isRearFpsSupported) BiometricType.REAR_FINGERPRINT else null
}
- override val isLockedOut: Flow<Boolean> =
+ override val isLockedOut: StateFlow<Boolean> by lazy {
conflatedCallbackFlow {
val sendLockoutUpdate =
fun() {
@@ -151,7 +151,12 @@
sendLockoutUpdate()
awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
}
- .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+ .stateIn(
+ scope,
+ started = Eagerly,
+ initialValue = keyguardUpdateMonitor.isFingerprintLockedOut
+ )
+ }
override val isRunning: Flow<Boolean>
get() =
@@ -309,6 +314,7 @@
) {
sendShouldUpdateIndicatorVisibility(true)
}
+
override fun onStrongAuthStateChanged(userId: Int) {
sendShouldUpdateIndicatorVisibility(true)
}
@@ -318,7 +324,7 @@
awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
}
.flowOn(mainDispatcher)
- .shareIn(scope, started = SharingStarted.WhileSubscribed(), replay = 1)
+ .shareIn(scope, started = WhileSubscribed(), replay = 1)
companion object {
const val TAG = "DeviceEntryFingerprintAuthRepositoryImpl"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 49e4c70..80a0cee 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -34,7 +34,6 @@
import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.Utils.Companion.sample
import com.android.systemui.util.kotlin.sample
@@ -155,12 +154,7 @@
if (!SceneContainerFlag.isEnabled) {
startTransitionTo(KeyguardState.GLANCEABLE_HUB)
}
- } else if (
- powerInteractor.detailedWakefulness.value.lastWakeReason ==
- WakeSleepReason.POWER_BUTTON &&
- isCommunalAvailable &&
- dreamManager.canStartDreaming(true)
- ) {
+ } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) {
// This case handles tapping the power button to transition through
// dream -> off -> hub.
if (!SceneContainerFlag.isEnabled) {
@@ -226,14 +220,7 @@
ownerReason = "waking from dozing"
)
}
- } else if (
- powerInteractor.detailedWakefulness.value.lastWakeReason ==
- WakeSleepReason.POWER_BUTTON &&
- isCommunalAvailable &&
- dreamManager.canStartDreaming(true)
- ) {
- // This case handles tapping the power button to transition through
- // dream -> off -> hub.
+ } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) {
if (!SceneContainerFlag.isEnabled) {
transitionToGlanceableHub()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 4cab2bb..a96d7a8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -19,6 +19,7 @@
import android.app.StatusBarManager
import android.graphics.Point
+import android.util.Log
import android.util.MathUtils
import com.android.app.animation.Interpolators
import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
@@ -38,6 +39,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.res.R
@@ -87,6 +89,7 @@
sceneInteractorProvider: Provider<SceneInteractor>,
private val fromGoneTransitionInteractor: Provider<FromGoneTransitionInteractor>,
private val fromLockscreenTransitionInteractor: Provider<FromLockscreenTransitionInteractor>,
+ private val fromOccludedTransitionInteractor: Provider<FromOccludedTransitionInteractor>,
sharedNotificationContainerInteractor: Provider<SharedNotificationContainerInteractor>,
@Application applicationScope: CoroutineScope,
) {
@@ -484,7 +487,11 @@
/** Temporary shim, until [KeyguardWmStateRefactor] is enabled */
fun dismissKeyguard() {
- fromLockscreenTransitionInteractor.get().dismissKeyguard()
+ when (keyguardTransitionInteractor.transitionState.value.to) {
+ LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard()
+ OCCLUDED -> fromOccludedTransitionInteractor.get().dismissFromOccluded()
+ else -> Log.v(TAG, "Keyguard was dismissed, no direct transition call needed")
+ }
}
fun onCameraLaunchDetected(source: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt
index 0a84886..6579ea1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.content.Context
+import com.android.internal.policy.SystemBarUtils
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.shared.model.ClockSizeSetting
import com.android.systemui.res.R
@@ -81,7 +82,7 @@
getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin)
} else {
getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
- getDimensionPixelSize(R.dimen.status_bar_header_height_keyguard) +
+ SystemBarUtils.getStatusBarHeight(context) +
getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 06f77bf..a96869d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -33,6 +33,8 @@
import com.android.systemui.keyguard.shared.model.Edge
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
@@ -45,6 +47,7 @@
import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.util.kotlin.BooleanFlowOperators.any
import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.kotlin.sample
@@ -159,30 +162,26 @@
private val alphaOnShadeExpansion: Flow<Float> =
combineTransform(
- keyguardTransitionInteractor.isInTransition(
- edge = Edge.create(from = LOCKSCREEN, to = Scenes.Gone),
- edgeWithoutSceneContainer = Edge.create(from = LOCKSCREEN, to = GONE),
- ),
- keyguardTransitionInteractor.isInTransition(
- edge = Edge.create(from = Scenes.Bouncer, to = LOCKSCREEN),
- edgeWithoutSceneContainer =
- Edge.create(from = PRIMARY_BOUNCER, to = LOCKSCREEN),
+ anyOf(
+ keyguardTransitionInteractor.isInTransition(
+ edge = Edge.create(from = LOCKSCREEN, to = Scenes.Gone),
+ edgeWithoutSceneContainer = Edge.create(from = LOCKSCREEN, to = GONE),
+ ),
+ keyguardTransitionInteractor.isInTransition(
+ edge = Edge.create(from = Scenes.Bouncer, to = LOCKSCREEN),
+ edgeWithoutSceneContainer =
+ Edge.create(from = PRIMARY_BOUNCER, to = LOCKSCREEN),
+ ),
+ keyguardTransitionInteractor.isInTransition(
+ Edge.create(from = LOCKSCREEN, to = DREAMING)
+ ),
),
isOnLockscreen,
shadeInteractor.qsExpansion,
shadeInteractor.shadeExpansion,
- ) {
- lockscreenToGoneTransitionRunning,
- primaryBouncerToLockscreenTransitionRunning,
- isOnLockscreen,
- qsExpansion,
- shadeExpansion ->
+ ) { disabledTransitionRunning, isOnLockscreen, qsExpansion, shadeExpansion ->
// Fade out quickly as the shade expands
- if (
- isOnLockscreen &&
- !lockscreenToGoneTransitionRunning &&
- !primaryBouncerToLockscreenTransitionRunning
- ) {
+ if (isOnLockscreen && !disabledTransitionRunning) {
val alpha =
1f -
MathUtils.constrainedMap(
@@ -198,29 +197,37 @@
.distinctUntilChanged()
/**
- * Keyguard should not show while the communal hub is fully visible. This check is added since
- * at the moment, closing the notification shade will cause the keyguard alpha to be set back to
- * 1. Also ensure keyguard is never visible when GONE.
+ * Keyguard states which should fully hide the keyguard.
+ *
+ * Note: [GONE] is not included as it is handled separately.
+ */
+ private val hiddenKeyguardStates = listOf(OCCLUDED, DREAMING, GLANCEABLE_HUB)
+
+ /**
+ * Keyguard should not show if fully transitioned into a hidden keyguard state or if
+ * transitioning between hidden states.
*/
private val hideKeyguard: Flow<Boolean> =
- combine(
- communalInteractor.isIdleOnCommunal,
+ (hiddenKeyguardStates.map { state ->
keyguardTransitionInteractor
- .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
+ .transitionValue(state)
.map { it == 1f }
- .onStart { emit(false) },
- keyguardTransitionInteractor
- .transitionValue(OCCLUDED)
- .map { it == 1f }
- .onStart { emit(false) },
- keyguardTransitionInteractor
- .transitionValue(KeyguardState.DREAMING)
- .map { it == 1f }
- .onStart { emit(false) },
- ) { isIdleOnCommunal, isGone, isOccluded, isDreaming ->
- isIdleOnCommunal || isGone || isOccluded || isDreaming
- }
- .distinctUntilChanged()
+ .onStart { emit(false) }
+ } +
+ listOf(
+ communalInteractor.isIdleOnCommunal,
+ keyguardTransitionInteractor
+ .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
+ .map { it == 1f }
+ .onStart { emit(false) },
+ keyguardTransitionInteractor
+ .isInTransitionWhere(
+ fromStatePredicate = { hiddenKeyguardStates.contains(it) },
+ toStatePredicate = { hiddenKeyguardStates.contains(it) },
+ )
+ .onStart { emit(false) },
+ ))
+ .any()
/** Last point that the root view was tapped */
val lastRootViewTapPosition: Flow<Point?> = keyguardInteractor.lastRootViewTapPosition
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 666c9f8..3e6dd8e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -20,6 +20,7 @@
import com.android.compose.animation.scene.ContentKey
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.biometrics.AuthController
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.shared.model.ClockSize
@@ -57,6 +58,7 @@
private val shadeInteractor: ShadeInteractor,
private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
private val occlusionInteractor: SceneContainerOcclusionInteractor,
+ private val deviceEntryInteractor: DeviceEntryInteractor,
) : SysUiViewModel() {
@VisibleForTesting val clockSize = clockInteractor.clockSize
@@ -73,6 +75,10 @@
/** Whether the content of the scene UI should be shown. */
val isContentVisible: StateFlow<Boolean> = _isContentVisible.asStateFlow()
+ /** @see DeviceEntryInteractor.isBypassEnabled */
+ val isBypassEnabled: StateFlow<Boolean>
+ get() = deviceEntryInteractor.isBypassEnabled
+
override suspend fun onActivated(): Nothing {
coroutineScope {
launch {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index cb0bb4a..e44069f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -563,10 +563,6 @@
}
@Override
- public void onRecentsAnimationStateChanged(boolean running) {
- }
-
- @Override
public void onNavigationModeChanged(int mode) {
mNavigationMode = mode;
mEdgeBackGestureHandler.onNavigationModeChanged(mode);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 5d81d4f..3613c11 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -37,6 +37,8 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
@@ -50,11 +52,17 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.compose.modifiers.height
import com.android.compose.modifiers.padding
+import com.android.compose.modifiers.thenIf
import com.android.compose.theme.PlatformTheme
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.view.MediaHost
+import com.android.systemui.media.dagger.MediaModule.QS_PANEL
+import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
import com.android.systemui.plugins.qs.QS
import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.qs.composefragment.ui.notificationScrimClip
import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
import com.android.systemui.qs.flags.QSComposeFragment
import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -65,6 +73,7 @@
import com.android.systemui.util.LifecycleFragment
import java.util.function.Consumer
import javax.inject.Inject
+import javax.inject.Named
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
@@ -77,6 +86,8 @@
@Inject
constructor(
private val qsFragmentComposeViewModelFactory: QSFragmentComposeViewModel.Factory,
+ @Named(QUICK_QS_PANEL) private val qqsMediaHost: MediaHost,
+ @Named(QS_PANEL) private val qsMediaHost: MediaHost,
) : LifecycleFragment(), QS {
private val scrollListener = MutableStateFlow<QS.ScrollListener?>(null)
@@ -93,12 +104,25 @@
private val qqsPositionOnRoot = Rect()
private val composeViewPositionOnScreen = Rect()
+ // Inside object for namespacing
+ private val notificationScrimClippingParams =
+ object {
+ var isEnabled by mutableStateOf(false)
+ var leftInset by mutableStateOf(0)
+ var rightInset by mutableStateOf(0)
+ var top by mutableStateOf(0)
+ var bottom by mutableStateOf(0)
+ var radius by mutableStateOf(0)
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
QSComposeFragment.isUnexpectedlyInLegacyMode()
viewModel = qsFragmentComposeViewModelFactory.create(lifecycleScope)
+ qqsMediaHost.init(MediaHierarchyManager.LOCATION_QQS)
+ qsMediaHost.init(MediaHierarchyManager.LOCATION_QS)
setListenerCollections()
}
@@ -117,7 +141,18 @@
AnimatedVisibility(
visible = visible,
- modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+ modifier =
+ Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf(
+ notificationScrimClippingParams.isEnabled
+ ) {
+ Modifier.notificationScrimClip(
+ notificationScrimClippingParams.leftInset,
+ notificationScrimClippingParams.top,
+ notificationScrimClippingParams.rightInset,
+ notificationScrimClippingParams.bottom,
+ notificationScrimClippingParams.radius,
+ )
+ }
) {
AnimatedContent(targetState = qsState) {
when (it) {
@@ -271,10 +306,19 @@
cornerRadius: Int,
visible: Boolean,
fullWidth: Boolean
- ) {}
+ ) {
+ notificationScrimClippingParams.isEnabled = visible
+ notificationScrimClippingParams.top = top
+ notificationScrimClippingParams.bottom = bottom
+ // Full width means that QS will show in the entire width allocated to it (for example
+ // phone) vs. showing in a narrower column (for example, tablet portrait).
+ notificationScrimClippingParams.leftInset = if (fullWidth) 0 else leftInset
+ notificationScrimClippingParams.rightInset = if (fullWidth) 0 else rightInset
+ notificationScrimClippingParams.radius = cornerRadius
+ }
override fun isFullyCollapsed(): Boolean {
- return !viewModel.isQSVisible
+ return viewModel.qsExpansionValue <= 0f
}
override fun setCollapsedMediaVisibilityChangedListener(listener: Consumer<Boolean>?) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
new file mode 100644
index 0000000..93c6445
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.qs.composefragment.ui
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ClipOp
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.asAndroidPath
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.clipPath
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+
+/**
+ * Clipping modifier for clipping out the notification scrim as it slides over QS. It will clip out
+ * ([ClipOp.Difference]) a `RoundRect(-leftInset, top, width + rightInset, bottom, radius, radius)`
+ * from the QS container.
+ */
+fun Modifier.notificationScrimClip(
+ leftInset: Int,
+ top: Int,
+ rightInset: Int,
+ bottom: Int,
+ radius: Int
+): Modifier {
+ return this then NotificationScrimClipElement(leftInset, top, rightInset, bottom, radius)
+}
+
+private class NotificationScrimClipNode(
+ var leftInset: Float,
+ var top: Float,
+ var rightInset: Float,
+ var bottom: Float,
+ var radius: Float,
+) : DrawModifierNode, Modifier.Node() {
+ private val path = Path()
+
+ var invalidated = true
+
+ override fun ContentDrawScope.draw() {
+ if (invalidated) {
+ path.rewind()
+ path
+ .asAndroidPath()
+ .addRoundRect(
+ -leftInset,
+ top,
+ size.width + rightInset,
+ bottom,
+ radius,
+ radius,
+ android.graphics.Path.Direction.CW
+ )
+ invalidated = false
+ }
+ clipPath(path, ClipOp.Difference) { this@draw.drawContent() }
+ }
+}
+
+private data class NotificationScrimClipElement(
+ val leftInset: Int,
+ val top: Int,
+ val rightInset: Int,
+ val bottom: Int,
+ val radius: Int,
+) : ModifierNodeElement<NotificationScrimClipNode>() {
+ override fun create(): NotificationScrimClipNode {
+ return NotificationScrimClipNode(
+ leftInset.toFloat(),
+ top.toFloat(),
+ rightInset.toFloat(),
+ bottom.toFloat(),
+ radius.toFloat(),
+ )
+ }
+
+ override fun update(node: NotificationScrimClipNode) {
+ val changed =
+ node.leftInset != leftInset.toFloat() ||
+ node.top != top.toFloat() ||
+ node.rightInset != rightInset.toFloat() ||
+ node.bottom != bottom.toFloat() ||
+ node.radius != radius.toFloat()
+ if (changed) {
+ node.leftInset = leftInset.toFloat()
+ node.top = top.toFloat()
+ node.rightInset = rightInset.toFloat()
+ node.bottom = bottom.toFloat()
+ node.radius = radius.toFloat()
+ node.invalidated = true
+ }
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "notificationScrimClip"
+ properties["leftInset"] = leftInset
+ properties["top"] = top
+ properties["rightInset"] = rightInset
+ properties["bottom"] = bottom
+ properties["radius"] = radius
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 9e109e4..4b1312c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -18,21 +18,27 @@
import android.content.res.Resources
import android.graphics.Rect
+import androidx.annotation.FloatRange
+import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LifecycleCoroutineScope
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.FooterActionsController
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
import com.android.systemui.shade.LargeScreenHeaderHelper
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
+import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.util.LargeScreenUtils
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -79,11 +85,17 @@
_qsVisible.value = value
}
- private val _qsExpansion = MutableStateFlow(0f)
+ // This can only be negative if undefined (in which case it will be -1f), else it will be
+ // in [0, 1]. In some cases, it could be set back to -1f internally to indicate that it's
+ // different to every value in [0, 1].
+ @FloatRange(from = -1.0, to = 1.0) private val _qsExpansion = MutableStateFlow(-1f)
var qsExpansionValue: Float
get() = _qsExpansion.value
set(value) {
- _qsExpansion.value = value
+ if (value < 0f) {
+ _qsExpansion.value = -1f
+ }
+ _qsExpansion.value = value.coerceIn(0f, 1f)
}
private val _panelFraction = MutableStateFlow(0f)
@@ -133,7 +145,34 @@
private val _keyguardAndExpanded = MutableStateFlow(false)
- private val _statusBarState = MutableStateFlow(-1)
+ /**
+ * Tracks the current [StatusBarState]. It will switch early if the upcoming state is
+ * [StatusBarState.KEYGUARD]
+ */
+ @get:VisibleForTesting
+ val statusBarState =
+ conflatedCallbackFlow {
+ val callback =
+ object : StatusBarStateController.StateListener {
+ override fun onStateChanged(newState: Int) {
+ trySend(newState)
+ }
+
+ override fun onUpcomingStateChanged(upcomingState: Int) {
+ if (upcomingState == StatusBarState.KEYGUARD) {
+ trySend(upcomingState)
+ }
+ }
+ }
+ sysuiStatusBarStateController.addCallback(callback)
+
+ awaitClose { sysuiStatusBarStateController.removeCallback(callback) }
+ }
+ .stateIn(
+ lifecycleScope,
+ SharingStarted.WhileSubscribed(),
+ sysuiStatusBarStateController.state,
+ )
private val _viewHeight = MutableStateFlow(0)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index 6eacb2e..79c2eb9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -24,6 +24,7 @@
import android.text.TextUtils
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
@@ -81,6 +82,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext
@@ -132,11 +134,16 @@
val uiState = remember(state) { state.toUiState() }
val colors = TileDefaults.getColorForState(uiState)
+ // TODO(b/361789146): Draw the shapes instead of clipping
+ val tileShape = TileDefaults.animateTileShape(uiState.state)
+ val iconShape = TileDefaults.animateIconShape(uiState.state)
+
TileContainer(
colors = colors,
showLabels = showLabels,
label = uiState.label,
iconOnly = iconOnly,
+ shape = if (iconOnly) iconShape else tileShape,
clickEnabled = true,
onClick = tile::onClick,
onLongClick = tile::onLongClick,
@@ -151,6 +158,7 @@
secondaryLabel = uiState.secondaryLabel,
icon = icon,
colors = colors,
+ iconShape = iconShape,
toggleClickSupported = state.handlesSecondaryClick,
onClick = {
if (state.handlesSecondaryClick) {
@@ -169,6 +177,7 @@
showLabels: Boolean,
label: String,
iconOnly: Boolean,
+ shape: Shape,
clickEnabled: Boolean = false,
onClick: (Expandable) -> Unit = {},
onLongClick: (Expandable) -> Unit = {},
@@ -189,10 +198,8 @@
}
Expandable(
color = backgroundColor,
- shape = TileDefaults.TileShape,
- modifier =
- Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
- .clip(TileDefaults.TileShape)
+ shape = shape,
+ modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height)).clip(shape)
) {
Box(
modifier =
@@ -227,6 +234,7 @@
secondaryLabel: String?,
icon: Icon,
colors: TileColors,
+ iconShape: Shape,
toggleClickSupported: Boolean = false,
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
@@ -239,7 +247,7 @@
Box(
modifier =
Modifier.fillMaxHeight().aspectRatio(1f).thenIf(toggleClickSupported) {
- Modifier.clip(TileDefaults.TileShape)
+ Modifier.clip(iconShape)
.background(colors.iconBackground, { 1f })
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
}
@@ -391,7 +399,7 @@
horizontalArrangement = tileHorizontalArrangement(),
modifier =
Modifier.fillMaxHeight()
- .border(1.dp, LocalContentColor.current, shape = TileDefaults.TileShape)
+ .border(1.dp, LocalContentColor.current, shape = CircleShape)
.padding(10.dp)
) {
Icon(imageVector = Icons.Default.Clear, contentDescription = null)
@@ -533,7 +541,7 @@
Modifier.background(
color = MaterialTheme.colorScheme.secondary,
alpha = { EditModeTileDefaults.PLACEHOLDER_ALPHA },
- shape = TileDefaults.TileShape
+ shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius)
)
.animateItem()
)
@@ -619,6 +627,7 @@
showLabels = showLabels,
label = label,
iconOnly = iconOnly,
+ shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius),
modifier = modifier,
) {
if (iconOnly) {
@@ -633,6 +642,7 @@
secondaryLabel = tileViewModel.appName?.load(),
icon = tileViewModel.icon,
colors = colors,
+ iconShape = RoundedCornerShape(TileDefaults.InactiveCornerRadius),
)
}
}
@@ -736,7 +746,9 @@
}
private object TileDefaults {
- val TileShape = CircleShape
+ val InactiveCornerRadius = 50.dp
+ val ActiveIconCornerRadius = 16.dp
+ val ActiveTileCornerRadius = 24.dp
val IconTileWithLabelHeight = 140.dp
/** An active tile without dual target uses the active color as background */
@@ -795,6 +807,39 @@
else -> unavailableTileColors()
}
}
+
+ @Composable
+ fun animateIconShape(state: Int): Shape {
+ return animateShape(
+ state = state,
+ activeCornerRadius = ActiveTileCornerRadius,
+ label = "QSTileCornerRadius",
+ )
+ }
+
+ @Composable
+ fun animateTileShape(state: Int): Shape {
+ return animateShape(
+ state = state,
+ activeCornerRadius = ActiveIconCornerRadius,
+ label = "QSTileIconCornerRadius",
+ )
+ }
+
+ @Composable
+ fun animateShape(state: Int, activeCornerRadius: Dp, label: String): Shape {
+ val animatedCornerRadius by
+ animateDpAsState(
+ targetValue =
+ if (state == STATE_ACTIVE) {
+ activeCornerRadius
+ } else {
+ InactiveCornerRadius
+ },
+ label = label
+ )
+ return RoundedCornerShape(animatedCornerRadius)
+ }
}
private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index dfcf216..ac6ebe7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -147,17 +147,17 @@
sealed interface State {
val isVisible: Boolean
- val expansion: Float
+ val expansion: () -> Float
val squishiness: () -> Float
data object CLOSED : State {
override val isVisible = false
- override val expansion = 0f
+ override val expansion = { 0f }
override val squishiness = { 1f }
}
/** State for expanding between QQS and QS */
- data class Expanding(override val expansion: Float) : State {
+ class Expanding(override val expansion: () -> Float) : State {
override val isVisible = true
override val squishiness = { 1f }
}
@@ -170,7 +170,7 @@
*/
class UnsquishingQQS(override val squishiness: () -> Float) : State {
override val isVisible = true
- override val expansion = 0f
+ override val expansion = { 0f }
}
/**
@@ -181,16 +181,16 @@
*/
class UnsquishingQS(override val squishiness: () -> Float) : State {
override val isVisible = true
- override val expansion = 1f
+ override val expansion = { 1f }
}
companion object {
// These are special cases of the expansion.
- val QQS = Expanding(0f)
- val QS = Expanding(1f)
+ val QQS = Expanding { 0f }
+ val QS = Expanding { 1f }
/** Collapsing from QS to QQS. [progress] is 0f in QS and 1f in QQS. */
- fun Collapsing(progress: Float) = Expanding(1f - progress)
+ fun Collapsing(progress: () -> Float) = Expanding { 1f - progress() }
}
}
}
@@ -418,14 +418,14 @@
private fun QSImpl.applyState(state: QSSceneAdapter.State) {
setQsVisible(state.isVisible)
- setExpanded(state.isVisible && state.expansion > 0f)
+ setExpanded(state.isVisible && state.expansion() > 0f)
setListening(state.isVisible)
}
override fun applyLatestExpansionAndSquishiness() {
val qsImpl = _qsImpl.value
val state = state.value
- qsImpl?.setQsExpansion(state.expansion, 1f, 0f, state.squishiness())
+ qsImpl?.setQsExpansion(state.expansion(), 1f, 0f, state.squishiness())
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index 863a899..3d6d00e 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -24,6 +24,7 @@
import android.net.Uri
import android.os.Handler
import android.os.UserHandle
+import android.provider.Settings
import android.util.Log
import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.DialogTransitionAnimator
@@ -90,7 +91,16 @@
// ViewCapture needs to save it's data before it is disabled, or else the data will
// be lost. This is expected to change in the near future, and when that happens
// this line should be removed.
- bgExecutor.execute { traceurMessageSender.stopTracing() }
+ bgExecutor.execute {
+ if (issueRecordingState.traceConfig.longTrace) {
+ Settings.Global.putInt(
+ contentResolver,
+ NOTIFY_SESSION_ENDED_SETTING,
+ DISABLED
+ )
+ }
+ traceurMessageSender.stopTracing()
+ }
issueRecordingState.isRecording = false
}
ACTION_SHARE -> {
@@ -125,6 +135,8 @@
companion object {
private const val TAG = "IssueRecordingService"
private const val CHANNEL_ID = "issue_record"
+ private const val NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended"
+ private const val DISABLED = 0
/**
* Get an intent to stop the issue recording service.
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
index 2d510e1..ea61bd3 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
@@ -118,8 +118,11 @@
get() =
when (this) {
is ObservableTransitionState.Idle -> currentScene.canBeOccluded
- is ObservableTransitionState.Transition ->
+ is ObservableTransitionState.Transition.ChangeCurrentScene ->
fromScene.canBeOccluded && toScene.canBeOccluded
+ is ObservableTransitionState.Transition.ReplaceOverlay,
+ is ObservableTransitionState.Transition.ShowOrHideOverlay ->
+ TODO("b/359173565: Handle overlay transitions")
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 5885193..1b9c346 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -28,8 +28,6 @@
import com.android.systemui.scene.shared.logger.SceneLogger
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.util.kotlin.getValue
-import com.android.systemui.util.kotlin.pairwiseBy
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -83,20 +81,7 @@
* Note that during a transition between scenes, more than one scene might be rendered but only
* one is considered the committed/current scene.
*/
- val currentScene: StateFlow<SceneKey> =
- repository.currentScene
- .pairwiseBy(initialValue = repository.currentScene.value) { from, to ->
- logger.logSceneChangeCommitted(
- from = from,
- to = to,
- )
- to
- }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = repository.currentScene.value,
- )
+ val currentScene: StateFlow<SceneKey> = repository.currentScene
/**
* The current state of the transition.
@@ -124,7 +109,15 @@
*/
val transitioningTo: StateFlow<SceneKey?> =
transitionState
- .map { state -> (state as? ObservableTransitionState.Transition)?.toScene }
+ .map { state ->
+ when (state) {
+ is ObservableTransitionState.Idle -> null
+ is ObservableTransitionState.Transition.ChangeCurrentScene -> state.toScene
+ is ObservableTransitionState.Transition.ShowOrHideOverlay,
+ is ObservableTransitionState.Transition.ReplaceOverlay ->
+ TODO("b/359173565: Handle overlay transitions")
+ }
+ }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
@@ -234,14 +227,15 @@
return
}
- logger.logSceneChangeRequested(
+ onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(resolvedScene, sceneState) }
+
+ logger.logSceneChanged(
from = currentSceneKey,
to = resolvedScene,
reason = loggingReason,
isInstant = false,
)
- onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(resolvedScene, sceneState) }
repository.changeScene(resolvedScene, transitionKey)
}
@@ -274,7 +268,7 @@
return
}
- logger.logSceneChangeRequested(
+ logger.logSceneChanged(
from = currentSceneKey,
to = resolvedScene,
reason = loggingReason,
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt
index c6f51b3..ec743ba 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt
@@ -112,7 +112,7 @@
// It
// happens only when unlocking or when dismissing a dismissible lockscreen.
val isTransitioningAwayFromKeyguard =
- transitionState is ObservableTransitionState.Transition &&
+ transitionState is ObservableTransitionState.Transition.ChangeCurrentScene &&
transitionState.fromScene.isKeyguard() &&
transitionState.toScene == Scenes.Gone
@@ -120,7 +120,7 @@
val isCurrentSceneShade = currentScene.isShade()
// This is true when moving into one of the shade scenes when a non-shade scene.
val isTransitioningToShade =
- transitionState is ObservableTransitionState.Transition &&
+ transitionState is ObservableTransitionState.Transition.ChangeCurrentScene &&
!transitionState.fromScene.isShade() &&
transitionState.toScene.isShade()
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
index cf1518e..94c94e2 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
@@ -43,7 +43,7 @@
)
}
- fun logSceneChangeRequested(
+ fun logSceneChanged(
from: SceneKey,
to: SceneKey,
reason: String,
@@ -60,7 +60,7 @@
},
messagePrinter = {
buildString {
- append("Scene change requested: $str1 → $str2")
+ append("Scene changed: $str1 → $str2")
if (isInstant) {
append(" (instant)")
}
@@ -70,21 +70,6 @@
)
}
- fun logSceneChangeCommitted(
- from: SceneKey,
- to: SceneKey,
- ) {
- logBuffer.log(
- tag = TAG,
- level = LogLevel.INFO,
- messageInitializer = {
- str1 = from.toString()
- str2 = to.toString()
- },
- messagePrinter = { "Scene change committed: $str1 → $str2" },
- )
- }
-
fun logSceneTransition(transitionState: ObservableTransitionState) {
when (transitionState) {
is ObservableTransitionState.Transition -> {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 7f7f0f1..f8a9f8c 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -27,6 +27,7 @@
import com.android.systemui.lifecycle.SysUiViewModel
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.logger.SceneLogger
import com.android.systemui.scene.shared.model.Scenes
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -42,6 +43,7 @@
private val sceneInteractor: SceneInteractor,
private val falsingInteractor: FalsingInteractor,
private val powerInteractor: PowerInteractor,
+ private val logger: SceneLogger,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : SysUiViewModel() {
/**
@@ -135,16 +137,29 @@
else -> null
}
- return interactionTypeOrNull?.let { interactionType ->
- // It's important that the falsing system is always queried, even if no enforcement will
- // occur. This helps build up the right signal in the system.
- val isFalseTouch = falsingInteractor.isFalseTouch(interactionType)
+ val fromScene = currentScene.value
+ val isAllowed =
+ interactionTypeOrNull?.let { interactionType ->
+ // It's important that the falsing system is always queried, even if no enforcement
+ // will occur. This helps build up the right signal in the system.
+ val isFalseTouch = falsingInteractor.isFalseTouch(interactionType)
- // Only enforce falsing if moving from the lockscreen scene to a new scene.
- val fromLockscreenScene = currentScene.value == Scenes.Lockscreen
+ // Only enforce falsing if moving from the lockscreen scene to a new scene.
+ val fromLockscreenScene = fromScene == Scenes.Lockscreen
- !fromLockscreenScene || !isFalseTouch
- } ?: true
+ !fromLockscreenScene || !isFalseTouch
+ } ?: true
+
+ if (isAllowed) {
+ // A scene change is guaranteed; log it.
+ logger.logSceneChanged(
+ from = fromScene,
+ to = toScene,
+ reason = "user interaction",
+ isInstant = false,
+ )
+ }
+ return isAllowed
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index 50ea3bb..448f7c4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -99,6 +99,9 @@
) {
val displays = getDisplaysToScreenshot(screenshotRequest.type)
val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
+ if (displays.isEmpty()) {
+ Log.wtf(TAG, "No displays found for screenshot.")
+ }
displays.forEach { display ->
val displayId = display.displayId
var screenshotHandler: ScreenshotHandler =
@@ -219,8 +222,7 @@
}
private fun getScreenshotController(display: Display): InteractiveScreenshotHandler {
- val controller =
- screenshotController ?: interactiveScreenshotHandlerFactory.create(display)
+ val controller = screenshotController ?: interactiveScreenshotHandlerFactory.create(display)
screenshotController = controller
return controller
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt
index 3fe3162..29450a2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt
@@ -26,7 +26,7 @@
import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult
import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult.NotMatched
import com.android.systemui.screenshot.policy.CaptureType.IsolatedTask
-import com.android.wm.shell.shared.desktopmode.DesktopModeFlags
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import javax.inject.Inject
import kotlinx.coroutines.flow.first
@@ -48,7 +48,7 @@
return NotMatched(policy = NAME, reason = SHADE_EXPANDED)
}
- if (DesktopModeFlags.DESKTOP_WINDOWING_MODE.isEnabled(context)) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
content.rootTasks.firstOrNull()?.also {
if (it.windowingMode == WINDOWING_MODE_FREEFORM) {
return NotMatched(policy = NAME, reason = DESKTOP_MODE_ENABLED)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/CropView.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/CropView.java
index ee1944e..ad27da9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/CropView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/CropView.java
@@ -214,8 +214,7 @@
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
- if (mCurrentDraggingBoundary != CropBoundary.NONE
- && mActivePointerId == event.getPointerId(mActivePointerId)) {
+ if (mCurrentDraggingBoundary != CropBoundary.NONE) {
updateListener(MotionEvent.ACTION_UP, event.getX(0));
return true;
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt
index 8006e94..7d67121 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt
@@ -64,7 +64,7 @@
0f
}
)
- is ObservableTransitionState.Transition ->
+ is ObservableTransitionState.Transition.ChangeCurrentScene ->
when {
state.fromScene == Scenes.Gone ->
if (state.toScene.isExpandable()) {
@@ -88,6 +88,9 @@
}
else -> flowOf(1f)
}
+ is ObservableTransitionState.Transition.ShowOrHideOverlay,
+ is ObservableTransitionState.Transition.ReplaceOverlay ->
+ TODO("b/359173565: Handle overlay transitions")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index a1477b5..f88fd7d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -1174,7 +1174,7 @@
}
}
- @Override
+ // This was previously called from WM, but is now called from WMShell
public void onRecentsAnimationStateChanged(boolean running) {
synchronized (mLock) {
mHandler.obtainMessage(MSG_RECENTS_ANIMATION_STATE_CHANGED, running ? 1 : 0, 0)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
index 693cc4a..2b9daef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
@@ -28,6 +28,9 @@
import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
+import static com.android.systemui.Flags.enableViewCaptureTracing;
+import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy;
+
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
@@ -73,12 +76,16 @@
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
+import com.android.app.viewcapture.ViewCapture;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.systemui.CoreStartable;
import com.android.systemui.res.R;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.util.settings.SecureSettings;
+import kotlin.Lazy;
+
import javax.inject.Inject;
/**
@@ -105,12 +112,12 @@
private final CommandQueue mCommandQueue;
private ClingWindowView mClingWindow;
- /** The last {@link WindowManager} that is used to add the confirmation window. */
+ /** The wrapper on the last {@link WindowManager} used to add the confirmation window. */
@Nullable
- private WindowManager mWindowManager;
+ private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
/**
- * The WindowContext that is registered with {@link #mWindowManager} with options to specify the
- * {@link RootDisplayArea} to attach the confirmation window.
+ * The WindowContext that is registered with {@link #mViewCaptureAwareWindowManager} with
+ * options to specify the {@link RootDisplayArea} to attach the confirmation window.
*/
@Nullable
private Context mWindowContext;
@@ -127,15 +134,19 @@
private ContentObserver mContentObserver;
+ private Lazy<ViewCapture> mLazyViewCapture;
+
@Inject
public ImmersiveModeConfirmation(Context context, CommandQueue commandQueue,
- SecureSettings secureSettings) {
+ SecureSettings secureSettings,
+ dagger.Lazy<ViewCapture> daggerLazyViewCapture) {
mSysUiContext = context;
final Display display = mSysUiContext.getDisplay();
mDisplayContext = display.getDisplayId() == DEFAULT_DISPLAY
? mSysUiContext : mSysUiContext.createDisplayContext(display);
mCommandQueue = commandQueue;
mSecureSettings = secureSettings;
+ mLazyViewCapture = toKotlinLazy(daggerLazyViewCapture);
}
boolean loadSetting(int currentUserId) {
@@ -239,14 +250,14 @@
private void handleHide() {
if (mClingWindow != null) {
if (DEBUG) Log.d(TAG, "Hiding immersive mode confirmation");
- if (mWindowManager != null) {
+ if (mViewCaptureAwareWindowManager != null) {
try {
- mWindowManager.removeView(mClingWindow);
+ mViewCaptureAwareWindowManager.removeView(mClingWindow);
} catch (WindowManager.InvalidDisplayException e) {
Log.w(TAG, "Fail to hide the immersive confirmation window because of "
+ e);
}
- mWindowManager = null;
+ mViewCaptureAwareWindowManager = null;
mWindowContext = null;
}
mClingWindow = null;
@@ -505,8 +516,8 @@
* confirmation window.
*/
@NonNull
- private WindowManager createWindowManager(int rootDisplayAreaId) {
- if (mWindowManager != null) {
+ private ViewCaptureAwareWindowManager createWindowManager(int rootDisplayAreaId) {
+ if (mViewCaptureAwareWindowManager != null) {
throw new IllegalStateException(
"Must not create a new WindowManager while there is an existing one");
}
@@ -515,8 +526,10 @@
mWindowContextRootDisplayAreaId = rootDisplayAreaId;
mWindowContext = mDisplayContext.createWindowContext(
IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options);
- mWindowManager = mWindowContext.getSystemService(WindowManager.class);
- return mWindowManager;
+ WindowManager wm = mWindowContext.getSystemService(WindowManager.class);
+ mViewCaptureAwareWindowManager = new ViewCaptureAwareWindowManager(wm, mLazyViewCapture,
+ enableViewCaptureTracing());
+ return mViewCaptureAwareWindowManager;
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 17f401a..0efd5f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -45,7 +45,6 @@
import android.service.notification.Flags
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
-import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -279,7 +278,8 @@
private val packageManager: PackageManager,
private val uiEventLogger: UiEventLogger,
private val context: Context,
- private val notificationManager: NotificationManager
+ private val notificationManager: NotificationManager,
+ private val logger: VisualInterruptionDecisionLogger
) :
VisualInterruptionFilter(
types = setOf(PEEK, PULSE),
@@ -354,15 +354,18 @@
override fun shouldSuppress(entry: NotificationEntry): Boolean {
if (!isCooldownEnabled()) {
+ logger.logAvalancheAllow("cooldown OFF")
return false
}
val timeSinceAvalancheMs = systemClock.currentTimeMillis() - avalancheProvider.startTime
val timedOut = timeSinceAvalancheMs >= avalancheProvider.timeoutMs
if (timedOut) {
+ logger.logAvalancheAllow("timedOut! timeSinceAvalancheMs=$timeSinceAvalancheMs")
return false
}
val state = calculateState(entry)
if (state != State.SUPPRESS) {
+ logger.logAvalancheAllow("state=$state")
return false
}
if (shouldShowEdu()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
index c204ea9..b83259d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
@@ -93,6 +93,15 @@
}
)
}
+
+ fun logAvalancheAllow(info: String) {
+ buffer.log(
+ TAG,
+ INFO,
+ { str1 = info },
+ { "AvalancheSuppressor: $str1" }
+ )
+ }
}
private const val TAG = "VisualInterruptionDecisionProvider"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index 8e8d9b6..2f8711a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -194,7 +194,8 @@
packageManager,
uiEventLogger,
context,
- notificationManager
+ notificationManager,
+ logger
)
)
avalancheProvider.register()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 7119145..48c974a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -117,6 +117,8 @@
protected HybridNotificationView mSingleLineView;
@Nullable public DisposableHandle mContractedBinderHandle;
+ @Nullable public DisposableHandle mExpandedBinderHandle;
+ @Nullable public DisposableHandle mHeadsUpBinderHandle;
private RemoteInputView mExpandedRemoteInput;
private RemoteInputView mHeadsUpRemoteInput;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index a5cd2a2..c342bcd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -46,6 +46,9 @@
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
import com.android.systemui.statusbar.notification.InflationException
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder
+import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView
+import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP
@@ -286,11 +289,15 @@
}
FLAG_CONTENT_VIEW_EXPANDED ->
row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_EXPANDED) {
+ row.privateLayout.mExpandedBinderHandle?.dispose()
+ row.privateLayout.mExpandedBinderHandle = null
row.privateLayout.setExpandedChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
}
FLAG_CONTENT_VIEW_HEADS_UP ->
row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_HEADSUP) {
+ row.privateLayout.mHeadsUpBinderHandle?.dispose()
+ row.privateLayout.mHeadsUpBinderHandle = null
row.privateLayout.setHeadsUpChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
row.privateLayout.setHeadsUpInflatedSmartReplies(null)
@@ -499,17 +506,87 @@
}
}
- if (reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0) {
+ val richOngoingContentModel = inflationProgress.contentModel.richOngoingContentModel
+
+ if (
+ richOngoingContentModel != null &&
+ reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0
+ ) {
logger.logAsyncTaskProgress(entry, "inflating RON view")
- inflationProgress.richOngoingNotificationViewHolder =
- inflationProgress.contentModel.richOngoingContentModel?.let {
+ val inflateContractedView = reInflateFlags and FLAG_CONTENT_VIEW_CONTRACTED != 0
+ val inflateExpandedView = reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0
+ val inflateHeadsUpView = reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0
+
+ inflationProgress.contractedRichOngoingNotificationViewHolder =
+ if (inflateContractedView) {
ronInflater.inflateView(
- contentModel = it,
+ contentModel = richOngoingContentModel,
existingView = row.privateLayout.contractedChild,
entry = entry,
systemUiContext = context,
- parentView = row.privateLayout
+ parentView = row.privateLayout,
+ viewType = RichOngoingNotificationViewType.Contracted
)
+ } else {
+ if (
+ ronInflater.canKeepView(
+ contentModel = richOngoingContentModel,
+ existingView = row.privateLayout.contractedChild,
+ viewType = RichOngoingNotificationViewType.Contracted
+ )
+ ) {
+ KeepExistingView
+ } else {
+ NullContentView
+ }
+ }
+
+ inflationProgress.expandedRichOngoingNotificationViewHolder =
+ if (inflateExpandedView) {
+ ronInflater.inflateView(
+ contentModel = richOngoingContentModel,
+ existingView = row.privateLayout.expandedChild,
+ entry = entry,
+ systemUiContext = context,
+ parentView = row.privateLayout,
+ viewType = RichOngoingNotificationViewType.Expanded
+ )
+ } else {
+ if (
+ ronInflater.canKeepView(
+ contentModel = richOngoingContentModel,
+ existingView = row.privateLayout.expandedChild,
+ viewType = RichOngoingNotificationViewType.Expanded
+ )
+ ) {
+ KeepExistingView
+ } else {
+ NullContentView
+ }
+ }
+
+ inflationProgress.headsUpRichOngoingNotificationViewHolder =
+ if (inflateHeadsUpView) {
+ ronInflater.inflateView(
+ contentModel = richOngoingContentModel,
+ existingView = row.privateLayout.headsUpChild,
+ entry = entry,
+ systemUiContext = context,
+ parentView = row.privateLayout,
+ viewType = RichOngoingNotificationViewType.HeadsUp
+ )
+ } else {
+ if (
+ ronInflater.canKeepView(
+ contentModel = richOngoingContentModel,
+ existingView = row.privateLayout.headsUpChild,
+ viewType = RichOngoingNotificationViewType.HeadsUp
+ )
+ ) {
+ KeepExistingView
+ } else {
+ NullContentView
+ }
}
}
@@ -618,7 +695,9 @@
var inflatedSmartReplyState: InflatedSmartReplyState? = null
var expandedInflatedSmartReplies: InflatedSmartReplyViewHolder? = null
var headsUpInflatedSmartReplies: InflatedSmartReplyViewHolder? = null
- var richOngoingNotificationViewHolder: InflatedContentViewHolder? = null
+ var contractedRichOngoingNotificationViewHolder: ContentViewInflationResult? = null
+ var expandedRichOngoingNotificationViewHolder: ContentViewInflationResult? = null
+ var headsUpRichOngoingNotificationViewHolder: ContentViewInflationResult? = null
// Inflated SingleLineView that lacks the UI State
var inflatedSingleLineView: HybridNotificationView? = null
@@ -1428,14 +1507,21 @@
logger.logAsyncTaskProgress(entry, "finishing")
// before updating the content model, stop existing binding if necessary
- val hasRichOngoingContentModel = result.contentModel.richOngoingContentModel != null
- val requestedRichOngoing = reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0
- val rejectedRichOngoing = requestedRichOngoing && !hasRichOngoingContentModel
- if (result.richOngoingNotificationViewHolder != null || rejectedRichOngoing) {
+ if (result.contractedRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) {
row.privateLayout.mContractedBinderHandle?.dispose()
row.privateLayout.mContractedBinderHandle = null
}
+ if (result.expandedRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) {
+ row.privateLayout.mExpandedBinderHandle?.dispose()
+ row.privateLayout.mExpandedBinderHandle = null
+ }
+
+ if (result.headsUpRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) {
+ row.privateLayout.mHeadsUpBinderHandle?.dispose()
+ row.privateLayout.mHeadsUpBinderHandle = null
+ }
+
// set the content model after disposal and before setting new rich ongoing view
entry.setContentModel(result.contentModel)
result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
@@ -1477,19 +1563,53 @@
}
}
- // after updating the content model, set the view, then start the new binder
- result.richOngoingNotificationViewHolder?.let { viewHolder ->
- row.privateLayout.contractedChild = viewHolder.view
- row.privateLayout.expandedChild = null
- row.privateLayout.headsUpChild = null
- row.privateLayout.setExpandedInflatedSmartReplies(null)
- row.privateLayout.setHeadsUpInflatedSmartReplies(null)
- row.privateLayout.mContractedBinderHandle =
- viewHolder.binder.setupContentViewBinder()
- row.setExpandable(false)
+ val hasRichOngoingViewHolder =
+ result.contractedRichOngoingNotificationViewHolder != null ||
+ result.expandedRichOngoingNotificationViewHolder != null ||
+ result.headsUpRichOngoingNotificationViewHolder != null
+
+ if (hasRichOngoingViewHolder) {
+ // after updating the content model, set the view, then start the new binder
+ result.contractedRichOngoingNotificationViewHolder?.let { contractedViewHolder ->
+ if (contractedViewHolder is InflatedContentViewHolder) {
+ row.privateLayout.contractedChild = contractedViewHolder.view
+ row.privateLayout.mContractedBinderHandle =
+ contractedViewHolder.binder.setupContentViewBinder()
+ } else if (contractedViewHolder == NullContentView) {
+ row.privateLayout.contractedChild = null
+ }
+ }
+
+ result.expandedRichOngoingNotificationViewHolder?.let { expandedViewHolder ->
+ if (expandedViewHolder is InflatedContentViewHolder) {
+ row.privateLayout.expandedChild = expandedViewHolder.view
+ row.privateLayout.mExpandedBinderHandle =
+ expandedViewHolder.binder.setupContentViewBinder()
+ } else if (expandedViewHolder == NullContentView) {
+ row.privateLayout.expandedChild = null
+ }
+ }
+
+ result.headsUpRichOngoingNotificationViewHolder?.let { headsUpViewHolder ->
+ if (headsUpViewHolder is InflatedContentViewHolder) {
+ row.privateLayout.headsUpChild = headsUpViewHolder.view
+ row.privateLayout.mHeadsUpBinderHandle =
+ headsUpViewHolder.binder.setupContentViewBinder()
+ } else if (headsUpViewHolder == NullContentView) {
+ row.privateLayout.headsUpChild = null
+ }
+ }
+
+ // clean remoteViewCache when we don't keep existing views.
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
+
+ // Since RONs don't support smart reply, remove them from HUNs and Expanded.
+ row.privateLayout.setExpandedInflatedSmartReplies(null)
+ row.privateLayout.setHeadsUpInflatedSmartReplies(null)
+
+ row.setExpandable(row.privateLayout.expandedChild != null)
}
Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row))
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
index e9c4960..828fc21 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
@@ -24,6 +24,9 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder
+import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView
+import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView
import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
import com.android.systemui.statusbar.notification.row.shared.StopwatchContentModel
@@ -39,7 +42,35 @@
fun setupContentViewBinder(): DisposableHandle
}
-class InflatedContentViewHolder(val view: View, val binder: DeferredContentViewBinder)
+enum class RichOngoingNotificationViewType {
+ Contracted,
+ Expanded,
+ HeadsUp,
+}
+
+/**
+ * * Supertype of the 3 different possible result types of
+ * [RichOngoingNotificationViewInflater.inflateView].
+ */
+sealed interface ContentViewInflationResult {
+
+ /** Indicates that the content view should be removed if present. */
+ data object NullContentView : ContentViewInflationResult
+
+ /**
+ * Indicates that the content view (which *must be* present) should be unmodified during this
+ * inflation.
+ */
+ data object KeepExistingView : ContentViewInflationResult
+
+ /**
+ * Contains the new view and binder that should replace any existing content view for this slot.
+ */
+ data class InflatedContentViewHolder(val view: View, val binder: DeferredContentViewBinder) :
+ ContentViewInflationResult
+}
+
+fun ContentViewInflationResult?.shouldDisposeViewBinder() = this !is KeepExistingView
/**
* Interface which provides a [RichOngoingContentModel] for a given [Notification] when one is
@@ -52,7 +83,14 @@
entry: NotificationEntry,
systemUiContext: Context,
parentView: ViewGroup,
- ): InflatedContentViewHolder?
+ viewType: RichOngoingNotificationViewType,
+ ): ContentViewInflationResult
+
+ fun canKeepView(
+ contentModel: RichOngoingContentModel,
+ existingView: View?,
+ viewType: RichOngoingNotificationViewType
+ ): Boolean
}
@SysUISingleton
@@ -68,8 +106,9 @@
entry: NotificationEntry,
systemUiContext: Context,
parentView: ViewGroup,
- ): InflatedContentViewHolder? {
- if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return null
+ viewType: RichOngoingNotificationViewType,
+ ): ContentViewInflationResult {
+ if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return NullContentView
val component = viewModelComponentFactory.create(entry)
return when (contentModel) {
is TimerContentModel ->
@@ -77,28 +116,55 @@
existingView,
component::createTimerViewModel,
systemUiContext,
- parentView
+ parentView,
+ viewType
)
is StopwatchContentModel -> TODO("Not yet implemented")
}
}
+ override fun canKeepView(
+ contentModel: RichOngoingContentModel,
+ existingView: View?,
+ viewType: RichOngoingNotificationViewType
+ ): Boolean {
+ if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return false
+ return when (contentModel) {
+ is TimerContentModel -> canKeepTimerView(contentModel, existingView, viewType)
+ is StopwatchContentModel -> TODO("Not yet implemented")
+ }
+ }
+
private fun inflateTimerView(
existingView: View?,
createViewModel: () -> TimerViewModel,
systemUiContext: Context,
parentView: ViewGroup,
- ): InflatedContentViewHolder? {
- if (existingView is TimerView && !existingView.isReinflateNeeded()) return null
- val newView =
- LayoutInflater.from(systemUiContext)
- .inflate(
- R.layout.rich_ongoing_timer_notification,
- parentView,
- /* attachToRoot= */ false
- ) as TimerView
- return InflatedContentViewHolder(newView) {
- TimerViewBinder.bindWhileAttached(newView, createViewModel())
+ viewType: RichOngoingNotificationViewType,
+ ): ContentViewInflationResult {
+ if (existingView is TimerView && !existingView.isReinflateNeeded()) return KeepExistingView
+
+ return when (viewType) {
+ RichOngoingNotificationViewType.Contracted -> {
+ val newView =
+ LayoutInflater.from(systemUiContext)
+ .inflate(
+ R.layout.rich_ongoing_timer_notification,
+ parentView,
+ /* attachToRoot= */ false
+ ) as TimerView
+ InflatedContentViewHolder(newView) {
+ TimerViewBinder.bindWhileAttached(newView, createViewModel())
+ }
+ }
+ RichOngoingNotificationViewType.Expanded,
+ RichOngoingNotificationViewType.HeadsUp -> NullContentView
}
}
+
+ private fun canKeepTimerView(
+ contentModel: TimerContentModel,
+ existingView: View?,
+ viewType: RichOngoingNotificationViewType
+ ): Boolean = true
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 7c3072d..6a2c602 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -139,6 +139,9 @@
/** Fraction of shade expansion. */
private float mExpansionFraction;
+ /** Fraction of QS expansion. 0 when in shade, 1 when in QS. */
+ private float mQsExpansionFraction;
+
/** Height of the notifications panel when expansion completes. */
private float mStackEndHeight;
@@ -208,6 +211,14 @@
}
/**
+ * @param expansionFraction Fraction of QS expansion.
+ */
+ public void setQsExpansionFraction(float expansionFraction) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ mQsExpansionFraction = expansionFraction;
+ }
+
+ /**
* @param isSwipingUp Whether we are swiping up.
*/
public void setSwipingUp(boolean isSwipingUp) {
@@ -258,6 +269,14 @@
}
/**
+ * @return Fraction of QS expansion.
+ */
+ public float getQsExpansionFraction() {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+ return mQsExpansionFraction;
+ }
+
+ /**
* @see #getStackHeight()
*/
public void setStackHeight(float stackHeight) {
@@ -837,6 +856,7 @@
pw.println("mAppearFraction=" + mAppearFraction);
pw.println("mAppearing=" + mAppearing);
pw.println("mExpansionFraction=" + mExpansionFraction);
+ pw.println("mQsExpansionFraction=" + mQsExpansionFraction);
pw.println("mExpandingVelocity=" + mExpandingVelocity);
pw.println("mOverScrollTopAmount=" + mOverScrollTopAmount);
pw.println("mOverScrollBottomAmount=" + mOverScrollBottomAmount);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 8f187f0..f26f840 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1244,6 +1244,7 @@
@Override
public void setHeadsUpTop(float headsUpTop) {
mAmbientState.setHeadsUpTop(headsUpTop);
+ requestChildrenUpdate();
}
@Override
@@ -1563,6 +1564,12 @@
}
}
+ @Override
+ public void setQsExpandFraction(float expandFraction) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ mAmbientState.setQsExpansionFraction(expandFraction);
+ }
+
/**
* Update the height of the panel.
*
@@ -1598,7 +1605,7 @@
if (mShouldShowShelfOnly) {
stackHeight = getTopPadding() + mShelf.getIntrinsicHeight();
} else if (mQsFullScreen) {
- int stackStartPosition = mContentHeight - getTopPadding() + mIntrinsicPadding;
+ int stackStartPosition = mContentHeight - getTopPadding() + getIntrinsicPadding();
int stackEndPosition = mMaxTopPadding + mShelf.getIntrinsicHeight();
if (stackStartPosition <= stackEndPosition) {
stackHeight = stackEndPosition;
@@ -1783,7 +1790,7 @@
} else {
appearPosition = mEmptyShadeView.getHeight();
}
- return appearPosition + (onKeyguard() ? getTopPadding() : mIntrinsicPadding);
+ return appearPosition + (onKeyguard() ? getTopPadding() : getIntrinsicPadding());
}
/**
@@ -1809,7 +1816,7 @@
} else {
appearPosition = mEmptyShadeView.getHeight();
}
- return appearPosition + (onKeyguard() ? getTopPadding() : mIntrinsicPadding);
+ return appearPosition + (onKeyguard() ? getTopPadding() : getIntrinsicPadding());
}
private boolean isHeadsUpTransition() {
@@ -2512,9 +2519,9 @@
// The topPadding can be bigger than the regular padding when qs is expanded, in that
// state the maxPanelHeight and the contentHeight should be bigger
mContentHeight =
- (int) (height + Math.max(mIntrinsicPadding, getTopPadding()) + mBottomPadding);
+ (int) (height + Math.max(getIntrinsicPadding(), getTopPadding()) + mBottomPadding);
mScrollViewFields.setIntrinsicStackHeight(
- (int) (mIntrinsicPadding + mIntrinsicContentHeight + footerIntrinsicHeight
+ (int) (getIntrinsicPadding() + mIntrinsicContentHeight + footerIntrinsicHeight
+ mBottomPadding));
updateScrollability();
clampScrollPosition();
@@ -2534,6 +2541,11 @@
return getTopHeadsUpIntrinsicHeight();
}
+ @Override
+ public int getHeadsUpInset() {
+ return mHeadsUpInset;
+ }
+
/**
* Calculate the gap height between two different views
*
@@ -4558,10 +4570,20 @@
}
void setIntrinsicPadding(int intrinsicPadding) {
+ SceneContainerFlag.assertInLegacyMode();
mIntrinsicPadding = intrinsicPadding;
}
+ /**
+ * Distance from the top of the screen in, where notifications should start when fully expanded
+ * or in the LS.
+ *
+ * Always 0 with SceneContainer enabled.
+ */
int getIntrinsicPadding() {
+ if (SceneContainerFlag.isEnabled()) {
+ return 0;
+ }
return mIntrinsicPadding;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 693e8ff..55f0566 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -132,6 +132,7 @@
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
import com.android.systemui.statusbar.phone.HeadsUpNotificationViewControllerEmptyImpl;
import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
+import com.android.systemui.statusbar.phone.HeadsUpTouchHelper.HeadsUpNotificationViewController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -773,7 +774,7 @@
mHeadsUpManager,
statusBarService.get(),
getHeadsUpCallback(),
- new HeadsUpNotificationViewControllerEmptyImpl()
+ getHeadsUpNotificationViewController()
);
}
mNotificationRoundnessManager = notificationRoundnessManager;
@@ -1186,6 +1187,7 @@
}
public void setIntrinsicPadding(int intrinsicPadding) {
+ SceneContainerFlag.assertInLegacyMode();
mView.setIntrinsicPadding(intrinsicPadding);
}
@@ -1850,6 +1852,32 @@
return mTouchHandler;
}
+ private HeadsUpNotificationViewController getHeadsUpNotificationViewController() {
+ HeadsUpNotificationViewController headsUpViewController;
+ if (SceneContainerFlag.isEnabled()) {
+ headsUpViewController = new HeadsUpNotificationViewController() {
+ @Override
+ public void setHeadsUpDraggingStartingHeight(int startHeight) {
+ // do nothing
+ }
+
+ @Override
+ public void setTrackedHeadsUp(ExpandableNotificationRow expandableNotificationRow) {
+ setTrackingHeadsUp(expandableNotificationRow);
+ }
+
+ @Override
+ public void startExpand(float newX, float newY, boolean startTracking,
+ float expandedHeight) {
+ // do nothing
+ }
+ };
+ } else {
+ headsUpViewController = new HeadsUpNotificationViewControllerEmptyImpl();
+ }
+ return headsUpViewController;
+ }
+
@Override
public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.println("mMaxAlphaFromView=" + mMaxAlphaFromView);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index aee1d3e..0c2b5ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -16,8 +16,6 @@
package com.android.systemui.statusbar.notification.stack;
-import static androidx.core.math.MathUtils.clamp;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -890,7 +888,14 @@
continue;
}
ExpandableViewState childState = row.getViewState();
- if (topHeadsUpEntry == null && row.mustStayOnScreen() && !childState.headsUpIsVisible) {
+ boolean shouldSetTopHeadsUpEntry;
+ if (SceneContainerFlag.isEnabled()) {
+ shouldSetTopHeadsUpEntry = row.isHeadsUp();
+ } else {
+ shouldSetTopHeadsUpEntry = row.mustStayOnScreen();
+ }
+ if (topHeadsUpEntry == null && shouldSetTopHeadsUpEntry
+ && !childState.headsUpIsVisible) {
topHeadsUpEntry = row;
childState.location = ExpandableViewState.LOCATION_FIRST_HUN;
}
@@ -898,7 +903,7 @@
float unmodifiedEndLocation = childState.getYTranslation() + childState.height;
if (mIsExpanded) {
if (SceneContainerFlag.isEnabled()) {
- if (shouldHunBeVisibleWhenScrolled(row.mustStayOnScreen(),
+ if (shouldHunBeVisibleWhenScrolled(row.isHeadsUp(),
childState.headsUpIsVisible, row.showingPulsing(),
ambientState.isOnKeyguard(), row.getEntry().isStickyAndNotDemoted())) {
// the height of this child before clamping it to the top
@@ -909,10 +914,19 @@
/* viewState = */ childState
);
float baseZ = ambientState.getBaseZHeight();
- if (headsUpTranslation < ambientState.getStackTop()) {
- // HUN displayed above the stack top, it needs a fix shadow
- childState.setZTranslation(baseZ + mPinnedZTranslationExtra);
- } else {
+ if (headsUpTranslation > ambientState.getStackTop()
+ && row.isAboveShelf()) {
+ // HUN displayed outside of the stack during transition from Gone/LS;
+ // add a shadow that corresponds to the transition progress.
+ float fraction = 1 - ambientState.getExpansionFraction();
+ childState.setZTranslation(baseZ + fraction * mPinnedZTranslationExtra);
+ } else if (headsUpTranslation < ambientState.getStackTop()
+ && row.isAboveShelf()) {
+ // HUN displayed outside of the stack during transition from QS;
+ // add a shadow that corresponds to the transition progress.
+ float fraction = ambientState.getQsExpansionFraction();
+ childState.setZTranslation(baseZ + fraction * mPinnedZTranslationExtra);
+ } else if (headsUpTranslation > ambientState.getStackTop()) {
// HUN displayed within the stack, add a shadow if it overlaps with
// other elements.
//
@@ -927,6 +941,8 @@
/* baseZ = */ baseZ,
/* viewState = */ childState
);
+ } else {
+ childState.setZTranslation(baseZ);
}
if (isTopEntry && row.isAboveShelf()) {
clampHunToMaxTranslation(
@@ -1081,7 +1097,7 @@
if (scrollingContentTopPadding > 0f) {
// scrollingContentTopPadding makes a gap between the bottom of the HUN and the top
// of the scrolling content. Use this to animate to the full shadow.
- shadowFraction = clamp(overlap / scrollingContentTopPadding, 0f, 1f);
+ shadowFraction = Math.clamp(overlap / scrollingContentTopPadding, 0f, 1f);
}
if (overlap > 0.0f) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 6226fe7..1289cec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -80,9 +80,18 @@
/** sets the current expand fraction */
fun setExpandFraction(expandFraction: Float)
+ /** sets the current QS expand fraction */
+ fun setQsExpandFraction(expandFraction: Float)
+
/** Sets whether the view is displayed in doze mode. */
fun setDozing(dozing: Boolean)
+ /** Sets whether the view is displayed in pulsing mode. */
+ fun setPulsing(pulsing: Boolean, animated: Boolean)
+
+ /** Gets the inset for HUNs when they are not visible */
+ fun getHeadsUpInset(): Int
+
/** Adds a listener to be notified, when the stack height might have changed. */
fun addStackHeightChangedListener(runnable: Runnable)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 950b14d..c044f6f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -85,9 +85,19 @@
launch {
viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) }
}
+ launch { viewModel.qsExpandFraction.collect { view.setQsExpandFraction(it) } }
launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } }
launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } }
- launch { viewModel.shouldResetStackTop.filter { it }.collect { view.setStackTop(0f) } }
+ launch {
+ viewModel.isPulsing.collect { isPulsing ->
+ view.setPulsing(isPulsing, viewModel.shouldAnimatePulse.value)
+ }
+ }
+ launch {
+ viewModel.shouldResetStackTop
+ .filter { it }
+ .collect { view.setStackTop(-(view.getHeadsUpInset().toFloat())) }
+ }
launchAndDispose {
view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index ed69e6f..0e984cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -39,6 +39,8 @@
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
@@ -72,7 +74,7 @@
}
private fun expandFractionForTransition(
- state: ObservableTransitionState.Transition,
+ state: ObservableTransitionState.Transition.ChangeCurrentScene,
shadeExpansion: Float,
shadeMode: ShadeMode,
qsExpansion: Float,
@@ -111,7 +113,7 @@
when (transitionState) {
is ObservableTransitionState.Idle ->
expandFractionForScene(transitionState.currentScene, shadeExpansion)
- is ObservableTransitionState.Transition ->
+ is ObservableTransitionState.Transition.ChangeCurrentScene ->
expandFractionForTransition(
transitionState,
shadeExpansion,
@@ -119,11 +121,17 @@
qsExpansion,
quickSettingsScene
)
+ is ObservableTransitionState.Transition.ShowOrHideOverlay,
+ is ObservableTransitionState.Transition.ReplaceOverlay ->
+ TODO("b/359173565: Handle overlay transitions")
}
}
.distinctUntilChanged()
.dumpWhileCollecting("expandFraction")
+ val qsExpandFraction: Flow<Float> =
+ shadeInteractor.qsExpansion.dumpWhileCollecting("qsExpandFraction")
+
val shouldResetStackTop: Flow<Boolean> =
sceneInteractor.transitionState
.mapNotNull { state ->
@@ -210,13 +218,30 @@
}
}
+ /** Whether the notification stack is displayed in pulsing mode. */
+ val isPulsing: Flow<Boolean> by lazy {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ keyguardInteractor.get().isPulsing.dumpWhileCollecting("isPulsing")
+ }
+ }
+
+ val shouldAnimatePulse: StateFlow<Boolean> by lazy {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+ MutableStateFlow(false)
+ } else {
+ keyguardInteractor.get().isAodAvailable
+ }
+ }
+
@AssistedFactory
interface Factory {
fun create(): NotificationScrollViewModel
}
}
-private fun ObservableTransitionState.Transition.isBetween(
+private fun ObservableTransitionState.Transition.ChangeCurrentScene.isBetween(
a: (SceneKey) -> Boolean,
b: (SceneKey) -> Boolean
): Boolean = (a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene))
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
index 0067316..f649418 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
@@ -25,6 +25,7 @@
import com.android.systemui.doze.DozeLog;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import javax.inject.Inject;
@@ -124,6 +125,11 @@
// Begin pulse. Note that it's very important that the pulse finished callback
// be invoked when we're done so that the caller can drop the pulse wakelock.
+ if (SceneContainerFlag.isEnabled()) {
+ // ScrimController.Callback#onDisplayBlanked is no longer triggered when flexiglass is
+ // on, but we still need to signal that pulsing has started.
+ callback.onPulseStarted();
+ }
mPulseCallback = callback;
mPulseReason = reason;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index 199b5b67..37f2f19 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -36,14 +36,12 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
/**
@@ -76,37 +74,10 @@
@DeviceBasedSatelliteInputLog logBuffer: LogBuffer,
@DeviceBasedSatelliteTableLog tableLog: TableLogBuffer,
) : DeviceBasedSatelliteViewModel {
- private val shouldShowIcon: Flow<Boolean> =
- interactor.areAllConnectionsOutOfService
- .flatMapLatest { allOos ->
- if (!allOos) {
- flowOf(false)
- } else {
- combine(
- interactor.isSatelliteAllowed,
- interactor.isSatelliteProvisioned,
- interactor.isWifiActive,
- airplaneModeRepository.isAirplaneMode
- ) { isSatelliteAllowed, isSatelliteProvisioned, isWifiActive, isAirplaneMode ->
- isSatelliteAllowed &&
- isSatelliteProvisioned &&
- !isWifiActive &&
- !isAirplaneMode
- }
- }
- }
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLog,
- columnPrefix = "vm",
- columnName = COL_VISIBLE_CONDITION,
- initialValue = false,
- )
// This adds a 10 seconds delay before showing the icon
- private val shouldActuallyShowIcon: StateFlow<Boolean> =
- shouldShowIcon
- .distinctUntilChanged()
+ private val shouldShowIconForOosAfterHysteresis: StateFlow<Boolean> =
+ interactor.areAllConnectionsOutOfService
.flatMapLatest { shouldShow ->
if (shouldShow) {
logBuffer.log(
@@ -125,6 +96,45 @@
.logDiffsForTable(
tableLog,
columnPrefix = "vm",
+ columnName = COL_VISIBLE_FOR_OOS,
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ private val canShowIcon =
+ combine(
+ interactor.isSatelliteAllowed,
+ interactor.isSatelliteProvisioned,
+ ) { allowed, provisioned ->
+ allowed && provisioned
+ }
+
+ private val showIcon =
+ canShowIcon
+ .flatMapLatest { canShow ->
+ if (!canShow) {
+ flowOf(false)
+ } else {
+ combine(
+ shouldShowIconForOosAfterHysteresis,
+ interactor.connectionState,
+ interactor.isWifiActive,
+ airplaneModeRepository.isAirplaneMode,
+ ) { showForOos, connectionState, isWifiActive, isAirplaneMode ->
+ if (isWifiActive || isAirplaneMode) {
+ false
+ } else {
+ showForOos ||
+ connectionState == SatelliteConnectionState.On ||
+ connectionState == SatelliteConnectionState.Connected
+ }
+ }
+ }
+ }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ tableLog,
+ columnPrefix = "vm",
columnName = COL_VISIBLE,
initialValue = false,
)
@@ -132,7 +142,7 @@
override val icon: StateFlow<Icon?> =
combine(
- shouldActuallyShowIcon,
+ showIcon,
interactor.connectionState,
interactor.signalStrength,
) { shouldShow, state, signalStrength ->
@@ -146,7 +156,7 @@
override val carrierText: StateFlow<String?> =
combine(
- shouldActuallyShowIcon,
+ showIcon,
interactor.connectionState,
) { shouldShow, connectionState ->
logBuffer.log(
@@ -156,7 +166,7 @@
bool1 = shouldShow
str1 = connectionState.name
},
- { "Updating carrier text. shouldActuallyShow=$bool1 connectionState=$str1" }
+ { "Updating carrier text. shouldShow=$bool1 connectionState=$str1" }
)
if (shouldShow) {
when (connectionState) {
@@ -165,28 +175,30 @@
context.getString(R.string.satellite_connected_carrier_text)
SatelliteConnectionState.Off,
SatelliteConnectionState.Unknown -> {
- null
+ // If we're showing the satellite icon opportunistically, use the
+ // emergency-only version of the carrier string
+ context.getString(R.string.satellite_emergency_only_carrier_text)
}
}
} else {
null
}
}
- .onEach {
- logBuffer.log(
- TAG,
- LogLevel.INFO,
- { str1 = it },
- { "Resulting carrier text = $str1" }
- )
- }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ tableLog,
+ columnPrefix = "vm",
+ columnName = COL_CARRIER_TEXT,
+ initialValue = null,
+ )
.stateIn(scope, SharingStarted.WhileSubscribed(), null)
companion object {
private const val TAG = "DeviceBasedSatelliteViewModel"
private val DELAY_DURATION = 10.seconds
- const val COL_VISIBLE_CONDITION = "visCondition"
+ const val COL_VISIBLE_FOR_OOS = "visibleForOos"
const val COL_VISIBLE = "visible"
+ const val COL_CARRIER_TEXT = "carrierText"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index 7586133..f16fcb5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -42,6 +42,7 @@
class ZenModeInteractor
@Inject
constructor(
+ private val context: Context,
private val zenModeRepository: ZenModeRepository,
private val notificationSettingsRepository: NotificationSettingsRepository,
) {
@@ -78,6 +79,26 @@
val activeModes: Flow<List<ZenMode>> =
modes.map { modes -> modes.filter { mode -> mode.isActive } }.distinctUntilChanged()
+ /** Flow returning the most prioritized of the active modes, if any. */
+ val mainActiveMode: Flow<ZenMode?> =
+ activeModes.map { modes -> getMainActiveMode(modes) }.distinctUntilChanged()
+
+ /**
+ * Given the list of modes (which may include zero or more currently active modes), returns the
+ * most prioritized of the active modes, if any.
+ */
+ private fun getMainActiveMode(modes: List<ZenMode>): ZenMode? {
+ return modes.sortedWith(ZenMode.PRIORITIZING_COMPARATOR).firstOrNull { it.isActive }
+ }
+
+ suspend fun getModeIcon(mode: ZenMode): Icon {
+ return mode.getIcon(context, iconLoader).await().asIcon()
+ }
+
+ suspend fun getLockscreenModeIcon(mode: ZenMode): Icon {
+ return mode.getLockscreenIcon(context, iconLoader).await().asIcon()
+ }
+
/**
* Given the list of modes (which may include zero or more currently active modes), returns an
* icon representing the active mode, if any (or, if multiple modes are active, to the most
@@ -86,16 +107,7 @@
* package).
*/
suspend fun getActiveModeIcon(context: Context, modes: List<ZenMode>): Icon? {
- return modes
- .sortedWith(ZenMode.PRIORITIZING_COMPARATOR)
- .firstOrNull { it.isActive }
- ?.getLockscreenIcon(context, iconLoader)
- ?.await()
- ?.asIcon()
- }
-
- suspend fun getModeIcon(context: Context, mode: ZenMode): Icon {
- return mode.getIcon(context, iconLoader).await().asIcon()
+ return getMainActiveMode(modes)?.let { m -> getLockscreenModeIcon(m) }
}
fun activateMode(zenMode: ZenMode) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
index 02b5e49..be90bec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
@@ -88,7 +88,7 @@
modesList.map { mode ->
ModeTileViewModel(
id = mode.id,
- icon = zenModeInteractor.getModeIcon(context, mode),
+ icon = zenModeInteractor.getModeIcon(mode),
text = mode.name,
subtext = getTileSubtext(mode),
enabled = mode.isActive,
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index 1c8041f..a3b1867 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -16,7 +16,6 @@
package com.android.systemui.touchpad.tutorial.ui.composable
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.airbnb.lottie.compose.rememberLottieDynamicProperties
@@ -67,7 +66,6 @@
val onTertiaryFixed = LocalAndroidColorScheme.current.onTertiaryFixed
val onTertiaryFixedVariant = LocalAndroidColorScheme.current.onTertiaryFixedVariant
val tertiaryFixedDim = LocalAndroidColorScheme.current.tertiaryFixedDim
- val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer
val dynamicProperties =
rememberLottieDynamicProperties(
rememberColorFilterProperty(".tertiaryFixedDim", tertiaryFixedDim),
@@ -76,10 +74,9 @@
rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant)
)
val screenColors =
- remember(onTertiaryFixed, surfaceContainer, tertiaryFixedDim, dynamicProperties) {
+ remember(dynamicProperties) {
TutorialScreenConfig.Colors(
background = onTertiaryFixed,
- successBackground = surfaceContainer,
title = tertiaryFixedDim,
animationColors = dynamicProperties,
)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
index 0a6283a..d4eb0cd 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
@@ -16,7 +16,6 @@
package com.android.systemui.touchpad.tutorial.ui.composable
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.airbnb.lottie.compose.rememberLottieDynamicProperties
@@ -66,7 +65,6 @@
val primaryFixedDim = LocalAndroidColorScheme.current.primaryFixedDim
val onPrimaryFixed = LocalAndroidColorScheme.current.onPrimaryFixed
val onPrimaryFixedVariant = LocalAndroidColorScheme.current.onPrimaryFixedVariant
- val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer
val dynamicProperties =
rememberLottieDynamicProperties(
rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim),
@@ -74,10 +72,9 @@
rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant)
)
val screenColors =
- remember(surfaceContainer, dynamicProperties) {
+ remember(dynamicProperties) {
TutorialScreenConfig.Colors(
background = onPrimaryFixed,
- successBackground = surfaceContainer,
title = primaryFixedDim,
animationColors = dynamicProperties,
)
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
index 28ac2c0..055671c 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -28,6 +28,7 @@
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -62,7 +63,9 @@
/**
* Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to
* [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners
- * during onViewAttached() and removing during onViewRemoved()
+ * during onViewAttached() and removing during onViewRemoved().
+ *
+ * @return a disposable handle in order to cancel the flow in the future.
*/
@JvmOverloads
fun <T> collectFlow(
@@ -71,8 +74,8 @@
consumer: Consumer<T>,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
state: Lifecycle.State = Lifecycle.State.CREATED,
-) {
- view.repeatWhenAttached(coroutineContext) {
+): DisposableHandle {
+ return view.repeatWhenAttached(coroutineContext) {
repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
index 5600b87..a18d272 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
@@ -711,6 +711,16 @@
}
@Test
+ public void testDestroy_cleansUpHandler() {
+ final TouchHandler touchHandler = createTouchHandler();
+
+ final Environment environment = new Environment(Stream.of(touchHandler)
+ .collect(Collectors.toCollection(HashSet::new)), mKosmos);
+ environment.destroyMonitor();
+ verify(touchHandler).onDestroy();
+ }
+
+ @Test
public void testLastSessionPop_createsNewInputSession() {
final TouchHandler touchHandler = createTouchHandler();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index f94a6f2..1e23690 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -41,6 +41,7 @@
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.app.viewcapture.ViewCapture
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.widget.LockPatternUtils
import com.android.launcher3.icons.IconProvider
@@ -111,6 +112,7 @@
@Mock lateinit var selectedUserInteractor: SelectedUserInteractor
@Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var activityTaskManager: ActivityTaskManager
+ @Mock private lateinit var lazyViewCapture: Lazy<ViewCapture>
private lateinit var displayRepository: FakeDisplayRepository
private lateinit var displayStateInteractor: DisplayStateInteractor
@@ -665,7 +667,8 @@
),
{ credentialViewModel },
fakeExecutor,
- vibrator
+ vibrator,
+ lazyViewCapture
) {
override fun postOnAnimation(runnable: Runnable) {
runnable.run()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index c425e82..5fc1971 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -18,6 +18,7 @@
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static com.android.systemui.Flags.FLAG_CLIPBOARD_SHARED_TRANSITIONS;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EXPANDED_FROM_MINIMIZED;
@@ -26,7 +27,6 @@
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_MINIMIZED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT;
-import static com.android.systemui.flags.Flags.CLIPBOARD_SHARED_TRANSITIONS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -47,6 +47,8 @@
import android.graphics.Rect;
import android.net.Uri;
import android.os.PersistableBundle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.view.WindowInsets;
import android.view.textclassifier.TextLinks;
@@ -130,7 +132,6 @@
new ClipData.Item("Test Item"));
mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, true); // turned off for legacy tests
- mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, true); // turned off for old tests
}
/**
@@ -299,8 +300,8 @@
}
@Test
+ @DisableFlags(FLAG_CLIPBOARD_SHARED_TRANSITIONS)
public void test_viewCallbacks_onShareTapped_sharedTransitionsOff() {
- mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, false);
initController();
mOverlayController.setClipData(mSampleClipData, "");
@@ -311,6 +312,7 @@
}
@Test
+ @EnableFlags(FLAG_CLIPBOARD_SHARED_TRANSITIONS)
public void test_viewCallbacks_onShareTapped() {
initController();
mOverlayController.setClipData(mSampleClipData, "");
@@ -324,8 +326,8 @@
}
@Test
+ @DisableFlags(FLAG_CLIPBOARD_SHARED_TRANSITIONS)
public void test_viewCallbacks_onDismissTapped_sharedTransitionsOff() {
- mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, false);
initController();
mOverlayController.setClipData(mSampleClipData, "");
@@ -336,6 +338,7 @@
}
@Test
+ @EnableFlags(FLAG_CLIPBOARD_SHARED_TRANSITIONS)
public void test_viewCallbacks_onDismissTapped() {
initController();
@@ -350,7 +353,6 @@
@Test
public void test_multipleDismissals_dismissesOnce_sharedTransitionsOff() {
- mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, false);
initController();
mCallbacks.onSwipeDismissInitiated(mAnimator);
mCallbacks.onDismissButtonTapped();
@@ -362,6 +364,7 @@
}
@Test
+ @EnableFlags(FLAG_CLIPBOARD_SHARED_TRANSITIONS)
public void test_multipleDismissals_dismissesOnce() {
initController();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt
index e60848b..6e9b24f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt
@@ -123,19 +123,19 @@
FakeWidgetMetadata(
widgetId = 11,
componentName = "com.android.fakePackage1/fakeWidget1",
- rank = 3,
+ rank = 0,
userSerialNumber = 0,
),
FakeWidgetMetadata(
widgetId = 12,
componentName = "com.android.fakePackage2/fakeWidget2",
- rank = 2,
+ rank = 1,
userSerialNumber = 0,
),
FakeWidgetMetadata(
widgetId = 13,
componentName = "com.android.fakePackage3/fakeWidget3",
- rank = 1,
+ rank = 2,
userSerialNumber = 10,
),
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
index 983a435..edc8c83 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
@@ -69,19 +69,19 @@
FakeWidgetMetadata(
widgetId = 11,
componentName = "com.android.fakePackage1/fakeWidget1",
- rank = 3,
+ rank = 0,
userSerialNumber = 0,
),
FakeWidgetMetadata(
widgetId = 12,
componentName = "com.android.fakePackage2/fakeWidget2",
- rank = 2,
+ rank = 1,
userSerialNumber = 0,
),
FakeWidgetMetadata(
widgetId = 13,
componentName = "com.android.fakePackage3/fakeWidget3",
- rank = 1,
+ rank = 2,
userSerialNumber = 10,
),
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt
index eb0ab78..ad25502 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt
@@ -72,6 +72,82 @@
databaseV2.verifyWidgetsV2(fakeWidgetsV1.map { it.getV2() })
}
+ @Test
+ fun migrate2To3_noGapBetweenRanks_ranksReversed() {
+ // Create a communal database in version 2
+ val databaseV2 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 2)
+
+ // Populate some fake data
+ val fakeRanks =
+ listOf(
+ FakeCommunalItemRank(3),
+ FakeCommunalItemRank(2),
+ FakeCommunalItemRank(1),
+ FakeCommunalItemRank(0),
+ )
+ databaseV2.insertRanks(fakeRanks)
+
+ // Verify fake ranks populated
+ databaseV2.verifyRanksInOrder(fakeRanks)
+
+ // Run migration and get database V3
+ val databaseV3 =
+ migrationTestHelper.runMigrationsAndValidate(
+ name = DATABASE_NAME,
+ version = 3,
+ validateDroppedTables = false,
+ CommunalDatabase.MIGRATION_2_3,
+ )
+
+ // Verify ranks are reversed
+ databaseV3.verifyRanksInOrder(
+ listOf(
+ FakeCommunalItemRank(0),
+ FakeCommunalItemRank(1),
+ FakeCommunalItemRank(2),
+ FakeCommunalItemRank(3),
+ )
+ )
+ }
+
+ @Test
+ fun migrate2To3_withGapBetweenRanks_ranksReversed() {
+ // Create a communal database in version 2
+ val databaseV2 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 2)
+
+ // Populate some fake data with gaps between ranks
+ val fakeRanks =
+ listOf(
+ FakeCommunalItemRank(9),
+ FakeCommunalItemRank(7),
+ FakeCommunalItemRank(2),
+ FakeCommunalItemRank(0),
+ )
+ databaseV2.insertRanks(fakeRanks)
+
+ // Verify fake ranks populated
+ databaseV2.verifyRanksInOrder(fakeRanks)
+
+ // Run migration and get database V3
+ val databaseV3 =
+ migrationTestHelper.runMigrationsAndValidate(
+ name = DATABASE_NAME,
+ version = 3,
+ validateDroppedTables = false,
+ CommunalDatabase.MIGRATION_2_3,
+ )
+
+ // Verify ranks are reversed
+ databaseV3.verifyRanksInOrder(
+ listOf(
+ FakeCommunalItemRank(0),
+ FakeCommunalItemRank(2),
+ FakeCommunalItemRank(7),
+ FakeCommunalItemRank(9),
+ )
+ )
+ }
+
private fun SupportSQLiteDatabase.insertWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) {
widgets.forEach { widget ->
execSQL(
@@ -117,6 +193,25 @@
assertThat(cursor.isAfterLast).isTrue()
}
+ private fun SupportSQLiteDatabase.insertRanks(ranks: List<FakeCommunalItemRank>) {
+ ranks.forEach { rank ->
+ execSQL("INSERT INTO communal_item_rank_table(rank) VALUES(${rank.rank})")
+ }
+ }
+
+ private fun SupportSQLiteDatabase.verifyRanksInOrder(ranks: List<FakeCommunalItemRank>) {
+ val cursor = query("SELECT * FROM communal_item_rank_table ORDER BY uid")
+ assertThat(cursor.moveToFirst()).isTrue()
+
+ ranks.forEach { rank ->
+ assertThat(cursor.getInt(cursor.getColumnIndex("rank"))).isEqualTo(rank.rank)
+ cursor.moveToNext()
+ }
+
+ // Verify there is no more columns
+ assertThat(cursor.isAfterLast).isTrue()
+ }
+
/**
* Returns the expected data after migration from V1 to V2, which is simply that the new user
* serial number field is now set to [CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED].
@@ -143,6 +238,10 @@
val userSerialNumber: Int,
)
+ private data class FakeCommunalItemRank(
+ val rank: Int,
+ )
+
companion object {
private const val DATABASE_NAME = "communal_db"
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
index d670508..d4d966a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
@@ -67,11 +67,11 @@
@Test
fun addWidget_readValueInDb() =
testScope.runTest {
- val (widgetId, provider, priority, userSerialNumber) = widgetInfo1
+ val (widgetId, provider, rank, userSerialNumber) = widgetInfo1
communalWidgetDao.addWidget(
widgetId = widgetId,
provider = provider,
- priority = priority,
+ rank = rank,
userSerialNumber = userSerialNumber,
)
val entry = communalWidgetDao.getWidgetByIdNow(id = 1)
@@ -81,11 +81,11 @@
@Test
fun deleteWidget_notInDb_returnsFalse() =
testScope.runTest {
- val (widgetId, provider, priority, userSerialNumber) = widgetInfo1
+ val (widgetId, provider, rank, userSerialNumber) = widgetInfo1
communalWidgetDao.addWidget(
widgetId = widgetId,
provider = provider,
- priority = priority,
+ rank = rank,
userSerialNumber = userSerialNumber,
)
assertThat(communalWidgetDao.deleteWidgetById(widgetId = 123)).isFalse()
@@ -97,11 +97,11 @@
val widgetsToAdd = listOf(widgetInfo1, widgetInfo2)
val widgets = collectLastValue(communalWidgetDao.getWidgets())
widgetsToAdd.forEach {
- val (widgetId, provider, priority, userSerialNumber) = it
+ val (widgetId, provider, rank, userSerialNumber) = it
communalWidgetDao.addWidget(
widgetId = widgetId,
provider = provider,
- priority = priority,
+ rank = rank,
userSerialNumber = userSerialNumber
)
}
@@ -115,17 +115,48 @@
}
@Test
+ fun addWidget_rankNotSpecified_widgetAddedAtTheEnd(): Unit =
+ testScope.runTest {
+ val widgets by collectLastValue(communalWidgetDao.getWidgets())
+
+ // Verify database is empty
+ assertThat(widgets).isEmpty()
+
+ // Add widgets one by one without specifying rank
+ val widgetsToAdd = listOf(widgetInfo1, widgetInfo2, widgetInfo3)
+ widgetsToAdd.forEach {
+ val (widgetId, provider, _, userSerialNumber) = it
+ communalWidgetDao.addWidget(
+ widgetId = widgetId,
+ provider = provider,
+ userSerialNumber = userSerialNumber
+ )
+ }
+
+ // Verify new each widget is added at the end
+ assertThat(widgets)
+ .containsExactly(
+ communalItemRankEntry1,
+ communalWidgetItemEntry1,
+ communalItemRankEntry2,
+ communalWidgetItemEntry2,
+ communalItemRankEntry3,
+ communalWidgetItemEntry3,
+ )
+ }
+
+ @Test
fun deleteWidget_emitsActiveWidgetsInDb() =
testScope.runTest {
val widgetsToAdd = listOf(widgetInfo1, widgetInfo2)
val widgets = collectLastValue(communalWidgetDao.getWidgets())
widgetsToAdd.forEach {
- val (widgetId, provider, priority, userSerialNumber) = it
+ val (widgetId, provider, rank, userSerialNumber) = it
communalWidgetDao.addWidget(
widgetId = widgetId,
provider = provider,
- priority = priority,
+ rank = rank,
userSerialNumber = userSerialNumber,
)
}
@@ -148,32 +179,32 @@
val widgets = collectLastValue(communalWidgetDao.getWidgets())
widgetsToAdd.forEach {
- val (widgetId, provider, priority, userSerialNumber) = it
+ val (widgetId, provider, rank, userSerialNumber) = it
communalWidgetDao.addWidget(
widgetId = widgetId,
provider = provider,
- priority = priority,
+ rank = rank,
userSerialNumber = userSerialNumber,
)
}
assertThat(widgets())
.containsExactly(
- communalItemRankEntry2,
- communalWidgetItemEntry2,
communalItemRankEntry1,
communalWidgetItemEntry1,
+ communalItemRankEntry2,
+ communalWidgetItemEntry2,
)
.inOrder()
- // swapped priorities
- val widgetIdsToPriorityMap = mapOf(widgetInfo1.widgetId to 2, widgetInfo2.widgetId to 1)
- communalWidgetDao.updateWidgetOrder(widgetIdsToPriorityMap)
+ // swapped ranks
+ val widgetIdsToRankMap = mapOf(widgetInfo1.widgetId to 1, widgetInfo2.widgetId to 0)
+ communalWidgetDao.updateWidgetOrder(widgetIdsToRankMap)
assertThat(widgets())
.containsExactly(
- communalItemRankEntry1.copy(rank = 2),
+ communalItemRankEntry2.copy(rank = 0),
+ communalWidgetItemEntry2,
+ communalItemRankEntry1.copy(rank = 1),
communalWidgetItemEntry1,
- communalItemRankEntry2.copy(rank = 1),
- communalWidgetItemEntry2
)
.inOrder()
}
@@ -181,53 +212,56 @@
@Test
fun addNewWidgetWithReorder_emitsWidgetsInNewOrder() =
testScope.runTest {
- val existingWidgets = listOf(widgetInfo1, widgetInfo2)
+ val existingWidgets = listOf(widgetInfo1, widgetInfo2, widgetInfo3)
val widgets = collectLastValue(communalWidgetDao.getWidgets())
existingWidgets.forEach {
- val (widgetId, provider, priority, userSerialNumber) = it
+ val (widgetId, provider, rank, userSerialNumber) = it
communalWidgetDao.addWidget(
widgetId = widgetId,
provider = provider,
- priority = priority,
+ rank = rank,
userSerialNumber = userSerialNumber,
)
}
assertThat(widgets())
.containsExactly(
- communalItemRankEntry2,
- communalWidgetItemEntry2,
communalItemRankEntry1,
communalWidgetItemEntry1,
+ communalItemRankEntry2,
+ communalWidgetItemEntry2,
+ communalItemRankEntry3,
+ communalWidgetItemEntry3,
)
.inOrder()
- // map with no item in the middle at index 1
- val widgetIdsToIndexMap = mapOf(widgetInfo1.widgetId to 1, widgetInfo2.widgetId to 3)
- communalWidgetDao.updateWidgetOrder(widgetIdsToIndexMap)
- assertThat(widgets())
- .containsExactly(
- communalItemRankEntry2.copy(rank = 3),
- communalWidgetItemEntry2,
- communalItemRankEntry1.copy(rank = 1),
- communalWidgetItemEntry1,
- )
- .inOrder()
- // add the new middle item that we left space for.
+ // add a new widget at rank 1.
communalWidgetDao.addWidget(
- widgetId = widgetInfo3.widgetId,
- provider = widgetInfo3.provider,
- priority = 2,
- userSerialNumber = widgetInfo3.userSerialNumber,
+ widgetId = 4,
+ provider = ComponentName("pk_name", "cls_name_4"),
+ rank = 1,
+ userSerialNumber = 0,
)
+
+ val newRankEntry = CommunalItemRank(uid = 4L, rank = 1)
+ val newWidgetEntry =
+ CommunalWidgetItem(
+ uid = 4L,
+ widgetId = 4,
+ componentName = "pk_name/cls_name_4",
+ itemId = 4L,
+ userSerialNumber = 0,
+ )
assertThat(widgets())
.containsExactly(
- communalItemRankEntry2.copy(rank = 3),
- communalWidgetItemEntry2,
- communalItemRankEntry3.copy(rank = 2),
- communalWidgetItemEntry3,
- communalItemRankEntry1.copy(rank = 1),
+ communalItemRankEntry1.copy(rank = 0),
communalWidgetItemEntry1,
+ newRankEntry,
+ newWidgetEntry,
+ communalItemRankEntry2.copy(rank = 2),
+ communalWidgetItemEntry2,
+ communalItemRankEntry3.copy(rank = 3),
+ communalWidgetItemEntry3,
)
.inOrder()
}
@@ -261,11 +295,11 @@
assertThat(widgets).containsExactlyEntriesIn(expected)
}
- private fun addWidget(metadata: FakeWidgetMetadata, priority: Int? = null) {
+ private fun addWidget(metadata: FakeWidgetMetadata, rank: Int? = null) {
communalWidgetDao.addWidget(
widgetId = metadata.widgetId,
provider = metadata.provider,
- priority = priority ?: metadata.priority,
+ rank = rank ?: metadata.rank,
userSerialNumber = metadata.userSerialNumber,
)
}
@@ -273,7 +307,7 @@
data class FakeWidgetMetadata(
val widgetId: Int,
val provider: ComponentName,
- val priority: Int,
+ val rank: Int,
val userSerialNumber: Int,
)
@@ -282,26 +316,26 @@
FakeWidgetMetadata(
widgetId = 1,
provider = ComponentName("pk_name", "cls_name_1"),
- priority = 1,
+ rank = 0,
userSerialNumber = 0,
)
val widgetInfo2 =
FakeWidgetMetadata(
widgetId = 2,
provider = ComponentName("pk_name", "cls_name_2"),
- priority = 2,
+ rank = 1,
userSerialNumber = 0,
)
val widgetInfo3 =
FakeWidgetMetadata(
widgetId = 3,
provider = ComponentName("pk_name", "cls_name_3"),
- priority = 3,
+ rank = 2,
userSerialNumber = 10,
)
- val communalItemRankEntry1 = CommunalItemRank(uid = 1L, rank = widgetInfo1.priority)
- val communalItemRankEntry2 = CommunalItemRank(uid = 2L, rank = widgetInfo2.priority)
- val communalItemRankEntry3 = CommunalItemRank(uid = 3L, rank = widgetInfo3.priority)
+ val communalItemRankEntry1 = CommunalItemRank(uid = 1L, rank = widgetInfo1.rank)
+ val communalItemRankEntry2 = CommunalItemRank(uid = 2L, rank = widgetInfo2.rank)
+ val communalItemRankEntry3 = CommunalItemRank(uid = 3L, rank = widgetInfo3.rank)
val communalWidgetItemEntry1 =
CommunalWidgetItem(
uid = 1L,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
index c2f035f1..19735e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
@@ -93,7 +93,7 @@
private val tileDataInteractor =
ModesTileDataInteractor(
context,
- ZenModeInteractor(zenModeRepository, mock<NotificationSettingsRepository>()),
+ ZenModeInteractor(context, zenModeRepository, mock<NotificationSettingsRepository>()),
testDispatcher
)
private val mapper =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt
index a5fbfb5..be9fcc2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt
@@ -18,11 +18,13 @@
import android.content.ComponentName
import android.content.Context
+import android.content.res.Resources
import android.os.UserHandle
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.internal.R
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.screenshot.data.model.DisplayContentModel
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.FILES
@@ -57,6 +59,7 @@
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class WorkProfilePolicyTest {
@@ -66,12 +69,17 @@
@JvmField @Rule(order = 2) val mockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock lateinit var mContext: Context
+ @Mock lateinit var mResources: Resources
private val kosmos = Kosmos()
private lateinit var policy: WorkProfilePolicy
@Before
fun setUp() {
+ // Set desktop mode supported
+ whenever(mContext.resources).thenReturn(mResources)
+ whenever(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true)
+
policy = WorkProfilePolicy(kosmos.profileTypeRepository, mContext)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
index 505f799..3f6617b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -197,6 +197,7 @@
() -> sceneInteractor,
() -> mKosmos.getFromGoneTransitionInteractor(),
() -> mKosmos.getFromLockscreenTransitionInteractor(),
+ () -> mKosmos.getFromOccludedTransitionInteractor(),
() -> mKosmos.getSharedNotificationContainerInteractor(),
mTestScope);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index d1b1f46..ed99705 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -98,16 +98,20 @@
// instead of VisualInterruptionDecisionProviderTestBase
// because avalanche code is based on the suppression refactor.
+ private fun getAvalancheSuppressor() : AvalancheSuppressor {
+ return AvalancheSuppressor(
+ avalancheProvider, systemClock, settingsInteractor, packageManager,
+ uiEventLogger, context, notificationManager, logger
+ )
+ }
+
@Test
fun testAvalancheFilter_suppress_hasNotSeenEdu_showEduHun() {
setAllowedEmergencyPkg(false)
whenever(avalancheProvider.timeoutMs).thenReturn(20)
whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))
- val avalancheSuppressor = AvalancheSuppressor(
- avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager
- )
+ val avalancheSuppressor = getAvalancheSuppressor()
avalancheSuppressor.hasSeenEdu = false
withFilter(avalancheSuppressor) {
@@ -128,10 +132,7 @@
whenever(avalancheProvider.timeoutMs).thenReturn(20)
whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))
- val avalancheSuppressor = AvalancheSuppressor(
- avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager
- )
+ val avalancheSuppressor = getAvalancheSuppressor()
avalancheSuppressor.hasSeenEdu = true
withFilter(avalancheSuppressor) {
@@ -151,8 +152,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
ensurePeekState()
assertShouldHeadsUp(
@@ -171,8 +171,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
ensurePeekState()
assertShouldNotHeadsUp(
@@ -191,8 +190,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
ensurePeekState()
assertShouldHeadsUp(
@@ -209,8 +207,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
ensurePeekState()
assertShouldHeadsUp(
@@ -227,8 +224,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
ensurePeekState()
assertShouldHeadsUp(
@@ -245,8 +241,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
ensurePeekState()
assertShouldHeadsUp(
@@ -263,8 +258,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
ensurePeekState()
assertShouldHeadsUp(
@@ -281,8 +275,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
ensurePeekState()
assertShouldHeadsUp(
@@ -300,8 +293,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
ensurePeekState()
assertShouldHeadsUp(
@@ -318,8 +310,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
assertFsiNotSuppressed()
}
@@ -330,8 +321,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
ensurePeekState()
assertShouldHeadsUp(
@@ -359,8 +349,7 @@
setAllowedEmergencyPkg(true)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
- uiEventLogger, context, notificationManager)
+ getAvalancheSuppressor()
) {
ensurePeekState()
assertShouldHeadsUp(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index 9d3d9c1..284efc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -139,6 +139,7 @@
protected val settingsInteractor: NotificationSettingsInteractor = mock()
protected val packageManager: PackageManager = mock()
protected val notificationManager: NotificationManager = mock()
+ protected val logger: VisualInterruptionDecisionLogger = mock()
protected abstract val provider: VisualInterruptionDecisionProvider
private val neverSuppresses = object : NotificationInterruptSuppressor {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index 491919a..30a1214 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
@@ -35,6 +35,9 @@
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder
+import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView
+import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
@@ -74,6 +77,7 @@
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyZeroInteractions
import org.mockito.kotlin.whenever
@SmallTest
@@ -125,8 +129,10 @@
): RichOngoingContentModel? = fakeRonContentModel
}
- private var fakeRonViewHolder: InflatedContentViewHolder? = null
- private val fakeRonViewInflater =
+ private var fakeContractedRonViewHolder: ContentViewInflationResult = NullContentView
+ private var fakeExpandedRonViewHolder: ContentViewInflationResult = NullContentView
+ private var fakeHeadsUpRonViewHolder: ContentViewInflationResult = NullContentView
+ private var fakeRonViewInflater =
spy(
object : RichOngoingNotificationViewInflater {
override fun inflateView(
@@ -134,8 +140,20 @@
existingView: View?,
entry: NotificationEntry,
systemUiContext: Context,
- parentView: ViewGroup
- ): InflatedContentViewHolder? = fakeRonViewHolder
+ parentView: ViewGroup,
+ viewType: RichOngoingNotificationViewType
+ ): ContentViewInflationResult =
+ when (viewType) {
+ RichOngoingNotificationViewType.Contracted -> fakeContractedRonViewHolder
+ RichOngoingNotificationViewType.Expanded -> fakeExpandedRonViewHolder
+ RichOngoingNotificationViewType.HeadsUp -> fakeHeadsUpRonViewHolder
+ }
+
+ override fun canKeepView(
+ contentModel: RichOngoingContentModel,
+ existingView: View?,
+ viewType: RichOngoingNotificationViewType
+ ): Boolean = false
}
)
@@ -149,6 +167,7 @@
.setContentText("Text")
.setStyle(Notification.BigTextStyle().bigText("big text"))
testHelper = NotificationTestHelper(mContext, mDependency)
+ testHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL)
row = spy(testHelper.createRow(builder.build()))
notificationInflater =
NotificationRowContentBinderImpl(
@@ -388,15 +407,62 @@
@Test
fun testRonModelRequiredForRonView() {
fakeRonContentModel = null
- val ronView = View(context)
- fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mock())
+ val contractedRonView = View(context)
+ val expandedRonView = View(context)
+ val headsUpRonView = View(context)
+ fakeContractedRonViewHolder =
+ InflatedContentViewHolder(view = contractedRonView, binder = mock())
+ fakeExpandedRonViewHolder =
+ InflatedContentViewHolder(view = expandedRonView, binder = mock())
+ fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = headsUpRonView, binder = mock())
+
// WHEN inflater inflates
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
- verify(fakeRonViewInflater, never()).inflateView(any(), any(), any(), any(), any())
+ val contentToInflate =
+ FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP
+ inflateAndWait(notificationInflater, contentToInflate, row)
+ verifyZeroInteractions(fakeRonViewInflater)
}
@Test
- fun testRonModelTriggersInflationOfRonView() {
+ fun testRonModelCleansUpRemoteViews() {
+ val ronView = View(context)
+
+ val entry = row.entry
+
+ fakeRonContentModel = mock<TimerContentModel>()
+ fakeContractedRonViewHolder =
+ InflatedContentViewHolder(view = ronView, binder = mock<DeferredContentViewBinder>())
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+
+ // VERIFY
+ verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_CONTRACTED))
+ verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_EXPANDED))
+ verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_HEADS_UP))
+ }
+
+ @Test
+ fun testRonModelCleansUpSmartReplies() {
+ val ronView = View(context)
+
+ val privateLayout = spy(row.privateLayout)
+
+ row.privateLayout = privateLayout
+
+ fakeRonContentModel = mock<TimerContentModel>()
+ fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mock())
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+
+ // VERIFY
+ verify(privateLayout).setExpandedInflatedSmartReplies(eq(null))
+ verify(privateLayout).setHeadsUpInflatedSmartReplies(eq(null))
+ }
+
+ @Test
+ fun testRonModelTriggersInflationOfContractedRonView() {
val mockRonModel = mock<TimerContentModel>()
val ronView = View(context)
val mockBinder = mock<DeferredContentViewBinder>()
@@ -405,18 +471,229 @@
val privateLayout = row.privateLayout
fakeRonContentModel = mockRonModel
- fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+ fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+
// WHEN inflater inflates
inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+
// VERIFY that the inflater is invoked
verify(fakeRonViewInflater)
- .inflateView(eq(mockRonModel), any(), eq(entry), any(), eq(privateLayout))
+ .inflateView(
+ eq(mockRonModel),
+ any(),
+ eq(entry),
+ any(),
+ eq(privateLayout),
+ eq(RichOngoingNotificationViewType.Contracted)
+ )
assertThat(row.privateLayout.contractedChild).isSameInstanceAs(ronView)
verify(mockBinder).setupContentViewBinder()
}
@Test
- fun ronViewAppliesElementsInOrder() {
+ fun testRonModelTriggersInflationOfExpandedRonView() {
+ val mockRonModel = mock<TimerContentModel>()
+ val ronView = View(context)
+ val mockBinder = mock<DeferredContentViewBinder>()
+
+ val entry = row.entry
+ val privateLayout = row.privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeExpandedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row)
+
+ // VERIFY that the inflater is invoked
+ verify(fakeRonViewInflater)
+ .inflateView(
+ eq(mockRonModel),
+ any(),
+ eq(entry),
+ any(),
+ eq(privateLayout),
+ eq(RichOngoingNotificationViewType.Expanded)
+ )
+ assertThat(row.privateLayout.expandedChild).isSameInstanceAs(ronView)
+ verify(mockBinder).setupContentViewBinder()
+ }
+
+ @Test
+ fun testRonModelTriggersInflationOfHeadsUpRonView() {
+ val mockRonModel = mock<TimerContentModel>()
+ val ronView = View(context)
+ val mockBinder = mock<DeferredContentViewBinder>()
+
+ val entry = row.entry
+ val privateLayout = row.privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row)
+
+ // VERIFY that the inflater is invoked
+ verify(fakeRonViewInflater)
+ .inflateView(
+ eq(mockRonModel),
+ any(),
+ eq(entry),
+ any(),
+ eq(privateLayout),
+ eq(RichOngoingNotificationViewType.HeadsUp)
+ )
+ assertThat(row.privateLayout.headsUpChild).isSameInstanceAs(ronView)
+ verify(mockBinder).setupContentViewBinder()
+ }
+
+ @Test
+ fun keepExistingViewForContractedRonNotChangingContractedChild() {
+ val oldHandle = mock<DisposableHandle>()
+ val mockRonModel = mock<TimerContentModel>()
+
+ row.privateLayout.mContractedBinderHandle = oldHandle
+ val entry = spy(row.entry)
+ row.entry = entry
+ val privateLayout = spy(row.privateLayout)
+ row.privateLayout = privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeContractedRonViewHolder = KeepExistingView
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+
+ // THEN do not dispose old contracted binder handle and change contracted child
+ verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
+ verifyZeroInteractions(oldHandle)
+ verify(privateLayout, never()).setContractedChild(any())
+ }
+
+ @Test
+ fun keepExistingViewForExpandedRonNotChangingExpandedChild() {
+ val oldHandle = mock<DisposableHandle>()
+ val mockRonModel = mock<TimerContentModel>()
+
+ row.privateLayout.mExpandedBinderHandle = oldHandle
+ val entry = spy(row.entry)
+ row.entry = entry
+ val privateLayout = spy(row.privateLayout)
+ row.privateLayout = privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeExpandedRonViewHolder = KeepExistingView
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row)
+
+ // THEN do not dispose old expanded binder handle and change expanded child
+ verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
+ verifyZeroInteractions(oldHandle)
+ verify(privateLayout, never()).setExpandedChild(any())
+ }
+
+ @Test
+ fun keepExistingViewForHeadsUpRonNotChangingHeadsUpChild() {
+ val oldHandle = mock<DisposableHandle>()
+ val mockRonModel = mock<TimerContentModel>()
+
+ row.privateLayout.mHeadsUpBinderHandle = oldHandle
+ val entry = spy(row.entry)
+ row.entry = entry
+ val privateLayout = spy(row.privateLayout)
+ row.privateLayout = privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeHeadsUpRonViewHolder = KeepExistingView
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row)
+
+ // THEN - do not dispose old heads up binder handle and change heads up child
+ verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
+ verifyZeroInteractions(oldHandle)
+ verify(privateLayout, never()).setHeadsUpChild(any())
+ }
+
+ @Test
+ fun nullContentViewForContractedRonAppliesElementsInOrder() {
+ val oldHandle = mock<DisposableHandle>()
+ val mockRonModel = mock<TimerContentModel>()
+
+ row.privateLayout.mContractedBinderHandle = oldHandle
+ val entry = spy(row.entry)
+ row.entry = entry
+ val privateLayout = spy(row.privateLayout)
+ row.privateLayout = privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeContractedRonViewHolder = NullContentView
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+
+ // Validate that these 4 steps happen in this precise order
+ inOrder(oldHandle, entry, privateLayout, cache) {
+ verify(oldHandle).dispose()
+ verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
+ verify(privateLayout).setContractedChild(eq(null))
+ }
+ }
+
+ @Test
+ fun nullContentViewForExpandedRonAppliesElementsInOrder() {
+ val oldHandle = mock<DisposableHandle>()
+ val mockRonModel = mock<TimerContentModel>()
+
+ row.privateLayout.mExpandedBinderHandle = oldHandle
+ val entry = spy(row.entry)
+ row.entry = entry
+ val privateLayout = spy(row.privateLayout)
+ row.privateLayout = privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeExpandedRonViewHolder = NullContentView
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row)
+
+ // Validate that these 4 steps happen in this precise order
+ inOrder(oldHandle, entry, privateLayout, cache) {
+ verify(oldHandle).dispose()
+ verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
+ verify(privateLayout).setExpandedChild(eq(null))
+ }
+ }
+
+ @Test
+ fun nullContentViewForHeadsUpRonAppliesElementsInOrder() {
+ val oldHandle = mock<DisposableHandle>()
+ val mockRonModel = mock<TimerContentModel>()
+
+ row.privateLayout.mHeadsUpBinderHandle = oldHandle
+ val entry = spy(row.entry)
+ row.entry = entry
+ val privateLayout = spy(row.privateLayout)
+ row.privateLayout = privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeHeadsUpRonViewHolder = NullContentView
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row)
+
+ // Validate that these 4 steps happen in this precise order
+ inOrder(oldHandle, entry, privateLayout, cache) {
+ verify(oldHandle).dispose()
+ verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
+ verify(privateLayout).setHeadsUpChild(eq(null))
+ }
+ }
+
+ @Test
+ fun contractedRonViewAppliesElementsInOrder() {
val oldHandle = mock<DisposableHandle>()
val mockRonModel = mock<TimerContentModel>()
val ronView = View(context)
@@ -429,7 +706,8 @@
row.privateLayout = privateLayout
fakeRonContentModel = mockRonModel
- fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+ fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+
// WHEN inflater inflates
inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
@@ -443,16 +721,89 @@
}
@Test
- fun testRonNotReinflating() {
- val handle0 = mock<DisposableHandle>()
- val handle1 = mock<DisposableHandle>()
+ fun expandedRonViewAppliesElementsInOrder() {
+ val oldHandle = mock<DisposableHandle>()
+ val mockRonModel = mock<TimerContentModel>()
val ronView = View(context)
+ val mockBinder = mock<DeferredContentViewBinder>()
+
+ row.privateLayout.mExpandedBinderHandle = oldHandle
+ val entry = spy(row.entry)
+ row.entry = entry
+ val privateLayout = spy(row.privateLayout)
+ row.privateLayout = privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeExpandedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row)
+
+ // Validate that these 4 steps happen in this precise order
+ inOrder(oldHandle, entry, privateLayout, mockBinder) {
+ verify(oldHandle).dispose()
+ verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
+ verify(privateLayout).setExpandedChild(eq(ronView))
+ verify(mockBinder).setupContentViewBinder()
+ }
+ }
+
+ @Test
+ fun headsUpRonViewAppliesElementsInOrder() {
+ val oldHandle = mock<DisposableHandle>()
+ val mockRonModel = mock<TimerContentModel>()
+ val ronView = View(context)
+ val mockBinder = mock<DeferredContentViewBinder>()
+
+ row.privateLayout.mHeadsUpBinderHandle = oldHandle
+ val entry = spy(row.entry)
+ row.entry = entry
+ val privateLayout = spy(row.privateLayout)
+ row.privateLayout = privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row)
+
+ // Validate that these 4 steps happen in this precise order
+ inOrder(oldHandle, entry, privateLayout, mockBinder) {
+ verify(oldHandle).dispose()
+ verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
+ verify(privateLayout).setHeadsUpChild(eq(ronView))
+ verify(mockBinder).setupContentViewBinder()
+ }
+ }
+
+ @Test
+ fun testRonNotReinflating() {
+ val oldContractedBinderHandle = mock<DisposableHandle>()
+ val oldExpandedBinderHandle = mock<DisposableHandle>()
+ val oldHeadsUpBinderHandle = mock<DisposableHandle>()
+
+ val contractedBinderHandle = mock<DisposableHandle>()
+ val expandedBinderHandle = mock<DisposableHandle>()
+ val headsUpBinderHandle = mock<DisposableHandle>()
+
+ val contractedRonView = View(context)
+ val expandedRonView = View(context)
+ val headsUpRonView = View(context)
+
val mockRonModel1 = mock<TimerContentModel>()
val mockRonModel2 = mock<TimerContentModel>()
- val mockBinder1 = mock<DeferredContentViewBinder>()
- doReturn(handle1).whenever(mockBinder1).setupContentViewBinder()
- row.privateLayout.mContractedBinderHandle = handle0
+ val mockContractedViewBinder = mock<DeferredContentViewBinder>()
+ val mockExpandedViewBinder = mock<DeferredContentViewBinder>()
+ val mockHeadsUpViewBinder = mock<DeferredContentViewBinder>()
+
+ doReturn(contractedBinderHandle).whenever(mockContractedViewBinder).setupContentViewBinder()
+ doReturn(expandedBinderHandle).whenever(mockExpandedViewBinder).setupContentViewBinder()
+ doReturn(headsUpBinderHandle).whenever(mockHeadsUpViewBinder).setupContentViewBinder()
+
+ row.privateLayout.mContractedBinderHandle = oldContractedBinderHandle
+ row.privateLayout.mExpandedBinderHandle = oldExpandedBinderHandle
+ row.privateLayout.mHeadsUpBinderHandle = oldHeadsUpBinderHandle
val entry = spy(row.entry)
row.entry = entry
val privateLayout = spy(row.privateLayout)
@@ -460,31 +811,87 @@
// WHEN inflater inflates both a model and a view
fakeRonContentModel = mockRonModel1
- fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder1)
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+ fakeContractedRonViewHolder =
+ InflatedContentViewHolder(view = contractedRonView, binder = mockContractedViewBinder)
+ fakeExpandedRonViewHolder =
+ InflatedContentViewHolder(view = expandedRonView, binder = mockExpandedViewBinder)
+ fakeHeadsUpRonViewHolder =
+ InflatedContentViewHolder(view = headsUpRonView, binder = mockHeadsUpViewBinder)
+
+ val contentToInflate =
+ FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP
+ inflateAndWait(notificationInflater, contentToInflate, row)
// Validate that these 4 steps happen in this precise order
- inOrder(handle0, entry, privateLayout, mockBinder1, handle1) {
- verify(handle0).dispose()
+ inOrder(
+ oldContractedBinderHandle,
+ oldExpandedBinderHandle,
+ oldHeadsUpBinderHandle,
+ entry,
+ privateLayout,
+ mockContractedViewBinder,
+ mockExpandedViewBinder,
+ mockHeadsUpViewBinder,
+ contractedBinderHandle,
+ expandedBinderHandle,
+ headsUpBinderHandle
+ ) {
+ verify(oldContractedBinderHandle).dispose()
+ verify(oldExpandedBinderHandle).dispose()
+ verify(oldHeadsUpBinderHandle).dispose()
+
verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel1 })
- verify(privateLayout).setContractedChild(eq(ronView))
- verify(mockBinder1).setupContentViewBinder()
- verify(handle1, never()).dispose()
+
+ verify(privateLayout).setContractedChild(eq(contractedRonView))
+ verify(mockContractedViewBinder).setupContentViewBinder()
+
+ verify(privateLayout).setExpandedChild(eq(expandedRonView))
+ verify(mockExpandedViewBinder).setupContentViewBinder()
+
+ verify(privateLayout).setHeadsUpChild(eq(headsUpRonView))
+ verify(mockHeadsUpViewBinder).setupContentViewBinder()
+
+ verify(contractedBinderHandle, never()).dispose()
+ verify(expandedBinderHandle, never()).dispose()
+ verify(headsUpBinderHandle, never()).dispose()
}
- clearInvocations(handle0, entry, privateLayout, mockBinder1, handle1)
+ clearInvocations(
+ oldContractedBinderHandle,
+ oldExpandedBinderHandle,
+ oldHeadsUpBinderHandle,
+ entry,
+ privateLayout,
+ mockContractedViewBinder,
+ mockExpandedViewBinder,
+ mockHeadsUpViewBinder,
+ contractedBinderHandle,
+ expandedBinderHandle,
+ headsUpBinderHandle
+ )
// THEN when the inflater inflates just a model
fakeRonContentModel = mockRonModel2
- fakeRonViewHolder = null
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+ fakeContractedRonViewHolder = KeepExistingView
+ fakeExpandedRonViewHolder = KeepExistingView
+ fakeHeadsUpRonViewHolder = KeepExistingView
+
+ inflateAndWait(notificationInflater, contentToInflate, row)
// Validate that for reinflation, the only thing we do us update the model
- verify(handle1, never()).dispose()
+ verify(contractedBinderHandle, never()).dispose()
+ verify(expandedBinderHandle, never()).dispose()
+ verify(headsUpBinderHandle, never()).dispose()
verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel2 })
verify(privateLayout, never()).setContractedChild(any())
- verify(mockBinder1, never()).setupContentViewBinder()
- verify(handle1, never()).dispose()
+ verify(privateLayout, never()).setExpandedChild(any())
+ verify(privateLayout, never()).setHeadsUpChild(any())
+ verify(mockContractedViewBinder, never()).setupContentViewBinder()
+ verify(mockExpandedViewBinder, never()).setupContentViewBinder()
+ verify(mockHeadsUpViewBinder, never()).setupContentViewBinder()
+ verify(contractedBinderHandle, never()).dispose()
+ verify(expandedBinderHandle, never()).dispose()
+ verify(headsUpBinderHandle, never()).dispose()
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index ad029d7..7e79019 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -296,6 +296,7 @@
collapsedHeight = 100,
intrinsicHeight = intrinsicHunHeight,
)
+ ambientState.qsExpansionFraction = 1.0f
whenever(notificationRow.isAboveShelf).thenReturn(true)
// When
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index c3cc33f..bf31f1e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -45,6 +45,7 @@
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.mock
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class DeviceBasedSatelliteViewModelTest : SysuiTestCase() {
@@ -88,7 +89,7 @@
}
@Test
- fun icon_nullWhenShouldNotShow_satelliteNotAllowed() =
+ fun icon_null_satelliteNotAllowed() =
testScope.runTest {
val latest by collectLastValue(underTest.icon)
@@ -108,7 +109,30 @@
}
@Test
- fun icon_nullWhenShouldNotShow_notAllOos() =
+ fun icon_null_connectedAndNotAllowed() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.icon)
+
+ // GIVEN satellite is not allowed
+ repo.isSatelliteAllowedForCurrentLocation.value = false
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN satellite state is Connected. (this should not ever occur, but still)
+ repo.connectionState.value = SatelliteConnectionState.Connected
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // THEN icon is null despite the connected state
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ fun icon_null_notAllOos() =
testScope.runTest {
val latest by collectLastValue(underTest.icon)
@@ -127,9 +151,28 @@
assertThat(latest).isNull()
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
- fun icon_nullWhenShouldNotShow_isEmergencyOnly() =
+ fun icon_null_allOosAndNotAllowed() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.icon)
+
+ // GIVEN satellite is allowed
+ repo.isSatelliteAllowedForCurrentLocation.value = false
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // THEN icon is null because it is not allowed
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ fun icon_null_isEmergencyOnly() =
testScope.runTest {
val latest by collectLastValue(underTest.icon)
@@ -158,7 +201,7 @@
}
@Test
- fun icon_nullWhenShouldNotShow_apmIsEnabled() =
+ fun icon_null_apmIsEnabled() =
testScope.runTest {
val latest by collectLastValue(underTest.icon)
@@ -177,9 +220,8 @@
assertThat(latest).isNull()
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
- fun icon_satelliteIsOn() =
+ fun icon_notNull_satelliteAllowedAndAllOos() =
testScope.runTest {
val latest by collectLastValue(underTest.icon)
@@ -201,7 +243,6 @@
assertThat(latest).isInstanceOf(Icon::class.java)
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun icon_hysteresisWhenEnablingIcon() =
testScope.runTest {
@@ -234,9 +275,56 @@
assertThat(latest).isNull()
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
- fun icon_deviceIsProvisioned() =
+ fun icon_ignoresHysteresis_whenConnected() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.icon)
+
+ // GIVEN satellite is allowed
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // GIVEN satellite reports that it is Connected
+ repo.connectionState.value = SatelliteConnectionState.Connected
+
+ // THEN icon is non null because we are connected, despite the normal OOS icon waiting
+ // 10 seconds for hysteresis
+ assertThat(latest).isInstanceOf(Icon::class.java)
+ }
+
+ @Test
+ fun icon_ignoresHysteresis_whenOn() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.icon)
+
+ // GIVEN satellite is allowed
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // GIVEN satellite reports that it is Connected
+ repo.connectionState.value = SatelliteConnectionState.On
+
+ // THEN icon is non null because the connection state is On, despite the normal OOS icon
+ // waiting 10 seconds for hysteresis
+ assertThat(latest).isInstanceOf(Icon::class.java)
+ }
+
+ @Test
+ fun icon_satelliteIsProvisioned() =
testScope.runTest {
val latest by collectLastValue(underTest.icon)
@@ -267,7 +355,6 @@
assertThat(latest).isInstanceOf(Icon::class.java)
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun icon_wifiIsActive() =
testScope.runTest {
@@ -324,7 +411,28 @@
}
@Test
- fun carrierText_nullWhenShouldNotShow_notAllOos() =
+ fun carrierText_null_notAllOos() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // GIVEN satellite is allowed + off
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ repo.connectionState.value = SatelliteConnectionState.Off
+
+ // GIVEN all icons are not OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = true
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // THEN carrier text is null because we have service
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ fun carrierText_notNull_notAllOos_butConnected() =
testScope.runTest {
val latest by collectLastValue(underTest.carrierText)
@@ -340,39 +448,9 @@
// GIVEN apm is disabled
airplaneModeRepository.setIsAirplaneMode(false)
- // THEN carrier text is null because we have service
- assertThat(latest).isNull()
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- @Test
- fun carrierText_nullWhenShouldNotShow_isEmergencyOnly() =
- testScope.runTest {
- val latest by collectLastValue(underTest.carrierText)
-
- // GIVEN satellite is allowed + connected
- repo.isSatelliteAllowedForCurrentLocation.value = true
- repo.connectionState.value = SatelliteConnectionState.Connected
-
- // GIVEN all icons are OOS
- val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
- i1.isInService.value = false
- i1.isEmergencyOnly.value = false
-
- // GIVEN apm is disabled
- airplaneModeRepository.setIsAirplaneMode(false)
-
- // Wait for delay to be completed
- advanceTimeBy(10.seconds)
-
- // THEN carrier text is set because we don't have service
+ // THEN carrier text is not null, because it is connected
+ // This case should never happen, but let's test it anyway
assertThat(latest).isNotNull()
-
- // GIVEN the connection is emergency only
- i1.isEmergencyOnly.value = true
-
- // THEN carrier text is null because we have emergency connection
- assertThat(latest).isNull()
}
@Test
@@ -396,7 +474,6 @@
assertThat(latest).isNull()
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun carrierText_satelliteIsOn() =
testScope.runTest {
@@ -421,9 +498,8 @@
assertThat(latest).isNotNull()
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
- fun carrierText_hysteresisWhenEnablingText() =
+ fun carrierText_noHysteresisWhenEnablingText_connected() =
testScope.runTest {
val latest by collectLastValue(underTest.carrierText)
@@ -439,23 +515,10 @@
// GIVEN apm is disabled
airplaneModeRepository.setIsAirplaneMode(false)
- // THEN carrier text is null because of the hysteresis
- assertThat(latest).isNull()
-
- // Wait for delay to be completed
- advanceTimeBy(10.seconds)
-
- // THEN carrier text is set after the delay
+ // THEN carrier text is not null because we skip hysteresis when connected
assertThat(latest).isNotNull()
-
- // GIVEN apm is enabled
- airplaneModeRepository.setIsAirplaneMode(true)
-
- // THEN carrier text is null immediately
- assertThat(latest).isNull()
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun carrierText_deviceIsProvisioned() =
testScope.runTest {
@@ -489,7 +552,6 @@
assertThat(latest).isNotNull()
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun carrierText_wifiIsActive() =
testScope.runTest {
@@ -526,9 +588,8 @@
assertThat(latest).isNotNull()
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
- fun carrierText_connectionStateUnknown_null() =
+ fun carrierText_connectionStateUnknown_usesEmergencyOnlyText() =
testScope.runTest {
val latest by collectLastValue(underTest.carrierText)
@@ -544,12 +605,12 @@
// Wait for delay to be completed
advanceTimeBy(10.seconds)
- assertThat(latest).isNull()
+ assertThat(latest)
+ .isEqualTo(context.getString(R.string.satellite_emergency_only_carrier_text))
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
- fun carrierText_connectionStateOff_null() =
+ fun carrierText_connectionStateOff_usesEmergencyOnlyText() =
testScope.runTest {
val latest by collectLastValue(underTest.carrierText)
@@ -565,10 +626,10 @@
// Wait for delay to be completed
advanceTimeBy(10.seconds)
- assertThat(latest).isNull()
+ assertThat(latest)
+ .isEqualTo(context.getString(R.string.satellite_emergency_only_carrier_text))
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun carrierText_connectionStateOn_notConnectedString() =
testScope.runTest {
@@ -590,7 +651,6 @@
.isEqualTo(context.getString(R.string.satellite_connected_carrier_text))
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun carrierText_connectionStateConnected_connectedString() =
testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
index 84cd79d..25ceea9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
@@ -26,8 +26,8 @@
val dialog: Dialog = mock()
whenever(
createDialog(
- /* activity = */ nullable(),
- /* activityStarter = */ nullable(),
+ /* activity = */ any(),
+ /* activityStarter = */ any(),
/* isMultipleAdminsEnabled = */ any(),
/* successCallback = */ nullable(),
/* cancelCallback = */ nullable()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index c00454f..5d7e7c7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -30,7 +30,7 @@
override fun addWidget(
provider: ComponentName,
user: UserHandle,
- priority: Int,
+ rank: Int?,
configurator: WidgetConfigurator?
) {
coroutineScope.launch {
@@ -38,7 +38,7 @@
val providerInfo = AppWidgetProviderInfo().apply { this.provider = provider }
val configured = configurator?.configureWidget(id) ?: true
if (configured) {
- onConfigured(id, providerInfo, priority)
+ onConfigured(id, providerInfo, rank ?: -1)
}
}
}
@@ -46,14 +46,14 @@
fun addWidget(
appWidgetId: Int,
componentName: String = "pkg/cls",
- priority: Int = 0,
+ rank: Int = 0,
category: Int = AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD,
userId: Int = 0,
) {
fakeDatabase[appWidgetId] =
CommunalWidgetContentModel.Available(
appWidgetId = appWidgetId,
- priority = priority,
+ rank = rank,
providerInfo =
AppWidgetProviderInfo().apply {
provider = ComponentName.unflattenFromString(componentName)!!
@@ -73,14 +73,14 @@
fun addPendingWidget(
appWidgetId: Int,
componentName: String = "pkg/cls",
- priority: Int = 0,
+ rank: Int = 0,
icon: Bitmap? = null,
userId: Int = 0,
) {
fakeDatabase[appWidgetId] =
CommunalWidgetContentModel.Pending(
appWidgetId = appWidgetId,
- priority = priority,
+ rank = rank,
componentName = ComponentName.unflattenFromString(componentName)!!,
icon = icon,
user = UserHandle(userId),
@@ -97,8 +97,8 @@
override fun abortRestoreWidgets() {}
- private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) {
+ private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, rank: Int) {
_communalWidgets.value +=
- listOf(CommunalWidgetContentModel.Available(id, providerInfo, priority))
+ listOf(CommunalWidgetContentModel.Available(id, providerInfo, rank))
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
index cdfb297..fb4e2fb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
@@ -17,6 +17,7 @@
package com.android.systemui.education.data.repository
import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.education.data.model.EduDeviceConnectionTime
import com.android.systemui.education.data.model.GestureEduModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -27,22 +28,34 @@
private val userGestureMap = mutableMapOf<Int, GestureEduModel>()
private val _gestureEduModels = MutableStateFlow(GestureEduModel(userId = 0))
private val gestureEduModelsFlow = _gestureEduModels.asStateFlow()
+
+ private val userEduDeviceConnectionTimeMap = mutableMapOf<Int, EduDeviceConnectionTime>()
+ private val _eduDeviceConnectionTime = MutableStateFlow(EduDeviceConnectionTime())
+ private val eduDeviceConnectionTime = _eduDeviceConnectionTime.asStateFlow()
+
private var currentUser: Int = 0
override fun setUser(userId: Int) {
if (!userGestureMap.contains(userId)) {
userGestureMap[userId] = GestureEduModel(userId = userId)
+ userEduDeviceConnectionTimeMap[userId] = EduDeviceConnectionTime()
}
// save data of current user to the map
userGestureMap[currentUser] = _gestureEduModels.value
+ userEduDeviceConnectionTimeMap[currentUser] = _eduDeviceConnectionTime.value
// switch to data of new user
_gestureEduModels.value = userGestureMap[userId]!!
+ _eduDeviceConnectionTime.value = userEduDeviceConnectionTimeMap[userId]!!
}
override fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> {
return gestureEduModelsFlow
}
+ override fun readEduDeviceConnectionTime(): Flow<EduDeviceConnectionTime> {
+ return eduDeviceConnectionTime
+ }
+
override suspend fun updateGestureEduModel(
gestureType: GestureType,
transform: (GestureEduModel) -> GestureEduModel
@@ -50,4 +63,11 @@
val currentModel = _gestureEduModels.value
_gestureEduModels.value = transform(currentModel)
}
+
+ override suspend fun updateEduDeviceConnectionTime(
+ transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime
+ ) {
+ val currentModel = _eduDeviceConnectionTime.value
+ _eduDeviceConnectionTime.value = transform(currentModel)
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
index 5088677..88ab170 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
@@ -17,14 +17,26 @@
package com.android.systemui.education.domain.interactor
import com.android.systemui.education.data.repository.fakeEduClock
+import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository
+import com.android.systemui.keyboard.data.repository.keyboardRepository
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
+import com.android.systemui.touchpad.data.repository.touchpadRepository
+import com.android.systemui.user.data.repository.userRepository
var Kosmos.keyboardTouchpadEduInteractor by
Kosmos.Fixture {
KeyboardTouchpadEduInteractor(
backgroundScope = testScope.backgroundScope,
contextualEducationInteractor = contextualEducationInteractor,
+ userInputDeviceRepository =
+ UserInputDeviceRepository(
+ testDispatcher,
+ keyboardRepository,
+ touchpadRepository,
+ userRepository
+ ),
clock = fakeEduClock
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
index a95609e..f5232ce 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
@@ -54,6 +54,7 @@
sceneInteractor: SceneInteractor = mock(),
fromGoneTransitionInteractor: FromGoneTransitionInteractor = mock(),
fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor = mock(),
+ fromOccludedTransitionInteractor: FromOccludedTransitionInteractor = mock(),
sharedNotificationContainerInteractor: SharedNotificationContainerInteractor? = null,
powerInteractor: PowerInteractor = PowerInteractorFactory.create().powerInteractor,
testScope: CoroutineScope = TestScope(),
@@ -100,6 +101,7 @@
sceneInteractorProvider = { sceneInteractor },
fromGoneTransitionInteractor = { fromGoneTransitionInteractor },
fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor },
+ fromOccludedTransitionInteractor = { fromOccludedTransitionInteractor },
sharedNotificationContainerInteractor = { sncInteractor },
applicationScope = testScope,
),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt
index 5ab56e9..e85114d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt
@@ -38,6 +38,7 @@
sceneInteractorProvider = { sceneInteractor },
fromGoneTransitionInteractor = { fromGoneTransitionInteractor },
fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor },
+ fromOccludedTransitionInteractor = { fromOccludedTransitionInteractor },
sharedNotificationContainerInteractor = { sharedNotificationContainerInteractor },
applicationScope = testScope.backgroundScope,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
index 19b32bc..f47b2df 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.biometrics.authController
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
import com.android.systemui.kosmos.Kosmos
@@ -34,5 +35,6 @@
shadeInteractor = shadeInteractor,
unfoldTransitionInteractor = unfoldTransitionInteractor,
occlusionInteractor = sceneContainerOcclusionInteractor,
+ deviceEntryInteractor = deviceEntryInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 9fe66eb..953363d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -42,6 +42,7 @@
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.fromGoneTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.fromOccludedTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
@@ -128,6 +129,7 @@
val deviceProvisioningInteractor by lazy { kosmos.deviceProvisioningInteractor }
val fakeDeviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
val fromLockscreenTransitionInteractor by lazy { kosmos.fromLockscreenTransitionInteractor }
+ val fromOccludedTransitionInteractor by lazy { kosmos.fromOccludedTransitionInteractor }
val fromPrimaryBouncerTransitionInteractor by lazy {
kosmos.fromPrimaryBouncerTransitionInteractor
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
index aef0828..66be7e7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.policy.domain.interactor
+import android.content.testableContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.shared.notifications.data.repository.notificationSettingsRepository
@@ -23,6 +24,7 @@
val Kosmos.zenModeInteractor by Fixture {
ZenModeInteractor(
+ context = testableContext,
zenModeRepository = zenModeRepository,
notificationSettingsRepository = notificationSettingsRepository,
)
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index fbf27fa..691d06e 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -12,9 +12,6 @@
{
"name": "RavenwoodBivalentTest_device"
},
- {
- "name": "RavenwoodResApkTest"
- },
// The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING
{
"name": "SystemUIGoogleTests",
@@ -59,6 +56,10 @@
"host": true
},
{
+ "name": "RavenwoodResApkTest",
+ "host": true
+ },
+ {
"name": "RavenwoodBivalentTest",
"host": true
}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
deleted file mode 100644
index 5a3589d..0000000
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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.platform.test.ravenwood.nativesubstitution;
-
-import com.android.ravenwood.common.JvmWorkaround;
-
-import java.io.FileDescriptor;
-
-public class ParcelFileDescriptor_host {
- public static void setFdInt(FileDescriptor fd, int fdInt) {
- JvmWorkaround.getInstance().setFdInt(fd, fdInt);
- }
-
- public static int getFdInt(FileDescriptor fd) {
- return JvmWorkaround.getInstance().getFdInt(fd);
- }
-}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
index 7371d0a..a5c0b54 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
@@ -15,8 +15,8 @@
*/
package android.system;
+import com.android.ravenwood.RavenwoodRuntimeNative;
import com.android.ravenwood.common.JvmWorkaround;
-import com.android.ravenwood.common.RavenwoodRuntimeNative;
import java.io.FileDescriptor;
import java.io.FileInputStream;
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java
new file mode 100644
index 0000000..96aed4b
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java
@@ -0,0 +1,49 @@
+/*
+ * 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.ravenwood;
+
+import com.android.ravenwood.common.JvmWorkaround;
+
+import java.io.FileDescriptor;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Class to host APIs that exist in libcore, but not in standard JRE.
+ */
+public class RavenwoodJdkPatch {
+ /**
+ * Implements FileDescriptor.getInt$()
+ */
+ public static int getInt$(FileDescriptor fd) {
+ return JvmWorkaround.getInstance().getFdInt(fd);
+ }
+
+ /**
+ * Implements FileDescriptor.setInt$(int)
+ */
+ public static void setInt$(FileDescriptor fd, int rawFd) {
+ JvmWorkaround.getInstance().setFdInt(fd, rawFd);
+ }
+
+ /**
+ * Implements LinkedHashMap.eldest()
+ */
+ public static <K, V> Map.Entry<K, V> eldest(LinkedHashMap<K, V> map) {
+ final var it = map.entrySet().iterator();
+ return it.hasNext() ? it.next() : null;
+ }
+}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
similarity index 95%
rename from ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
rename to ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
index beba833..0d8408c 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
@@ -13,11 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.ravenwood.common;
+package com.android.ravenwood;
import android.system.ErrnoException;
import android.system.StructStat;
+import com.android.ravenwood.common.JvmWorkaround;
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
import java.io.FileDescriptor;
/**
diff --git a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
index ed5a587..ba89f71 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
@@ -34,11 +34,11 @@
}
public boolean is64Bit() {
- return true;
+ return "amd64".equals(System.getProperty("os.arch"));
}
public static boolean is64BitAbi(String abi) {
- return true;
+ return abi.contains("64");
}
public Object newUnpaddedArray(Class<?> componentType, int minLength) {
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java
index 65c285e..2bd1ae8 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java
@@ -16,7 +16,13 @@
package libcore.io;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.ravenwood.common.JvmWorkaround;
+
import java.io.File;
+import java.io.FileDescriptor;
import java.io.IOException;
import java.net.Socket;
@@ -47,6 +53,13 @@
}
}
+ public static void closeQuietly(FileDescriptor fd) {
+ try {
+ Os.close(fd);
+ } catch (ErrnoException ignored) {
+ }
+ }
+
public static void deleteContents(File dir) throws IOException {
File[] files = dir.listFiles();
if (files != null) {
@@ -58,4 +71,17 @@
}
}
}
+
+ /**
+ * FD owners currently unsupported under Ravenwood; ignored
+ */
+ public static void setFdOwner(FileDescriptor fd, Object owner) {
+ }
+
+ /**
+ * FD owners currently unsupported under Ravenwood; return FD directly
+ */
+ public static int acquireRawFd(FileDescriptor fd) {
+ return JvmWorkaround.getInstance().getFdInt(fd);
+ }
}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
index 14b5a4f..4e7dc5d 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
@@ -15,7 +15,7 @@
*/
package libcore.util;
-import com.android.ravenwood.common.RavenwoodRuntimeNative;
+import com.android.ravenwood.RavenwoodRuntimeNative;
import java.lang.ref.Cleaner;
import java.lang.ref.Reference;
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
index c804928..f5cb019f 100644
--- a/ravenwood/runtime-jni/ravenwood_runtime.cpp
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -245,7 +245,7 @@
g_StructStat = findClass(env, "android/system/StructStat");
g_StructTimespecClass = findClass(env, "android/system/StructTimespec");
- jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/common/RavenwoodRuntimeNative",
+ jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/RavenwoodRuntimeNative",
sMethods, NELEM(sMethods));
if (res < 0) {
return res;
diff --git a/ravenwood/texts/ravenwood-framework-policies.txt b/ravenwood/texts/ravenwood-framework-policies.txt
index 2d49128..d962c82 100644
--- a/ravenwood/texts/ravenwood-framework-policies.txt
+++ b/ravenwood/texts/ravenwood-framework-policies.txt
@@ -17,6 +17,13 @@
rename com/.*/nano/ devicenano/
rename android/.*/nano/ devicenano/
+# Support APIs not available in standard JRE
+class java.io.FileDescriptor keep
+ method getInt$ ()I @com.android.ravenwood.RavenwoodJdkPatch.getInt$
+ method setInt$ (I)V @com.android.ravenwood.RavenwoodJdkPatch.setInt$
+class java.util.LinkedHashMap keep
+ method eldest ()Ljava/util/Map$Entry; @com.android.ravenwood.RavenwoodJdkPatch.eldest
+
# Exported to Mainline modules; cannot use annotations
class com.android.internal.util.FastXmlSerializer keepclass
class com.android.internal.util.FileRotator keepclass
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index edb6390..3224b27 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -34,6 +34,7 @@
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
+import static com.android.server.pm.UserManagerService.enforceCurrentUserIfVisibleBackgroundEnabled;
import static com.android.window.flags.Flags.deleteCaptureDisplay;
import android.accessibilityservice.AccessibilityGestureEvent;
@@ -1100,11 +1101,14 @@
if (svcConnTracingEnabled()) {
logTraceSvcConn("performGlobalAction", "action=" + action);
}
+ int currentUserId;
synchronized (mLock) {
if (!hasRightsToCurrentUserLocked()) {
return false;
}
+ currentUserId = mSystemSupport.getCurrentUserIdLocked();
}
+ enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
final long identity = Binder.clearCallingIdentity();
try {
return mSystemActionPerformer.performSystemAction(action);
@@ -2750,6 +2754,11 @@
@RequiresNoPermission
@Override
public void setAnimationScale(float scale) {
+ int currentUserId;
+ synchronized (mLock) {
+ currentUserId = mSystemSupport.getCurrentUserIdLocked();
+ }
+ enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
final long identity = Binder.clearCallingIdentity();
try {
Settings.Global.putFloat(
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 0aa750e..7cbb97e 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -63,6 +63,7 @@
import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain;
+import static com.android.server.pm.UserManagerService.enforceCurrentUserIfVisibleBackgroundEnabled;
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import android.accessibilityservice.AccessibilityGestureEvent;
@@ -309,6 +310,8 @@
private final PowerManager mPowerManager;
+ private final UserManager mUserManager;
+
private final WindowManagerInternal mWindowManagerService;
private final AccessibilitySecurityPolicy mSecurityPolicy;
@@ -507,6 +510,7 @@
super(permissionEnforcer);
mContext = context;
mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ mUserManager = mContext.getSystemService(UserManager.class);
mWindowManagerService = LocalServices.getService(WindowManagerInternal.class);
mTraceManager = AccessibilityTraceManager.getInstance(
mWindowManagerService.getAccessibilityController(), this, mLock);
@@ -542,6 +546,7 @@
super(PermissionEnforcer.fromContext(context));
mContext = context;
mPowerManager = context.getSystemService(PowerManager.class);
+ mUserManager = context.getSystemService(UserManager.class);
mWindowManagerService = LocalServices.getService(WindowManagerInternal.class);
mTraceManager = AccessibilityTraceManager.getInstance(
mWindowManagerService.getAccessibilityController(), this, mLock);
@@ -1263,6 +1268,11 @@
@EnforcePermission(MANAGE_ACCESSIBILITY)
public void registerSystemAction(RemoteAction action, int actionId) {
registerSystemAction_enforcePermission();
+ int currentUserId;
+ synchronized (mLock) {
+ currentUserId = mCurrentUserId;
+ }
+ enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
mTraceManager.logTrace(LOG_TAG + ".registerSystemAction",
FLAGS_ACCESSIBILITY_MANAGER, "action=" + action + ";actionId=" + actionId);
@@ -1279,6 +1289,11 @@
@EnforcePermission(MANAGE_ACCESSIBILITY)
public void unregisterSystemAction(int actionId) {
unregisterSystemAction_enforcePermission();
+ int currentUserId;
+ synchronized (mLock) {
+ currentUserId = mCurrentUserId;
+ }
+ enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
mTraceManager.logTrace(LOG_TAG + ".unregisterSystemAction",
FLAGS_ACCESSIBILITY_MANAGER, "actionId=" + actionId);
@@ -1606,6 +1621,11 @@
@EnforcePermission(STATUS_BAR_SERVICE)
public void notifyAccessibilityButtonClicked(int displayId, String targetName) {
notifyAccessibilityButtonClicked_enforcePermission();
+ int currentUserId;
+ synchronized (mLock) {
+ currentUserId = mCurrentUserId;
+ }
+ enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
mTraceManager.logTrace(LOG_TAG + ".notifyAccessibilityButtonClicked",
FLAGS_ACCESSIBILITY_MANAGER,
@@ -1634,6 +1654,11 @@
@EnforcePermission(STATUS_BAR_SERVICE)
public void notifyAccessibilityButtonVisibilityChanged(boolean shown) {
notifyAccessibilityButtonVisibilityChanged_enforcePermission();
+ int currentUserId;
+ synchronized (mLock) {
+ currentUserId = mCurrentUserId;
+ }
+ enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
mTraceManager.logTrace(LOG_TAG + ".notifyAccessibilityButtonVisibilityChanged",
FLAGS_ACCESSIBILITY_MANAGER, "shown=" + shown);
@@ -1974,9 +1999,8 @@
this, 0, oldUserState.mUserId));
}
- // Announce user changes only if more that one exist.
- UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
- final boolean announceNewUser = userManager.getUsers().size() > 1;
+ // Announce user changes only if more than one exist.
+ final boolean announceNewUser = mUserManager.getUsers().size() > 1;
// The user changed.
mCurrentUserId = userId;
@@ -2017,10 +2041,8 @@
synchronized (mLock) {
AccessibilityUserState userState = getCurrentUserStateLocked();
if (userState.isHandlingAccessibilityEventsLocked()) {
- UserManager userManager = (UserManager) mContext.getSystemService(
- Context.USER_SERVICE);
String message = mContext.getString(R.string.user_switched,
- userManager.getUserInfo(mCurrentUserId).name);
+ mUserManager.getUserInfo(mCurrentUserId).name);
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_ANNOUNCEMENT);
event.getText().add(message);
@@ -3185,6 +3207,7 @@
}
}
+ @GuardedBy("mLock")
private void updateWindowsForAccessibilityCallbackLocked(AccessibilityUserState userState) {
// We observe windows for accessibility only if there is at least
// one bound service that can retrieve window content that specified
@@ -3211,6 +3234,14 @@
for (int i = 0; i < displays.size(); i++) {
final Display display = displays.get(i);
if (display != null) {
+ // When supporting visible background users, only track windows on the display
+ // assigned to the current user. The proxy displays are registered only to the
+ // current user.
+ if (UserManager.isVisibleBackgroundUsersEnabled()
+ && !mProxyManager.isProxyedDisplay(display.getDisplayId())
+ && !mUmi.isUserVisible(mCurrentUserId, display.getDisplayId())) {
+ continue;
+ }
if (observingWindows) {
mA11yWindowManager.startTrackingWindows(display.getDisplayId(),
mProxyManager.isProxyedDisplay(display.getDisplayId()));
@@ -4799,6 +4830,11 @@
throws RemoteException {
registerProxyForDisplay_enforcePermission();
mSecurityPolicy.checkForAccessibilityPermissionOrRole();
+ int currentUserId;
+ synchronized (mLock) {
+ currentUserId = mCurrentUserId;
+ }
+ enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId);
if (client == null) {
return false;
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
index 6007bfd..9a81aa6 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
@@ -594,10 +594,6 @@
private boolean windowMattersToAccessibilityLocked(AccessibilityWindow a11yWindow,
int windowId, Region regionInScreen, Region unaccountedSpace) {
- if (a11yWindow.ignoreRecentsAnimationForAccessibility()) {
- return false;
- }
-
if (a11yWindow.isFocused()) {
return true;
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 9c6bcdf..53885fc 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -140,7 +140,7 @@
bindAppFunctionServiceUnchecked(requestInternal, serviceIntent, targetUser,
safeExecuteAppFunctionCallback,
/*bindFlags=*/ Context.BIND_AUTO_CREATE,
- /*timeoutInMillis=*/ mServiceConfig.getExecutionTimeoutConfig());
+ /*timeoutInMillis=*/ mServiceConfig.getExecuteAppFunctionTimeoutMillis());
}
private void bindAppFunctionServiceUnchecked(
diff --git a/services/appfunctions/java/com/android/server/appfunctions/ServiceConfig.java b/services/appfunctions/java/com/android/server/appfunctions/ServiceConfig.java
index 35c6c78..4bc6e70 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/ServiceConfig.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/ServiceConfig.java
@@ -26,5 +26,5 @@
/**
* Returns the maximum time to wait for an app function execution to be complete.
*/
- long getExecutionTimeoutConfig();
+ long getExecuteAppFunctionTimeoutMillis();
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/ServiceConfigImpl.java b/services/appfunctions/java/com/android/server/appfunctions/ServiceConfigImpl.java
index b203ead..e090317 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/ServiceConfigImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/ServiceConfigImpl.java
@@ -22,16 +22,17 @@
* Implementation of {@link ServiceConfig}
*/
public class ServiceConfigImpl implements ServiceConfig {
- static final String DEVICE_CONFIG_PROPERTY_EXECUTION_TIMEOUT = "execution_timeout";
- static final long DEFAULT_EXECUTION_TIMEOUT_MS = 5000L;
+ static final String DEVICE_CONFIG_PROPERTY_EXECUTION_TIMEOUT =
+ "execute_app_function_timeout_millis";
+ static final long DEFAULT_EXECUTE_APP_FUNCTION_TIMEOUT_MS = 5000L;
@Override
- public long getExecutionTimeoutConfig() {
+ public long getExecuteAppFunctionTimeoutMillis() {
return DeviceConfig.getLong(
NAMESPACE_APP_FUNCTIONS,
DEVICE_CONFIG_PROPERTY_EXECUTION_TIMEOUT,
- DEFAULT_EXECUTION_TIMEOUT_MS
+ DEFAULT_EXECUTE_APP_FUNCTION_TIMEOUT_MS
);
}
}
diff --git a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
index 8abbe56..22eefb3 100644
--- a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
+++ b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
@@ -73,6 +73,8 @@
/**
* Utility methods to read backup tar file.
+ * Exteranl depenency:
+ * <li> @android.provider.Settings.Secure.V_TO_U_RESTORE_ALLOWLIST
*/
public class TarBackupReader {
private static final int TAR_HEADER_OFFSET_TYPE_CHAR = 156;
diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
index b3a2da4..d56f17b 100644
--- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
@@ -20,6 +20,7 @@
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static android.companion.CompanionDeviceManager.RESULT_INTERNAL_ERROR;
+import static android.companion.CompanionDeviceManager.RESULT_SECURITY_ERROR;
import static android.content.ComponentName.createRelative;
import static android.content.pm.PackageManager.FEATURE_WATCH;
@@ -40,6 +41,7 @@
import android.companion.AssociatedDevice;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
+import android.companion.Flags;
import android.companion.IAssociationRequestCallback;
import android.content.ComponentName;
import android.content.Context;
@@ -182,7 +184,11 @@
String errorMessage = "3p apps are not allowed to create associations on watch.";
Slog.e(TAG, errorMessage);
try {
- callback.onFailure(RESULT_INTERNAL_ERROR);
+ if (Flags.associationFailureCode()) {
+ callback.onFailure(RESULT_SECURITY_ERROR, errorMessage);
+ } else {
+ callback.onFailure(RESULT_INTERNAL_ERROR, errorMessage);
+ }
} catch (RemoteException e) {
// ignored
}
@@ -251,9 +257,12 @@
} catch (SecurityException e) {
// Since, at this point the caller is our own UI, we need to catch the exception on
// forward it back to the application via the callback.
- Slog.e(TAG, e.getMessage());
try {
- callback.onFailure(RESULT_INTERNAL_ERROR);
+ if (Flags.associationFailureCode()) {
+ callback.onFailure(RESULT_SECURITY_ERROR, e.getMessage());
+ } else {
+ callback.onFailure(RESULT_INTERNAL_ERROR, e.getMessage());
+ }
} catch (RemoteException ignore) {
}
return;
@@ -378,7 +387,7 @@
// Send the association back via the app's callback
if (callback != null) {
try {
- callback.onFailure(RESULT_INTERNAL_ERROR);
+ callback.onFailure(RESULT_INTERNAL_ERROR, "Association doesn't exist.");
} catch (RemoteException ignore) {
}
}
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 8da58cf..d3e808f 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -689,13 +689,15 @@
}
/** A helper class used to wait for an input device to be registered. */
- private class WaitForDevice implements AutoCloseable {
+ private class WaitForDevice implements AutoCloseable {
private final CountDownLatch mDeviceAddedLatch = new CountDownLatch(1);
+ private final String mDeviceName;
private final InputManager.InputDeviceListener mListener;
private int mInputDeviceId = IInputConstants.INVALID_INPUT_DEVICE_ID;
WaitForDevice(String deviceName, int vendorId, int productId, int associatedDisplayId) {
+ mDeviceName = deviceName;
mListener = new InputManager.InputDeviceListener() {
@Override
public void onInputDeviceAdded(int deviceId) {
@@ -741,15 +743,17 @@
try {
if (!mDeviceAddedLatch.await(1, TimeUnit.MINUTES)) {
throw new DeviceCreationException(
- "Timed out waiting for virtual device to be created.");
+ "Timed out waiting for virtual input device " + mDeviceName
+ + " to be created.");
}
} catch (InterruptedException e) {
throw new DeviceCreationException(
- "Interrupted while waiting for virtual device to be created.", e);
+ "Interrupted while waiting for virtual input device " + mDeviceName
+ + " to be created.", e);
}
if (mInputDeviceId == IInputConstants.INVALID_INPUT_DEVICE_ID) {
throw new IllegalStateException(
- "Virtual input device was created with an invalid "
+ "Virtual input device " + mDeviceName + " was created with an invalid "
+ "id=" + mInputDeviceId);
}
return mInputDeviceId;
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 89d7961..1b5b7e8 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -233,6 +233,7 @@
"stats_flags_lib",
"core_os_flags_lib",
"connectivity_flags_lib",
+ "device_config_service_flags_java",
"dreams_flags_lib",
"aconfig_new_storage_flags_lib",
"powerstats_flags_lib",
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index e84250d..47203fb 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -296,7 +296,9 @@
this::onSyncRequestNotified);
setPropertyChangedListenerLocked();
updateConfigs();
- registerConnectivityModuleHealthListener();
+ if (!Flags.refactorCrashrecovery()) {
+ registerConnectivityModuleHealthListener();
+ }
}
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 68d9221..f1bdc05 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -50,7 +50,6 @@
import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_CREATED;
import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED;
import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_BACKUP;
-import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FINISH_RECEIVER;
import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_BEGIN;
import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_END;
import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SHELL;
@@ -101,15 +100,12 @@
import static android.os.Process.BLUETOOTH_UID;
import static android.os.Process.FIRST_APPLICATION_UID;
import static android.os.Process.INVALID_UID;
-import static android.os.Process.NETWORK_STACK_UID;
-import static android.os.Process.NFC_UID;
import static android.os.Process.PHONE_UID;
import static android.os.Process.PROC_OUT_LONG;
import static android.os.Process.PROC_SPACE_TERM;
import static android.os.Process.ROOT_UID;
import static android.os.Process.SCHED_FIFO;
import static android.os.Process.SCHED_RESET_ON_FORK;
-import static android.os.Process.SE_UID;
import static android.os.Process.SHELL_UID;
import static android.os.Process.SIGNAL_USR1;
import static android.os.Process.SYSTEM_UID;
@@ -145,8 +141,6 @@
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALLOWLISTS;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKGROUND_CHECK;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKUP;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_LIGHT;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_NETWORK;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_OOM_ADJ;
@@ -155,7 +149,6 @@
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_SERVICE;
import static com.android.server.am.ActivityManagerDebugConfig.LOG_WRITER_INFO;
import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_BACKUP;
-import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_BROADCAST;
import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_CLEANUP;
import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_LRU;
import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_MU;
@@ -265,10 +258,7 @@
import android.app.usage.UsageEvents.Event;
import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
-import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetManagerInternal;
-import android.compat.annotation.ChangeId;
-import android.compat.annotation.EnabledSince;
import android.content.AttributionSource;
import android.content.AutofillOptions;
import android.content.BroadcastReceiver;
@@ -316,9 +306,6 @@
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerInternal;
-import android.media.audiofx.AudioEffect;
-import android.net.ConnectivityManager;
-import android.net.Proxy;
import android.net.Uri;
import android.os.AppZygote;
import android.os.BatteryStats;
@@ -374,9 +361,7 @@
import android.server.ServerProtoEnums;
import android.system.Os;
import android.system.OsConstants;
-import android.telephony.TelephonyManager;
import android.text.TextUtils;
-import android.text.style.SuggestionSpan;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.EventLog;
@@ -386,7 +371,6 @@
import android.util.Log;
import android.util.MathUtils;
import android.util.Pair;
-import android.util.PrintWriterPrinter;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
@@ -435,11 +419,11 @@
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.MemInfoReader;
import com.android.internal.util.Preconditions;
+import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.AlarmManagerInternal;
import com.android.server.BootReceiver;
import com.android.server.DeviceIdleInternal;
import com.android.server.DisplayThread;
-import com.android.server.IntentResolver;
import com.android.server.IoThread;
import com.android.server.LocalManagerRegistry;
import com.android.server.LocalServices;
@@ -451,7 +435,6 @@
import com.android.server.SystemServiceManager;
import com.android.server.ThreadPriorityBooster;
import com.android.server.Watchdog;
-import com.android.server.am.ComponentAliasResolver.Resolution;
import com.android.server.am.LowMemDetector.MemFactor;
import com.android.server.appop.AppOpsService;
import com.android.server.compat.PlatformCompat;
@@ -462,14 +445,12 @@
import com.android.server.job.JobSchedulerInternal;
import com.android.server.net.NetworkManagementInternal;
import com.android.server.os.NativeTombstoneManager;
-import com.android.server.pm.Computer;
import com.android.server.pm.Installer;
import com.android.server.pm.SaferIntentUtils;
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.permission.PermissionManagerServiceInternal;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.SELinuxUtil;
-import com.android.server.pm.snapshot.PackageDataSnapshot;
import com.android.server.power.stats.BatteryStatsImpl;
import com.android.server.sdksandbox.SdkSandboxManagerLocal;
import com.android.server.stats.pull.StatsPullAtomService;
@@ -512,7 +493,6 @@
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@@ -537,7 +517,6 @@
static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityManagerService" : TAG_AM;
static final String TAG_BACKUP = TAG + POSTFIX_BACKUP;
- private static final String TAG_BROADCAST = TAG + POSTFIX_BROADCAST;
private static final String TAG_CLEANUP = TAG + POSTFIX_CLEANUP;
private static final String TAG_CONFIGURATION = TAG + POSTFIX_CONFIGURATION;
private static final String TAG_LOCKTASK = TAG + POSTFIX_LOCKTASK;
@@ -565,9 +544,6 @@
static final String SYSTEM_USER_HOME_NEEDED = "ro.system_user_home_needed";
- // Maximum number of receivers an app can register.
- private static final int MAX_RECEIVERS_ALLOWED_PER_APP = 1000;
-
// How long we wait for a launched process to attach to the activity manager
// before we decide it's never going to come up for real.
static final int PROC_START_TIMEOUT = 10 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
@@ -652,15 +628,6 @@
static final String EXTRA_BUGREPORT_NONCE = "android.intent.extra.BUGREPORT_NONCE";
static final String EXTRA_EXTRA_ATTACHMENT_URI =
"android.intent.extra.EXTRA_ATTACHMENT_URI";
- /**
- * It is now required for apps to explicitly set either
- * {@link android.content.Context#RECEIVER_EXPORTED} or
- * {@link android.content.Context#RECEIVER_NOT_EXPORTED} when registering a receiver for an
- * unprotected broadcast in code.
- */
- @ChangeId
- @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
- private static final long DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED = 161145287L;
/**
* The maximum number of bytes that {@link #setProcessStateSummary} accepts.
@@ -737,11 +704,9 @@
// so that dispatch of foreground broadcasts gets precedence.
private BroadcastQueue mBroadcastQueue;
- @GuardedBy("this")
- BroadcastStats mLastBroadcastStats;
-
- @GuardedBy("this")
- BroadcastStats mCurBroadcastStats;
+ // TODO: Add a consistent way of accessing the methods within this class. Currently, some
+ // methods require access while holding a lock, while others do not.
+ BroadcastController mBroadcastController;
TraceErrorLogger mTraceErrorLogger;
@@ -763,6 +728,7 @@
final AppErrors mAppErrors;
final PackageWatchdog mPackageWatchdog;
+ final CrashRecoveryHelper mCrashRecoveryHelper;
@GuardedBy("mDeliveryGroupPolicyIgnoredActions")
private final ArraySet<String> mDeliveryGroupPolicyIgnoredActions = new ArraySet();
@@ -860,12 +826,6 @@
};
/**
- * Broadcast actions that will always be deliverable to unlaunched/background apps
- */
- @GuardedBy("this")
- private ArraySet<String> mBackgroundLaunchBroadcasts;
-
- /**
* When an app has restrictions on the other apps that can have associations with it,
* it appears here with a set of the allowed apps and also track debuggability of the app.
*/
@@ -1133,97 +1093,6 @@
private final HashSet<Integer> mAlreadyLoggedViolatedStacks = new HashSet<Integer>();
private static final int MAX_DUP_SUPPRESSED_STACKS = 5000;
- /**
- * Keeps track of all IIntentReceivers that have been registered for broadcasts.
- * Hash keys are the receiver IBinder, hash value is a ReceiverList.
- */
- @GuardedBy("this")
- final HashMap<IBinder, ReceiverList> mRegisteredReceivers = new HashMap<>();
-
- /**
- * Resolver for broadcast intents to registered receivers.
- * Holds BroadcastFilter (subclass of IntentFilter).
- */
- final IntentResolver<BroadcastFilter, BroadcastFilter> mReceiverResolver
- = new IntentResolver<BroadcastFilter, BroadcastFilter>() {
- @Override
- protected boolean allowFilterResult(
- BroadcastFilter filter, List<BroadcastFilter> dest) {
- IBinder target = filter.receiverList.receiver.asBinder();
- for (int i = dest.size() - 1; i >= 0; i--) {
- if (dest.get(i).receiverList.receiver.asBinder() == target) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- protected BroadcastFilter newResult(@NonNull Computer computer, BroadcastFilter filter,
- int match, int userId, long customFlags) {
- if (userId == UserHandle.USER_ALL || filter.owningUserId == UserHandle.USER_ALL
- || userId == filter.owningUserId) {
- return super.newResult(computer, filter, match, userId, customFlags);
- }
- return null;
- }
-
- @Override
- protected IntentFilter getIntentFilter(@NonNull BroadcastFilter input) {
- return input;
- }
-
- @Override
- protected BroadcastFilter[] newArray(int size) {
- return new BroadcastFilter[size];
- }
-
- @Override
- protected boolean isPackageForFilter(String packageName, BroadcastFilter filter) {
- return packageName.equals(filter.packageName);
- }
- };
-
- /**
- * State of all active sticky broadcasts per user. Keys are the action of the
- * sticky Intent, values are an ArrayList of all broadcasted intents with
- * that action (which should usually be one). The SparseArray is keyed
- * by the user ID the sticky is for, and can include UserHandle.USER_ALL
- * for stickies that are sent to all users.
- */
- @GuardedBy("mStickyBroadcasts")
- final SparseArray<ArrayMap<String, ArrayList<StickyBroadcast>>> mStickyBroadcasts =
- new SparseArray<>();
-
- @VisibleForTesting
- static final class StickyBroadcast {
- public Intent intent;
- public boolean deferUntilActive;
- public int originalCallingUid;
- /** The snapshot process state of the app who sent this broadcast */
- public int originalCallingAppProcessState;
- public String resolvedDataType;
-
- public static StickyBroadcast create(Intent intent, boolean deferUntilActive,
- int originalCallingUid, int originalCallingAppProcessState,
- String resolvedDataType) {
- final StickyBroadcast b = new StickyBroadcast();
- b.intent = intent;
- b.deferUntilActive = deferUntilActive;
- b.originalCallingUid = originalCallingUid;
- b.originalCallingAppProcessState = originalCallingAppProcessState;
- b.resolvedDataType = resolvedDataType;
- return b;
- }
-
- @Override
- public String toString() {
- return "{intent=" + intent + ", defer=" + deferUntilActive + ", originalCallingUid="
- + originalCallingUid + ", originalCallingAppProcessState="
- + originalCallingAppProcessState + ", type=" + resolvedDataType + "}";
- }
- }
-
final ActiveServices mServices;
final static class Association {
@@ -1685,7 +1554,7 @@
// Encapsulates the global setting "hidden_api_blacklist_exemptions"
final HiddenApiSettings mHiddenApiBlacklist;
- private final PlatformCompat mPlatformCompat;
+ final PlatformCompat mPlatformCompat;
PackageManagerInternal mPackageManagerInt;
PermissionManagerServiceInternal mPermissionManagerInt;
@@ -2326,10 +2195,12 @@
mService.mBatteryStatsService.systemServicesReady();
mService.mServices.systemServicesReady();
} else if (phase == PHASE_ACTIVITY_MANAGER_READY) {
- mService.startBroadcastObservers();
+ mService.mBroadcastController.startBroadcastObservers();
} else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
if (!refactorCrashrecovery()) {
mService.mPackageWatchdog.onPackagesReady();
+ } else {
+ mService.mCrashRecoveryHelper.registerConnectivityModuleHealthListener();
}
mService.scheduleHomeTimeout();
}
@@ -2500,6 +2371,7 @@
mUiContext = null;
mAppErrors = injector.getAppErrors();
mPackageWatchdog = null;
+ mCrashRecoveryHelper = null;
mAppOpsService = mInjector.getAppOpsService(null /* recentAccessesFile */,
null /* storageFile */, null /* handler */);
mBatteryStatsService = mInjector.getBatteryStatsService();
@@ -2537,6 +2409,7 @@
mPendingStartActivityUids = new PendingStartActivityUids();
mUseFifoUiScheduling = false;
mBroadcastQueue = injector.getBroadcastQueue(this);
+ mBroadcastController = new BroadcastController(mContext, this, mBroadcastQueue);
mComponentAliasResolver = new ComponentAliasResolver(this);
}
@@ -2579,9 +2452,11 @@
: new OomAdjuster(this, mProcessList, activeUids);
mBroadcastQueue = mInjector.getBroadcastQueue(this);
+ mBroadcastController = new BroadcastController(mContext, this, mBroadcastQueue);
mServices = new ActiveServices(this);
mCpHelper = new ContentProviderHelper(this, true);
+ mCrashRecoveryHelper = new CrashRecoveryHelper(mUiContext);
mPackageWatchdog = PackageWatchdog.getInstance(mUiContext);
mAppErrors = new AppErrors(mUiContext, this, mPackageWatchdog);
mUidObserverController = new UidObserverController(mUiHandler);
@@ -2646,6 +2521,7 @@
void setBroadcastQueueForTest(BroadcastQueue broadcastQueue) {
mBroadcastQueue = broadcastQueue;
+ mBroadcastController.setBroadcastQueueForTest(broadcastQueue);
}
BroadcastQueue getBroadcastQueue() {
@@ -2680,18 +2556,6 @@
mLocalPowerManager = LocalServices.getService(PowerManagerInternal.class);
}
- private ArraySet<String> getBackgroundLaunchBroadcasts() {
- if (mBackgroundLaunchBroadcasts == null) {
- mBackgroundLaunchBroadcasts = SystemConfig.getInstance().getAllowImplicitBroadcasts();
- }
- return mBackgroundLaunchBroadcasts;
- }
-
- private String getWearRemoteIntentAction() {
- return mContext.getResources().getString(
- com.android.internal.R.string.config_wearRemoteIntentAction);
- }
-
/**
* Ensures that the given package name has an explicit set of allowed associations.
* If it does not, give it an empty set.
@@ -2762,7 +2626,7 @@
}
/** Updates allowed associations for app info (specifically, based on debuggability). */
- private void updateAssociationForApp(ApplicationInfo appInfo) {
+ void updateAssociationForApp(ApplicationInfo appInfo) {
ensureAllowedAssociations();
PackageAssociationInfo pai = mAllowedAssociations.get(appInfo.packageName);
if (pai != null) {
@@ -3888,7 +3752,7 @@
forceStopPackage(packageName, userId, ActivityManager.FLAG_OR_STOPPED, null);
}
- private void forceStopPackage(final String packageName, int userId, int userRunningFlags,
+ void forceStopPackage(final String packageName, int userId, int userRunningFlags,
String reason) {
if (checkCallingPermission(android.Manifest.permission.FORCE_STOP_PACKAGES)
!= PackageManager.PERMISSION_GRANTED) {
@@ -4216,7 +4080,7 @@
mPackageManagerInt.sendPackageRestartedBroadcast(packageName, uid, flags);
}
- private void cleanupDisabledPackageComponentsLocked(
+ void cleanupDisabledPackageComponentsLocked(
String packageName, int userId, String[] changedClasses) {
Set<String> disabledClasses = null;
@@ -4454,9 +4318,7 @@
if (packageName == null) {
// Remove all sticky broadcasts from this user.
- synchronized (mStickyBroadcasts) {
- mStickyBroadcasts.remove(userId);
- }
+ mBroadcastController.removeStickyBroadcasts(userId);
}
ArrayList<ContentProviderRecord> providers = new ArrayList<>();
@@ -9329,10 +9191,6 @@
Settings.Global.DEVICE_PROVISIONED, 0) != 0;
}
- private void startBroadcastObservers() {
- mBroadcastQueue.start(mContext.getContentResolver());
- }
-
private void updateForceBackgroundCheck(boolean enabled) {
synchronized (this) {
synchronized (mProcLock) {
@@ -10524,14 +10382,15 @@
pw.println(
"-------------------------------------------------------------------------------");
}
- dumpBroadcastsLocked(fd, pw, args, opti, dumpAll, dumpPackage);
+ mBroadcastController.dumpBroadcastsLocked(fd, pw, args, opti, dumpAll, dumpPackage);
pw.println();
if (dumpAll) {
pw.println(
"-------------------------------------------------------------------------------");
}
if (dumpAll || dumpPackage != null) {
- dumpBroadcastStatsLocked(fd, pw, args, opti, dumpAll, dumpPackage);
+ mBroadcastController.dumpBroadcastStatsLocked(fd, pw, args, opti, dumpAll,
+ dumpPackage);
pw.println();
if (dumpAll) {
pw.println(
@@ -10782,7 +10641,7 @@
} else if ("broadcasts".equals(cmd) || "b".equals(cmd)) {
// output proto is ActivityManagerServiceDumpBroadcastsProto
synchronized (this) {
- writeBroadcastsToProtoLocked(proto);
+ mBroadcastController.writeBroadcastsToProtoLocked(proto);
}
} else if ("provider".equals(cmd)) {
String[] newArgs;
@@ -10846,7 +10705,7 @@
proto.end(activityToken);
long broadcastToken = proto.start(ActivityManagerServiceProto.BROADCASTS);
- writeBroadcastsToProtoLocked(proto);
+ mBroadcastController.writeBroadcastsToProtoLocked(proto);
proto.end(broadcastToken);
long serviceToken = proto.start(ActivityManagerServiceProto.SERVICES);
@@ -10906,7 +10765,8 @@
opti++;
}
synchronized (this) {
- dumpBroadcastsLocked(fd, pw, args, opti, /* dumpAll= */ true, dumpPackage);
+ mBroadcastController.dumpBroadcastsLocked(fd, pw, args, opti,
+ /* dumpAll= */ true, dumpPackage);
}
} else if ("broadcast-stats".equals(cmd)) {
if (opti < args.length) {
@@ -10915,10 +10775,11 @@
}
synchronized (this) {
if (dumpCheckinFormat) {
- dumpBroadcastStatsCheckinLocked(fd, pw, args, opti, dumpCheckin,
- dumpPackage);
+ mBroadcastController.dumpBroadcastStatsCheckinLocked(fd, pw, args, opti,
+ dumpCheckin, dumpPackage);
} else {
- dumpBroadcastStatsLocked(fd, pw, args, opti, true, dumpPackage);
+ mBroadcastController.dumpBroadcastStatsLocked(fd, pw, args, opti, true,
+ dumpPackage);
}
}
} else if ("intents".equals(cmd) || "i".equals(cmd)) {
@@ -11072,7 +10933,8 @@
// No piece of data specified, dump everything.
if (dumpCheckinFormat) {
- dumpBroadcastStatsCheckinLocked(fd, pw, args, opti, dumpCheckin, dumpPackage);
+ mBroadcastController.dumpBroadcastStatsCheckinLocked(fd, pw, args, opti, dumpCheckin,
+ dumpPackage);
} else {
if (dumpClient) {
// dumpEverything() will take the lock when needed, and momentarily drop
@@ -11783,42 +11645,6 @@
}
}
- void writeBroadcastsToProtoLocked(ProtoOutputStream proto) {
- if (mRegisteredReceivers.size() > 0) {
- Iterator it = mRegisteredReceivers.values().iterator();
- while (it.hasNext()) {
- ReceiverList r = (ReceiverList)it.next();
- r.dumpDebug(proto, ActivityManagerServiceDumpBroadcastsProto.RECEIVER_LIST);
- }
- }
- mReceiverResolver.dumpDebug(proto, ActivityManagerServiceDumpBroadcastsProto.RECEIVER_RESOLVER);
- mBroadcastQueue.dumpDebug(proto, ActivityManagerServiceDumpBroadcastsProto.BROADCAST_QUEUE);
- synchronized (mStickyBroadcasts) {
- for (int user = 0; user < mStickyBroadcasts.size(); user++) {
- long token = proto.start(
- ActivityManagerServiceDumpBroadcastsProto.STICKY_BROADCASTS);
- proto.write(StickyBroadcastProto.USER, mStickyBroadcasts.keyAt(user));
- for (Map.Entry<String, ArrayList<StickyBroadcast>> ent
- : mStickyBroadcasts.valueAt(user).entrySet()) {
- long actionToken = proto.start(StickyBroadcastProto.ACTIONS);
- proto.write(StickyBroadcastProto.StickyAction.NAME, ent.getKey());
- for (StickyBroadcast broadcast : ent.getValue()) {
- broadcast.intent.dumpDebug(proto, StickyBroadcastProto.StickyAction.INTENTS,
- false, true, true, false);
- }
- proto.end(actionToken);
- }
- proto.end(token);
- }
- }
-
- long handlerToken = proto.start(ActivityManagerServiceDumpBroadcastsProto.HANDLER);
- proto.write(ActivityManagerServiceDumpBroadcastsProto.MainHandler.HANDLER, mHandler.toString());
- mHandler.getLooper().dumpDebug(proto,
- ActivityManagerServiceDumpBroadcastsProto.MainHandler.LOOPER);
- proto.end(handlerToken);
- }
-
void dumpAllowedAssociationsLocked(FileDescriptor fd, PrintWriter pw, String[] args,
int opti, boolean dumpAll, String dumpPackage) {
pw.println(
@@ -11854,219 +11680,6 @@
}
}
- @NeverCompile
- void dumpBroadcastsLocked(FileDescriptor fd, PrintWriter pw, String[] args,
- int opti, boolean dumpAll, String dumpPackage) {
- boolean dumpConstants = true;
- boolean dumpHistory = true;
- boolean needSep = false;
- boolean onlyHistory = false;
- boolean printedAnything = false;
- boolean onlyReceivers = false;
- int filteredUid = Process.INVALID_UID;
-
- if ("history".equals(dumpPackage)) {
- if (opti < args.length && "-s".equals(args[opti])) {
- dumpAll = false;
- }
- onlyHistory = true;
- dumpPackage = null;
- }
- if ("receivers".equals(dumpPackage)) {
- onlyReceivers = true;
- dumpPackage = null;
- if (opti + 2 <= args.length) {
- for (int i = opti; i < args.length; i++) {
- String arg = args[i];
- switch (arg) {
- case "--uid":
- filteredUid = getIntArg(pw, args, ++i, Process.INVALID_UID);
- if (filteredUid == Process.INVALID_UID) {
- return;
- }
- break;
- default:
- pw.printf("Invalid argument at index %d: %s\n", i, arg);
- return;
- }
- }
- }
- }
- if (DEBUG_BROADCAST) {
- Slogf.d(TAG_BROADCAST, "dumpBroadcastsLocked(): dumpPackage=%s, onlyHistory=%b, "
- + "onlyReceivers=%b, filteredUid=%d", dumpPackage, onlyHistory, onlyReceivers,
- filteredUid);
- }
-
- pw.println("ACTIVITY MANAGER BROADCAST STATE (dumpsys activity broadcasts)");
- if (!onlyHistory && dumpAll) {
- if (mRegisteredReceivers.size() > 0) {
- boolean printed = false;
- Iterator it = mRegisteredReceivers.values().iterator();
- while (it.hasNext()) {
- ReceiverList r = (ReceiverList)it.next();
- if (dumpPackage != null && (r.app == null ||
- !dumpPackage.equals(r.app.info.packageName))) {
- continue;
- }
- if (filteredUid != Process.INVALID_UID && filteredUid != r.app.uid) {
- if (DEBUG_BROADCAST) {
- Slogf.v(TAG_BROADCAST, "dumpBroadcastsLocked(): skipping receiver whose"
- + " uid (%d) is not %d: %s", r.app.uid, filteredUid, r.app);
- }
- continue;
- }
- if (!printed) {
- pw.println(" Registered Receivers:");
- needSep = true;
- printed = true;
- printedAnything = true;
- }
- pw.print(" * "); pw.println(r);
- r.dump(pw, " ");
- }
- } else {
- if (onlyReceivers) {
- pw.println(" (no registered receivers)");
- }
- }
-
- if (!onlyReceivers) {
- if (mReceiverResolver.dump(pw, needSep
- ? "\n Receiver Resolver Table:" : " Receiver Resolver Table:",
- " ", dumpPackage, false, false)) {
- needSep = true;
- printedAnything = true;
- }
- }
- }
-
- if (!onlyReceivers) {
- needSep = mBroadcastQueue.dumpLocked(fd, pw, args, opti,
- dumpConstants, dumpHistory, dumpAll, dumpPackage, needSep);
- printedAnything |= needSep;
- }
-
- needSep = true;
-
- synchronized (mStickyBroadcasts) {
- if (!onlyHistory && !onlyReceivers && mStickyBroadcasts != null
- && dumpPackage == null) {
- for (int user = 0; user < mStickyBroadcasts.size(); user++) {
- if (needSep) {
- pw.println();
- }
- needSep = true;
- printedAnything = true;
- pw.print(" Sticky broadcasts for user ");
- pw.print(mStickyBroadcasts.keyAt(user));
- pw.println(":");
- StringBuilder sb = new StringBuilder(128);
- for (Map.Entry<String, ArrayList<StickyBroadcast>> ent
- : mStickyBroadcasts.valueAt(user).entrySet()) {
- pw.print(" * Sticky action ");
- pw.print(ent.getKey());
- if (dumpAll) {
- pw.println(":");
- ArrayList<StickyBroadcast> broadcasts = ent.getValue();
- final int N = broadcasts.size();
- for (int i = 0; i < N; i++) {
- final Intent intent = broadcasts.get(i).intent;
- final boolean deferUntilActive = broadcasts.get(i).deferUntilActive;
- sb.setLength(0);
- sb.append(" Intent: ");
- intent.toShortString(sb, false, true, false, false);
- pw.print(sb);
- if (deferUntilActive) {
- pw.print(" [D]");
- }
- pw.println();
- pw.print(" originalCallingUid: ");
- pw.println(broadcasts.get(i).originalCallingUid);
- pw.println();
- Bundle bundle = intent.getExtras();
- if (bundle != null) {
- pw.print(" extras: ");
- pw.println(bundle);
- }
- }
- } else {
- pw.println("");
- }
- }
- }
- }
- }
-
- if (!onlyHistory && !onlyReceivers && dumpAll) {
- pw.println();
- pw.println(" Queue " + mBroadcastQueue.toString() + ": "
- + mBroadcastQueue.describeStateLocked());
- pw.println(" mHandler:");
- mHandler.dump(new PrintWriterPrinter(pw), " ");
- needSep = true;
- printedAnything = true;
- }
-
- if (!printedAnything) {
- pw.println(" (nothing)");
- }
- }
-
- @NeverCompile
- void dumpBroadcastStatsLocked(FileDescriptor fd, PrintWriter pw, String[] args,
- int opti, boolean dumpAll, String dumpPackage) {
- if (mCurBroadcastStats == null) {
- return;
- }
-
- pw.println("ACTIVITY MANAGER BROADCAST STATS STATE (dumpsys activity broadcast-stats)");
- final long now = SystemClock.elapsedRealtime();
- if (mLastBroadcastStats != null) {
- pw.print(" Last stats (from ");
- TimeUtils.formatDuration(mLastBroadcastStats.mStartRealtime, now, pw);
- pw.print(" to ");
- TimeUtils.formatDuration(mLastBroadcastStats.mEndRealtime, now, pw);
- pw.print(", ");
- TimeUtils.formatDuration(mLastBroadcastStats.mEndUptime
- - mLastBroadcastStats.mStartUptime, pw);
- pw.println(" uptime):");
- if (!mLastBroadcastStats.dumpStats(pw, " ", dumpPackage)) {
- pw.println(" (nothing)");
- }
- pw.println();
- }
- pw.print(" Current stats (from ");
- TimeUtils.formatDuration(mCurBroadcastStats.mStartRealtime, now, pw);
- pw.print(" to now, ");
- TimeUtils.formatDuration(SystemClock.uptimeMillis()
- - mCurBroadcastStats.mStartUptime, pw);
- pw.println(" uptime):");
- if (!mCurBroadcastStats.dumpStats(pw, " ", dumpPackage)) {
- pw.println(" (nothing)");
- }
- }
-
- @NeverCompile
- void dumpBroadcastStatsCheckinLocked(FileDescriptor fd, PrintWriter pw, String[] args,
- int opti, boolean fullCheckin, String dumpPackage) {
- if (mCurBroadcastStats == null) {
- return;
- }
-
- if (mLastBroadcastStats != null) {
- mLastBroadcastStats.dumpCheckinStats(pw, dumpPackage);
- if (fullCheckin) {
- mLastBroadcastStats = null;
- return;
- }
- }
- mCurBroadcastStats.dumpCheckinStats(pw, dumpPackage);
- if (fullCheckin) {
- mCurBroadcastStats = null;
- }
- }
-
void dumpPermissions(FileDescriptor fd, PrintWriter pw, String[] args,
int opti, boolean dumpAll, String dumpPackage) {
@@ -14588,33 +14201,6 @@
// BROADCASTS
// =========================================================
- private boolean isInstantApp(ProcessRecord record, @Nullable String callerPackage, int uid) {
- if (UserHandle.getAppId(uid) < FIRST_APPLICATION_UID) {
- return false;
- }
- // Easy case -- we have the app's ProcessRecord.
- if (record != null) {
- return record.info.isInstantApp();
- }
- // Otherwise check with PackageManager.
- IPackageManager pm = AppGlobals.getPackageManager();
- try {
- if (callerPackage == null) {
- final String[] packageNames = pm.getPackagesForUid(uid);
- if (packageNames == null || packageNames.length == 0) {
- throw new IllegalArgumentException("Unable to determine caller package name");
- }
- // Instant Apps can't use shared uids, so its safe to only check the first package.
- callerPackage = packageNames[0];
- }
- mAppOpsService.checkPackage(uid, callerPackage);
- return pm.isInstantApp(callerPackage, UserHandle.getUserId(uid));
- } catch (RemoteException e) {
- Slog.e(TAG, "Error looking up if " + callerPackage + " is an instant app.", e);
- return true;
- }
- }
-
/**
* @deprecated Use {@link #registerReceiverWithFeature}
*/
@@ -14629,657 +14215,12 @@
public Intent registerReceiverWithFeature(IApplicationThread caller, String callerPackage,
String callerFeatureId, String receiverId, IIntentReceiver receiver,
IntentFilter filter, String permission, int userId, int flags) {
- traceRegistrationBegin(receiverId, receiver, filter, userId);
- try {
- return registerReceiverWithFeatureTraced(caller, callerPackage, callerFeatureId,
- receiverId, receiver, filter, permission, userId, flags);
- } finally {
- traceRegistrationEnd();
- }
- }
-
- private static void traceRegistrationBegin(String receiverId, IIntentReceiver receiver,
- IntentFilter filter, int userId) {
- if (!Flags.traceReceiverRegistration()) {
- return;
- }
- if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
- final StringBuilder sb = new StringBuilder("registerReceiver: ");
- sb.append(Binder.getCallingUid()); sb.append('/');
- sb.append(receiverId == null ? "null" : receiverId); sb.append('/');
- final int actionsCount = filter.safeCountActions();
- if (actionsCount > 0) {
- for (int i = 0; i < actionsCount; ++i) {
- sb.append(filter.getAction(i));
- if (i != actionsCount - 1) sb.append(',');
- }
- } else {
- sb.append("null");
- }
- sb.append('/');
- sb.append('u'); sb.append(userId); sb.append('/');
- sb.append(receiver == null ? "null" : receiver.asBinder());
- Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, sb.toString());
- }
- }
-
- private static void traceRegistrationEnd() {
- if (!Flags.traceReceiverRegistration()) {
- return;
- }
- if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
- Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
- }
- }
-
- private Intent registerReceiverWithFeatureTraced(IApplicationThread caller,
- String callerPackage, String callerFeatureId, String receiverId,
- IIntentReceiver receiver, IntentFilter filter, String permission,
- int userId, int flags) {
- enforceNotIsolatedCaller("registerReceiver");
- ArrayList<StickyBroadcast> stickyBroadcasts = null;
- ProcessRecord callerApp = null;
- final boolean visibleToInstantApps
- = (flags & Context.RECEIVER_VISIBLE_TO_INSTANT_APPS) != 0;
-
- int callingUid;
- int callingPid;
- boolean instantApp;
- synchronized (mProcLock) {
- callerApp = getRecordForAppLOSP(caller);
- if (callerApp == null) {
- Slog.w(TAG, "registerReceiverWithFeature: no app for " + caller);
- return null;
- }
- if (callerApp.info.uid != SYSTEM_UID
- && !callerApp.getPkgList().containsKey(callerPackage)
- && !"android".equals(callerPackage)) {
- throw new SecurityException("Given caller package " + callerPackage
- + " is not running in process " + callerApp);
- }
- callingUid = callerApp.info.uid;
- callingPid = callerApp.getPid();
-
- instantApp = isInstantApp(callerApp, callerPackage, callingUid);
- }
- userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true,
- ALLOW_FULL_ONLY, "registerReceiver", callerPackage);
-
- // Warn if system internals are registering for important broadcasts
- // without also using a priority to ensure they process the event
- // before normal apps hear about it
- if (UserHandle.isCore(callingUid)) {
- final int priority = filter.getPriority();
- final boolean systemPriority = (priority >= IntentFilter.SYSTEM_HIGH_PRIORITY)
- || (priority <= IntentFilter.SYSTEM_LOW_PRIORITY);
- if (!systemPriority) {
- final int N = filter.countActions();
- for (int i = 0; i < N; i++) {
- // TODO: expand to additional important broadcasts over time
- final String action = filter.getAction(i);
- if (action.startsWith("android.intent.action.USER_")
- || action.startsWith("android.intent.action.PACKAGE_")
- || action.startsWith("android.intent.action.UID_")
- || action.startsWith("android.intent.action.EXTERNAL_")
- || action.startsWith("android.bluetooth.")
- || action.equals(Intent.ACTION_SHUTDOWN)) {
- if (DEBUG_BROADCAST) {
- Slog.wtf(TAG,
- "System internals registering for " + filter.toLongString()
- + " with app priority; this will race with apps!",
- new Throwable());
- }
-
- // When undefined, assume that system internals need
- // to hear about the event first; they can use
- // SYSTEM_LOW_PRIORITY if they need to hear last
- if (priority == 0) {
- filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
- }
- break;
- }
- }
- }
- }
-
- Iterator<String> actions = filter.actionsIterator();
- if (actions == null) {
- ArrayList<String> noAction = new ArrayList<String>(1);
- noAction.add(null);
- actions = noAction.iterator();
- }
- boolean onlyProtectedBroadcasts = true;
-
- // Collect stickies of users and check if broadcast is only registered for protected
- // broadcasts
- int[] userIds = { UserHandle.USER_ALL, UserHandle.getUserId(callingUid) };
- synchronized (mStickyBroadcasts) {
- while (actions.hasNext()) {
- String action = actions.next();
- for (int id : userIds) {
- ArrayMap<String, ArrayList<StickyBroadcast>> stickies =
- mStickyBroadcasts.get(id);
- if (stickies != null) {
- ArrayList<StickyBroadcast> broadcasts = stickies.get(action);
- if (broadcasts != null) {
- if (stickyBroadcasts == null) {
- stickyBroadcasts = new ArrayList<>();
- }
- stickyBroadcasts.addAll(broadcasts);
- }
- }
- }
- if (onlyProtectedBroadcasts) {
- try {
- onlyProtectedBroadcasts &=
- AppGlobals.getPackageManager().isProtectedBroadcast(action);
- } catch (RemoteException e) {
- onlyProtectedBroadcasts = false;
- Slog.w(TAG, "Remote exception", e);
- }
- }
- }
- }
-
- if (Process.isSdkSandboxUid(Binder.getCallingUid())) {
- SdkSandboxManagerLocal sdkSandboxManagerLocal =
- LocalManagerRegistry.getManager(SdkSandboxManagerLocal.class);
- if (sdkSandboxManagerLocal == null) {
- throw new IllegalStateException("SdkSandboxManagerLocal not found when checking"
- + " whether SDK sandbox uid can register to broadcast receivers.");
- }
- if (!sdkSandboxManagerLocal.canRegisterBroadcastReceiver(
- /*IntentFilter=*/ filter, flags, onlyProtectedBroadcasts)) {
- throw new SecurityException("SDK sandbox not allowed to register receiver"
- + " with the given IntentFilter: " + filter.toLongString());
- }
- }
-
- // If the change is enabled, but neither exported or not exported is set, we need to log
- // an error so the consumer can know to explicitly set the value for their flag.
- // If the caller is registering for a sticky broadcast with a null receiver, we won't
- // require a flag
- final boolean explicitExportStateDefined =
- (flags & (Context.RECEIVER_EXPORTED | Context.RECEIVER_NOT_EXPORTED)) != 0;
- if (((flags & Context.RECEIVER_EXPORTED) != 0) && (
- (flags & Context.RECEIVER_NOT_EXPORTED) != 0)) {
- throw new IllegalArgumentException(
- "Receiver can't specify both RECEIVER_EXPORTED and RECEIVER_NOT_EXPORTED"
- + "flag");
- }
-
- // Don't enforce the flag check if we're EITHER registering for only protected
- // broadcasts, or the receiver is null (a sticky broadcast). Sticky broadcasts should
- // not be used generally, so we will be marking them as exported by default
- boolean requireExplicitFlagForDynamicReceivers = CompatChanges.isChangeEnabled(
- DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED, callingUid);
-
- // A receiver that is visible to instant apps must also be exported.
- final boolean unexportedReceiverVisibleToInstantApps =
- ((flags & Context.RECEIVER_VISIBLE_TO_INSTANT_APPS) != 0) && (
- (flags & Context.RECEIVER_NOT_EXPORTED) != 0);
- if (unexportedReceiverVisibleToInstantApps && requireExplicitFlagForDynamicReceivers) {
- throw new IllegalArgumentException(
- "Receiver can't specify both RECEIVER_VISIBLE_TO_INSTANT_APPS and "
- + "RECEIVER_NOT_EXPORTED flag");
- }
-
- if (!onlyProtectedBroadcasts) {
- if (receiver == null && !explicitExportStateDefined) {
- // sticky broadcast, no flag specified (flag isn't required)
- flags |= Context.RECEIVER_EXPORTED;
- } else if (requireExplicitFlagForDynamicReceivers && !explicitExportStateDefined) {
- throw new SecurityException(
- callerPackage + ": One of RECEIVER_EXPORTED or "
- + "RECEIVER_NOT_EXPORTED should be specified when a receiver "
- + "isn't being registered exclusively for system broadcasts");
- // Assume default behavior-- flag check is not enforced
- } else if (!requireExplicitFlagForDynamicReceivers && (
- (flags & Context.RECEIVER_NOT_EXPORTED) == 0)) {
- // Change is not enabled, assume exported unless otherwise specified.
- flags |= Context.RECEIVER_EXPORTED;
- }
- } else if ((flags & Context.RECEIVER_NOT_EXPORTED) == 0) {
- flags |= Context.RECEIVER_EXPORTED;
- }
-
- // Dynamic receivers are exported by default for versions prior to T
- final boolean exported = (flags & Context.RECEIVER_EXPORTED) != 0;
-
- ArrayList<StickyBroadcast> allSticky = null;
- if (stickyBroadcasts != null) {
- final ContentResolver resolver = mContext.getContentResolver();
- // Look for any matching sticky broadcasts...
- for (int i = 0, N = stickyBroadcasts.size(); i < N; i++) {
- final StickyBroadcast broadcast = stickyBroadcasts.get(i);
- Intent intent = broadcast.intent;
- // Don't provided intents that aren't available to instant apps.
- if (instantApp &&
- (intent.getFlags() & Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS) == 0) {
- continue;
- }
- // If intent has scheme "content", it will need to access
- // provider that needs to lock mProviderMap in ActivityThread
- // and also it may need to wait application response, so we
- // cannot lock ActivityManagerService here.
- final int match;
- if (Flags.avoidResolvingType()) {
- match = filter.match(intent.getAction(), broadcast.resolvedDataType,
- intent.getScheme(), intent.getData(), intent.getCategories(),
- TAG, false /* supportsWildcards */, null /* ignoreActions */,
- intent.getExtras());
- } else {
- match = filter.match(resolver, intent, true, TAG);
- }
- if (match >= 0) {
- if (allSticky == null) {
- allSticky = new ArrayList<>();
- }
- allSticky.add(broadcast);
- }
- }
- }
-
- // The first sticky in the list is returned directly back to the client.
- Intent sticky = allSticky != null ? allSticky.get(0).intent : null;
- if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Register receiver " + filter + ": " + sticky);
- if (receiver == null) {
- return sticky;
- }
-
- // SafetyNet logging for b/177931370. If any process other than system_server tries to
- // listen to this broadcast action, then log it.
- if (callingPid != Process.myPid()) {
- if (filter.hasAction("com.android.server.net.action.SNOOZE_WARNING")
- || filter.hasAction("com.android.server.net.action.SNOOZE_RAPID")) {
- EventLog.writeEvent(0x534e4554, "177931370", callingUid, "");
- }
- }
-
- synchronized (this) {
- IApplicationThread thread;
- if (callerApp != null && ((thread = callerApp.getThread()) == null
- || thread.asBinder() != caller.asBinder())) {
- // Original caller already died
- return null;
- }
- ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());
- if (rl == null) {
- rl = new ReceiverList(this, callerApp, callingPid, callingUid,
- userId, receiver);
- if (rl.app != null) {
- final int totalReceiversForApp = rl.app.mReceivers.numberOfReceivers();
- if (totalReceiversForApp >= MAX_RECEIVERS_ALLOWED_PER_APP) {
- throw new IllegalStateException("Too many receivers, total of "
- + totalReceiversForApp + ", registered for pid: "
- + rl.pid + ", callerPackage: " + callerPackage);
- }
- rl.app.mReceivers.addReceiver(rl);
- } else {
- try {
- receiver.asBinder().linkToDeath(rl, 0);
- } catch (RemoteException e) {
- return sticky;
- }
- rl.linkedToDeath = true;
- }
- mRegisteredReceivers.put(receiver.asBinder(), rl);
- } else if (rl.uid != callingUid) {
- throw new IllegalArgumentException(
- "Receiver requested to register for uid " + callingUid
- + " was previously registered for uid " + rl.uid
- + " callerPackage is " + callerPackage);
- } else if (rl.pid != callingPid) {
- throw new IllegalArgumentException(
- "Receiver requested to register for pid " + callingPid
- + " was previously registered for pid " + rl.pid
- + " callerPackage is " + callerPackage);
- } else if (rl.userId != userId) {
- throw new IllegalArgumentException(
- "Receiver requested to register for user " + userId
- + " was previously registered for user " + rl.userId
- + " callerPackage is " + callerPackage);
- }
- BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage, callerFeatureId,
- receiverId, permission, callingUid, userId, instantApp, visibleToInstantApps,
- exported);
- if (rl.containsFilter(filter)) {
- Slog.w(TAG, "Receiver with filter " + filter
- + " already registered for pid " + rl.pid
- + ", callerPackage is " + callerPackage);
- } else {
- rl.add(bf);
- if (!bf.debugCheck()) {
- Slog.w(TAG, "==> For Dynamic broadcast");
- }
- mReceiverResolver.addFilter(getPackageManagerInternal().snapshot(), bf);
- }
-
- // Enqueue broadcasts for all existing stickies that match
- // this filter.
- if (allSticky != null) {
- ArrayList receivers = new ArrayList();
- receivers.add(bf);
- sticky = null;
-
- final int stickyCount = allSticky.size();
- for (int i = 0; i < stickyCount; i++) {
- final StickyBroadcast broadcast = allSticky.get(i);
- final int originalStickyCallingUid = allSticky.get(i).originalCallingUid;
- // TODO(b/281889567): consider using checkComponentPermission instead of
- // canAccessUnexportedComponents
- if (sticky == null && (exported || originalStickyCallingUid == callingUid
- || ActivityManager.canAccessUnexportedComponents(
- originalStickyCallingUid))) {
- sticky = broadcast.intent;
- }
- BroadcastQueue queue = mBroadcastQueue;
- BroadcastRecord r = new BroadcastRecord(queue, broadcast.intent, null,
- null, null, -1, -1, false, null, null, null, null, OP_NONE,
- BroadcastOptions.makeWithDeferUntilActive(broadcast.deferUntilActive),
- receivers, null, null, 0, null, null, false, true, true, -1,
- originalStickyCallingUid, BackgroundStartPrivileges.NONE,
- false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */,
- null /* filterExtrasForReceiver */,
- broadcast.originalCallingAppProcessState);
- queue.enqueueBroadcastLocked(r);
- }
- }
-
- return sticky;
- }
+ return mBroadcastController.registerReceiverWithFeature(caller, callerPackage,
+ callerFeatureId, receiverId, receiver, filter, permission, userId, flags);
}
public void unregisterReceiver(IIntentReceiver receiver) {
- traceUnregistrationBegin(receiver);
- try {
- unregisterReceiverTraced(receiver);
- } finally {
- traceUnregistrationEnd();
- }
- }
-
- private static void traceUnregistrationBegin(IIntentReceiver receiver) {
- if (!Flags.traceReceiverRegistration()) {
- return;
- }
- if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
- Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
- TextUtils.formatSimple("unregisterReceiver: %d/%s", Binder.getCallingUid(),
- receiver == null ? "null" : receiver.asBinder()));
- }
- }
-
- private static void traceUnregistrationEnd() {
- if (!Flags.traceReceiverRegistration()) {
- return;
- }
- if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
- Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
- }
- }
-
- private void unregisterReceiverTraced(IIntentReceiver receiver) {
- if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Unregister receiver: " + receiver);
-
- final long origId = Binder.clearCallingIdentity();
- try {
- boolean doTrim = false;
- synchronized(this) {
- ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());
- if (rl != null) {
- final BroadcastRecord r = rl.curBroadcast;
- if (r != null) {
- final boolean doNext = r.queue.finishReceiverLocked(
- rl.app, r.resultCode, r.resultData, r.resultExtras,
- r.resultAbort, false);
- if (doNext) {
- doTrim = true;
- }
- }
- if (rl.app != null) {
- rl.app.mReceivers.removeReceiver(rl);
- }
- removeReceiverLocked(rl);
- if (rl.linkedToDeath) {
- rl.linkedToDeath = false;
- rl.receiver.asBinder().unlinkToDeath(rl, 0);
- }
- }
-
- // If we actually concluded any broadcasts, we might now be able
- // to trim the recipients' apps from our working set
- if (doTrim) {
- trimApplicationsLocked(false, OOM_ADJ_REASON_FINISH_RECEIVER);
- return;
- }
- }
-
- } finally {
- Binder.restoreCallingIdentity(origId);
- }
- }
-
- void removeReceiverLocked(ReceiverList rl) {
- mRegisteredReceivers.remove(rl.receiver.asBinder());
- for (int i = rl.size() - 1; i >= 0; i--) {
- mReceiverResolver.removeFilter(rl.get(i));
- }
- }
-
- private final void sendPackageBroadcastLocked(int cmd, String[] packages, int userId) {
- mProcessList.sendPackageBroadcastLocked(cmd, packages, userId);
- }
-
- private List<ResolveInfo> collectReceiverComponents(
- Intent intent, String resolvedType, int callingUid, int callingPid,
- int[] users, int[] broadcastAllowList) {
- // TODO: come back and remove this assumption to triage all broadcasts
- long pmFlags = STOCK_PM_FLAGS | MATCH_DEBUG_TRIAGED_MISSING;
-
- List<ResolveInfo> receivers = null;
- HashSet<ComponentName> singleUserReceivers = null;
- boolean scannedFirstReceivers = false;
- for (int user : users) {
- // Skip users that have Shell restrictions
- if (callingUid == SHELL_UID
- && mUserController.hasUserRestriction(
- UserManager.DISALLOW_DEBUGGING_FEATURES, user)) {
- continue;
- }
- List<ResolveInfo> newReceivers = mPackageManagerInt.queryIntentReceivers(
- intent, resolvedType, pmFlags, callingUid, callingPid, user, /* forSend */true);
- if (user != UserHandle.USER_SYSTEM && newReceivers != null) {
- // If this is not the system user, we need to check for
- // any receivers that should be filtered out.
- for (int i = 0; i < newReceivers.size(); i++) {
- ResolveInfo ri = newReceivers.get(i);
- if ((ri.activityInfo.flags & ActivityInfo.FLAG_SYSTEM_USER_ONLY) != 0) {
- newReceivers.remove(i);
- i--;
- }
- }
- }
- // Replace the alias receivers with their targets.
- if (newReceivers != null) {
- for (int i = newReceivers.size() - 1; i >= 0; i--) {
- final ResolveInfo ri = newReceivers.get(i);
- final Resolution<ResolveInfo> resolution =
- mComponentAliasResolver.resolveReceiver(intent, ri, resolvedType,
- pmFlags, user, callingUid, callingPid);
- if (resolution == null) {
- // It was an alias, but the target was not found.
- newReceivers.remove(i);
- continue;
- }
- if (resolution.isAlias()) {
- newReceivers.set(i, resolution.getTarget());
- }
- }
- }
- if (newReceivers != null && newReceivers.size() == 0) {
- newReceivers = null;
- }
-
- if (receivers == null) {
- receivers = newReceivers;
- } else if (newReceivers != null) {
- // We need to concatenate the additional receivers
- // found with what we have do far. This would be easy,
- // but we also need to de-dup any receivers that are
- // singleUser.
- if (!scannedFirstReceivers) {
- // Collect any single user receivers we had already retrieved.
- scannedFirstReceivers = true;
- for (int i = 0; i < receivers.size(); i++) {
- ResolveInfo ri = receivers.get(i);
- if ((ri.activityInfo.flags&ActivityInfo.FLAG_SINGLE_USER) != 0) {
- ComponentName cn = new ComponentName(
- ri.activityInfo.packageName, ri.activityInfo.name);
- if (singleUserReceivers == null) {
- singleUserReceivers = new HashSet<ComponentName>();
- }
- singleUserReceivers.add(cn);
- }
- }
- }
- // Add the new results to the existing results, tracking
- // and de-dupping single user receivers.
- for (int i = 0; i < newReceivers.size(); i++) {
- ResolveInfo ri = newReceivers.get(i);
- if ((ri.activityInfo.flags & ActivityInfo.FLAG_SINGLE_USER) != 0) {
- ComponentName cn = new ComponentName(
- ri.activityInfo.packageName, ri.activityInfo.name);
- if (singleUserReceivers == null) {
- singleUserReceivers = new HashSet<ComponentName>();
- }
- if (!singleUserReceivers.contains(cn)) {
- singleUserReceivers.add(cn);
- receivers.add(ri);
- }
- } else {
- receivers.add(ri);
- }
- }
- }
- }
- if (receivers != null && broadcastAllowList != null) {
- for (int i = receivers.size() - 1; i >= 0; i--) {
- final int receiverAppId = UserHandle.getAppId(
- receivers.get(i).activityInfo.applicationInfo.uid);
- if (receiverAppId >= Process.FIRST_APPLICATION_UID
- && Arrays.binarySearch(broadcastAllowList, receiverAppId) < 0) {
- receivers.remove(i);
- }
- }
- }
- return receivers;
- }
-
- private void checkBroadcastFromSystem(Intent intent, ProcessRecord callerApp,
- String callerPackage, int callingUid, boolean isProtectedBroadcast, List receivers) {
- if ((intent.getFlags() & Intent.FLAG_RECEIVER_FROM_SHELL) != 0) {
- // Don't yell about broadcasts sent via shell
- return;
- }
-
- final String action = intent.getAction();
- if (isProtectedBroadcast
- || Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)
- || Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(action)
- || Intent.ACTION_MEDIA_BUTTON.equals(action)
- || Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action)
- || Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(action)
- || Intent.ACTION_MASTER_CLEAR.equals(action)
- || Intent.ACTION_FACTORY_RESET.equals(action)
- || AppWidgetManager.ACTION_APPWIDGET_CONFIGURE.equals(action)
- || AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)
- || TelephonyManager.ACTION_REQUEST_OMADM_CONFIGURATION_UPDATE.equals(action)
- || SuggestionSpan.ACTION_SUGGESTION_PICKED.equals(action)
- || AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION.equals(action)
- || AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION.equals(action)) {
- // Broadcast is either protected, or it's a public action that
- // we've relaxed, so it's fine for system internals to send.
- return;
- }
-
- // This broadcast may be a problem... but there are often system components that
- // want to send an internal broadcast to themselves, which is annoying to have to
- // explicitly list each action as a protected broadcast, so we will check for that
- // one safe case and allow it: an explicit broadcast, only being received by something
- // that has protected itself.
- if (intent.getPackage() != null || intent.getComponent() != null) {
- if (receivers == null || receivers.size() == 0) {
- // Intent is explicit and there's no receivers.
- // This happens, e.g. , when a system component sends a broadcast to
- // its own runtime receiver, and there's no manifest receivers for it,
- // because this method is called twice for each broadcast,
- // for runtime receivers and manifest receivers and the later check would find
- // no receivers.
- return;
- }
- boolean allProtected = true;
- for (int i = receivers.size()-1; i >= 0; i--) {
- Object target = receivers.get(i);
- if (target instanceof ResolveInfo) {
- ResolveInfo ri = (ResolveInfo)target;
- if (ri.activityInfo.exported && ri.activityInfo.permission == null) {
- allProtected = false;
- break;
- }
- } else {
- BroadcastFilter bf = (BroadcastFilter)target;
- if (bf.exported && bf.requiredPermission == null) {
- allProtected = false;
- break;
- }
- }
- }
- if (allProtected) {
- // All safe!
- return;
- }
- }
-
- // The vast majority of broadcasts sent from system internals
- // should be protected to avoid security holes, so yell loudly
- // to ensure we examine these cases.
- if (callerApp != null) {
- Log.wtf(TAG, "Sending non-protected broadcast " + action
- + " from system " + callerApp.toShortString() + " pkg " + callerPackage,
- new Throwable());
- } else {
- Log.wtf(TAG, "Sending non-protected broadcast " + action
- + " from system uid " + UserHandle.formatUid(callingUid)
- + " pkg " + callerPackage,
- new Throwable());
- }
- }
-
- // Apply permission policy around the use of specific broadcast options
- void enforceBroadcastOptionPermissionsInternal(
- @Nullable Bundle options, int callingUid) {
- enforceBroadcastOptionPermissionsInternal(BroadcastOptions.fromBundleNullable(options),
- callingUid);
- }
-
- void enforceBroadcastOptionPermissionsInternal(
- @Nullable BroadcastOptions options, int callingUid) {
- if (options != null && callingUid != Process.SYSTEM_UID) {
- if (options.isAlarmBroadcast()) {
- if (DEBUG_BROADCAST_LIGHT) {
- Slog.w(TAG, "Non-system caller " + callingUid
- + " may not flag broadcast as alarm");
- }
- throw new SecurityException(
- "Non-system callers may not flag broadcasts as alarm");
- }
- if (options.isInteractive()) {
- enforceCallingPermission(
- android.Manifest.permission.BROADCAST_OPTION_INTERACTIVE,
- "setInteractive");
- }
- }
+ mBroadcastController.unregisterReceiver(receiver);
}
@GuardedBy("this")
@@ -15290,1033 +14231,14 @@
String[] excludedPackages, int appOp, Bundle bOptions, boolean ordered,
boolean sticky, int callingPid,
int callingUid, int realCallingUid, int realCallingPid, int userId) {
- return broadcastIntentLocked(callerApp, callerPackage, callerFeatureId, intent,
- resolvedType, null, resultTo, resultCode, resultData, resultExtras,
+ return mBroadcastController.broadcastIntentLocked(callerApp, callerPackage, callerFeatureId,
+ intent, resolvedType, null, resultTo, resultCode, resultData, resultExtras,
requiredPermissions, excludedPermissions, excludedPackages, appOp, bOptions,
ordered, sticky, callingPid, callingUid, realCallingUid, realCallingPid, userId,
BackgroundStartPrivileges.NONE,
null /* broadcastAllowList */, null /* filterExtrasForReceiver */);
}
- @GuardedBy("this")
- final int broadcastIntentLocked(ProcessRecord callerApp, String callerPackage,
- @Nullable String callerFeatureId, Intent intent, String resolvedType,
- ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode, String resultData,
- Bundle resultExtras, String[] requiredPermissions,
- String[] excludedPermissions, String[] excludedPackages, int appOp, Bundle bOptions,
- boolean ordered, boolean sticky, int callingPid, int callingUid,
- int realCallingUid, int realCallingPid, int userId,
- BackgroundStartPrivileges backgroundStartPrivileges,
- @Nullable int[] broadcastAllowList,
- @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) {
- final int cookie = traceBroadcastIntentBegin(intent, resultTo, ordered, sticky,
- callingUid, realCallingUid, userId);
- try {
- final BroadcastSentEventRecord broadcastSentEventRecord =
- new BroadcastSentEventRecord();
- final int res = broadcastIntentLockedTraced(callerApp, callerPackage, callerFeatureId,
- intent, resolvedType, resultToApp, resultTo, resultCode, resultData,
- resultExtras, requiredPermissions, excludedPermissions, excludedPackages,
- appOp, BroadcastOptions.fromBundleNullable(bOptions), ordered, sticky,
- callingPid, callingUid, realCallingUid, realCallingPid, userId,
- backgroundStartPrivileges, broadcastAllowList, filterExtrasForReceiver,
- broadcastSentEventRecord);
- broadcastSentEventRecord.setResult(res);
- broadcastSentEventRecord.logToStatsd();
- return res;
- } finally {
- traceBroadcastIntentEnd(cookie);
- }
- }
-
- private static int traceBroadcastIntentBegin(Intent intent, IIntentReceiver resultTo,
- boolean ordered, boolean sticky, int callingUid, int realCallingUid, int userId) {
- if (!Flags.traceReceiverRegistration()) {
- return BroadcastQueue.traceBegin("broadcastIntentLockedTraced");
- }
- if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
- final StringBuilder sb = new StringBuilder("broadcastIntent: ");
- sb.append(callingUid); sb.append('/');
- final String action = intent.getAction();
- sb.append(action == null ? null : action); sb.append('/');
- sb.append("0x"); sb.append(Integer.toHexString(intent.getFlags())); sb.append('/');
- sb.append(ordered ? "O" : "_");
- sb.append(sticky ? "S" : "_");
- sb.append(resultTo != null ? "C" : "_");
- sb.append('/');
- sb.append('u'); sb.append(userId);
- if (callingUid != realCallingUid) {
- sb.append('/');
- sb.append("sender="); sb.append(realCallingUid);
- }
- return BroadcastQueue.traceBegin(sb.toString());
- }
- return 0;
- }
-
- private static void traceBroadcastIntentEnd(int cookie) {
- if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
- BroadcastQueue.traceEnd(cookie);
- }
- }
-
- @GuardedBy("this")
- final int broadcastIntentLockedTraced(ProcessRecord callerApp, String callerPackage,
- @Nullable String callerFeatureId, Intent intent, String resolvedType,
- ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode, String resultData,
- Bundle resultExtras, String[] requiredPermissions,
- String[] excludedPermissions, String[] excludedPackages, int appOp,
- BroadcastOptions brOptions, boolean ordered, boolean sticky, int callingPid,
- int callingUid, int realCallingUid, int realCallingPid, int userId,
- BackgroundStartPrivileges backgroundStartPrivileges,
- @Nullable int[] broadcastAllowList,
- @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
- @NonNull BroadcastSentEventRecord broadcastSentEventRecord) {
- // Ensure all internal loopers are registered for idle checks
- BroadcastLoopers.addMyLooper();
-
- if (Process.isSdkSandboxUid(realCallingUid)) {
- final SdkSandboxManagerLocal sdkSandboxManagerLocal = LocalManagerRegistry.getManager(
- SdkSandboxManagerLocal.class);
- if (sdkSandboxManagerLocal == null) {
- throw new IllegalStateException("SdkSandboxManagerLocal not found when sending"
- + " a broadcast from an SDK sandbox uid.");
- }
- if (!sdkSandboxManagerLocal.canSendBroadcast(intent)) {
- throw new SecurityException(
- "Intent " + intent.getAction() + " may not be broadcast from an SDK sandbox"
- + " uid. Given caller package " + callerPackage + " (pid=" + callingPid
- + ", realCallingUid=" + realCallingUid + ", callingUid= " + callingUid
- + ")");
- }
- }
-
- if ((resultTo != null) && (resultToApp == null)) {
- if (resultTo.asBinder() instanceof BinderProxy) {
- // Warn when requesting results without a way to deliver them
- Slog.wtf(TAG, "Sending broadcast " + intent.getAction()
- + " with resultTo requires resultToApp", new Throwable());
- } else {
- // If not a BinderProxy above, then resultTo is an in-process
- // receiver, so splice in system_server process
- resultToApp = getProcessRecordLocked("system", SYSTEM_UID);
- }
- }
-
- intent = new Intent(intent);
- broadcastSentEventRecord.setIntent(intent);
- broadcastSentEventRecord.setOriginalIntentFlags(intent.getFlags());
- broadcastSentEventRecord.setSenderUid(callingUid);
- broadcastSentEventRecord.setRealSenderUid(realCallingUid);
- broadcastSentEventRecord.setSticky(sticky);
- broadcastSentEventRecord.setOrdered(ordered);
- broadcastSentEventRecord.setResultRequested(resultTo != null);
- final int callerAppProcessState = getRealProcessStateLocked(callerApp, realCallingPid);
- broadcastSentEventRecord.setSenderProcState(callerAppProcessState);
- broadcastSentEventRecord.setSenderUidState(getRealUidStateLocked(callerApp,
- realCallingPid));
-
- final boolean callerInstantApp = isInstantApp(callerApp, callerPackage, callingUid);
- // Instant Apps cannot use FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS
- if (callerInstantApp) {
- intent.setFlags(intent.getFlags() & ~Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
- }
-
- if (userId == UserHandle.USER_ALL && broadcastAllowList != null) {
- Slog.e(TAG, "broadcastAllowList only applies when sending to individual users. "
- + "Assuming restrictive whitelist.");
- broadcastAllowList = new int[]{};
- }
-
- // By default broadcasts do not go to stopped apps.
- intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);
-
- // If we have not finished booting, don't allow this to launch new processes.
- if (!mProcessesReady && (intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) == 0) {
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- }
-
- if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST,
- (sticky ? "Broadcast sticky: ": "Broadcast: ") + intent
- + " ordered=" + ordered + " userid=" + userId
- + " options=" + (brOptions == null ? "null" : brOptions.toBundle()));
- if ((resultTo != null) && !ordered) {
- if (!UserHandle.isCore(callingUid)) {
- String msg = "Unauthorized unordered resultTo broadcast "
- + intent + " sent from uid " + callingUid;
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- }
- }
-
- userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true,
- ALLOW_NON_FULL, "broadcast", callerPackage);
-
- // Make sure that the user who is receiving this broadcast or its parent is running.
- // If not, we will just skip it. Make an exception for shutdown broadcasts, upgrade steps.
- if (userId != UserHandle.USER_ALL && !mUserController.isUserOrItsParentRunning(userId)) {
- if ((callingUid != SYSTEM_UID
- || (intent.getFlags() & Intent.FLAG_RECEIVER_BOOT_UPGRADE) == 0)
- && !Intent.ACTION_SHUTDOWN.equals(intent.getAction())) {
- Slog.w(TAG, "Skipping broadcast of " + intent
- + ": user " + userId + " and its parent (if any) are stopped");
- scheduleCanceledResultTo(resultToApp, resultTo, intent, userId,
- brOptions, callingUid, callerPackage);
- return ActivityManager.BROADCAST_FAILED_USER_STOPPED;
- }
- }
-
- final String action = intent.getAction();
- if (brOptions != null) {
- if (brOptions.getTemporaryAppAllowlistDuration() > 0) {
- // See if the caller is allowed to do this. Note we are checking against
- // the actual real caller (not whoever provided the operation as say a
- // PendingIntent), because that who is actually supplied the arguments.
- if (checkComponentPermission(CHANGE_DEVICE_IDLE_TEMP_WHITELIST,
- realCallingPid, realCallingUid, -1, true)
- != PackageManager.PERMISSION_GRANTED
- && checkComponentPermission(START_ACTIVITIES_FROM_BACKGROUND,
- realCallingPid, realCallingUid, -1, true)
- != PackageManager.PERMISSION_GRANTED
- && checkComponentPermission(START_FOREGROUND_SERVICES_FROM_BACKGROUND,
- realCallingPid, realCallingUid, -1, true)
- != PackageManager.PERMISSION_GRANTED) {
- String msg = "Permission Denial: " + intent.getAction()
- + " broadcast from " + callerPackage + " (pid=" + callingPid
- + ", uid=" + callingUid + ")"
- + " requires "
- + CHANGE_DEVICE_IDLE_TEMP_WHITELIST + " or "
- + START_ACTIVITIES_FROM_BACKGROUND + " or "
- + START_FOREGROUND_SERVICES_FROM_BACKGROUND;
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- }
- }
- if (brOptions.isDontSendToRestrictedApps()
- && !isUidActiveLOSP(callingUid)
- && isBackgroundRestrictedNoCheck(callingUid, callerPackage)) {
- Slog.i(TAG, "Not sending broadcast " + action + " - app " + callerPackage
- + " has background restrictions");
- return ActivityManager.START_CANCELED;
- }
- if (brOptions.allowsBackgroundActivityStarts()) {
- // See if the caller is allowed to do this. Note we are checking against
- // the actual real caller (not whoever provided the operation as say a
- // PendingIntent), because that who is actually supplied the arguments.
- if (checkComponentPermission(
- android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND,
- realCallingPid, realCallingUid, -1, true)
- != PackageManager.PERMISSION_GRANTED) {
- String msg = "Permission Denial: " + intent.getAction()
- + " broadcast from " + callerPackage + " (pid=" + callingPid
- + ", uid=" + callingUid + ")"
- + " requires "
- + android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND;
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- } else {
- // We set the token to null since if it wasn't for it we'd allow anyway here
- backgroundStartPrivileges = BackgroundStartPrivileges.ALLOW_BAL;
- }
- }
-
- if (brOptions.getIdForResponseEvent() > 0) {
- enforcePermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS,
- callingPid, callingUid, "recordResponseEventWhileInBackground");
- }
- }
-
- // Verify that protected broadcasts are only being sent by system code,
- // and that system code is only sending protected broadcasts.
- final boolean isProtectedBroadcast;
- try {
- isProtectedBroadcast = AppGlobals.getPackageManager().isProtectedBroadcast(action);
- } catch (RemoteException e) {
- Slog.w(TAG, "Remote exception", e);
- scheduleCanceledResultTo(resultToApp, resultTo, intent,
- userId, brOptions, callingUid, callerPackage);
- return ActivityManager.BROADCAST_SUCCESS;
- }
-
- final boolean isCallerSystem;
- switch (UserHandle.getAppId(callingUid)) {
- case ROOT_UID:
- case SYSTEM_UID:
- case PHONE_UID:
- case BLUETOOTH_UID:
- case NFC_UID:
- case SE_UID:
- case NETWORK_STACK_UID:
- isCallerSystem = true;
- break;
- default:
- isCallerSystem = (callerApp != null) && callerApp.isPersistent();
- break;
- }
-
- // First line security check before anything else: stop non-system apps from
- // sending protected broadcasts.
- if (!isCallerSystem) {
- if (isProtectedBroadcast) {
- String msg = "Permission Denial: not allowed to send broadcast "
- + action + " from pid="
- + callingPid + ", uid=" + callingUid;
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
-
- } else if (AppWidgetManager.ACTION_APPWIDGET_CONFIGURE.equals(action)
- || AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
- // Special case for compatibility: we don't want apps to send this,
- // but historically it has not been protected and apps may be using it
- // to poke their own app widget. So, instead of making it protected,
- // just limit it to the caller.
- if (callerPackage == null) {
- String msg = "Permission Denial: not allowed to send broadcast "
- + action + " from unknown caller.";
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- } else if (intent.getComponent() != null) {
- // They are good enough to send to an explicit component... verify
- // it is being sent to the calling app.
- if (!intent.getComponent().getPackageName().equals(
- callerPackage)) {
- String msg = "Permission Denial: not allowed to send broadcast "
- + action + " to "
- + intent.getComponent().getPackageName() + " from "
- + callerPackage;
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- }
- } else {
- // Limit broadcast to their own package.
- intent.setPackage(callerPackage);
- }
- }
- }
-
- boolean timeoutExempt = false;
-
- if (action != null) {
- if (getBackgroundLaunchBroadcasts().contains(action)) {
- if (DEBUG_BACKGROUND_CHECK) {
- Slog.i(TAG, "Broadcast action " + action + " forcing include-background");
- }
- intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
- }
-
- // TODO: b/329211459 - Remove this after background remote intent is fixed.
- if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
- && getWearRemoteIntentAction().equals(action)) {
- final int callerProcState = callerApp != null
- ? callerApp.getCurProcState()
- : ActivityManager.PROCESS_STATE_NONEXISTENT;
- if (ActivityManager.RunningAppProcessInfo.procStateToImportance(callerProcState)
- > ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
- return ActivityManager.START_CANCELED;
- }
- }
-
- switch (action) {
- case Intent.ACTION_MEDIA_SCANNER_SCAN_FILE:
- UserManagerInternal umInternal = LocalServices.getService(
- UserManagerInternal.class);
- UserInfo userInfo = umInternal.getUserInfo(userId);
- if (userInfo != null && userInfo.isCloneProfile()) {
- userId = umInternal.getProfileParentId(userId);
- }
- break;
- case Intent.ACTION_UID_REMOVED:
- case Intent.ACTION_PACKAGE_REMOVED:
- case Intent.ACTION_PACKAGE_CHANGED:
- case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE:
- case Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE:
- case Intent.ACTION_PACKAGES_SUSPENDED:
- case Intent.ACTION_PACKAGES_UNSUSPENDED:
- // Handle special intents: if this broadcast is from the package
- // manager about a package being removed, we need to remove all of
- // its activities from the history stack.
- if (checkComponentPermission(
- android.Manifest.permission.BROADCAST_PACKAGE_REMOVED,
- callingPid, callingUid, -1, true)
- != PackageManager.PERMISSION_GRANTED) {
- String msg = "Permission Denial: " + intent.getAction()
- + " broadcast from " + callerPackage + " (pid=" + callingPid
- + ", uid=" + callingUid + ")"
- + " requires "
- + android.Manifest.permission.BROADCAST_PACKAGE_REMOVED;
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- }
- switch (action) {
- case Intent.ACTION_UID_REMOVED:
- final int uid = getUidFromIntent(intent);
- if (uid >= 0) {
- mBatteryStatsService.removeUid(uid);
- if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
- mAppOpsService.resetAllModes(UserHandle.getUserId(uid),
- intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME));
- } else {
- mAppOpsService.uidRemoved(uid);
- mServices.onUidRemovedLocked(uid);
- }
- }
- break;
- case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE:
- // If resources are unavailable just force stop all those packages
- // and flush the attribute cache as well.
- String list[] =
- intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
- if (list != null && list.length > 0) {
- for (int i = 0; i < list.length; i++) {
- forceStopPackageLocked(list[i], -1, false, true, true,
- false, false, false, userId, "storage unmount");
- }
- mAtmInternal.cleanupRecentTasksForUser(UserHandle.USER_ALL);
- sendPackageBroadcastLocked(
- ApplicationThreadConstants.EXTERNAL_STORAGE_UNAVAILABLE,
- list, userId);
- }
- break;
- case Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE:
- mAtmInternal.cleanupRecentTasksForUser(UserHandle.USER_ALL);
- break;
- case Intent.ACTION_PACKAGE_REMOVED:
- case Intent.ACTION_PACKAGE_CHANGED:
- Uri data = intent.getData();
- String ssp;
- if (data != null && (ssp=data.getSchemeSpecificPart()) != null) {
- boolean removed = Intent.ACTION_PACKAGE_REMOVED.equals(action);
- final boolean replacing =
- intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
- final boolean killProcess =
- !intent.getBooleanExtra(Intent.EXTRA_DONT_KILL_APP, false);
- final boolean fullUninstall = removed && !replacing;
-
- if (removed) {
- if (killProcess) {
- forceStopPackageLocked(ssp, UserHandle.getAppId(
- intent.getIntExtra(Intent.EXTRA_UID, -1)),
- false, true, true, false, fullUninstall, false,
- userId, "pkg removed");
- getPackageManagerInternal()
- .onPackageProcessKilledForUninstall(ssp);
- } else {
- // Kill any app zygotes always, since they can't fork new
- // processes with references to the old code
- forceStopAppZygoteLocked(ssp, UserHandle.getAppId(
- intent.getIntExtra(Intent.EXTRA_UID, -1)),
- userId);
- }
- final int cmd = killProcess
- ? ApplicationThreadConstants.PACKAGE_REMOVED
- : ApplicationThreadConstants.PACKAGE_REMOVED_DONT_KILL;
- sendPackageBroadcastLocked(cmd,
- new String[] {ssp}, userId);
- if (fullUninstall) {
- // Remove all permissions granted from/to this package
- mUgmInternal.removeUriPermissionsForPackage(ssp, userId,
- true, false);
-
- mAtmInternal.removeRecentTasksByPackageName(ssp, userId);
-
- mServices.forceStopPackageLocked(ssp, userId);
- mAtmInternal.onPackageUninstalled(ssp, userId);
- mBatteryStatsService.notePackageUninstalled(ssp);
- }
- } else {
- if (killProcess) {
- int reason;
- int subReason;
- if (replacing) {
- reason = ApplicationExitInfo.REASON_PACKAGE_UPDATED;
- subReason = ApplicationExitInfo.SUBREASON_UNKNOWN;
- } else {
- reason =
- ApplicationExitInfo.REASON_PACKAGE_STATE_CHANGE;
- subReason = ApplicationExitInfo.SUBREASON_UNKNOWN;
- }
-
- final int extraUid = intent.getIntExtra(Intent.EXTRA_UID,
- -1);
- synchronized (mProcLock) {
- mProcessList.killPackageProcessesLSP(ssp,
- UserHandle.getAppId(extraUid),
- userId, ProcessList.INVALID_ADJ,
- reason,
- subReason,
- "change " + ssp);
- }
- }
- cleanupDisabledPackageComponentsLocked(ssp, userId,
- intent.getStringArrayExtra(
- Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST));
- mServices.schedulePendingServiceStartLocked(ssp, userId);
- }
- }
- break;
- case Intent.ACTION_PACKAGES_SUSPENDED:
- case Intent.ACTION_PACKAGES_UNSUSPENDED:
- final boolean suspended = Intent.ACTION_PACKAGES_SUSPENDED.equals(
- intent.getAction());
- final String[] packageNames = intent.getStringArrayExtra(
- Intent.EXTRA_CHANGED_PACKAGE_LIST);
- final int userIdExtra = intent.getIntExtra(
- Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
-
- mAtmInternal.onPackagesSuspendedChanged(packageNames, suspended,
- userIdExtra);
-
- final boolean quarantined = intent.getBooleanExtra(
- Intent.EXTRA_QUARANTINED, false);
- if (suspended && quarantined && packageNames != null) {
- for (int i = 0; i < packageNames.length; i++) {
- forceStopPackage(packageNames[i], userId,
- ActivityManager.FLAG_OR_STOPPED, "quarantined");
- }
- }
-
- break;
- }
- break;
- case Intent.ACTION_PACKAGE_REPLACED:
- {
- final Uri data = intent.getData();
- final String ssp;
- if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {
- ApplicationInfo aInfo = null;
- try {
- aInfo = AppGlobals.getPackageManager()
- .getApplicationInfo(ssp, STOCK_PM_FLAGS, userId);
- } catch (RemoteException ignore) {}
- if (aInfo == null) {
- Slog.w(TAG, "Dropping ACTION_PACKAGE_REPLACED for non-existent pkg:"
- + " ssp=" + ssp + " data=" + data);
- scheduleCanceledResultTo(resultToApp, resultTo, intent,
- userId, brOptions, callingUid, callerPackage);
- return ActivityManager.BROADCAST_SUCCESS;
- }
- updateAssociationForApp(aInfo);
- mAtmInternal.onPackageReplaced(aInfo);
- mServices.updateServiceApplicationInfoLocked(aInfo);
- sendPackageBroadcastLocked(ApplicationThreadConstants.PACKAGE_REPLACED,
- new String[] {ssp}, userId);
- }
- break;
- }
- case Intent.ACTION_PACKAGE_ADDED:
- {
- // Special case for adding a package: by default turn on compatibility mode.
- Uri data = intent.getData();
- String ssp;
- if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {
- final boolean replacing =
- intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
- mAtmInternal.onPackageAdded(ssp, replacing);
-
- try {
- ApplicationInfo ai = AppGlobals.getPackageManager().
- getApplicationInfo(ssp, STOCK_PM_FLAGS, 0);
- mBatteryStatsService.notePackageInstalled(ssp,
- ai != null ? ai.longVersionCode : 0);
- } catch (RemoteException e) {
- }
- }
- break;
- }
- case Intent.ACTION_PACKAGE_DATA_CLEARED:
- {
- Uri data = intent.getData();
- String ssp;
- if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {
- mAtmInternal.onPackageDataCleared(ssp, userId);
- }
- break;
- }
- case Intent.ACTION_TIMEZONE_CHANGED:
- // If this is the time zone changed action, queue up a message that will reset
- // the timezone of all currently running processes. This message will get
- // queued up before the broadcast happens.
- mHandler.sendEmptyMessage(UPDATE_TIME_ZONE);
- break;
- case Intent.ACTION_TIME_CHANGED:
- // EXTRA_TIME_PREF_24_HOUR_FORMAT is optional so we must distinguish between
- // the tri-state value it may contain and "unknown".
- // For convenience we re-use the Intent extra values.
- final int NO_EXTRA_VALUE_FOUND = -1;
- final int timeFormatPreferenceMsgValue = intent.getIntExtra(
- Intent.EXTRA_TIME_PREF_24_HOUR_FORMAT,
- NO_EXTRA_VALUE_FOUND /* defaultValue */);
- // Only send a message if the time preference is available.
- if (timeFormatPreferenceMsgValue != NO_EXTRA_VALUE_FOUND) {
- Message updateTimePreferenceMsg =
- mHandler.obtainMessage(UPDATE_TIME_PREFERENCE_MSG,
- timeFormatPreferenceMsgValue, 0);
- mHandler.sendMessage(updateTimePreferenceMsg);
- }
- mBatteryStatsService.noteCurrentTimeChanged();
- break;
- case ConnectivityManager.ACTION_CLEAR_DNS_CACHE:
- mHandler.sendEmptyMessage(CLEAR_DNS_CACHE_MSG);
- break;
- case Proxy.PROXY_CHANGE_ACTION:
- mHandler.sendMessage(mHandler.obtainMessage(UPDATE_HTTP_PROXY_MSG));
- break;
- case android.hardware.Camera.ACTION_NEW_PICTURE:
- case android.hardware.Camera.ACTION_NEW_VIDEO:
- // In N we just turned these off; in O we are turing them back on partly,
- // only for registered receivers. This will still address the main problem
- // (a spam of apps waking up when a picture is taken putting significant
- // memory pressure on the system at a bad point), while still allowing apps
- // that are already actively running to know about this happening.
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- break;
- case android.security.KeyChain.ACTION_TRUST_STORE_CHANGED:
- mHandler.sendEmptyMessage(HANDLE_TRUST_STORAGE_UPDATE_MSG);
- break;
- case "com.android.launcher.action.INSTALL_SHORTCUT":
- // As of O, we no longer support this broadcasts, even for pre-O apps.
- // Apps should now be using ShortcutManager.pinRequestShortcut().
- Log.w(TAG, "Broadcast " + action
- + " no longer supported. It will not be delivered.");
- scheduleCanceledResultTo(resultToApp, resultTo, intent,
- userId, brOptions, callingUid, callerPackage);
- return ActivityManager.BROADCAST_SUCCESS;
- case Intent.ACTION_PRE_BOOT_COMPLETED:
- timeoutExempt = true;
- break;
- case Intent.ACTION_CLOSE_SYSTEM_DIALOGS:
- if (!mAtmInternal.checkCanCloseSystemDialogs(callingPid, callingUid,
- callerPackage)) {
- scheduleCanceledResultTo(resultToApp, resultTo, intent,
- userId, brOptions, callingUid, callerPackage);
- // Returning success seems to be the pattern here
- return ActivityManager.BROADCAST_SUCCESS;
- }
- break;
- }
-
- if (Intent.ACTION_PACKAGE_ADDED.equals(action) ||
- Intent.ACTION_PACKAGE_REMOVED.equals(action) ||
- Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
- final int uid = getUidFromIntent(intent);
- if (uid != -1) {
- final UidRecord uidRec = mProcessList.getUidRecordLOSP(uid);
- if (uidRec != null) {
- uidRec.updateHasInternetPermission();
- }
- }
- }
- }
-
- // Add to the sticky list if requested.
- if (sticky) {
- if (checkPermission(android.Manifest.permission.BROADCAST_STICKY,
- callingPid, callingUid)
- != PackageManager.PERMISSION_GRANTED) {
- String msg =
- "Permission Denial: broadcastIntent() requesting a sticky broadcast from"
- + " pid="
- + callingPid
- + ", uid="
- + callingUid
- + " requires "
- + android.Manifest.permission.BROADCAST_STICKY;
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- }
- if (requiredPermissions != null && requiredPermissions.length > 0) {
- Slog.w(TAG, "Can't broadcast sticky intent " + intent
- + " and enforce permissions " + Arrays.toString(requiredPermissions));
- scheduleCanceledResultTo(resultToApp, resultTo, intent,
- userId, brOptions, callingUid, callerPackage);
- return ActivityManager.BROADCAST_STICKY_CANT_HAVE_PERMISSION;
- }
- if (intent.getComponent() != null) {
- throw new SecurityException(
- "Sticky broadcasts can't target a specific component");
- }
- synchronized (mStickyBroadcasts) {
- // We use userId directly here, since the "all" target is maintained
- // as a separate set of sticky broadcasts.
- if (userId != UserHandle.USER_ALL) {
- // But first, if this is not a broadcast to all users, then
- // make sure it doesn't conflict with an existing broadcast to
- // all users.
- ArrayMap<String, ArrayList<StickyBroadcast>> stickies = mStickyBroadcasts.get(
- UserHandle.USER_ALL);
- if (stickies != null) {
- ArrayList<StickyBroadcast> list = stickies.get(intent.getAction());
- if (list != null) {
- int N = list.size();
- int i;
- for (i = 0; i < N; i++) {
- if (intent.filterEquals(list.get(i).intent)) {
- throw new IllegalArgumentException("Sticky broadcast " + intent
- + " for user " + userId
- + " conflicts with existing global broadcast");
- }
- }
- }
- }
- }
- ArrayMap<String, ArrayList<StickyBroadcast>> stickies =
- mStickyBroadcasts.get(userId);
- if (stickies == null) {
- stickies = new ArrayMap<>();
- mStickyBroadcasts.put(userId, stickies);
- }
- ArrayList<StickyBroadcast> list = stickies.get(intent.getAction());
- if (list == null) {
- list = new ArrayList<>();
- stickies.put(intent.getAction(), list);
- }
- final boolean deferUntilActive = BroadcastRecord.calculateDeferUntilActive(
- callingUid, brOptions, resultTo, ordered,
- BroadcastRecord.calculateUrgent(intent, brOptions));
- final int stickiesCount = list.size();
- int i;
- for (i = 0; i < stickiesCount; i++) {
- if (intent.filterEquals(list.get(i).intent)) {
- // This sticky already exists, replace it.
- list.set(i, StickyBroadcast.create(new Intent(intent), deferUntilActive,
- callingUid, callerAppProcessState, resolvedType));
- break;
- }
- }
- if (i >= stickiesCount) {
- list.add(StickyBroadcast.create(new Intent(intent), deferUntilActive,
- callingUid, callerAppProcessState, resolvedType));
- }
- }
- }
-
- int[] users;
- if (userId == UserHandle.USER_ALL) {
- // Caller wants broadcast to go to all started users.
- users = mUserController.getStartedUserArray();
- } else {
- // Caller wants broadcast to go to one specific user.
- users = new int[] {userId};
- }
-
- var args = new SaferIntentUtils.IntentArgs(intent, resolvedType,
- true /* isReceiver */, true /* resolveForStart */, callingUid, callingPid);
- args.platformCompat = mPlatformCompat;
-
- // Figure out who all will receive this broadcast.
- final int cookie = BroadcastQueue.traceBegin("queryReceivers");
- List receivers = null;
- List<BroadcastFilter> registeredReceivers = null;
- // Need to resolve the intent to interested receivers...
- if ((intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
- receivers = collectReceiverComponents(
- intent, resolvedType, callingUid, callingPid, users, broadcastAllowList);
- }
- if (intent.getComponent() == null) {
- final PackageDataSnapshot snapshot = getPackageManagerInternal().snapshot();
- if (userId == UserHandle.USER_ALL && callingUid == SHELL_UID) {
- // Query one target user at a time, excluding shell-restricted users
- for (int i = 0; i < users.length; i++) {
- if (mUserController.hasUserRestriction(
- UserManager.DISALLOW_DEBUGGING_FEATURES, users[i])) {
- continue;
- }
- List<BroadcastFilter> registeredReceiversForUser =
- mReceiverResolver.queryIntent(snapshot, intent,
- resolvedType, false /*defaultOnly*/, users[i]);
- if (registeredReceivers == null) {
- registeredReceivers = registeredReceiversForUser;
- } else if (registeredReceiversForUser != null) {
- registeredReceivers.addAll(registeredReceiversForUser);
- }
- }
- } else {
- registeredReceivers = mReceiverResolver.queryIntent(snapshot, intent,
- resolvedType, false /*defaultOnly*/, userId);
- }
- if (registeredReceivers != null) {
- SaferIntentUtils.blockNullAction(args, registeredReceivers);
- }
- }
- BroadcastQueue.traceEnd(cookie);
-
- final boolean replacePending =
- (intent.getFlags()&Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0;
-
- if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing broadcast: " + intent.getAction()
- + " replacePending=" + replacePending);
- if (registeredReceivers != null && broadcastAllowList != null) {
- // if a uid whitelist was provided, remove anything in the application space that wasn't
- // in it.
- for (int i = registeredReceivers.size() - 1; i >= 0; i--) {
- final int owningAppId = UserHandle.getAppId(registeredReceivers.get(i).owningUid);
- if (owningAppId >= Process.FIRST_APPLICATION_UID
- && Arrays.binarySearch(broadcastAllowList, owningAppId) < 0) {
- registeredReceivers.remove(i);
- }
- }
- }
-
- int NR = registeredReceivers != null ? registeredReceivers.size() : 0;
-
- // Merge into one list.
- int ir = 0;
- if (receivers != null) {
- // A special case for PACKAGE_ADDED: do not allow the package
- // being added to see this broadcast. This prevents them from
- // using this as a back door to get run as soon as they are
- // installed. Maybe in the future we want to have a special install
- // broadcast or such for apps, but we'd like to deliberately make
- // this decision.
- String skipPackages[] = null;
- if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())
- || Intent.ACTION_PACKAGE_RESTARTED.equals(intent.getAction())
- || Intent.ACTION_PACKAGE_DATA_CLEARED.equals(intent.getAction())) {
- Uri data = intent.getData();
- if (data != null) {
- String pkgName = data.getSchemeSpecificPart();
- if (pkgName != null) {
- skipPackages = new String[] { pkgName };
- }
- }
- } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(intent.getAction())) {
- skipPackages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
- }
- if (skipPackages != null && (skipPackages.length > 0)) {
- for (String skipPackage : skipPackages) {
- if (skipPackage != null) {
- int NT = receivers.size();
- for (int it=0; it<NT; it++) {
- ResolveInfo curt = (ResolveInfo)receivers.get(it);
- if (curt.activityInfo.packageName.equals(skipPackage)) {
- receivers.remove(it);
- it--;
- NT--;
- }
- }
- }
- }
- }
-
- int NT = receivers != null ? receivers.size() : 0;
- int it = 0;
- ResolveInfo curt = null;
- BroadcastFilter curr = null;
- while (it < NT && ir < NR) {
- if (curt == null) {
- curt = (ResolveInfo)receivers.get(it);
- }
- if (curr == null) {
- curr = registeredReceivers.get(ir);
- }
- if (curr.getPriority() >= curt.priority) {
- // Insert this broadcast record into the final list.
- receivers.add(it, curr);
- ir++;
- curr = null;
- it++;
- NT++;
- } else {
- // Skip to the next ResolveInfo in the final list.
- it++;
- curt = null;
- }
- }
- }
- while (ir < NR) {
- if (receivers == null) {
- receivers = new ArrayList();
- }
- receivers.add(registeredReceivers.get(ir));
- ir++;
- }
-
- if (isCallerSystem) {
- checkBroadcastFromSystem(intent, callerApp, callerPackage, callingUid,
- isProtectedBroadcast, receivers);
- }
-
- if ((receivers != null && receivers.size() > 0)
- || resultTo != null) {
- BroadcastQueue queue = mBroadcastQueue;
- SaferIntentUtils.filterNonExportedComponents(args, receivers);
- BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
- callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
- requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
- receivers, resultToApp, resultTo, resultCode, resultData, resultExtras,
- ordered, sticky, false, userId,
- backgroundStartPrivileges, timeoutExempt, filterExtrasForReceiver,
- callerAppProcessState);
- broadcastSentEventRecord.setBroadcastRecord(r);
-
- if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing ordered broadcast " + r);
- queue.enqueueBroadcastLocked(r);
- } else {
- // There was nobody interested in the broadcast, but we still want to record
- // that it happened.
- if (intent.getComponent() == null && intent.getPackage() == null
- && (intent.getFlags()&Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
- // This was an implicit broadcast... let's record it for posterity.
- addBroadcastStatLocked(intent.getAction(), callerPackage, 0, 0, 0);
- }
- }
-
- return ActivityManager.BROADCAST_SUCCESS;
- }
-
- @GuardedBy("this")
- private void scheduleCanceledResultTo(ProcessRecord resultToApp, IIntentReceiver resultTo,
- Intent intent, int userId, BroadcastOptions options, int callingUid,
- String callingPackage) {
- if (resultTo == null) {
- return;
- }
- final ProcessRecord app = resultToApp;
- final IApplicationThread thread = (app != null) ? app.getOnewayThread() : null;
- if (thread != null) {
- try {
- final boolean shareIdentity = (options != null && options.isShareIdentityEnabled());
- thread.scheduleRegisteredReceiver(
- resultTo, intent, Activity.RESULT_CANCELED, null, null,
- false, false, true, userId, app.mState.getReportedProcState(),
- shareIdentity ? callingUid : Process.INVALID_UID,
- shareIdentity ? callingPackage : null);
- } catch (RemoteException e) {
- final String msg = "Failed to schedule result of " + intent + " via "
- + app + ": " + e;
- app.killLocked("Can't schedule resultTo", ApplicationExitInfo.REASON_OTHER,
- ApplicationExitInfo.SUBREASON_UNDELIVERED_BROADCAST, true);
- Slog.d(TAG, msg);
- }
- }
- }
-
- @GuardedBy("this")
- private int getRealProcessStateLocked(ProcessRecord app, int pid) {
- if (app == null) {
- synchronized (mPidsSelfLocked) {
- app = mPidsSelfLocked.get(pid);
- }
- }
- if (app != null && app.getThread() != null && !app.isKilled()) {
- return app.mState.getCurProcState();
- }
- return PROCESS_STATE_NONEXISTENT;
- }
-
- @GuardedBy("this")
- private int getRealUidStateLocked(ProcessRecord app, int pid) {
- if (app == null) {
- synchronized (mPidsSelfLocked) {
- app = mPidsSelfLocked.get(pid);
- }
- }
- if (app != null && app.getThread() != null && !app.isKilled()) {
- final UidRecord uidRecord = app.getUidRecord();
- if (uidRecord != null) {
- return uidRecord.getCurProcState();
- }
- }
- return PROCESS_STATE_NONEXISTENT;
- }
-
- @VisibleForTesting
- ArrayList<StickyBroadcast> getStickyBroadcastsForTest(String action, int userId) {
- synchronized (mStickyBroadcasts) {
- final ArrayMap<String, ArrayList<StickyBroadcast>> stickyBroadcasts =
- mStickyBroadcasts.get(userId);
- if (stickyBroadcasts == null) {
- return null;
- }
- return stickyBroadcasts.get(action);
- }
- }
-
- /**
- * @return uid from the extra field {@link Intent#EXTRA_UID} if present, Otherwise -1
- */
- private int getUidFromIntent(Intent intent) {
- if (intent == null) {
- return -1;
- }
- final Bundle intentExtras = intent.getExtras();
- return intent.hasExtra(Intent.EXTRA_UID)
- ? intentExtras.getInt(Intent.EXTRA_UID) : -1;
- }
-
- final void rotateBroadcastStatsIfNeededLocked() {
- final long now = SystemClock.elapsedRealtime();
- if (mCurBroadcastStats == null ||
- (mCurBroadcastStats.mStartRealtime +(24*60*60*1000) < now)) {
- mLastBroadcastStats = mCurBroadcastStats;
- if (mLastBroadcastStats != null) {
- mLastBroadcastStats.mEndRealtime = SystemClock.elapsedRealtime();
- mLastBroadcastStats.mEndUptime = SystemClock.uptimeMillis();
- }
- mCurBroadcastStats = new BroadcastStats();
- }
- }
-
- final void addBroadcastStatLocked(String action, String srcPackage, int receiveCount,
- int skipCount, long dispatchTime) {
- rotateBroadcastStatsIfNeededLocked();
- mCurBroadcastStats.addBroadcast(action, srcPackage, receiveCount, skipCount, dispatchTime);
- }
-
- final void addBackgroundCheckViolationLocked(String action, String targetPackage) {
- rotateBroadcastStatsIfNeededLocked();
- mCurBroadcastStats.addBackgroundCheckViolation(action, targetPackage);
- }
-
- final void notifyBroadcastFinishedLocked(@NonNull BroadcastRecord original) {
- final ApplicationInfo info = original.callerApp != null ? original.callerApp.info : null;
- final String callerPackage = info != null ? info.packageName : original.callerPackage;
- if (callerPackage != null) {
- mHandler.obtainMessage(ActivityManagerService.DISPATCH_SENDING_BROADCAST_EVENT,
- original.callingUid, 0, callerPackage).sendToTarget();
- }
- }
-
- final Intent verifyBroadcastLocked(Intent intent) {
- if (intent != null) {
- intent.prepareToEnterSystemServer();
- }
-
- int flags = intent.getFlags();
-
- if (!mProcessesReady) {
- // if the caller really truly claims to know what they're doing, go
- // ahead and allow the broadcast without launching any receivers
- if ((flags&Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT) != 0) {
- // This will be turned into a FLAG_RECEIVER_REGISTERED_ONLY later on if needed.
- } else if ((flags&Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
- Slog.e(TAG, "Attempt to launch receivers of broadcast intent " + intent
- + " before boot completion");
- throw new IllegalStateException("Cannot broadcast before boot completed");
- }
- }
-
- if ((flags&Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0) {
- throw new IllegalArgumentException(
- "Can't use FLAG_RECEIVER_BOOT_UPGRADE here");
- }
-
- if ((flags & Intent.FLAG_RECEIVER_FROM_SHELL) != 0) {
- switch (Binder.getCallingUid()) {
- case ROOT_UID:
- case SHELL_UID:
- break;
- default:
- Slog.w(TAG, "Removing FLAG_RECEIVER_FROM_SHELL because caller is UID "
- + Binder.getCallingUid());
- intent.removeFlags(Intent.FLAG_RECEIVER_FROM_SHELL);
- break;
- }
- }
-
- return intent;
- }
-
/**
* @deprecated Use {@link #broadcastIntentWithFeature}
*/
@@ -16338,110 +14260,14 @@
String[] requiredPermissions, String[] excludedPermissions,
String[] excludedPackages, int appOp, Bundle bOptions,
boolean serialized, boolean sticky, int userId) {
- enforceNotIsolatedCaller("broadcastIntent");
-
- synchronized(this) {
- intent = verifyBroadcastLocked(intent);
-
- final ProcessRecord callerApp = getRecordForAppLOSP(caller);
- final int callingPid = Binder.getCallingPid();
- final int callingUid = Binder.getCallingUid();
-
- // We're delivering the result to the caller
- final ProcessRecord resultToApp = callerApp;
-
- // Permission regimes around sender-supplied broadcast options.
- enforceBroadcastOptionPermissionsInternal(bOptions, callingUid);
-
- final ComponentName cn = intent.getComponent();
-
- Trace.traceBegin(
- Trace.TRACE_TAG_ACTIVITY_MANAGER,
- "broadcastIntent:" + (cn != null ? cn.toString() : intent.getAction()));
-
- final long origId = Binder.clearCallingIdentity();
- try {
- return broadcastIntentLocked(callerApp,
- callerApp != null ? callerApp.info.packageName : null, callingFeatureId,
- intent, resolvedType, resultToApp, resultTo, resultCode, resultData,
- resultExtras, requiredPermissions, excludedPermissions, excludedPackages,
- appOp, bOptions, serialized, sticky, callingPid, callingUid, callingUid,
- callingPid, userId, BackgroundStartPrivileges.NONE, null, null);
- } finally {
- Binder.restoreCallingIdentity(origId);
- Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
- }
- }
- }
-
- // Not the binder call surface
- int broadcastIntentInPackage(String packageName, @Nullable String featureId, int uid,
- int realCallingUid, int realCallingPid, Intent intent, String resolvedType,
- ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode,
- String resultData, Bundle resultExtras, String requiredPermission, Bundle bOptions,
- boolean serialized, boolean sticky, int userId,
- BackgroundStartPrivileges backgroundStartPrivileges,
- @Nullable int[] broadcastAllowList) {
- synchronized(this) {
- intent = verifyBroadcastLocked(intent);
-
- final long origId = Binder.clearCallingIdentity();
- String[] requiredPermissions = requiredPermission == null ? null
- : new String[] {requiredPermission};
- try {
- return broadcastIntentLocked(null, packageName, featureId, intent, resolvedType,
- resultToApp, resultTo, resultCode, resultData, resultExtras,
- requiredPermissions, null, null, OP_NONE, bOptions, serialized, sticky, -1,
- uid, realCallingUid, realCallingPid, userId,
- backgroundStartPrivileges, broadcastAllowList,
- null /* filterExtrasForReceiver */);
- } finally {
- Binder.restoreCallingIdentity(origId);
- }
- }
+ return mBroadcastController.broadcastIntentWithFeature(caller, callingFeatureId, intent,
+ resolvedType, resultTo, resultCode, resultData, resultExtras, requiredPermissions,
+ excludedPermissions, excludedPackages, appOp, bOptions, serialized, sticky, userId);
}
@Override
public final void unbroadcastIntent(IApplicationThread caller, Intent intent, int userId) {
- // Refuse possible leaked file descriptors
- if (intent != null && intent.hasFileDescriptors() == true) {
- throw new IllegalArgumentException("File descriptors passed in Intent");
- }
-
- userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
- userId, true, ALLOW_NON_FULL, "removeStickyBroadcast", null);
-
- if (checkCallingPermission(android.Manifest.permission.BROADCAST_STICKY)
- != PackageManager.PERMISSION_GRANTED) {
- String msg = "Permission Denial: unbroadcastIntent() from pid="
- + Binder.getCallingPid()
- + ", uid=" + Binder.getCallingUid()
- + " requires " + android.Manifest.permission.BROADCAST_STICKY;
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- }
- synchronized (mStickyBroadcasts) {
- ArrayMap<String, ArrayList<StickyBroadcast>> stickies = mStickyBroadcasts.get(userId);
- if (stickies != null) {
- ArrayList<StickyBroadcast> list = stickies.get(intent.getAction());
- if (list != null) {
- int N = list.size();
- int i;
- for (i = 0; i < N; i++) {
- if (intent.filterEquals(list.get(i).intent)) {
- list.remove(i);
- break;
- }
- }
- if (list.size() <= 0) {
- stickies.remove(intent.getAction());
- }
- }
- if (stickies.size() <= 0) {
- mStickyBroadcasts.remove(userId);
- }
- }
- }
+ mBroadcastController.unbroadcastIntent(caller, intent, userId);
}
void backgroundServicesFinishedLocked(int userId) {
@@ -16450,31 +14276,32 @@
public void finishReceiver(IBinder caller, int resultCode, String resultData,
Bundle resultExtras, boolean resultAbort, int flags) {
- if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Finish receiver: " + caller);
+ mBroadcastController.finishReceiver(caller, resultCode, resultData, resultExtras,
+ resultAbort, flags);
+ }
- // Refuse possible leaked file descriptors
- if (resultExtras != null && resultExtras.hasFileDescriptors()) {
- throw new IllegalArgumentException("File descriptors passed in Bundle");
- }
+ @VisibleForTesting
+ ArrayList<BroadcastController.StickyBroadcast> getStickyBroadcastsForTest(String action,
+ int userId) {
+ return mBroadcastController.getStickyBroadcastsForTest(action, userId);
+ }
- final long origId = Binder.clearCallingIdentity();
- try {
- synchronized(this) {
- final ProcessRecord callerApp = getRecordForAppLOSP(caller);
- if (callerApp == null) {
- Slog.w(TAG, "finishReceiver: no app for " + caller);
- return;
- }
+ final void notifyBroadcastFinishedLocked(@NonNull BroadcastRecord original) {
+ mBroadcastController.notifyBroadcastFinishedLocked(original);
+ }
- mBroadcastQueue.finishReceiverLocked(callerApp, resultCode,
- resultData, resultExtras, resultAbort, true);
- // updateOomAdjLocked() will be done here
- trimApplicationsLocked(false, OOM_ADJ_REASON_FINISH_RECEIVER);
- }
+ final void addBroadcastStatLocked(String action, String srcPackage, int receiveCount,
+ int skipCount, long dispatchTime) {
+ mBroadcastController.addBroadcastStatLocked(action, srcPackage, receiveCount, skipCount,
+ dispatchTime);
+ }
- } finally {
- Binder.restoreCallingIdentity(origId);
- }
+ final void addBackgroundCheckViolationLocked(String action, String targetPackage) {
+ mBroadcastController.addBackgroundCheckViolationLocked(action, targetPackage);
+ }
+
+ void removeReceiverLocked(ReceiverList rl) {
+ mBroadcastController.removeReceiverLocked(rl);
}
// =========================================================
@@ -17839,7 +15666,7 @@
}
@GuardedBy("this")
- private void trimApplicationsLocked(boolean forceFullOomAdj, @OomAdjReason int oomAdjReason) {
+ void trimApplicationsLocked(boolean forceFullOomAdj, @OomAdjReason int oomAdjReason) {
// First remove any unused application processes whose package
// has been removed.
boolean didSomething = false;
@@ -18829,7 +16656,7 @@
@Override
public void enforceBroadcastOptionsPermissions(Bundle options, int callingUid) {
- enforceBroadcastOptionPermissionsInternal(options, callingUid);
+ mBroadcastController.enforceBroadcastOptionPermissionsInternal(options, callingUid);
}
/**
@@ -19219,7 +17046,7 @@
@Nullable int[] broadcastAllowList) {
synchronized (ActivityManagerService.this) {
final ProcessRecord resultToApp = getRecordForAppLOSP(resultToThread);
- return ActivityManagerService.this.broadcastIntentInPackage(packageName, featureId,
+ return mBroadcastController.broadcastIntentInPackage(packageName, featureId,
uid, realCallingUid, realCallingPid, intent, resolvedType, resultToApp,
resultTo, resultCode, resultData, resultExtras, requiredPermission,
bOptions, serialized, sticky, userId,
@@ -19236,13 +17063,13 @@
@Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
@Nullable Bundle bOptions) {
synchronized (ActivityManagerService.this) {
- intent = verifyBroadcastLocked(intent);
+ intent = mBroadcastController.verifyBroadcastLocked(intent);
final int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
final long origId = Binder.clearCallingIdentity();
try {
- return ActivityManagerService.this.broadcastIntentLocked(null /*callerApp*/,
+ return mBroadcastController.broadcastIntentLocked(null /*callerApp*/,
null /*callerPackage*/, null /*callingFeatureId*/, intent,
null /* resolvedType */, null /* resultToApp */, resultTo,
0 /* resultCode */, null /* resultData */,
@@ -21159,26 +18986,6 @@
}
}
- /**
- * Gets an {@code int} argument from the given {@code index} on {@code args}, logging an error
- * message on {@code pw} when it cannot be parsed.
- *
- * Returns {@code int} argument or {@code invalidValue} if it could not be parsed.
- */
- private static int getIntArg(PrintWriter pw, String[] args, int index, int invalidValue) {
- if (index > args.length) {
- pw.println("Missing argument");
- return invalidValue;
- }
- String arg = args[index];
- try {
- return Integer.parseInt(arg);
- } catch (Exception e) {
- pw.printf("Non-numeric argument at index %d: %s\n", index, arg);
- return invalidValue;
- }
- }
-
private void notifyMediaProjectionEvent(int uid, @NonNull IBinder projectionToken,
@MediaProjectionTokenEvent int event) {
synchronized (mMediaProjectionTokenMap) {
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 4a7ad31..1b00cec 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -16,7 +16,6 @@
package com.android.server.am;
-import static android.app.ApplicationStartInfo.START_TIMESTAMP_LAUNCH;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
@@ -51,6 +50,8 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ProcessMap;
+import com.android.internal.os.Clock;
+import com.android.internal.os.MonotonicClock;
import com.android.server.IoThread;
import com.android.server.ServiceThread;
import com.android.server.SystemServiceManager;
@@ -107,6 +108,16 @@
@VisibleForTesting boolean mEnabled = false;
+ /**
+ * Monotonic clock which does not reset on reboot.
+ *
+ * Time for offset is persisted along with records, see {@link #persistProcessStartInfo}.
+ * This does not follow the recommendation of {@link MonotonicClock} to persist on shutdown as
+ * it's ok in this case to lose any time change past the last persist as records added since
+ * then will be lost as well and the purpose of this clock is to keep records in order.
+ */
+ @VisibleForTesting MonotonicClock mMonotonicClock = null;
+
/** Initialized in {@link #init} and read-only after that. */
@VisibleForTesting ActivityManagerService mService;
@@ -203,6 +214,15 @@
IoThread.getHandler().post(() -> {
loadExistingProcessStartInfo();
});
+
+ if (mMonotonicClock == null) {
+ // This should only happen if there are no persisted records, or if the records were
+ // persisted by a version without the monotonic clock. Either way, create a new clock
+ // with no offset. In the case of records with no monotonic time the value will default
+ // to 0 and all new records will correctly end up in front of them.
+ mMonotonicClock = new MonotonicClock(Clock.SYSTEM_CLOCK.elapsedRealtime(),
+ Clock.SYSTEM_CLOCK);
+ }
}
/**
@@ -264,7 +284,7 @@
if (!mEnabled) {
return;
}
- ApplicationStartInfo start = new ApplicationStartInfo();
+ ApplicationStartInfo start = new ApplicationStartInfo(getMonotonicTime());
start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
start.setIntent(intent);
start.setStartType(ApplicationStartInfo.START_TYPE_UNSET);
@@ -396,7 +416,7 @@
if (!mEnabled) {
return;
}
- ApplicationStartInfo start = new ApplicationStartInfo();
+ ApplicationStartInfo start = new ApplicationStartInfo(getMonotonicTime());
addBaseFieldsFromProcessRecord(start, app);
start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
start.addStartupTimestamp(
@@ -422,7 +442,7 @@
if (!mEnabled) {
return;
}
- ApplicationStartInfo start = new ApplicationStartInfo();
+ ApplicationStartInfo start = new ApplicationStartInfo(getMonotonicTime());
addBaseFieldsFromProcessRecord(start, app);
start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
start.addStartupTimestamp(
@@ -444,7 +464,7 @@
if (!mEnabled) {
return;
}
- ApplicationStartInfo start = new ApplicationStartInfo();
+ ApplicationStartInfo start = new ApplicationStartInfo(getMonotonicTime());
addBaseFieldsFromProcessRecord(start, app);
start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
start.addStartupTimestamp(
@@ -461,7 +481,7 @@
if (!mEnabled) {
return;
}
- ApplicationStartInfo start = new ApplicationStartInfo();
+ ApplicationStartInfo start = new ApplicationStartInfo(getMonotonicTime());
addBaseFieldsFromProcessRecord(start, app);
start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
start.addStartupTimestamp(
@@ -632,7 +652,8 @@
Collections.sort(
list, (a, b) ->
- Long.compare(getStartTimestamp(b), getStartTimestamp(a)));
+ Long.compare(b.getMonoticCreationTimeMs(),
+ a.getMonoticCreationTimeMs()));
int size = list.size();
if (maxNum > 0) {
size = Math.min(size, maxNum);
@@ -898,6 +919,10 @@
case (int) AppsStartInfoProto.PACKAGES:
loadPackagesFromProto(proto, next);
break;
+ case (int) AppsStartInfoProto.MONOTONIC_TIME:
+ long monotonicTime = proto.readLong(AppsStartInfoProto.MONOTONIC_TIME);
+ mMonotonicClock = new MonotonicClock(monotonicTime, Clock.SYSTEM_CLOCK);
+ break;
}
}
} catch (IOException | IllegalArgumentException | WireTypeMismatchException
@@ -979,6 +1004,7 @@
mLastAppStartInfoPersistTimestamp = now;
}
}
+ proto.write(AppsStartInfoProto.MONOTONIC_TIME, getMonotonicTime());
if (succeeded) {
proto.flush();
af.finishWrite(out);
@@ -1099,13 +1125,12 @@
}
}
- /** Convenience method to obtain timestamp of beginning of start.*/
- private static long getStartTimestamp(ApplicationStartInfo startInfo) {
- if (startInfo.getStartupTimestamps() == null
- || !startInfo.getStartupTimestamps().containsKey(START_TIMESTAMP_LAUNCH)) {
- return -1;
+ private long getMonotonicTime() {
+ if (mMonotonicClock == null) {
+ // This should never happen. Return 0 to not interfere with past or future records.
+ return 0;
}
- return startInfo.getStartupTimestamps().get(START_TIMESTAMP_LAUNCH);
+ return mMonotonicClock.monotonicTime();
}
/** A container class of (@link android.app.ApplicationStartInfo) */
@@ -1143,7 +1168,7 @@
// Sort records so we can remove the least recent ones.
Collections.sort(mInfos, (a, b) ->
- Long.compare(getStartTimestamp(b), getStartTimestamp(a)));
+ Long.compare(b.getMonoticCreationTimeMs(), a.getMonoticCreationTimeMs()));
// Remove records and trim list object back to size.
mInfos.subList(0, mInfos.size() - getMaxCapacity()).clear();
@@ -1165,8 +1190,8 @@
long oldestTimeStamp = Long.MAX_VALUE;
for (int i = 0; i < size; i++) {
ApplicationStartInfo startInfo = mInfos.get(i);
- if (getStartTimestamp(startInfo) < oldestTimeStamp) {
- oldestTimeStamp = getStartTimestamp(startInfo);
+ if (startInfo.getMonoticCreationTimeMs() < oldestTimeStamp) {
+ oldestTimeStamp = startInfo.getMonoticCreationTimeMs();
oldestIndex = i;
}
}
@@ -1176,7 +1201,7 @@
}
mInfos.add(info);
Collections.sort(mInfos, (a, b) ->
- Long.compare(getStartTimestamp(b), getStartTimestamp(a)));
+ Long.compare(b.getMonoticCreationTimeMs(), a.getMonoticCreationTimeMs()));
}
/**
@@ -1337,7 +1362,9 @@
mUid = proto.readInt(AppsStartInfoProto.Package.User.UID);
break;
case (int) AppsStartInfoProto.Package.User.APP_START_INFO:
- ApplicationStartInfo info = new ApplicationStartInfo();
+ // Create record with monotonic time 0 in case the persisted record does not
+ // have a create time.
+ ApplicationStartInfo info = new ApplicationStartInfo(0);
info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO);
mInfos.add(info);
break;
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
new file mode 100644
index 0000000..32026b2
--- /dev/null
+++ b/services/core/java/com/android/server/am/BroadcastController.java
@@ -0,0 +1,2410 @@
+/*
+ * 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.server.am;
+
+import static android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST;
+import static android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND;
+import static android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND;
+import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT;
+import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
+import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
+import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FINISH_RECEIVER;
+import static android.app.AppOpsManager.OP_NONE;
+import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
+import static android.os.Process.BLUETOOTH_UID;
+import static android.os.Process.FIRST_APPLICATION_UID;
+import static android.os.Process.NETWORK_STACK_UID;
+import static android.os.Process.NFC_UID;
+import static android.os.Process.PHONE_UID;
+import static android.os.Process.ROOT_UID;
+import static android.os.Process.SE_UID;
+import static android.os.Process.SHELL_UID;
+import static android.os.Process.SYSTEM_UID;
+
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKGROUND_CHECK;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_LIGHT;
+import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_BROADCAST;
+import static com.android.server.am.ActivityManagerService.CLEAR_DNS_CACHE_MSG;
+import static com.android.server.am.ActivityManagerService.HANDLE_TRUST_STORAGE_UPDATE_MSG;
+import static com.android.server.am.ActivityManagerService.STOCK_PM_FLAGS;
+import static com.android.server.am.ActivityManagerService.TAG;
+import static com.android.server.am.ActivityManagerService.UPDATE_HTTP_PROXY_MSG;
+import static com.android.server.am.ActivityManagerService.UPDATE_TIME_PREFERENCE_MSG;
+import static com.android.server.am.ActivityManagerService.UPDATE_TIME_ZONE;
+import static com.android.server.am.ActivityManagerService.checkComponentPermission;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.app.ApplicationExitInfo;
+import android.app.ApplicationThreadConstants;
+import android.app.BackgroundStartPrivileges;
+import android.app.BroadcastOptions;
+import android.app.IApplicationThread;
+import android.app.compat.CompatChanges;
+import android.appwidget.AppWidgetManager;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.media.audiofx.AudioEffect;
+import android.net.ConnectivityManager;
+import android.net.Proxy;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.BinderProxy;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.text.style.SuggestionSpan;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.PrintWriterPrinter;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.IntentResolver;
+import com.android.server.LocalManagerRegistry;
+import com.android.server.LocalServices;
+import com.android.server.SystemConfig;
+import com.android.server.pm.Computer;
+import com.android.server.pm.SaferIntentUtils;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.pm.snapshot.PackageDataSnapshot;
+import com.android.server.sdksandbox.SdkSandboxManagerLocal;
+import com.android.server.utils.Slogf;
+
+import dalvik.annotation.optimization.NeverCompile;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+class BroadcastController {
+ private static final String TAG_BROADCAST = TAG + POSTFIX_BROADCAST;
+
+ /**
+ * It is now required for apps to explicitly set either
+ * {@link android.content.Context#RECEIVER_EXPORTED} or
+ * {@link android.content.Context#RECEIVER_NOT_EXPORTED} when registering a receiver for an
+ * unprotected broadcast in code.
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private static final long DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED = 161145287L;
+
+ // Maximum number of receivers an app can register.
+ private static final int MAX_RECEIVERS_ALLOWED_PER_APP = 1000;
+
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final ActivityManagerService mService;
+ @NonNull
+ private BroadcastQueue mBroadcastQueue;
+
+ @GuardedBy("mService")
+ BroadcastStats mLastBroadcastStats;
+
+ @GuardedBy("mService")
+ BroadcastStats mCurBroadcastStats;
+
+ /**
+ * Broadcast actions that will always be deliverable to unlaunched/background apps
+ */
+ @GuardedBy("mService")
+ private ArraySet<String> mBackgroundLaunchBroadcasts;
+
+ /**
+ * State of all active sticky broadcasts per user. Keys are the action of the
+ * sticky Intent, values are an ArrayList of all broadcasted intents with
+ * that action (which should usually be one). The SparseArray is keyed
+ * by the user ID the sticky is for, and can include UserHandle.USER_ALL
+ * for stickies that are sent to all users.
+ */
+ @GuardedBy("mStickyBroadcasts")
+ final SparseArray<ArrayMap<String, ArrayList<StickyBroadcast>>> mStickyBroadcasts =
+ new SparseArray<>();
+
+ /**
+ * Keeps track of all IIntentReceivers that have been registered for broadcasts.
+ * Hash keys are the receiver IBinder, hash value is a ReceiverList.
+ */
+ @GuardedBy("mService")
+ final HashMap<IBinder, ReceiverList> mRegisteredReceivers = new HashMap<>();
+
+ /**
+ * Resolver for broadcast intents to registered receivers.
+ * Holds BroadcastFilter (subclass of IntentFilter).
+ */
+ final IntentResolver<BroadcastFilter, BroadcastFilter> mReceiverResolver =
+ new IntentResolver<>() {
+ @Override
+ protected boolean allowFilterResult(
+ BroadcastFilter filter, List<BroadcastFilter> dest) {
+ IBinder target = filter.receiverList.receiver.asBinder();
+ for (int i = dest.size() - 1; i >= 0; i--) {
+ if (dest.get(i).receiverList.receiver.asBinder() == target) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected BroadcastFilter newResult(@NonNull Computer computer, BroadcastFilter filter,
+ int match, int userId, long customFlags) {
+ if (userId == UserHandle.USER_ALL || filter.owningUserId == UserHandle.USER_ALL
+ || userId == filter.owningUserId) {
+ return super.newResult(computer, filter, match, userId, customFlags);
+ }
+ return null;
+ }
+
+ @Override
+ protected IntentFilter getIntentFilter(@NonNull BroadcastFilter input) {
+ return input;
+ }
+
+ @Override
+ protected BroadcastFilter[] newArray(int size) {
+ return new BroadcastFilter[size];
+ }
+
+ @Override
+ protected boolean isPackageForFilter(String packageName, BroadcastFilter filter) {
+ return packageName.equals(filter.packageName);
+ }
+ };
+
+ BroadcastController(Context context, ActivityManagerService service, BroadcastQueue queue) {
+ mContext = context;
+ mService = service;
+ mBroadcastQueue = queue;
+ }
+
+ void setBroadcastQueueForTest(BroadcastQueue broadcastQueue) {
+ mBroadcastQueue = broadcastQueue;
+ }
+
+ Intent registerReceiverWithFeature(IApplicationThread caller, String callerPackage,
+ String callerFeatureId, String receiverId, IIntentReceiver receiver,
+ IntentFilter filter, String permission, int userId, int flags) {
+ traceRegistrationBegin(receiverId, receiver, filter, userId);
+ try {
+ return registerReceiverWithFeatureTraced(caller, callerPackage, callerFeatureId,
+ receiverId, receiver, filter, permission, userId, flags);
+ } finally {
+ traceRegistrationEnd();
+ }
+ }
+
+ private static void traceRegistrationBegin(String receiverId, IIntentReceiver receiver,
+ IntentFilter filter, int userId) {
+ if (!Flags.traceReceiverRegistration()) {
+ return;
+ }
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ final StringBuilder sb = new StringBuilder("registerReceiver: ");
+ sb.append(Binder.getCallingUid()); sb.append('/');
+ sb.append(receiverId == null ? "null" : receiverId); sb.append('/');
+ final int actionsCount = filter.safeCountActions();
+ if (actionsCount > 0) {
+ for (int i = 0; i < actionsCount; ++i) {
+ sb.append(filter.getAction(i));
+ if (i != actionsCount - 1) sb.append(',');
+ }
+ } else {
+ sb.append("null");
+ }
+ sb.append('/');
+ sb.append('u'); sb.append(userId); sb.append('/');
+ sb.append(receiver == null ? "null" : receiver.asBinder());
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, sb.toString());
+ }
+ }
+
+ private static void traceRegistrationEnd() {
+ if (!Flags.traceReceiverRegistration()) {
+ return;
+ }
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ }
+ }
+
+ private Intent registerReceiverWithFeatureTraced(IApplicationThread caller,
+ String callerPackage, String callerFeatureId, String receiverId,
+ IIntentReceiver receiver, IntentFilter filter, String permission,
+ int userId, int flags) {
+ mService.enforceNotIsolatedCaller("registerReceiver");
+ ArrayList<StickyBroadcast> stickyBroadcasts = null;
+ ProcessRecord callerApp = null;
+ final boolean visibleToInstantApps =
+ (flags & Context.RECEIVER_VISIBLE_TO_INSTANT_APPS) != 0;
+
+ int callingUid;
+ int callingPid;
+ boolean instantApp;
+ synchronized (mService.mProcLock) {
+ callerApp = mService.getRecordForAppLOSP(caller);
+ if (callerApp == null) {
+ Slog.w(TAG, "registerReceiverWithFeature: no app for " + caller);
+ return null;
+ }
+ if (callerApp.info.uid != SYSTEM_UID
+ && !callerApp.getPkgList().containsKey(callerPackage)
+ && !"android".equals(callerPackage)) {
+ throw new SecurityException("Given caller package " + callerPackage
+ + " is not running in process " + callerApp);
+ }
+ callingUid = callerApp.info.uid;
+ callingPid = callerApp.getPid();
+
+ instantApp = isInstantApp(callerApp, callerPackage, callingUid);
+ }
+ userId = mService.mUserController.handleIncomingUser(callingPid, callingUid, userId, true,
+ ALLOW_FULL_ONLY, "registerReceiver", callerPackage);
+
+ // Warn if system internals are registering for important broadcasts
+ // without also using a priority to ensure they process the event
+ // before normal apps hear about it
+ if (UserHandle.isCore(callingUid)) {
+ final int priority = filter.getPriority();
+ final boolean systemPriority = (priority >= IntentFilter.SYSTEM_HIGH_PRIORITY)
+ || (priority <= IntentFilter.SYSTEM_LOW_PRIORITY);
+ if (!systemPriority) {
+ final int N = filter.countActions();
+ for (int i = 0; i < N; i++) {
+ // TODO: expand to additional important broadcasts over time
+ final String action = filter.getAction(i);
+ if (action.startsWith("android.intent.action.USER_")
+ || action.startsWith("android.intent.action.PACKAGE_")
+ || action.startsWith("android.intent.action.UID_")
+ || action.startsWith("android.intent.action.EXTERNAL_")
+ || action.startsWith("android.bluetooth.")
+ || action.equals(Intent.ACTION_SHUTDOWN)) {
+ if (DEBUG_BROADCAST) {
+ Slog.wtf(TAG,
+ "System internals registering for " + filter.toLongString()
+ + " with app priority; this will race with apps!",
+ new Throwable());
+ }
+
+ // When undefined, assume that system internals need
+ // to hear about the event first; they can use
+ // SYSTEM_LOW_PRIORITY if they need to hear last
+ if (priority == 0) {
+ filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ Iterator<String> actions = filter.actionsIterator();
+ if (actions == null) {
+ ArrayList<String> noAction = new ArrayList<String>(1);
+ noAction.add(null);
+ actions = noAction.iterator();
+ }
+ boolean onlyProtectedBroadcasts = true;
+
+ // Collect stickies of users and check if broadcast is only registered for protected
+ // broadcasts
+ int[] userIds = { UserHandle.USER_ALL, UserHandle.getUserId(callingUid) };
+ synchronized (mStickyBroadcasts) {
+ while (actions.hasNext()) {
+ String action = actions.next();
+ for (int id : userIds) {
+ ArrayMap<String, ArrayList<StickyBroadcast>> stickies =
+ mStickyBroadcasts.get(id);
+ if (stickies != null) {
+ ArrayList<StickyBroadcast> broadcasts = stickies.get(action);
+ if (broadcasts != null) {
+ if (stickyBroadcasts == null) {
+ stickyBroadcasts = new ArrayList<>();
+ }
+ stickyBroadcasts.addAll(broadcasts);
+ }
+ }
+ }
+ if (onlyProtectedBroadcasts) {
+ try {
+ onlyProtectedBroadcasts &=
+ AppGlobals.getPackageManager().isProtectedBroadcast(action);
+ } catch (RemoteException e) {
+ onlyProtectedBroadcasts = false;
+ Slog.w(TAG, "Remote exception", e);
+ }
+ }
+ }
+ }
+
+ if (Process.isSdkSandboxUid(Binder.getCallingUid())) {
+ SdkSandboxManagerLocal sdkSandboxManagerLocal =
+ LocalManagerRegistry.getManager(SdkSandboxManagerLocal.class);
+ if (sdkSandboxManagerLocal == null) {
+ throw new IllegalStateException("SdkSandboxManagerLocal not found when checking"
+ + " whether SDK sandbox uid can register to broadcast receivers.");
+ }
+ if (!sdkSandboxManagerLocal.canRegisterBroadcastReceiver(
+ /*IntentFilter=*/ filter, flags, onlyProtectedBroadcasts)) {
+ throw new SecurityException("SDK sandbox not allowed to register receiver"
+ + " with the given IntentFilter: " + filter.toLongString());
+ }
+ }
+
+ // If the change is enabled, but neither exported or not exported is set, we need to log
+ // an error so the consumer can know to explicitly set the value for their flag.
+ // If the caller is registering for a sticky broadcast with a null receiver, we won't
+ // require a flag
+ final boolean explicitExportStateDefined =
+ (flags & (Context.RECEIVER_EXPORTED | Context.RECEIVER_NOT_EXPORTED)) != 0;
+ if (((flags & Context.RECEIVER_EXPORTED) != 0) && (
+ (flags & Context.RECEIVER_NOT_EXPORTED) != 0)) {
+ throw new IllegalArgumentException(
+ "Receiver can't specify both RECEIVER_EXPORTED and RECEIVER_NOT_EXPORTED"
+ + "flag");
+ }
+
+ // Don't enforce the flag check if we're EITHER registering for only protected
+ // broadcasts, or the receiver is null (a sticky broadcast). Sticky broadcasts should
+ // not be used generally, so we will be marking them as exported by default
+ boolean requireExplicitFlagForDynamicReceivers = CompatChanges.isChangeEnabled(
+ DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED, callingUid);
+
+ // A receiver that is visible to instant apps must also be exported.
+ final boolean unexportedReceiverVisibleToInstantApps =
+ ((flags & Context.RECEIVER_VISIBLE_TO_INSTANT_APPS) != 0) && (
+ (flags & Context.RECEIVER_NOT_EXPORTED) != 0);
+ if (unexportedReceiverVisibleToInstantApps && requireExplicitFlagForDynamicReceivers) {
+ throw new IllegalArgumentException(
+ "Receiver can't specify both RECEIVER_VISIBLE_TO_INSTANT_APPS and "
+ + "RECEIVER_NOT_EXPORTED flag");
+ }
+
+ if (!onlyProtectedBroadcasts) {
+ if (receiver == null && !explicitExportStateDefined) {
+ // sticky broadcast, no flag specified (flag isn't required)
+ flags |= Context.RECEIVER_EXPORTED;
+ } else if (requireExplicitFlagForDynamicReceivers && !explicitExportStateDefined) {
+ throw new SecurityException(
+ callerPackage + ": One of RECEIVER_EXPORTED or "
+ + "RECEIVER_NOT_EXPORTED should be specified when a receiver "
+ + "isn't being registered exclusively for system broadcasts");
+ // Assume default behavior-- flag check is not enforced
+ } else if (!requireExplicitFlagForDynamicReceivers && (
+ (flags & Context.RECEIVER_NOT_EXPORTED) == 0)) {
+ // Change is not enabled, assume exported unless otherwise specified.
+ flags |= Context.RECEIVER_EXPORTED;
+ }
+ } else if ((flags & Context.RECEIVER_NOT_EXPORTED) == 0) {
+ flags |= Context.RECEIVER_EXPORTED;
+ }
+
+ // Dynamic receivers are exported by default for versions prior to T
+ final boolean exported = (flags & Context.RECEIVER_EXPORTED) != 0;
+
+ ArrayList<StickyBroadcast> allSticky = null;
+ if (stickyBroadcasts != null) {
+ final ContentResolver resolver = mContext.getContentResolver();
+ // Look for any matching sticky broadcasts...
+ for (int i = 0, N = stickyBroadcasts.size(); i < N; i++) {
+ final StickyBroadcast broadcast = stickyBroadcasts.get(i);
+ Intent intent = broadcast.intent;
+ // Don't provided intents that aren't available to instant apps.
+ if (instantApp && (intent.getFlags() & Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS)
+ == 0) {
+ continue;
+ }
+ // If intent has scheme "content", it will need to access
+ // provider that needs to lock mProviderMap in ActivityThread
+ // and also it may need to wait application response, so we
+ // cannot lock ActivityManagerService here.
+ final int match;
+ if (Flags.avoidResolvingType()) {
+ match = filter.match(intent.getAction(), broadcast.resolvedDataType,
+ intent.getScheme(), intent.getData(), intent.getCategories(),
+ TAG, false /* supportsWildcards */, null /* ignoreActions */,
+ intent.getExtras());
+ } else {
+ match = filter.match(resolver, intent, true, TAG);
+ }
+ if (match >= 0) {
+ if (allSticky == null) {
+ allSticky = new ArrayList<>();
+ }
+ allSticky.add(broadcast);
+ }
+ }
+ }
+
+ // The first sticky in the list is returned directly back to the client.
+ Intent sticky = allSticky != null ? allSticky.get(0).intent : null;
+ if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Register receiver " + filter + ": " + sticky);
+ if (receiver == null) {
+ return sticky;
+ }
+
+ // SafetyNet logging for b/177931370. If any process other than system_server tries to
+ // listen to this broadcast action, then log it.
+ if (callingPid != Process.myPid()) {
+ if (filter.hasAction("com.android.server.net.action.SNOOZE_WARNING")
+ || filter.hasAction("com.android.server.net.action.SNOOZE_RAPID")) {
+ EventLog.writeEvent(0x534e4554, "177931370", callingUid, "");
+ }
+ }
+
+ synchronized (mService) {
+ IApplicationThread thread;
+ if (callerApp != null && ((thread = callerApp.getThread()) == null
+ || thread.asBinder() != caller.asBinder())) {
+ // Original caller already died
+ return null;
+ }
+ ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());
+ if (rl == null) {
+ rl = new ReceiverList(mService, callerApp, callingPid, callingUid,
+ userId, receiver);
+ if (rl.app != null) {
+ final int totalReceiversForApp = rl.app.mReceivers.numberOfReceivers();
+ if (totalReceiversForApp >= MAX_RECEIVERS_ALLOWED_PER_APP) {
+ throw new IllegalStateException("Too many receivers, total of "
+ + totalReceiversForApp + ", registered for pid: "
+ + rl.pid + ", callerPackage: " + callerPackage);
+ }
+ rl.app.mReceivers.addReceiver(rl);
+ } else {
+ try {
+ receiver.asBinder().linkToDeath(rl, 0);
+ } catch (RemoteException e) {
+ return sticky;
+ }
+ rl.linkedToDeath = true;
+ }
+ mRegisteredReceivers.put(receiver.asBinder(), rl);
+ } else if (rl.uid != callingUid) {
+ throw new IllegalArgumentException(
+ "Receiver requested to register for uid " + callingUid
+ + " was previously registered for uid " + rl.uid
+ + " callerPackage is " + callerPackage);
+ } else if (rl.pid != callingPid) {
+ throw new IllegalArgumentException(
+ "Receiver requested to register for pid " + callingPid
+ + " was previously registered for pid " + rl.pid
+ + " callerPackage is " + callerPackage);
+ } else if (rl.userId != userId) {
+ throw new IllegalArgumentException(
+ "Receiver requested to register for user " + userId
+ + " was previously registered for user " + rl.userId
+ + " callerPackage is " + callerPackage);
+ }
+ BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage, callerFeatureId,
+ receiverId, permission, callingUid, userId, instantApp, visibleToInstantApps,
+ exported);
+ if (rl.containsFilter(filter)) {
+ Slog.w(TAG, "Receiver with filter " + filter
+ + " already registered for pid " + rl.pid
+ + ", callerPackage is " + callerPackage);
+ } else {
+ rl.add(bf);
+ if (!bf.debugCheck()) {
+ Slog.w(TAG, "==> For Dynamic broadcast");
+ }
+ mReceiverResolver.addFilter(mService.getPackageManagerInternal().snapshot(), bf);
+ }
+
+ // Enqueue broadcasts for all existing stickies that match
+ // this filter.
+ if (allSticky != null) {
+ ArrayList receivers = new ArrayList();
+ receivers.add(bf);
+ sticky = null;
+
+ final int stickyCount = allSticky.size();
+ for (int i = 0; i < stickyCount; i++) {
+ final StickyBroadcast broadcast = allSticky.get(i);
+ final int originalStickyCallingUid = allSticky.get(i).originalCallingUid;
+ // TODO(b/281889567): consider using checkComponentPermission instead of
+ // canAccessUnexportedComponents
+ if (sticky == null && (exported || originalStickyCallingUid == callingUid
+ || ActivityManager.canAccessUnexportedComponents(
+ originalStickyCallingUid))) {
+ sticky = broadcast.intent;
+ }
+ BroadcastQueue queue = mBroadcastQueue;
+ BroadcastRecord r = new BroadcastRecord(queue, broadcast.intent, null,
+ null, null, -1, -1, false, null, null, null, null, OP_NONE,
+ BroadcastOptions.makeWithDeferUntilActive(broadcast.deferUntilActive),
+ receivers, null, null, 0, null, null, false, true, true, -1,
+ originalStickyCallingUid, BackgroundStartPrivileges.NONE,
+ false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */,
+ null /* filterExtrasForReceiver */,
+ broadcast.originalCallingAppProcessState);
+ queue.enqueueBroadcastLocked(r);
+ }
+ }
+
+ return sticky;
+ }
+ }
+
+ void unregisterReceiver(IIntentReceiver receiver) {
+ traceUnregistrationBegin(receiver);
+ try {
+ unregisterReceiverTraced(receiver);
+ } finally {
+ traceUnregistrationEnd();
+ }
+ }
+
+ private static void traceUnregistrationBegin(IIntentReceiver receiver) {
+ if (!Flags.traceReceiverRegistration()) {
+ return;
+ }
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+ TextUtils.formatSimple("unregisterReceiver: %d/%s", Binder.getCallingUid(),
+ receiver == null ? "null" : receiver.asBinder()));
+ }
+ }
+
+ private static void traceUnregistrationEnd() {
+ if (!Flags.traceReceiverRegistration()) {
+ return;
+ }
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ }
+ }
+
+ private void unregisterReceiverTraced(IIntentReceiver receiver) {
+ if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Unregister receiver: " + receiver);
+
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ boolean doTrim = false;
+ synchronized (mService) {
+ ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());
+ if (rl != null) {
+ final BroadcastRecord r = rl.curBroadcast;
+ if (r != null) {
+ final boolean doNext = r.queue.finishReceiverLocked(
+ rl.app, r.resultCode, r.resultData, r.resultExtras,
+ r.resultAbort, false);
+ if (doNext) {
+ doTrim = true;
+ }
+ }
+ if (rl.app != null) {
+ rl.app.mReceivers.removeReceiver(rl);
+ }
+ removeReceiverLocked(rl);
+ if (rl.linkedToDeath) {
+ rl.linkedToDeath = false;
+ rl.receiver.asBinder().unlinkToDeath(rl, 0);
+ }
+ }
+
+ // If we actually concluded any broadcasts, we might now be able
+ // to trim the recipients' apps from our working set
+ if (doTrim) {
+ mService.trimApplicationsLocked(false, OOM_ADJ_REASON_FINISH_RECEIVER);
+ return;
+ }
+ }
+
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+
+ void removeReceiverLocked(ReceiverList rl) {
+ mRegisteredReceivers.remove(rl.receiver.asBinder());
+ for (int i = rl.size() - 1; i >= 0; i--) {
+ mReceiverResolver.removeFilter(rl.get(i));
+ }
+ }
+
+ int broadcastIntentWithFeature(IApplicationThread caller, String callingFeatureId,
+ Intent intent, String resolvedType, IIntentReceiver resultTo,
+ int resultCode, String resultData, Bundle resultExtras,
+ String[] requiredPermissions, String[] excludedPermissions,
+ String[] excludedPackages, int appOp, Bundle bOptions,
+ boolean serialized, boolean sticky, int userId) {
+ mService.enforceNotIsolatedCaller("broadcastIntent");
+
+ synchronized (mService) {
+ intent = verifyBroadcastLocked(intent);
+
+ final ProcessRecord callerApp = mService.getRecordForAppLOSP(caller);
+ final int callingPid = Binder.getCallingPid();
+ final int callingUid = Binder.getCallingUid();
+
+ // We're delivering the result to the caller
+ final ProcessRecord resultToApp = callerApp;
+
+ // Permission regimes around sender-supplied broadcast options.
+ enforceBroadcastOptionPermissionsInternal(bOptions, callingUid);
+
+ final ComponentName cn = intent.getComponent();
+
+ Trace.traceBegin(
+ Trace.TRACE_TAG_ACTIVITY_MANAGER,
+ "broadcastIntent:" + (cn != null ? cn.toString() : intent.getAction()));
+
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ return broadcastIntentLocked(callerApp,
+ callerApp != null ? callerApp.info.packageName : null, callingFeatureId,
+ intent, resolvedType, resultToApp, resultTo, resultCode, resultData,
+ resultExtras, requiredPermissions, excludedPermissions, excludedPackages,
+ appOp, bOptions, serialized, sticky, callingPid, callingUid, callingUid,
+ callingPid, userId, BackgroundStartPrivileges.NONE, null, null);
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ }
+ }
+ }
+
+ // Not the binder call surface
+ int broadcastIntentInPackage(String packageName, @Nullable String featureId, int uid,
+ int realCallingUid, int realCallingPid, Intent intent, String resolvedType,
+ ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode,
+ String resultData, Bundle resultExtras, String requiredPermission, Bundle bOptions,
+ boolean serialized, boolean sticky, int userId,
+ BackgroundStartPrivileges backgroundStartPrivileges,
+ @Nullable int[] broadcastAllowList) {
+ synchronized (mService) {
+ intent = verifyBroadcastLocked(intent);
+
+ final long origId = Binder.clearCallingIdentity();
+ String[] requiredPermissions = requiredPermission == null ? null
+ : new String[] {requiredPermission};
+ try {
+ return broadcastIntentLocked(null, packageName, featureId, intent, resolvedType,
+ resultToApp, resultTo, resultCode, resultData, resultExtras,
+ requiredPermissions, null, null, OP_NONE, bOptions, serialized, sticky, -1,
+ uid, realCallingUid, realCallingPid, userId,
+ backgroundStartPrivileges, broadcastAllowList,
+ null /* filterExtrasForReceiver */);
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+ }
+
+ @GuardedBy("mService")
+ final int broadcastIntentLocked(ProcessRecord callerApp, String callerPackage,
+ @Nullable String callerFeatureId, Intent intent, String resolvedType,
+ ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode, String resultData,
+ Bundle resultExtras, String[] requiredPermissions,
+ String[] excludedPermissions, String[] excludedPackages, int appOp, Bundle bOptions,
+ boolean ordered, boolean sticky, int callingPid, int callingUid,
+ int realCallingUid, int realCallingPid, int userId,
+ BackgroundStartPrivileges backgroundStartPrivileges,
+ @Nullable int[] broadcastAllowList,
+ @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) {
+ final int cookie = traceBroadcastIntentBegin(intent, resultTo, ordered, sticky,
+ callingUid, realCallingUid, userId);
+ try {
+ final BroadcastSentEventRecord broadcastSentEventRecord =
+ new BroadcastSentEventRecord();
+ final int res = broadcastIntentLockedTraced(callerApp, callerPackage, callerFeatureId,
+ intent, resolvedType, resultToApp, resultTo, resultCode, resultData,
+ resultExtras, requiredPermissions, excludedPermissions, excludedPackages,
+ appOp, BroadcastOptions.fromBundleNullable(bOptions), ordered, sticky,
+ callingPid, callingUid, realCallingUid, realCallingPid, userId,
+ backgroundStartPrivileges, broadcastAllowList, filterExtrasForReceiver,
+ broadcastSentEventRecord);
+ broadcastSentEventRecord.setResult(res);
+ broadcastSentEventRecord.logToStatsd();
+ return res;
+ } finally {
+ traceBroadcastIntentEnd(cookie);
+ }
+ }
+
+ private static int traceBroadcastIntentBegin(Intent intent, IIntentReceiver resultTo,
+ boolean ordered, boolean sticky, int callingUid, int realCallingUid, int userId) {
+ if (!Flags.traceReceiverRegistration()) {
+ return BroadcastQueue.traceBegin("broadcastIntentLockedTraced");
+ }
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ final StringBuilder sb = new StringBuilder("broadcastIntent: ");
+ sb.append(callingUid); sb.append('/');
+ final String action = intent.getAction();
+ sb.append(action == null ? null : action); sb.append('/');
+ sb.append("0x"); sb.append(Integer.toHexString(intent.getFlags())); sb.append('/');
+ sb.append(ordered ? "O" : "_");
+ sb.append(sticky ? "S" : "_");
+ sb.append(resultTo != null ? "C" : "_");
+ sb.append('/');
+ sb.append('u'); sb.append(userId);
+ if (callingUid != realCallingUid) {
+ sb.append('/');
+ sb.append("sender="); sb.append(realCallingUid);
+ }
+ return BroadcastQueue.traceBegin(sb.toString());
+ }
+ return 0;
+ }
+
+ private static void traceBroadcastIntentEnd(int cookie) {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ BroadcastQueue.traceEnd(cookie);
+ }
+ }
+
+ @GuardedBy("mService")
+ final int broadcastIntentLockedTraced(ProcessRecord callerApp, String callerPackage,
+ @Nullable String callerFeatureId, Intent intent, String resolvedType,
+ ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode, String resultData,
+ Bundle resultExtras, String[] requiredPermissions,
+ String[] excludedPermissions, String[] excludedPackages, int appOp,
+ BroadcastOptions brOptions, boolean ordered, boolean sticky, int callingPid,
+ int callingUid, int realCallingUid, int realCallingPid, int userId,
+ BackgroundStartPrivileges backgroundStartPrivileges,
+ @Nullable int[] broadcastAllowList,
+ @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
+ @NonNull BroadcastSentEventRecord broadcastSentEventRecord) {
+ // Ensure all internal loopers are registered for idle checks
+ BroadcastLoopers.addMyLooper();
+
+ if (Process.isSdkSandboxUid(realCallingUid)) {
+ final SdkSandboxManagerLocal sdkSandboxManagerLocal = LocalManagerRegistry.getManager(
+ SdkSandboxManagerLocal.class);
+ if (sdkSandboxManagerLocal == null) {
+ throw new IllegalStateException("SdkSandboxManagerLocal not found when sending"
+ + " a broadcast from an SDK sandbox uid.");
+ }
+ if (!sdkSandboxManagerLocal.canSendBroadcast(intent)) {
+ throw new SecurityException(
+ "Intent " + intent.getAction() + " may not be broadcast from an SDK sandbox"
+ + " uid. Given caller package " + callerPackage
+ + " (pid=" + callingPid + ", realCallingUid=" + realCallingUid
+ + ", callingUid= " + callingUid + ")");
+ }
+ }
+
+ if ((resultTo != null) && (resultToApp == null)) {
+ if (resultTo.asBinder() instanceof BinderProxy) {
+ // Warn when requesting results without a way to deliver them
+ Slog.wtf(TAG, "Sending broadcast " + intent.getAction()
+ + " with resultTo requires resultToApp", new Throwable());
+ } else {
+ // If not a BinderProxy above, then resultTo is an in-process
+ // receiver, so splice in system_server process
+ resultToApp = mService.getProcessRecordLocked("system", SYSTEM_UID);
+ }
+ }
+
+ intent = new Intent(intent);
+ broadcastSentEventRecord.setIntent(intent);
+ broadcastSentEventRecord.setOriginalIntentFlags(intent.getFlags());
+ broadcastSentEventRecord.setSenderUid(callingUid);
+ broadcastSentEventRecord.setRealSenderUid(realCallingUid);
+ broadcastSentEventRecord.setSticky(sticky);
+ broadcastSentEventRecord.setOrdered(ordered);
+ broadcastSentEventRecord.setResultRequested(resultTo != null);
+ final int callerAppProcessState = getRealProcessStateLocked(callerApp, realCallingPid);
+ broadcastSentEventRecord.setSenderProcState(callerAppProcessState);
+ broadcastSentEventRecord.setSenderUidState(getRealUidStateLocked(callerApp,
+ realCallingPid));
+
+ final boolean callerInstantApp = isInstantApp(callerApp, callerPackage, callingUid);
+ // Instant Apps cannot use FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS
+ if (callerInstantApp) {
+ intent.setFlags(intent.getFlags() & ~Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
+ }
+
+ if (userId == UserHandle.USER_ALL && broadcastAllowList != null) {
+ Slog.e(TAG, "broadcastAllowList only applies when sending to individual users. "
+ + "Assuming restrictive whitelist.");
+ broadcastAllowList = new int[]{};
+ }
+
+ // By default broadcasts do not go to stopped apps.
+ intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);
+
+ // If we have not finished booting, don't allow this to launch new processes.
+ if (!mService.mProcessesReady
+ && (intent.getFlags() & Intent.FLAG_RECEIVER_BOOT_UPGRADE) == 0) {
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ }
+
+ if (DEBUG_BROADCAST_LIGHT) {
+ Slog.v(TAG_BROADCAST,
+ (sticky ? "Broadcast sticky: " : "Broadcast: ") + intent
+ + " ordered=" + ordered + " userid=" + userId
+ + " options=" + (brOptions == null ? "null" : brOptions.toBundle()));
+ }
+ if ((resultTo != null) && !ordered) {
+ if (!UserHandle.isCore(callingUid)) {
+ String msg = "Unauthorized unordered resultTo broadcast "
+ + intent + " sent from uid " + callingUid;
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+ }
+
+ userId = mService.mUserController.handleIncomingUser(callingPid, callingUid, userId, true,
+ ALLOW_NON_FULL, "broadcast", callerPackage);
+
+ // Make sure that the user who is receiving this broadcast or its parent is running.
+ // If not, we will just skip it. Make an exception for shutdown broadcasts, upgrade steps.
+ if (userId != UserHandle.USER_ALL && !mService.mUserController.isUserOrItsParentRunning(
+ userId)) {
+ if ((callingUid != SYSTEM_UID
+ || (intent.getFlags() & Intent.FLAG_RECEIVER_BOOT_UPGRADE) == 0)
+ && !Intent.ACTION_SHUTDOWN.equals(intent.getAction())) {
+ Slog.w(TAG, "Skipping broadcast of " + intent
+ + ": user " + userId + " and its parent (if any) are stopped");
+ scheduleCanceledResultTo(resultToApp, resultTo, intent, userId,
+ brOptions, callingUid, callerPackage);
+ return ActivityManager.BROADCAST_FAILED_USER_STOPPED;
+ }
+ }
+
+ final String action = intent.getAction();
+ if (brOptions != null) {
+ if (brOptions.getTemporaryAppAllowlistDuration() > 0) {
+ // See if the caller is allowed to do this. Note we are checking against
+ // the actual real caller (not whoever provided the operation as say a
+ // PendingIntent), because that who is actually supplied the arguments.
+ if (checkComponentPermission(CHANGE_DEVICE_IDLE_TEMP_WHITELIST,
+ realCallingPid, realCallingUid, -1, true)
+ != PackageManager.PERMISSION_GRANTED
+ && checkComponentPermission(START_ACTIVITIES_FROM_BACKGROUND,
+ realCallingPid, realCallingUid, -1, true)
+ != PackageManager.PERMISSION_GRANTED
+ && checkComponentPermission(START_FOREGROUND_SERVICES_FROM_BACKGROUND,
+ realCallingPid, realCallingUid, -1, true)
+ != PackageManager.PERMISSION_GRANTED) {
+ String msg = "Permission Denial: " + intent.getAction()
+ + " broadcast from " + callerPackage + " (pid=" + callingPid
+ + ", uid=" + callingUid + ")"
+ + " requires "
+ + CHANGE_DEVICE_IDLE_TEMP_WHITELIST + " or "
+ + START_ACTIVITIES_FROM_BACKGROUND + " or "
+ + START_FOREGROUND_SERVICES_FROM_BACKGROUND;
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+ }
+ if (brOptions.isDontSendToRestrictedApps()
+ && !mService.isUidActiveLOSP(callingUid)
+ && mService.isBackgroundRestrictedNoCheck(callingUid, callerPackage)) {
+ Slog.i(TAG, "Not sending broadcast " + action + " - app " + callerPackage
+ + " has background restrictions");
+ return ActivityManager.START_CANCELED;
+ }
+ if (brOptions.allowsBackgroundActivityStarts()) {
+ // See if the caller is allowed to do this. Note we are checking against
+ // the actual real caller (not whoever provided the operation as say a
+ // PendingIntent), because that who is actually supplied the arguments.
+ if (checkComponentPermission(
+ android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND,
+ realCallingPid, realCallingUid, -1, true)
+ != PackageManager.PERMISSION_GRANTED) {
+ String msg = "Permission Denial: " + intent.getAction()
+ + " broadcast from " + callerPackage + " (pid=" + callingPid
+ + ", uid=" + callingUid + ")"
+ + " requires "
+ + android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND;
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ } else {
+ // We set the token to null since if it wasn't for it we'd allow anyway here
+ backgroundStartPrivileges = BackgroundStartPrivileges.ALLOW_BAL;
+ }
+ }
+
+ if (brOptions.getIdForResponseEvent() > 0) {
+ mService.enforcePermission(
+ android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS,
+ callingPid, callingUid, "recordResponseEventWhileInBackground");
+ }
+ }
+
+ // Verify that protected broadcasts are only being sent by system code,
+ // and that system code is only sending protected broadcasts.
+ final boolean isProtectedBroadcast;
+ try {
+ isProtectedBroadcast = AppGlobals.getPackageManager().isProtectedBroadcast(action);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Remote exception", e);
+ scheduleCanceledResultTo(resultToApp, resultTo, intent,
+ userId, brOptions, callingUid, callerPackage);
+ return ActivityManager.BROADCAST_SUCCESS;
+ }
+
+ final boolean isCallerSystem;
+ switch (UserHandle.getAppId(callingUid)) {
+ case ROOT_UID:
+ case SYSTEM_UID:
+ case PHONE_UID:
+ case BLUETOOTH_UID:
+ case NFC_UID:
+ case SE_UID:
+ case NETWORK_STACK_UID:
+ isCallerSystem = true;
+ break;
+ default:
+ isCallerSystem = (callerApp != null) && callerApp.isPersistent();
+ break;
+ }
+
+ // First line security check before anything else: stop non-system apps from
+ // sending protected broadcasts.
+ if (!isCallerSystem) {
+ if (isProtectedBroadcast) {
+ String msg = "Permission Denial: not allowed to send broadcast "
+ + action + " from pid="
+ + callingPid + ", uid=" + callingUid;
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+
+ } else if (AppWidgetManager.ACTION_APPWIDGET_CONFIGURE.equals(action)
+ || AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
+ // Special case for compatibility: we don't want apps to send this,
+ // but historically it has not been protected and apps may be using it
+ // to poke their own app widget. So, instead of making it protected,
+ // just limit it to the caller.
+ if (callerPackage == null) {
+ String msg = "Permission Denial: not allowed to send broadcast "
+ + action + " from unknown caller.";
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ } else if (intent.getComponent() != null) {
+ // They are good enough to send to an explicit component... verify
+ // it is being sent to the calling app.
+ if (!intent.getComponent().getPackageName().equals(
+ callerPackage)) {
+ String msg = "Permission Denial: not allowed to send broadcast "
+ + action + " to "
+ + intent.getComponent().getPackageName() + " from "
+ + callerPackage;
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+ } else {
+ // Limit broadcast to their own package.
+ intent.setPackage(callerPackage);
+ }
+ }
+ }
+
+ boolean timeoutExempt = false;
+
+ if (action != null) {
+ if (getBackgroundLaunchBroadcasts().contains(action)) {
+ if (DEBUG_BACKGROUND_CHECK) {
+ Slog.i(TAG, "Broadcast action " + action + " forcing include-background");
+ }
+ intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+ }
+
+ // TODO: b/329211459 - Remove this after background remote intent is fixed.
+ if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
+ && getWearRemoteIntentAction().equals(action)) {
+ final int callerProcState = callerApp != null
+ ? callerApp.getCurProcState()
+ : ActivityManager.PROCESS_STATE_NONEXISTENT;
+ if (ActivityManager.RunningAppProcessInfo.procStateToImportance(callerProcState)
+ > ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
+ return ActivityManager.START_CANCELED;
+ }
+ }
+
+ switch (action) {
+ case Intent.ACTION_MEDIA_SCANNER_SCAN_FILE:
+ UserManagerInternal umInternal = LocalServices.getService(
+ UserManagerInternal.class);
+ UserInfo userInfo = umInternal.getUserInfo(userId);
+ if (userInfo != null && userInfo.isCloneProfile()) {
+ userId = umInternal.getProfileParentId(userId);
+ }
+ break;
+ case Intent.ACTION_UID_REMOVED:
+ case Intent.ACTION_PACKAGE_REMOVED:
+ case Intent.ACTION_PACKAGE_CHANGED:
+ case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE:
+ case Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE:
+ case Intent.ACTION_PACKAGES_SUSPENDED:
+ case Intent.ACTION_PACKAGES_UNSUSPENDED:
+ // Handle special intents: if this broadcast is from the package
+ // manager about a package being removed, we need to remove all of
+ // its activities from the history stack.
+ if (checkComponentPermission(
+ android.Manifest.permission.BROADCAST_PACKAGE_REMOVED,
+ callingPid, callingUid, -1, true)
+ != PackageManager.PERMISSION_GRANTED) {
+ String msg = "Permission Denial: " + intent.getAction()
+ + " broadcast from " + callerPackage + " (pid=" + callingPid
+ + ", uid=" + callingUid + ")"
+ + " requires "
+ + android.Manifest.permission.BROADCAST_PACKAGE_REMOVED;
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+ switch (action) {
+ case Intent.ACTION_UID_REMOVED:
+ final int uid = getUidFromIntent(intent);
+ if (uid >= 0) {
+ mService.mBatteryStatsService.removeUid(uid);
+ if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ mService.mAppOpsService.resetAllModes(UserHandle.getUserId(uid),
+ intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME));
+ } else {
+ mService.mAppOpsService.uidRemoved(uid);
+ mService.mServices.onUidRemovedLocked(uid);
+ }
+ }
+ break;
+ case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE:
+ // If resources are unavailable just force stop all those packages
+ // and flush the attribute cache as well.
+ String[] list = intent.getStringArrayExtra(
+ Intent.EXTRA_CHANGED_PACKAGE_LIST);
+ if (list != null && list.length > 0) {
+ for (int i = 0; i < list.length; i++) {
+ mService.forceStopPackageLocked(list[i], -1, false, true, true,
+ false, false, false, userId, "storage unmount");
+ }
+ mService.mAtmInternal.cleanupRecentTasksForUser(
+ UserHandle.USER_ALL);
+ sendPackageBroadcastLocked(
+ ApplicationThreadConstants.EXTERNAL_STORAGE_UNAVAILABLE,
+ list, userId);
+ }
+ break;
+ case Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE:
+ mService.mAtmInternal.cleanupRecentTasksForUser(UserHandle.USER_ALL);
+ break;
+ case Intent.ACTION_PACKAGE_REMOVED:
+ case Intent.ACTION_PACKAGE_CHANGED:
+ Uri data = intent.getData();
+ String ssp;
+ if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {
+ boolean removed = Intent.ACTION_PACKAGE_REMOVED.equals(action);
+ final boolean replacing =
+ intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
+ final boolean killProcess =
+ !intent.getBooleanExtra(Intent.EXTRA_DONT_KILL_APP, false);
+ final boolean fullUninstall = removed && !replacing;
+
+ if (removed) {
+ if (killProcess) {
+ mService.forceStopPackageLocked(ssp, UserHandle.getAppId(
+ intent.getIntExtra(Intent.EXTRA_UID, -1)),
+ false, true, true, false, fullUninstall, false,
+ userId, "pkg removed");
+ mService.getPackageManagerInternal()
+ .onPackageProcessKilledForUninstall(ssp);
+ } else {
+ // Kill any app zygotes always, since they can't fork new
+ // processes with references to the old code
+ mService.forceStopAppZygoteLocked(ssp, UserHandle.getAppId(
+ intent.getIntExtra(Intent.EXTRA_UID, -1)),
+ userId);
+ }
+ final int cmd = killProcess
+ ? ApplicationThreadConstants.PACKAGE_REMOVED
+ : ApplicationThreadConstants.PACKAGE_REMOVED_DONT_KILL;
+ sendPackageBroadcastLocked(cmd,
+ new String[] {ssp}, userId);
+ if (fullUninstall) {
+ // Remove all permissions granted from/to this package
+ mService.mUgmInternal.removeUriPermissionsForPackage(ssp,
+ userId, true, false);
+
+ mService.mAtmInternal.removeRecentTasksByPackageName(ssp,
+ userId);
+
+ mService.mServices.forceStopPackageLocked(ssp, userId);
+ mService.mAtmInternal.onPackageUninstalled(ssp, userId);
+ mService.mBatteryStatsService.notePackageUninstalled(ssp);
+ }
+ } else {
+ if (killProcess) {
+ int reason;
+ int subReason;
+ if (replacing) {
+ reason = ApplicationExitInfo.REASON_PACKAGE_UPDATED;
+ subReason = ApplicationExitInfo.SUBREASON_UNKNOWN;
+ } else {
+ reason =
+ ApplicationExitInfo.REASON_PACKAGE_STATE_CHANGE;
+ subReason = ApplicationExitInfo.SUBREASON_UNKNOWN;
+ }
+
+ final int extraUid = intent.getIntExtra(Intent.EXTRA_UID,
+ -1);
+ synchronized (mService.mProcLock) {
+ mService.mProcessList.killPackageProcessesLSP(ssp,
+ UserHandle.getAppId(extraUid),
+ userId, ProcessList.INVALID_ADJ,
+ reason,
+ subReason,
+ "change " + ssp);
+ }
+ }
+ mService.cleanupDisabledPackageComponentsLocked(ssp, userId,
+ intent.getStringArrayExtra(
+ Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST));
+ mService.mServices.schedulePendingServiceStartLocked(
+ ssp, userId);
+ }
+ }
+ break;
+ case Intent.ACTION_PACKAGES_SUSPENDED:
+ case Intent.ACTION_PACKAGES_UNSUSPENDED:
+ final boolean suspended = Intent.ACTION_PACKAGES_SUSPENDED.equals(
+ intent.getAction());
+ final String[] packageNames = intent.getStringArrayExtra(
+ Intent.EXTRA_CHANGED_PACKAGE_LIST);
+ final int userIdExtra = intent.getIntExtra(
+ Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+
+ mService.mAtmInternal.onPackagesSuspendedChanged(packageNames,
+ suspended, userIdExtra);
+
+ final boolean quarantined = intent.getBooleanExtra(
+ Intent.EXTRA_QUARANTINED, false);
+ if (suspended && quarantined && packageNames != null) {
+ for (int i = 0; i < packageNames.length; i++) {
+ mService.forceStopPackage(packageNames[i], userId,
+ ActivityManager.FLAG_OR_STOPPED, "quarantined");
+ }
+ }
+
+ break;
+ }
+ break;
+ case Intent.ACTION_PACKAGE_REPLACED: {
+ final Uri data = intent.getData();
+ final String ssp;
+ if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {
+ ApplicationInfo aInfo = null;
+ try {
+ aInfo = AppGlobals.getPackageManager()
+ .getApplicationInfo(ssp, STOCK_PM_FLAGS, userId);
+ } catch (RemoteException ignore) {
+ }
+ if (aInfo == null) {
+ Slog.w(TAG, "Dropping ACTION_PACKAGE_REPLACED for non-existent pkg:"
+ + " ssp=" + ssp + " data=" + data);
+ scheduleCanceledResultTo(resultToApp, resultTo, intent,
+ userId, brOptions, callingUid, callerPackage);
+ return ActivityManager.BROADCAST_SUCCESS;
+ }
+ mService.updateAssociationForApp(aInfo);
+ mService.mAtmInternal.onPackageReplaced(aInfo);
+ mService.mServices.updateServiceApplicationInfoLocked(aInfo);
+ sendPackageBroadcastLocked(ApplicationThreadConstants.PACKAGE_REPLACED,
+ new String[] {ssp}, userId);
+ }
+ break;
+ }
+ case Intent.ACTION_PACKAGE_ADDED: {
+ // Special case for adding a package: by default turn on compatibility mode.
+ Uri data = intent.getData();
+ String ssp;
+ if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {
+ final boolean replacing =
+ intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
+ mService.mAtmInternal.onPackageAdded(ssp, replacing);
+
+ try {
+ ApplicationInfo ai = AppGlobals.getPackageManager()
+ .getApplicationInfo(ssp, STOCK_PM_FLAGS, 0);
+ mService.mBatteryStatsService.notePackageInstalled(ssp,
+ ai != null ? ai.longVersionCode : 0);
+ } catch (RemoteException e) {
+ }
+ }
+ break;
+ }
+ case Intent.ACTION_PACKAGE_DATA_CLEARED: {
+ Uri data = intent.getData();
+ String ssp;
+ if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {
+ mService.mAtmInternal.onPackageDataCleared(ssp, userId);
+ }
+ break;
+ }
+ case Intent.ACTION_TIMEZONE_CHANGED:
+ // If this is the time zone changed action, queue up a message that will reset
+ // the timezone of all currently running processes. This message will get
+ // queued up before the broadcast happens.
+ mService.mHandler.sendEmptyMessage(UPDATE_TIME_ZONE);
+ break;
+ case Intent.ACTION_TIME_CHANGED:
+ // EXTRA_TIME_PREF_24_HOUR_FORMAT is optional so we must distinguish between
+ // the tri-state value it may contain and "unknown".
+ // For convenience we re-use the Intent extra values.
+ final int NO_EXTRA_VALUE_FOUND = -1;
+ final int timeFormatPreferenceMsgValue = intent.getIntExtra(
+ Intent.EXTRA_TIME_PREF_24_HOUR_FORMAT,
+ NO_EXTRA_VALUE_FOUND /* defaultValue */);
+ // Only send a message if the time preference is available.
+ if (timeFormatPreferenceMsgValue != NO_EXTRA_VALUE_FOUND) {
+ Message updateTimePreferenceMsg =
+ mService.mHandler.obtainMessage(UPDATE_TIME_PREFERENCE_MSG,
+ timeFormatPreferenceMsgValue, 0);
+ mService.mHandler.sendMessage(updateTimePreferenceMsg);
+ }
+ mService.mBatteryStatsService.noteCurrentTimeChanged();
+ break;
+ case ConnectivityManager.ACTION_CLEAR_DNS_CACHE:
+ mService.mHandler.sendEmptyMessage(CLEAR_DNS_CACHE_MSG);
+ break;
+ case Proxy.PROXY_CHANGE_ACTION:
+ mService.mHandler.sendMessage(mService.mHandler.obtainMessage(
+ UPDATE_HTTP_PROXY_MSG));
+ break;
+ case android.hardware.Camera.ACTION_NEW_PICTURE:
+ case android.hardware.Camera.ACTION_NEW_VIDEO:
+ // In N we just turned these off; in O we are turing them back on partly,
+ // only for registered receivers. This will still address the main problem
+ // (a spam of apps waking up when a picture is taken putting significant
+ // memory pressure on the system at a bad point), while still allowing apps
+ // that are already actively running to know about this happening.
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ break;
+ case android.security.KeyChain.ACTION_TRUST_STORE_CHANGED:
+ mService.mHandler.sendEmptyMessage(HANDLE_TRUST_STORAGE_UPDATE_MSG);
+ break;
+ case "com.android.launcher.action.INSTALL_SHORTCUT":
+ // As of O, we no longer support this broadcasts, even for pre-O apps.
+ // Apps should now be using ShortcutManager.pinRequestShortcut().
+ Log.w(TAG, "Broadcast " + action
+ + " no longer supported. It will not be delivered.");
+ scheduleCanceledResultTo(resultToApp, resultTo, intent,
+ userId, brOptions, callingUid, callerPackage);
+ return ActivityManager.BROADCAST_SUCCESS;
+ case Intent.ACTION_PRE_BOOT_COMPLETED:
+ timeoutExempt = true;
+ break;
+ case Intent.ACTION_CLOSE_SYSTEM_DIALOGS:
+ if (!mService.mAtmInternal.checkCanCloseSystemDialogs(callingPid, callingUid,
+ callerPackage)) {
+ scheduleCanceledResultTo(resultToApp, resultTo, intent,
+ userId, brOptions, callingUid, callerPackage);
+ // Returning success seems to be the pattern here
+ return ActivityManager.BROADCAST_SUCCESS;
+ }
+ break;
+ }
+
+ if (Intent.ACTION_PACKAGE_ADDED.equals(action)
+ || Intent.ACTION_PACKAGE_REMOVED.equals(action)
+ || Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
+ final int uid = getUidFromIntent(intent);
+ if (uid != -1) {
+ final UidRecord uidRec = mService.mProcessList.getUidRecordLOSP(uid);
+ if (uidRec != null) {
+ uidRec.updateHasInternetPermission();
+ }
+ }
+ }
+ }
+
+ // Add to the sticky list if requested.
+ if (sticky) {
+ if (mService.checkPermission(android.Manifest.permission.BROADCAST_STICKY,
+ callingPid, callingUid)
+ != PackageManager.PERMISSION_GRANTED) {
+ String msg =
+ "Permission Denial: broadcastIntent() requesting a sticky broadcast from"
+ + " pid="
+ + callingPid
+ + ", uid="
+ + callingUid
+ + " requires "
+ + android.Manifest.permission.BROADCAST_STICKY;
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+ if (requiredPermissions != null && requiredPermissions.length > 0) {
+ Slog.w(TAG, "Can't broadcast sticky intent " + intent
+ + " and enforce permissions " + Arrays.toString(requiredPermissions));
+ scheduleCanceledResultTo(resultToApp, resultTo, intent,
+ userId, brOptions, callingUid, callerPackage);
+ return ActivityManager.BROADCAST_STICKY_CANT_HAVE_PERMISSION;
+ }
+ if (intent.getComponent() != null) {
+ throw new SecurityException(
+ "Sticky broadcasts can't target a specific component");
+ }
+ synchronized (mStickyBroadcasts) {
+ // We use userId directly here, since the "all" target is maintained
+ // as a separate set of sticky broadcasts.
+ if (userId != UserHandle.USER_ALL) {
+ // But first, if this is not a broadcast to all users, then
+ // make sure it doesn't conflict with an existing broadcast to
+ // all users.
+ ArrayMap<String, ArrayList<StickyBroadcast>> stickies = mStickyBroadcasts.get(
+ UserHandle.USER_ALL);
+ if (stickies != null) {
+ ArrayList<StickyBroadcast> list = stickies.get(intent.getAction());
+ if (list != null) {
+ int N = list.size();
+ int i;
+ for (i = 0; i < N; i++) {
+ if (intent.filterEquals(list.get(i).intent)) {
+ throw new IllegalArgumentException("Sticky broadcast " + intent
+ + " for user " + userId
+ + " conflicts with existing global broadcast");
+ }
+ }
+ }
+ }
+ }
+ ArrayMap<String, ArrayList<StickyBroadcast>> stickies =
+ mStickyBroadcasts.get(userId);
+ if (stickies == null) {
+ stickies = new ArrayMap<>();
+ mStickyBroadcasts.put(userId, stickies);
+ }
+ ArrayList<StickyBroadcast> list = stickies.get(intent.getAction());
+ if (list == null) {
+ list = new ArrayList<>();
+ stickies.put(intent.getAction(), list);
+ }
+ final boolean deferUntilActive = BroadcastRecord.calculateDeferUntilActive(
+ callingUid, brOptions, resultTo, ordered,
+ BroadcastRecord.calculateUrgent(intent, brOptions));
+ final int stickiesCount = list.size();
+ int i;
+ for (i = 0; i < stickiesCount; i++) {
+ if (intent.filterEquals(list.get(i).intent)) {
+ // This sticky already exists, replace it.
+ list.set(i, StickyBroadcast.create(new Intent(intent), deferUntilActive,
+ callingUid, callerAppProcessState, resolvedType));
+ break;
+ }
+ }
+ if (i >= stickiesCount) {
+ list.add(StickyBroadcast.create(new Intent(intent), deferUntilActive,
+ callingUid, callerAppProcessState, resolvedType));
+ }
+ }
+ }
+
+ int[] users;
+ if (userId == UserHandle.USER_ALL) {
+ // Caller wants broadcast to go to all started users.
+ users = mService.mUserController.getStartedUserArray();
+ } else {
+ // Caller wants broadcast to go to one specific user.
+ users = new int[] {userId};
+ }
+
+ var args = new SaferIntentUtils.IntentArgs(intent, resolvedType,
+ true /* isReceiver */, true /* resolveForStart */, callingUid, callingPid);
+ args.platformCompat = mService.mPlatformCompat;
+
+ // Figure out who all will receive this broadcast.
+ final int cookie = BroadcastQueue.traceBegin("queryReceivers");
+ List receivers = null;
+ List<BroadcastFilter> registeredReceivers = null;
+ // Need to resolve the intent to interested receivers...
+ if ((intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
+ receivers = collectReceiverComponents(
+ intent, resolvedType, callingUid, callingPid, users, broadcastAllowList);
+ }
+ if (intent.getComponent() == null) {
+ final PackageDataSnapshot snapshot = mService.getPackageManagerInternal().snapshot();
+ if (userId == UserHandle.USER_ALL && callingUid == SHELL_UID) {
+ // Query one target user at a time, excluding shell-restricted users
+ for (int i = 0; i < users.length; i++) {
+ if (mService.mUserController.hasUserRestriction(
+ UserManager.DISALLOW_DEBUGGING_FEATURES, users[i])) {
+ continue;
+ }
+ List<BroadcastFilter> registeredReceiversForUser =
+ mReceiverResolver.queryIntent(snapshot, intent,
+ resolvedType, false /*defaultOnly*/, users[i]);
+ if (registeredReceivers == null) {
+ registeredReceivers = registeredReceiversForUser;
+ } else if (registeredReceiversForUser != null) {
+ registeredReceivers.addAll(registeredReceiversForUser);
+ }
+ }
+ } else {
+ registeredReceivers = mReceiverResolver.queryIntent(snapshot, intent,
+ resolvedType, false /*defaultOnly*/, userId);
+ }
+ if (registeredReceivers != null) {
+ SaferIntentUtils.blockNullAction(args, registeredReceivers);
+ }
+ }
+ BroadcastQueue.traceEnd(cookie);
+
+ final boolean replacePending =
+ (intent.getFlags() & Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0;
+
+ if (DEBUG_BROADCAST) {
+ Slog.v(TAG_BROADCAST, "Enqueueing broadcast: " + intent.getAction()
+ + " replacePending=" + replacePending);
+ }
+ if (registeredReceivers != null && broadcastAllowList != null) {
+ // if a uid whitelist was provided, remove anything in the application space that wasn't
+ // in it.
+ for (int i = registeredReceivers.size() - 1; i >= 0; i--) {
+ final int owningAppId = UserHandle.getAppId(registeredReceivers.get(i).owningUid);
+ if (owningAppId >= Process.FIRST_APPLICATION_UID
+ && Arrays.binarySearch(broadcastAllowList, owningAppId) < 0) {
+ registeredReceivers.remove(i);
+ }
+ }
+ }
+
+ int NR = registeredReceivers != null ? registeredReceivers.size() : 0;
+
+ // Merge into one list.
+ int ir = 0;
+ if (receivers != null) {
+ // A special case for PACKAGE_ADDED: do not allow the package
+ // being added to see this broadcast. This prevents them from
+ // using this as a back door to get run as soon as they are
+ // installed. Maybe in the future we want to have a special install
+ // broadcast or such for apps, but we'd like to deliberately make
+ // this decision.
+ String[] skipPackages = null;
+ if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())
+ || Intent.ACTION_PACKAGE_RESTARTED.equals(intent.getAction())
+ || Intent.ACTION_PACKAGE_DATA_CLEARED.equals(intent.getAction())) {
+ Uri data = intent.getData();
+ if (data != null) {
+ String pkgName = data.getSchemeSpecificPart();
+ if (pkgName != null) {
+ skipPackages = new String[] { pkgName };
+ }
+ }
+ } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(intent.getAction())) {
+ skipPackages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+ }
+ if (skipPackages != null && (skipPackages.length > 0)) {
+ for (String skipPackage : skipPackages) {
+ if (skipPackage != null) {
+ int NT = receivers.size();
+ for (int it = 0; it < NT; it++) {
+ ResolveInfo curt = (ResolveInfo) receivers.get(it);
+ if (curt.activityInfo.packageName.equals(skipPackage)) {
+ receivers.remove(it);
+ it--;
+ NT--;
+ }
+ }
+ }
+ }
+ }
+
+ int NT = receivers != null ? receivers.size() : 0;
+ int it = 0;
+ ResolveInfo curt = null;
+ BroadcastFilter curr = null;
+ while (it < NT && ir < NR) {
+ if (curt == null) {
+ curt = (ResolveInfo) receivers.get(it);
+ }
+ if (curr == null) {
+ curr = registeredReceivers.get(ir);
+ }
+ if (curr.getPriority() >= curt.priority) {
+ // Insert this broadcast record into the final list.
+ receivers.add(it, curr);
+ ir++;
+ curr = null;
+ it++;
+ NT++;
+ } else {
+ // Skip to the next ResolveInfo in the final list.
+ it++;
+ curt = null;
+ }
+ }
+ }
+ while (ir < NR) {
+ if (receivers == null) {
+ receivers = new ArrayList();
+ }
+ receivers.add(registeredReceivers.get(ir));
+ ir++;
+ }
+
+ if (isCallerSystem) {
+ checkBroadcastFromSystem(intent, callerApp, callerPackage, callingUid,
+ isProtectedBroadcast, receivers);
+ }
+
+ if ((receivers != null && receivers.size() > 0)
+ || resultTo != null) {
+ BroadcastQueue queue = mBroadcastQueue;
+ SaferIntentUtils.filterNonExportedComponents(args, receivers);
+ BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
+ callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
+ requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
+ receivers, resultToApp, resultTo, resultCode, resultData, resultExtras,
+ ordered, sticky, false, userId,
+ backgroundStartPrivileges, timeoutExempt, filterExtrasForReceiver,
+ callerAppProcessState);
+ broadcastSentEventRecord.setBroadcastRecord(r);
+
+ if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing ordered broadcast " + r);
+ queue.enqueueBroadcastLocked(r);
+ } else {
+ // There was nobody interested in the broadcast, but we still want to record
+ // that it happened.
+ if (intent.getComponent() == null && intent.getPackage() == null
+ && (intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
+ // This was an implicit broadcast... let's record it for posterity.
+ addBroadcastStatLocked(intent.getAction(), callerPackage, 0, 0, 0);
+ }
+ }
+
+ return ActivityManager.BROADCAST_SUCCESS;
+ }
+
+ @GuardedBy("mService")
+ private void scheduleCanceledResultTo(ProcessRecord resultToApp, IIntentReceiver resultTo,
+ Intent intent, int userId, BroadcastOptions options, int callingUid,
+ String callingPackage) {
+ if (resultTo == null) {
+ return;
+ }
+ final ProcessRecord app = resultToApp;
+ final IApplicationThread thread = (app != null) ? app.getOnewayThread() : null;
+ if (thread != null) {
+ try {
+ final boolean shareIdentity = (options != null && options.isShareIdentityEnabled());
+ thread.scheduleRegisteredReceiver(
+ resultTo, intent, Activity.RESULT_CANCELED, null, null,
+ false, false, true, userId, app.mState.getReportedProcState(),
+ shareIdentity ? callingUid : Process.INVALID_UID,
+ shareIdentity ? callingPackage : null);
+ } catch (RemoteException e) {
+ final String msg = "Failed to schedule result of " + intent + " via "
+ + app + ": " + e;
+ app.killLocked("Can't schedule resultTo", ApplicationExitInfo.REASON_OTHER,
+ ApplicationExitInfo.SUBREASON_UNDELIVERED_BROADCAST, true);
+ Slog.d(TAG, msg);
+ }
+ }
+ }
+
+ @GuardedBy("mService")
+ private int getRealProcessStateLocked(ProcessRecord app, int pid) {
+ if (app == null) {
+ synchronized (mService.mPidsSelfLocked) {
+ app = mService.mPidsSelfLocked.get(pid);
+ }
+ }
+ if (app != null && app.getThread() != null && !app.isKilled()) {
+ return app.mState.getCurProcState();
+ }
+ return PROCESS_STATE_NONEXISTENT;
+ }
+
+ @GuardedBy("mService")
+ private int getRealUidStateLocked(ProcessRecord app, int pid) {
+ if (app == null) {
+ synchronized (mService.mPidsSelfLocked) {
+ app = mService.mPidsSelfLocked.get(pid);
+ }
+ }
+ if (app != null && app.getThread() != null && !app.isKilled()) {
+ final UidRecord uidRecord = app.getUidRecord();
+ if (uidRecord != null) {
+ return uidRecord.getCurProcState();
+ }
+ }
+ return PROCESS_STATE_NONEXISTENT;
+ }
+
+ @VisibleForTesting
+ ArrayList<StickyBroadcast> getStickyBroadcastsForTest(String action, int userId) {
+ synchronized (mStickyBroadcasts) {
+ final ArrayMap<String, ArrayList<StickyBroadcast>> stickyBroadcasts =
+ mStickyBroadcasts.get(userId);
+ if (stickyBroadcasts == null) {
+ return null;
+ }
+ return stickyBroadcasts.get(action);
+ }
+ }
+
+ void unbroadcastIntent(IApplicationThread caller, Intent intent, int userId) {
+ // Refuse possible leaked file descriptors
+ if (intent != null && intent.hasFileDescriptors()) {
+ throw new IllegalArgumentException("File descriptors passed in Intent");
+ }
+
+ userId = mService.mUserController.handleIncomingUser(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, true, ALLOW_NON_FULL,
+ "removeStickyBroadcast", null);
+
+ if (mService.checkCallingPermission(android.Manifest.permission.BROADCAST_STICKY)
+ != PackageManager.PERMISSION_GRANTED) {
+ String msg = "Permission Denial: unbroadcastIntent() from pid="
+ + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid()
+ + " requires " + android.Manifest.permission.BROADCAST_STICKY;
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+ synchronized (mStickyBroadcasts) {
+ ArrayMap<String, ArrayList<StickyBroadcast>> stickies = mStickyBroadcasts.get(userId);
+ if (stickies != null) {
+ ArrayList<StickyBroadcast> list = stickies.get(intent.getAction());
+ if (list != null) {
+ int N = list.size();
+ int i;
+ for (i = 0; i < N; i++) {
+ if (intent.filterEquals(list.get(i).intent)) {
+ list.remove(i);
+ break;
+ }
+ }
+ if (list.size() <= 0) {
+ stickies.remove(intent.getAction());
+ }
+ }
+ if (stickies.size() <= 0) {
+ mStickyBroadcasts.remove(userId);
+ }
+ }
+ }
+ }
+
+ void finishReceiver(IBinder caller, int resultCode, String resultData,
+ Bundle resultExtras, boolean resultAbort, int flags) {
+ if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Finish receiver: " + caller);
+
+ // Refuse possible leaked file descriptors
+ if (resultExtras != null && resultExtras.hasFileDescriptors()) {
+ throw new IllegalArgumentException("File descriptors passed in Bundle");
+ }
+
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ synchronized (mService) {
+ final ProcessRecord callerApp = mService.getRecordForAppLOSP(caller);
+ if (callerApp == null) {
+ Slog.w(TAG, "finishReceiver: no app for " + caller);
+ return;
+ }
+
+ mBroadcastQueue.finishReceiverLocked(callerApp, resultCode,
+ resultData, resultExtras, resultAbort, true);
+ // updateOomAdjLocked() will be done here
+ mService.trimApplicationsLocked(false, OOM_ADJ_REASON_FINISH_RECEIVER);
+ }
+
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+
+ /**
+ * @return uid from the extra field {@link Intent#EXTRA_UID} if present, Otherwise -1
+ */
+ private int getUidFromIntent(Intent intent) {
+ if (intent == null) {
+ return -1;
+ }
+ final Bundle intentExtras = intent.getExtras();
+ return intent.hasExtra(Intent.EXTRA_UID)
+ ? intentExtras.getInt(Intent.EXTRA_UID) : -1;
+ }
+
+ final void rotateBroadcastStatsIfNeededLocked() {
+ final long now = SystemClock.elapsedRealtime();
+ if (mCurBroadcastStats == null
+ || (mCurBroadcastStats.mStartRealtime + (24 * 60 * 60 * 1000) < now)) {
+ mLastBroadcastStats = mCurBroadcastStats;
+ if (mLastBroadcastStats != null) {
+ mLastBroadcastStats.mEndRealtime = SystemClock.elapsedRealtime();
+ mLastBroadcastStats.mEndUptime = SystemClock.uptimeMillis();
+ }
+ mCurBroadcastStats = new BroadcastStats();
+ }
+ }
+
+ final void addBroadcastStatLocked(String action, String srcPackage, int receiveCount,
+ int skipCount, long dispatchTime) {
+ rotateBroadcastStatsIfNeededLocked();
+ mCurBroadcastStats.addBroadcast(action, srcPackage, receiveCount, skipCount, dispatchTime);
+ }
+
+ final void addBackgroundCheckViolationLocked(String action, String targetPackage) {
+ rotateBroadcastStatsIfNeededLocked();
+ mCurBroadcastStats.addBackgroundCheckViolation(action, targetPackage);
+ }
+
+ final void notifyBroadcastFinishedLocked(@NonNull BroadcastRecord original) {
+ final ApplicationInfo info = original.callerApp != null ? original.callerApp.info : null;
+ final String callerPackage = info != null ? info.packageName : original.callerPackage;
+ if (callerPackage != null) {
+ mService.mHandler.obtainMessage(ActivityManagerService.DISPATCH_SENDING_BROADCAST_EVENT,
+ original.callingUid, 0, callerPackage).sendToTarget();
+ }
+ }
+
+ final Intent verifyBroadcastLocked(Intent intent) {
+ if (intent != null) {
+ intent.prepareToEnterSystemServer();
+ }
+
+ int flags = intent.getFlags();
+
+ if (!mService.mProcessesReady) {
+ // if the caller really truly claims to know what they're doing, go
+ // ahead and allow the broadcast without launching any receivers
+ if ((flags & Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT) != 0) {
+ // This will be turned into a FLAG_RECEIVER_REGISTERED_ONLY later on if needed.
+ } else if ((flags & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
+ Slog.e(TAG, "Attempt to launch receivers of broadcast intent " + intent
+ + " before boot completion");
+ throw new IllegalStateException("Cannot broadcast before boot completed");
+ }
+ }
+
+ if ((flags & Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0) {
+ throw new IllegalArgumentException(
+ "Can't use FLAG_RECEIVER_BOOT_UPGRADE here");
+ }
+
+ if ((flags & Intent.FLAG_RECEIVER_FROM_SHELL) != 0) {
+ switch (Binder.getCallingUid()) {
+ case ROOT_UID:
+ case SHELL_UID:
+ break;
+ default:
+ Slog.w(TAG, "Removing FLAG_RECEIVER_FROM_SHELL because caller is UID "
+ + Binder.getCallingUid());
+ intent.removeFlags(Intent.FLAG_RECEIVER_FROM_SHELL);
+ break;
+ }
+ }
+
+ return intent;
+ }
+
+ private ArraySet<String> getBackgroundLaunchBroadcasts() {
+ if (mBackgroundLaunchBroadcasts == null) {
+ mBackgroundLaunchBroadcasts = SystemConfig.getInstance().getAllowImplicitBroadcasts();
+ }
+ return mBackgroundLaunchBroadcasts;
+ }
+
+ private boolean isInstantApp(ProcessRecord record, @Nullable String callerPackage, int uid) {
+ if (UserHandle.getAppId(uid) < FIRST_APPLICATION_UID) {
+ return false;
+ }
+ // Easy case -- we have the app's ProcessRecord.
+ if (record != null) {
+ return record.info.isInstantApp();
+ }
+ // Otherwise check with PackageManager.
+ IPackageManager pm = AppGlobals.getPackageManager();
+ try {
+ if (callerPackage == null) {
+ final String[] packageNames = pm.getPackagesForUid(uid);
+ if (packageNames == null || packageNames.length == 0) {
+ throw new IllegalArgumentException("Unable to determine caller package name");
+ }
+ // Instant Apps can't use shared uids, so its safe to only check the first package.
+ callerPackage = packageNames[0];
+ }
+ mService.mAppOpsService.checkPackage(uid, callerPackage);
+ return pm.isInstantApp(callerPackage, UserHandle.getUserId(uid));
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error looking up if " + callerPackage + " is an instant app.", e);
+ return true;
+ }
+ }
+
+ private String getWearRemoteIntentAction() {
+ return mContext.getResources().getString(
+ com.android.internal.R.string.config_wearRemoteIntentAction);
+ }
+
+ private void sendPackageBroadcastLocked(int cmd, String[] packages, int userId) {
+ mService.mProcessList.sendPackageBroadcastLocked(cmd, packages, userId);
+ }private List<ResolveInfo> collectReceiverComponents(
+ Intent intent, String resolvedType, int callingUid, int callingPid,
+ int[] users, int[] broadcastAllowList) {
+ // TODO: come back and remove this assumption to triage all broadcasts
+ long pmFlags = STOCK_PM_FLAGS | MATCH_DEBUG_TRIAGED_MISSING;
+
+ List<ResolveInfo> receivers = null;
+ HashSet<ComponentName> singleUserReceivers = null;
+ boolean scannedFirstReceivers = false;
+ for (int user : users) {
+ // Skip users that have Shell restrictions
+ if (callingUid == SHELL_UID
+ && mService.mUserController.hasUserRestriction(
+ UserManager.DISALLOW_DEBUGGING_FEATURES, user)) {
+ continue;
+ }
+ List<ResolveInfo> newReceivers = mService.mPackageManagerInt.queryIntentReceivers(
+ intent, resolvedType, pmFlags, callingUid, callingPid, user, /* forSend */true);
+ if (user != UserHandle.USER_SYSTEM && newReceivers != null) {
+ // If this is not the system user, we need to check for
+ // any receivers that should be filtered out.
+ for (int i = 0; i < newReceivers.size(); i++) {
+ ResolveInfo ri = newReceivers.get(i);
+ if ((ri.activityInfo.flags & ActivityInfo.FLAG_SYSTEM_USER_ONLY) != 0) {
+ newReceivers.remove(i);
+ i--;
+ }
+ }
+ }
+ // Replace the alias receivers with their targets.
+ if (newReceivers != null) {
+ for (int i = newReceivers.size() - 1; i >= 0; i--) {
+ final ResolveInfo ri = newReceivers.get(i);
+ final ComponentAliasResolver.Resolution<ResolveInfo> resolution =
+ mService.mComponentAliasResolver.resolveReceiver(intent, ri,
+ resolvedType, pmFlags, user, callingUid, callingPid);
+ if (resolution == null) {
+ // It was an alias, but the target was not found.
+ newReceivers.remove(i);
+ continue;
+ }
+ if (resolution.isAlias()) {
+ newReceivers.set(i, resolution.getTarget());
+ }
+ }
+ }
+ if (newReceivers != null && newReceivers.size() == 0) {
+ newReceivers = null;
+ }
+
+ if (receivers == null) {
+ receivers = newReceivers;
+ } else if (newReceivers != null) {
+ // We need to concatenate the additional receivers
+ // found with what we have do far. This would be easy,
+ // but we also need to de-dup any receivers that are
+ // singleUser.
+ if (!scannedFirstReceivers) {
+ // Collect any single user receivers we had already retrieved.
+ scannedFirstReceivers = true;
+ for (int i = 0; i < receivers.size(); i++) {
+ ResolveInfo ri = receivers.get(i);
+ if ((ri.activityInfo.flags & ActivityInfo.FLAG_SINGLE_USER) != 0) {
+ ComponentName cn = new ComponentName(
+ ri.activityInfo.packageName, ri.activityInfo.name);
+ if (singleUserReceivers == null) {
+ singleUserReceivers = new HashSet<ComponentName>();
+ }
+ singleUserReceivers.add(cn);
+ }
+ }
+ }
+ // Add the new results to the existing results, tracking
+ // and de-dupping single user receivers.
+ for (int i = 0; i < newReceivers.size(); i++) {
+ ResolveInfo ri = newReceivers.get(i);
+ if ((ri.activityInfo.flags & ActivityInfo.FLAG_SINGLE_USER) != 0) {
+ ComponentName cn = new ComponentName(
+ ri.activityInfo.packageName, ri.activityInfo.name);
+ if (singleUserReceivers == null) {
+ singleUserReceivers = new HashSet<ComponentName>();
+ }
+ if (!singleUserReceivers.contains(cn)) {
+ singleUserReceivers.add(cn);
+ receivers.add(ri);
+ }
+ } else {
+ receivers.add(ri);
+ }
+ }
+ }
+ }
+ if (receivers != null && broadcastAllowList != null) {
+ for (int i = receivers.size() - 1; i >= 0; i--) {
+ final int receiverAppId = UserHandle.getAppId(
+ receivers.get(i).activityInfo.applicationInfo.uid);
+ if (receiverAppId >= Process.FIRST_APPLICATION_UID
+ && Arrays.binarySearch(broadcastAllowList, receiverAppId) < 0) {
+ receivers.remove(i);
+ }
+ }
+ }
+ return receivers;
+ }
+
+ private void checkBroadcastFromSystem(Intent intent, ProcessRecord callerApp,
+ String callerPackage, int callingUid, boolean isProtectedBroadcast, List receivers) {
+ if ((intent.getFlags() & Intent.FLAG_RECEIVER_FROM_SHELL) != 0) {
+ // Don't yell about broadcasts sent via shell
+ return;
+ }
+
+ final String action = intent.getAction();
+ if (isProtectedBroadcast
+ || Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)
+ || Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(action)
+ || Intent.ACTION_MEDIA_BUTTON.equals(action)
+ || Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action)
+ || Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(action)
+ || Intent.ACTION_MASTER_CLEAR.equals(action)
+ || Intent.ACTION_FACTORY_RESET.equals(action)
+ || AppWidgetManager.ACTION_APPWIDGET_CONFIGURE.equals(action)
+ || AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)
+ || TelephonyManager.ACTION_REQUEST_OMADM_CONFIGURATION_UPDATE.equals(action)
+ || SuggestionSpan.ACTION_SUGGESTION_PICKED.equals(action)
+ || AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION.equals(action)
+ || AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION.equals(action)) {
+ // Broadcast is either protected, or it's a public action that
+ // we've relaxed, so it's fine for system internals to send.
+ return;
+ }
+
+ // This broadcast may be a problem... but there are often system components that
+ // want to send an internal broadcast to themselves, which is annoying to have to
+ // explicitly list each action as a protected broadcast, so we will check for that
+ // one safe case and allow it: an explicit broadcast, only being received by something
+ // that has protected itself.
+ if (intent.getPackage() != null || intent.getComponent() != null) {
+ if (receivers == null || receivers.size() == 0) {
+ // Intent is explicit and there's no receivers.
+ // This happens, e.g. , when a system component sends a broadcast to
+ // its own runtime receiver, and there's no manifest receivers for it,
+ // because this method is called twice for each broadcast,
+ // for runtime receivers and manifest receivers and the later check would find
+ // no receivers.
+ return;
+ }
+ boolean allProtected = true;
+ for (int i = receivers.size() - 1; i >= 0; i--) {
+ Object target = receivers.get(i);
+ if (target instanceof ResolveInfo) {
+ ResolveInfo ri = (ResolveInfo) target;
+ if (ri.activityInfo.exported && ri.activityInfo.permission == null) {
+ allProtected = false;
+ break;
+ }
+ } else {
+ BroadcastFilter bf = (BroadcastFilter) target;
+ if (bf.exported && bf.requiredPermission == null) {
+ allProtected = false;
+ break;
+ }
+ }
+ }
+ if (allProtected) {
+ // All safe!
+ return;
+ }
+ }
+
+ // The vast majority of broadcasts sent from system internals
+ // should be protected to avoid security holes, so yell loudly
+ // to ensure we examine these cases.
+ if (callerApp != null) {
+ Log.wtf(TAG, "Sending non-protected broadcast " + action
+ + " from system " + callerApp.toShortString() + " pkg " + callerPackage,
+ new Throwable());
+ } else {
+ Log.wtf(TAG, "Sending non-protected broadcast " + action
+ + " from system uid " + UserHandle.formatUid(callingUid)
+ + " pkg " + callerPackage,
+ new Throwable());
+ }
+ }
+
+ // Apply permission policy around the use of specific broadcast options
+ void enforceBroadcastOptionPermissionsInternal(
+ @Nullable Bundle options, int callingUid) {
+ enforceBroadcastOptionPermissionsInternal(BroadcastOptions.fromBundleNullable(options),
+ callingUid);
+ }
+
+ private void enforceBroadcastOptionPermissionsInternal(
+ @Nullable BroadcastOptions options, int callingUid) {
+ if (options != null && callingUid != Process.SYSTEM_UID) {
+ if (options.isAlarmBroadcast()) {
+ if (DEBUG_BROADCAST_LIGHT) {
+ Slog.w(TAG, "Non-system caller " + callingUid
+ + " may not flag broadcast as alarm");
+ }
+ throw new SecurityException(
+ "Non-system callers may not flag broadcasts as alarm");
+ }
+ if (options.isInteractive()) {
+ mService.enforceCallingPermission(
+ android.Manifest.permission.BROADCAST_OPTION_INTERACTIVE,
+ "setInteractive");
+ }
+ }
+ }
+
+ void startBroadcastObservers() {
+ mBroadcastQueue.start(mContext.getContentResolver());
+ }
+
+ void removeStickyBroadcasts(int userId) {
+ synchronized (mStickyBroadcasts) {
+ mStickyBroadcasts.remove(userId);
+ }
+ }
+
+ @NeverCompile
+ void dumpBroadcastsLocked(FileDescriptor fd, PrintWriter pw, String[] args,
+ int opti, boolean dumpAll, String dumpPackage) {
+ boolean dumpConstants = true;
+ boolean dumpHistory = true;
+ boolean needSep = false;
+ boolean onlyHistory = false;
+ boolean printedAnything = false;
+ boolean onlyReceivers = false;
+ int filteredUid = Process.INVALID_UID;
+
+ if ("history".equals(dumpPackage)) {
+ if (opti < args.length && "-s".equals(args[opti])) {
+ dumpAll = false;
+ }
+ onlyHistory = true;
+ dumpPackage = null;
+ }
+ if ("receivers".equals(dumpPackage)) {
+ onlyReceivers = true;
+ dumpPackage = null;
+ if (opti + 2 <= args.length) {
+ for (int i = opti; i < args.length; i++) {
+ String arg = args[i];
+ switch (arg) {
+ case "--uid":
+ filteredUid = getIntArg(pw, args, ++i, Process.INVALID_UID);
+ if (filteredUid == Process.INVALID_UID) {
+ return;
+ }
+ break;
+ default:
+ pw.printf("Invalid argument at index %d: %s\n", i, arg);
+ return;
+ }
+ }
+ }
+ }
+ if (DEBUG_BROADCAST) {
+ Slogf.d(TAG_BROADCAST, "dumpBroadcastsLocked(): dumpPackage=%s, onlyHistory=%b, "
+ + "onlyReceivers=%b, filteredUid=%d", dumpPackage, onlyHistory,
+ onlyReceivers, filteredUid);
+ }
+
+ pw.println("ACTIVITY MANAGER BROADCAST STATE (dumpsys activity broadcasts)");
+ if (!onlyHistory && dumpAll) {
+ if (mRegisteredReceivers.size() > 0) {
+ boolean printed = false;
+ Iterator it = mRegisteredReceivers.values().iterator();
+ while (it.hasNext()) {
+ ReceiverList r = (ReceiverList) it.next();
+ if (dumpPackage != null && (r.app == null
+ || !dumpPackage.equals(r.app.info.packageName))) {
+ continue;
+ }
+ if (filteredUid != Process.INVALID_UID && filteredUid != r.app.uid) {
+ if (DEBUG_BROADCAST) {
+ Slogf.v(TAG_BROADCAST, "dumpBroadcastsLocked(): skipping receiver whose"
+ + " uid (%d) is not %d: %s", r.app.uid, filteredUid, r.app);
+ }
+ continue;
+ }
+ if (!printed) {
+ pw.println(" Registered Receivers:");
+ needSep = true;
+ printed = true;
+ printedAnything = true;
+ }
+ pw.print(" * "); pw.println(r);
+ r.dump(pw, " ");
+ }
+ } else {
+ if (onlyReceivers) {
+ pw.println(" (no registered receivers)");
+ }
+ }
+
+ if (!onlyReceivers) {
+ if (mReceiverResolver.dump(pw, needSep
+ ? "\n Receiver Resolver Table:" : " Receiver Resolver Table:",
+ " ", dumpPackage, false, false)) {
+ needSep = true;
+ printedAnything = true;
+ }
+ }
+ }
+
+ if (!onlyReceivers) {
+ needSep = mBroadcastQueue.dumpLocked(fd, pw, args, opti,
+ dumpConstants, dumpHistory, dumpAll, dumpPackage, needSep);
+ printedAnything |= needSep;
+ }
+
+ needSep = true;
+
+ synchronized (mStickyBroadcasts) {
+ if (!onlyHistory && !onlyReceivers && mStickyBroadcasts != null
+ && dumpPackage == null) {
+ for (int user = 0; user < mStickyBroadcasts.size(); user++) {
+ if (needSep) {
+ pw.println();
+ }
+ needSep = true;
+ printedAnything = true;
+ pw.print(" Sticky broadcasts for user ");
+ pw.print(mStickyBroadcasts.keyAt(user));
+ pw.println(":");
+ StringBuilder sb = new StringBuilder(128);
+ for (Map.Entry<String, ArrayList<StickyBroadcast>> ent
+ : mStickyBroadcasts.valueAt(user).entrySet()) {
+ pw.print(" * Sticky action ");
+ pw.print(ent.getKey());
+ if (dumpAll) {
+ pw.println(":");
+ ArrayList<StickyBroadcast> broadcasts = ent.getValue();
+ final int N = broadcasts.size();
+ for (int i = 0; i < N; i++) {
+ final Intent intent = broadcasts.get(i).intent;
+ final boolean deferUntilActive = broadcasts.get(i).deferUntilActive;
+ sb.setLength(0);
+ sb.append(" Intent: ");
+ intent.toShortString(sb, false, true, false, false);
+ pw.print(sb);
+ if (deferUntilActive) {
+ pw.print(" [D]");
+ }
+ pw.println();
+ pw.print(" originalCallingUid: ");
+ pw.println(broadcasts.get(i).originalCallingUid);
+ pw.println();
+ Bundle bundle = intent.getExtras();
+ if (bundle != null) {
+ pw.print(" extras: ");
+ pw.println(bundle);
+ }
+ }
+ } else {
+ pw.println("");
+ }
+ }
+ }
+ }
+ }
+
+ if (!onlyHistory && !onlyReceivers && dumpAll) {
+ pw.println();
+ pw.println(" Queue " + mBroadcastQueue.toString() + ": "
+ + mBroadcastQueue.describeStateLocked());
+ pw.println(" mHandler:");
+ mService.mHandler.dump(new PrintWriterPrinter(pw), " ");
+ needSep = true;
+ printedAnything = true;
+ }
+
+ if (!printedAnything) {
+ pw.println(" (nothing)");
+ }
+ }
+
+ /**
+ * Gets an {@code int} argument from the given {@code index} on {@code args}, logging an error
+ * message on {@code pw} when it cannot be parsed.
+ *
+ * Returns {@code int} argument or {@code invalidValue} if it could not be parsed.
+ */
+ private static int getIntArg(PrintWriter pw, String[] args, int index, int invalidValue) {
+ if (index > args.length) {
+ pw.println("Missing argument");
+ return invalidValue;
+ }
+ String arg = args[index];
+ try {
+ return Integer.parseInt(arg);
+ } catch (Exception e) {
+ pw.printf("Non-numeric argument at index %d: %s\n", index, arg);
+ return invalidValue;
+ }
+ }
+
+ @NeverCompile
+ void dumpBroadcastStatsLocked(FileDescriptor fd, PrintWriter pw, String[] args,
+ int opti, boolean dumpAll, String dumpPackage) {
+ if (mCurBroadcastStats == null) {
+ return;
+ }
+
+ pw.println("ACTIVITY MANAGER BROADCAST STATS STATE (dumpsys activity broadcast-stats)");
+ final long now = SystemClock.elapsedRealtime();
+ if (mLastBroadcastStats != null) {
+ pw.print(" Last stats (from ");
+ TimeUtils.formatDuration(mLastBroadcastStats.mStartRealtime, now, pw);
+ pw.print(" to ");
+ TimeUtils.formatDuration(mLastBroadcastStats.mEndRealtime, now, pw);
+ pw.print(", ");
+ TimeUtils.formatDuration(mLastBroadcastStats.mEndUptime
+ - mLastBroadcastStats.mStartUptime, pw);
+ pw.println(" uptime):");
+ if (!mLastBroadcastStats.dumpStats(pw, " ", dumpPackage)) {
+ pw.println(" (nothing)");
+ }
+ pw.println();
+ }
+ pw.print(" Current stats (from ");
+ TimeUtils.formatDuration(mCurBroadcastStats.mStartRealtime, now, pw);
+ pw.print(" to now, ");
+ TimeUtils.formatDuration(SystemClock.uptimeMillis()
+ - mCurBroadcastStats.mStartUptime, pw);
+ pw.println(" uptime):");
+ if (!mCurBroadcastStats.dumpStats(pw, " ", dumpPackage)) {
+ pw.println(" (nothing)");
+ }
+ }
+
+ @NeverCompile
+ void dumpBroadcastStatsCheckinLocked(FileDescriptor fd, PrintWriter pw, String[] args,
+ int opti, boolean fullCheckin, String dumpPackage) {
+ if (mCurBroadcastStats == null) {
+ return;
+ }
+
+ if (mLastBroadcastStats != null) {
+ mLastBroadcastStats.dumpCheckinStats(pw, dumpPackage);
+ if (fullCheckin) {
+ mLastBroadcastStats = null;
+ return;
+ }
+ }
+ mCurBroadcastStats.dumpCheckinStats(pw, dumpPackage);
+ if (fullCheckin) {
+ mCurBroadcastStats = null;
+ }
+ }
+
+ void writeBroadcastsToProtoLocked(ProtoOutputStream proto) {
+ if (mRegisteredReceivers.size() > 0) {
+ Iterator it = mRegisteredReceivers.values().iterator();
+ while (it.hasNext()) {
+ ReceiverList r = (ReceiverList) it.next();
+ r.dumpDebug(proto, ActivityManagerServiceDumpBroadcastsProto.RECEIVER_LIST);
+ }
+ }
+ mReceiverResolver.dumpDebug(proto,
+ ActivityManagerServiceDumpBroadcastsProto.RECEIVER_RESOLVER);
+ mBroadcastQueue.dumpDebug(proto, ActivityManagerServiceDumpBroadcastsProto.BROADCAST_QUEUE);
+ synchronized (mStickyBroadcasts) {
+ for (int user = 0; user < mStickyBroadcasts.size(); user++) {
+ long token = proto.start(
+ ActivityManagerServiceDumpBroadcastsProto.STICKY_BROADCASTS);
+ proto.write(StickyBroadcastProto.USER, mStickyBroadcasts.keyAt(user));
+ for (Map.Entry<String, ArrayList<StickyBroadcast>> ent
+ : mStickyBroadcasts.valueAt(user).entrySet()) {
+ long actionToken = proto.start(StickyBroadcastProto.ACTIONS);
+ proto.write(StickyBroadcastProto.StickyAction.NAME, ent.getKey());
+ for (StickyBroadcast broadcast : ent.getValue()) {
+ broadcast.intent.dumpDebug(proto, StickyBroadcastProto.StickyAction.INTENTS,
+ false, true, true, false);
+ }
+ proto.end(actionToken);
+ }
+ proto.end(token);
+ }
+ }
+
+ long handlerToken = proto.start(ActivityManagerServiceDumpBroadcastsProto.HANDLER);
+ proto.write(ActivityManagerServiceDumpBroadcastsProto.MainHandler.HANDLER,
+ mService.mHandler.toString());
+ mService.mHandler.getLooper().dumpDebug(proto,
+ ActivityManagerServiceDumpBroadcastsProto.MainHandler.LOOPER);
+ proto.end(handlerToken);
+ }
+
+ @VisibleForTesting
+ static final class StickyBroadcast {
+ public Intent intent;
+ public boolean deferUntilActive;
+ public int originalCallingUid;
+ /** The snapshot process state of the app who sent this broadcast */
+ public int originalCallingAppProcessState;
+ public String resolvedDataType;
+
+ public static StickyBroadcast create(Intent intent, boolean deferUntilActive,
+ int originalCallingUid, int originalCallingAppProcessState,
+ String resolvedDataType) {
+ final StickyBroadcast b = new StickyBroadcast();
+ b.intent = intent;
+ b.deferUntilActive = deferUntilActive;
+ b.originalCallingUid = originalCallingUid;
+ b.originalCallingAppProcessState = originalCallingAppProcessState;
+ b.resolvedDataType = resolvedDataType;
+ return b;
+ }
+
+ @Override
+ public String toString() {
+ return "{intent=" + intent + ", defer=" + deferUntilActive + ", originalCallingUid="
+ + originalCallingUid + ", originalCallingAppProcessState="
+ + originalCallingAppProcessState + ", type=" + resolvedDataType + "}";
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index a7b2eb1..8fe33d1 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -929,9 +929,9 @@
// For ordered broadcast, check if the receivers for the new broadcast is a superset
// of those for the previous one as skipping and removing only one of them could result
// in an inconsistent state.
- if (testRecord.ordered || testRecord.prioritized) {
+ if (testRecord.ordered) {
return containsAllReceivers(r, testRecord, recordsLookupCache);
- } else if (testRecord.resultTo != null) {
+ } else if (testRecord.prioritized || testRecord.resultTo != null) {
return testRecord.getDeliveryState(testIndex) == DELIVERY_DEFERRED
? r.containsReceiver(testRecord.receivers.get(testIndex))
: containsAllReceivers(r, testRecord, recordsLookupCache);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 5d48d09..2937307 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -33,6 +33,7 @@
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.providers.settings.Flags;
import android.aconfigd.Aconfigd.StorageRequestMessage;
import android.aconfigd.Aconfigd.StorageRequestMessages;
@@ -51,6 +52,7 @@
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
+import java.util.Set;
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
/**
@@ -457,6 +459,24 @@
}
/**
+ * Send a request to aconfig storage to remove a flag local override.
+ *
+ * @param proto
+ * @param packageName the package of the flag
+ * @param flagName the name of the flag
+ */
+ static void writeFlagOverrideRemovalRequest(
+ ProtoOutputStream proto, String packageName, String flagName) {
+ long msgsToken = proto.start(StorageRequestMessages.MSGS);
+ long msgToken = proto.start(StorageRequestMessage.REMOVE_LOCAL_OVERRIDE_MESSAGE);
+ proto.write(StorageRequestMessage.RemoveLocalOverrideMessage.PACKAGE_NAME, packageName);
+ proto.write(StorageRequestMessage.RemoveLocalOverrideMessage.FLAG_NAME, flagName);
+ proto.write(StorageRequestMessage.RemoveLocalOverrideMessage.REMOVE_ALL, false);
+ proto.end(msgToken);
+ proto.end(msgsToken);
+ }
+
+ /**
* deserialize a flag input proto stream and log
* @param proto
*/
@@ -501,8 +521,15 @@
ProtoOutputStream requests = new ProtoOutputStream();
for (String flagName : props.getKeyset()) {
String flagValue = props.getString(flagName, null);
- if (flagName == null || flagValue == null) {
- continue;
+
+ if (Flags.syncLocalOverridesRemovalNewStorage()) {
+ if (flagName == null) {
+ continue;
+ }
+ } else {
+ if (flagName == null || flagValue == null) {
+ continue;
+ }
}
int idx = flagName.indexOf(":");
@@ -519,7 +546,13 @@
}
String packageName = fullFlagName.substring(0, idx);
String realFlagName = fullFlagName.substring(idx+1);
- writeFlagOverrideRequest(requests, packageName, realFlagName, flagValue, true);
+
+ if (Flags.syncLocalOverridesRemovalNewStorage() && flagValue == null) {
+ writeFlagOverrideRemovalRequest(requests, packageName, realFlagName);
+ } else {
+ writeFlagOverrideRequest(requests, packageName, realFlagName, flagValue, true);
+ }
+
++num_requests;
}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 8d8a54e..082dca6 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -1457,7 +1457,7 @@
private int setDevicesRoleForCapturePreset(int capturePreset, int role,
@NonNull List<AudioDeviceAttributes> devices) {
return setDevicesRole(mAppliedPresetRoles, (p, r, d) -> {
- return mAudioSystem.addDevicesRoleForCapturePreset(p, r, d);
+ return mAudioSystem.setDevicesRoleForCapturePreset(p, r, d);
}, (p, r, d) -> {
return mAudioSystem.clearDevicesRoleForCapturePreset(p, r);
}, capturePreset, role, devices);
diff --git a/services/core/java/com/android/server/display/BrightnessSetting.java b/services/core/java/com/android/server/display/BrightnessSetting.java
index 7d26004..5488446 100644
--- a/services/core/java/com/android/server/display/BrightnessSetting.java
+++ b/services/core/java/com/android/server/display/BrightnessSetting.java
@@ -150,6 +150,13 @@
}
/**
+ * Flush the brightness update that has been made to the persistent data store.
+ */
+ public void saveIfNeeded() {
+ mPersistentDataStore.saveIfNeeded();
+ }
+
+ /**
* @return The brightness for the default display in nits. Used when the underlying display
* device has changed but we want to persist the nit value.
*/
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 55a6ce7..187caba 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1181,7 +1181,10 @@
private DisplayInfo getDisplayInfoForFrameRateOverride(DisplayEventReceiver.FrameRateOverride[]
frameRateOverrides, DisplayInfo info, int callingUid) {
+ // Start with the display frame rate
float frameRateHz = info.renderFrameRate;
+
+ // If the app has a specific override, use that instead
for (DisplayEventReceiver.FrameRateOverride frameRateOverride : frameRateOverrides) {
if (frameRateOverride.uid == callingUid) {
frameRateHz = frameRateOverride.frameRateHz;
@@ -1200,18 +1203,21 @@
DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE, callingUid);
// Override the refresh rate only if it is a divisor of the current
- // refresh rate. This calculation needs to be in sync with the native code
+ // vsync rate. This calculation needs to be in sync with the native code
// in RefreshRateSelector::getFrameRateDivisor
Display.Mode currentMode = info.getMode();
- float numPeriods = currentMode.getRefreshRate() / frameRateHz;
+ float vsyncRate = currentMode.getVsyncRate();
+ float numPeriods = vsyncRate / frameRateHz;
float numPeriodsRound = Math.round(numPeriods);
if (Math.abs(numPeriods - numPeriodsRound) > THRESHOLD_FOR_REFRESH_RATES_DIVISORS) {
return info;
}
- frameRateHz = currentMode.getRefreshRate() / numPeriodsRound;
+ frameRateHz = vsyncRate / numPeriodsRound;
DisplayInfo overriddenInfo = new DisplayInfo();
overriddenInfo.copyFrom(info);
+
+ // If there is a mode that matches the override, use that one
for (Display.Mode mode : info.supportedModes) {
if (!mode.equalsExceptRefreshRate(currentMode)) {
continue;
@@ -1231,8 +1237,9 @@
return overriddenInfo;
}
}
-
overriddenInfo.refreshRateOverride = frameRateHz;
+
+ // Create a fake mode for app compat
if (!displayModeReturnsPhysicalRefreshRate) {
overriddenInfo.supportedModes = Arrays.copyOf(info.supportedModes,
info.supportedModes.length + 1);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index ab79713..a887f6d 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -2484,6 +2484,11 @@
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
mAutomaticBrightnessStrategy.setUseAutoBrightness(screenBrightnessModeSetting
== Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+
+ if (screenBrightnessModeSetting == Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL) {
+ // In manual mode, all brightness changes should be saved immediately.
+ mDisplayBrightnessController.saveBrightnessIfNeeded();
+ }
}
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
index e157b05..72a91d5 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
@@ -339,6 +339,13 @@
}
/**
+ * Flush the brightness update that has been made to the persistent data store.
+ */
+ public void saveBrightnessIfNeeded() {
+ mBrightnessSetting.saveIfNeeded();
+ }
+
+ /**
* Sets the current screen brightness, and notifies the BrightnessSetting about the change.
*/
public void updateScreenBrightnessSetting(float brightnessValue, float maxBrightness) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index f34b4e9..7ce9ee6 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -3037,6 +3037,7 @@
intent.putExtra("input_method_id", id);
mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
}
+ bindingController.unbindCurrentMethod();
unbindCurrentClientLocked(UnbindReason.SWITCH_IME, userId);
} finally {
Binder.restoreCallingIdentity(ident);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
index d9e9e00..cf2cdc1 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
@@ -33,6 +33,7 @@
import android.content.DialogInterface;
import android.content.Intent;
import android.os.UserHandle;
+import android.provider.Settings;
import android.text.TextUtils;
import android.util.Printer;
import android.util.Slog;
@@ -115,7 +116,11 @@
final var selectedImi = selectedIndex >= 0 ? items.get(selectedIndex).mImi : null;
final var languageSettingsIntent = selectedImi != null
? selectedImi.createImeLanguageSettingsActivityIntent() : null;
- final boolean hasLanguageSettingsButton = languageSettingsIntent != null;
+ final boolean isDeviceProvisioned = Settings.Global.getInt(
+ dialogWindowContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED,
+ 0) != 0;
+ final boolean hasLanguageSettingsButton = languageSettingsIntent != null
+ && isDeviceProvisioned;
if (hasLanguageSettingsButton) {
final View buttonBar = contentView
.requireViewById(com.android.internal.R.id.button_bar);
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index ee3f48d..06d1285 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -485,7 +485,7 @@
newConfig = mConfig.copy();
ZenRule rule = new ZenRule();
populateZenRule(pkg, automaticZenRule, rule, origin, /* isNew= */ true);
- rule = maybeRestoreRemovedRule(newConfig, rule, automaticZenRule, origin);
+ rule = maybeRestoreRemovedRule(newConfig, pkg, rule, automaticZenRule, origin);
newConfig.automaticRules.put(rule.id, rule);
maybeReplaceDefaultRule(newConfig, automaticZenRule);
@@ -498,7 +498,7 @@
}
@GuardedBy("mConfigLock")
- private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, ZenRule ruleToAdd,
+ private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, String pkg, ZenRule ruleToAdd,
AutomaticZenRule azrToAdd, @ConfigOrigin int origin) {
if (!Flags.modesApi()) {
return ruleToAdd;
@@ -522,10 +522,18 @@
if (origin != ORIGIN_APP) {
return ruleToAdd; // Okay to create anew.
}
+ if (Flags.modesUi()) {
+ if (!Objects.equals(ruleToRestore.pkg, pkg)
+ || !Objects.equals(ruleToRestore.component, azrToAdd.getOwner())) {
+ // Apps are not allowed to change the owner via updateAutomaticZenRule(). Thus, if
+ // they have to, delete+add is their only option.
+ return ruleToAdd;
+ }
+ }
// "Preserve" the previous rule by considering the azrToAdd an update instead.
// Only app-modifiable fields will actually be modified.
- populateZenRule(ruleToRestore.pkg, azrToAdd, ruleToRestore, origin, /* isNew= */ false);
+ populateZenRule(pkg, azrToAdd, ruleToRestore, origin, /* isNew= */ false);
return ruleToRestore;
}
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index ee0159d..4665a72 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -3065,10 +3065,9 @@
case DumpState.DUMP_PREFERRED_XML:
{
pw.flush();
- FileOutputStream fout = new FileOutputStream(fd);
- BufferedOutputStream str = new BufferedOutputStream(fout);
TypedXmlSerializer serializer = Xml.newFastSerializer();
- try {
+ try (BufferedOutputStream str =
+ new BufferedOutputStream(new FileOutputStream(fd))) {
serializer.setOutput(str, StandardCharsets.UTF_8.name());
serializer.startDocument(null, true);
serializer.setFeature(
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 5105fd3..1317866 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1415,8 +1415,13 @@
+ " an sdk library <"
+ parsedPackage.getSdkLibraryName() + ">"
+ " without changing the versionMajor, but the"
- + " targetSdkVersion or minSdkVersion has changed."
- );
+ + " targetSdkVersion or minSdkVersion has changed:"
+ + " Old targetSdkVersion: " + oldTargetSdk
+ + " new targetSdkVersion: " + newTargetSdk
+ + " Old minSdkVersion: " + oldMinSdk
+ + " new minSdkVersion: " + newMinSdk
+ + " versionMajor: " + newVersionMajor
+ );
}
}
}
@@ -2231,8 +2236,9 @@
// by apexd to be more accurate.
installRequest.setScannedPackageSettingFirstInstallTimeFromReplaced(
deletedPkgSetting, allUsers);
- installRequest.setScannedPackageSettingLastUpdateTime(
- System.currentTimeMillis());
+ long currentTime = System.currentTimeMillis();
+ installRequest.setScannedPackageSettingLastUpdateTime(currentTime);
+ installRequest.setScannedPackageSettingFirstInstallTime(currentTime);
installRequest.getRemovedInfo().mBroadcastAllowList =
mPm.mAppsFilter.getVisibilityAllowList(mPm.snapshotComputer(),
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index dd2583a0d..ae7749b 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -58,6 +58,7 @@
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageState;
import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.pkg.PackageUserStateInternal;
import java.io.File;
import java.util.ArrayList;
@@ -867,6 +868,14 @@
mScanResult.mPkgSetting.setLastUpdateTime(lastUpdateTim);
}
+ public void setScannedPackageSettingFirstInstallTime(long firstInstallTime) {
+ assertScanResultExists();
+ PackageUserStateInternal userState = mScanResult.mPkgSetting.getUserStates().get(mUserId);
+ if (userState != null && userState.getFirstInstallTimeMillis() == 0) {
+ mScanResult.mPkgSetting.setFirstInstallTime(firstInstallTime, mUserId);
+ }
+ }
+
public void setRemovedAppId(int appId) {
if (mRemovedInfo != null) {
mRemovedInfo.mUid = appId;
diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java
index 61fddba..0802e9e 100644
--- a/services/core/java/com/android/server/pm/ScanPackageUtils.java
+++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java
@@ -443,17 +443,16 @@
// Take care of first install / last update times.
final long scanFileTime = getLastModifiedTime(parsedPackage);
- final long existingFirstInstallTime = userId == UserHandle.USER_ALL
- ? PackageStateUtils.getEarliestFirstInstallTime(pkgSetting.getUserStates())
- : pkgSetting.readUserState(userId).getFirstInstallTimeMillis();
+ final long earliestFirstInstallTime =
+ PackageStateUtils.getEarliestFirstInstallTime((pkgSetting.getUserStates()));
if (currentTime != 0) {
- if (existingFirstInstallTime == 0) {
+ if (earliestFirstInstallTime == 0) {
pkgSetting.setFirstInstallTime(currentTime, userId)
.setLastUpdateTime(currentTime);
} else if ((scanFlags & SCAN_UPDATE_TIME) != 0) {
pkgSetting.setLastUpdateTime(currentTime);
}
- } else if (existingFirstInstallTime == 0) {
+ } else if (earliestFirstInstallTime == 0) {
// We need *something*. Take time stamp of the file.
pkgSetting.setFirstInstallTime(scanFileTime, userId)
.setLastUpdateTime(scanFileTime);
diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java
index 00582bf..045d4db 100644
--- a/services/core/java/com/android/server/pm/ShortcutLauncher.java
+++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java
@@ -282,6 +282,12 @@
for (int j = 0; j < idSize; j++) {
ShortcutService.writeTagValue(out, TAG_PIN, ids.valueAt(j));
}
+ if (ShortcutService.DEBUG_REBOOT) {
+ Slog.d(TAG, "Persist shortcut ids pinned by "
+ + getPackageName() + " from "
+ + up.userId + "@" + up.packageName + " ids=["
+ + String.join(", ", ids) + "]");
+ }
out.endTag(null, TAG_PACKAGE);
}
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 84674b2..60056eb 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1850,9 +1850,17 @@
}
getPackageInfo().saveToXml(mShortcutUser.mService, out, forBackup);
+ if (ShortcutService.DEBUG_REBOOT) {
+ Slog.d(TAG, "Persisting shortcuts from "
+ + getOwnerUserId() + "@" + getPackageName());
+ }
for (int j = 0; j < size; j++) {
+ final ShortcutInfo si = mShortcuts.valueAt(j);
saveShortcut(
- out, mShortcuts.valueAt(j), forBackup, getPackageInfo().isBackupAllowed());
+ out, si, forBackup, getPackageInfo().isBackupAllowed());
+ if (ShortcutService.DEBUG_REBOOT) {
+ Slog.d(TAG, si.toSimpleString());
+ }
}
if (!forBackup) {
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 021f7aa..25468fa 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -172,7 +172,7 @@
static final boolean DEBUG = false; // STOPSHIP if true
static final boolean DEBUG_LOAD = false; // STOPSHIP if true
static final boolean DEBUG_PROCSTATE = false; // STOPSHIP if true
- static final boolean DEBUG_REBOOT = true;
+ static final boolean DEBUG_REBOOT = Build.IS_DEBUGGABLE;
@VisibleForTesting
static final long DEFAULT_RESET_INTERVAL_SEC = 24 * 60 * 60; // 1 day
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 2b639fa..a683a8c 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -25,6 +25,7 @@
import static android.content.pm.PackageManager.FEATURE_EMBEDDED;
import static android.content.pm.PackageManager.FEATURE_LEANBACK;
import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.os.UserHandle.USER_SYSTEM;
import static android.os.UserManager.DEV_CREATE_OVERRIDE_PROPERTY;
import static android.os.UserManager.DISALLOW_USER_SWITCH;
import static android.os.UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY;
@@ -1372,6 +1373,10 @@
}
if (isHeadlessSystemUserMode()) {
+ if (mContext.getResources()
+ .getBoolean(com.android.internal.R.bool.config_bootToHeadlessSystemUser)) {
+ return UserHandle.USER_SYSTEM;
+ }
// Return the previous foreground user, if there is one.
final int previousUser = getPreviousFullUserToEnterForeground();
if (previousUser != UserHandle.USER_NULL) {
@@ -2514,6 +2519,38 @@
}
/**
+ * This method validates whether calling user is valid in visible background users feature.
+ * Valid user is the current user or the system or in the same profile group as the current
+ * user. Visible background users are not valid calling users.
+ */
+ public static void enforceCurrentUserIfVisibleBackgroundEnabled(@UserIdInt int currentUserId) {
+ if (!UserManager.isVisibleBackgroundUsersEnabled()) {
+ return;
+ }
+ final int callingUserId = UserHandle.getCallingUserId();
+ if (DBG) {
+ Slog.d(LOG_TAG, "enforceValidCallingUser: callingUserId=" + callingUserId
+ + " isSystemUser=" + (callingUserId == USER_SYSTEM)
+ + " currentUserId=" + currentUserId
+ + " callingPid=" + Binder.getCallingPid()
+ + " callingUid=" + Binder.getCallingUid());
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ if (callingUserId != USER_SYSTEM && callingUserId != currentUserId
+ && !UserManagerService.getInstance()
+ .isSameProfileGroup(callingUserId, currentUserId)) {
+ throw new SecurityException(
+ "Invalid calling user on devices that enable visible background users. "
+ + "callingUserId=" + callingUserId + " currentUserId="
+ + currentUserId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
* Gets the current and target user ids as a {@link Pair}, calling
* {@link ActivityManagerInternal} directly (and without performing any permission check).
*
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index ba3de33..f96706e 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1076,16 +1076,6 @@
private void interceptPowerKeyUp(KeyEvent event, boolean canceled) {
// Inform the StatusBar; but do not allow it to consume the event.
sendSystemKeyToStatusBarAsync(event);
-
- final boolean handled = canceled || mPowerKeyHandled;
-
- if (!handled) {
- if ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) == 0) {
- // Abort possibly stuck animations only when power key up without long press case.
- mHandler.post(mWindowManagerFuncs::triggerAnimationFailsafe);
- }
- }
-
finishPowerKeyPress();
}
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 67f5f27..989c8a8 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -313,12 +313,6 @@
}
/**
- * Hint to window manager that the user has started a navigation action that should
- * abort animations that have no timeout, in case they got stuck.
- */
- void triggerAnimationFailsafe();
-
- /**
* The keyguard showing state has changed
*/
void onKeyguardShowingAndNotOccludedChanged();
diff --git a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
index 2f16419..46e779f 100644
--- a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
+++ b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
@@ -149,8 +149,7 @@
// WiFi keeps an accumulated total of stats. Keep the last WiFi stats so we can compute a delta.
// (This is unlike Bluetooth, where BatteryStatsImpl is left responsible for taking the delta.)
@GuardedBy("mWorkerLock")
- private WifiActivityEnergyInfo mLastWifiInfo =
- new WifiActivityEnergyInfo(0, 0, 0, 0, 0, 0);
+ private WifiActivityEnergyInfo mLastWifiInfo = null;
/**
* Maps an {@link EnergyConsumerType} to it's corresponding {@link EnergyConsumer#id}s,
@@ -827,8 +826,18 @@
return null;
}
+ /**
+ * Return a delta WifiActivityEnergyInfo from the last WifiActivityEnergyInfo passed to the
+ * method.
+ */
+ @VisibleForTesting
@GuardedBy("mWorkerLock")
- private WifiActivityEnergyInfo extractDeltaLocked(WifiActivityEnergyInfo latest) {
+ public WifiActivityEnergyInfo extractDeltaLocked(WifiActivityEnergyInfo latest) {
+ if (mLastWifiInfo == null) {
+ // This is the first time WifiActivityEnergyInfo has been collected since system boot.
+ // Use this first WifiActivityEnergyInfo as the starting point for all accumulations.
+ mLastWifiInfo = latest;
+ }
final long timePeriodMs = latest.getTimeSinceBootMillis()
- mLastWifiInfo.getTimeSinceBootMillis();
final long lastScanMs = mLastWifiInfo.getControllerScanDurationMillis();
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 1d3de57..d04b733 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -15487,7 +15487,8 @@
final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(which);
final long totalControllerActivityTimeMs =
computeBatteryRealtime(mClock.elapsedRealtime() * 1000, which) / 1000;
- final long sleepTimeMs = totalControllerActivityTimeMs - (idleTimeMs + rxTimeMs + txTimeMs);
+ final long sleepTimeMs = Math.max(0,
+ totalControllerActivityTimeMs - (idleTimeMs + rxTimeMs + txTimeMs));
final long energyConsumedMaMs = counter.getPowerCounter().getCountLocked(which);
final long monitoredRailChargeConsumedMaMs =
counter.getMonitoredRailChargeConsumedMaMs().getCountLocked(which);
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 2faa68a..09d2a02 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -168,11 +168,6 @@
*/
void onDisplayReady(int displayId);
- /**
- * Notifies System UI whether the recents animation is running.
- */
- void onRecentsAnimationStateChanged(boolean running);
-
/** @see com.android.internal.statusbar.IStatusBar#onSystemBarAttributesChanged */
void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 7d812ee..0fd5967 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -699,17 +699,6 @@
}
@Override
- public void onRecentsAnimationStateChanged(boolean running) {
- IStatusBar bar = mBar;
- if (bar != null) {
- try {
- bar.onRecentsAnimationStateChanged(running);
- } catch (RemoteException ex) {}
- }
-
- }
-
- @Override
public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
@Behavior int behavior, @InsetsType int requestedVisibleTypes,
diff --git a/services/core/java/com/android/server/tracing/TracingServiceProxy.java b/services/core/java/com/android/server/tracing/TracingServiceProxy.java
index 480db25..8e24e9f 100644
--- a/services/core/java/com/android/server/tracing/TracingServiceProxy.java
+++ b/services/core/java/com/android/server/tracing/TracingServiceProxy.java
@@ -38,6 +38,7 @@
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.os.UserHandle;
+import android.provider.Settings;
import android.service.tracing.TraceReportService;
import android.tracing.ITracingServiceProxy;
import android.tracing.TraceReportParams;
@@ -87,6 +88,8 @@
TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_SVC_PERM_MISSING;
private static final int REPORT_SVC_COMM_ERROR =
TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_SVC_COMM_ERROR;
+ private static final String NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended";
+ private static final int ENABLED = 1;
private final Context mContext;
private final PackageManager mPackageManager;
@@ -97,10 +100,22 @@
/**
* Notifies system tracing app that a tracing session has ended. sessionStolen is ignored,
* as trace sessions are no longer stolen and are always cloned instead.
+ * <p>
+ * Cases exist where user-flows besides Traceur's QS Tile may end long-trace sessions. In
+ * these cases, a Global int will be set to flag the upcoming notifyTraceSessionEnded call
+ * as purposely muted once.
*/
@Override
public void notifyTraceSessionEnded(boolean sessionStolen /* unused */) {
- TracingServiceProxy.this.notifyTraceur();
+ long identity = Binder.clearCallingIdentity();
+ if (Settings.Global.getInt(mContext.getContentResolver(),
+ NOTIFY_SESSION_ENDED_SETTING, ENABLED) == ENABLED) {
+ TracingServiceProxy.this.notifyTraceur();
+ } else {
+ Settings.Global.putInt(mContext.getContentResolver(), NOTIFY_SESSION_ENDED_SETTING,
+ ENABLED);
+ }
+ Binder.restoreCallingIdentity(identity);
}
@Override
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index cda86fa..6b3b5bd 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -19,6 +19,7 @@
import static android.media.AudioManager.DEVICE_NONE;
import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED;
import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED_STANDBY;
+import static android.media.tv.flags.Flags.tifUnbindInactiveTis;
import static android.media.tv.flags.Flags.kidsModeTvdbSharing;
import android.annotation.NonNull;
@@ -4515,12 +4516,14 @@
break;
}
case MSG_UPDATE_HARDWARE_TIS_BINDING:
- SomeArgs args = (SomeArgs) msg.obj;
- int userId = (int) args.arg1;
- synchronized (mLock) {
- updateHardwareTvInputServiceBindingLocked(userId);
+ if (tifUnbindInactiveTis()) {
+ SomeArgs args = (SomeArgs) msg.obj;
+ int userId = (int) args.arg1;
+ synchronized (mLock) {
+ updateHardwareTvInputServiceBindingLocked(userId);
+ }
+ args.recycle();
}
- args.recycle();
break;
default: {
Slog.w(TAG, "unhandled message code: " + msg.what);
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 10ce8c2..7cbacd6 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -1764,15 +1764,7 @@
IBinder topFocusedWindowToken = null;
synchronized (mService.mGlobalLock) {
- // If there is a recents animation running, then use the animation target as the
- // top window state. Otherwise,do not send the windows if there is no top focus as
- // the window manager is still looking for where to put it. We will do the work when
- // we get a focus change callback.
- final RecentsAnimationController controller =
- mService.getRecentsAnimationController();
- final WindowState topFocusedWindowState = controller != null
- ? controller.getTargetAppMainWindow()
- : getTopFocusWindow();
+ final WindowState topFocusedWindowState = getTopFocusWindow();
if (topFocusedWindowState == null) {
if (DEBUG) {
Slog.d(LOG_TAG, "top focused window is null, compute it again later");
@@ -1907,10 +1899,6 @@
private boolean windowMattersToAccessibility(AccessibilityWindow a11yWindow,
Region regionInScreen, Region unaccountedSpace) {
- if (a11yWindow.ignoreRecentsAnimationForAccessibility()) {
- return false;
- }
-
if (a11yWindow.isFocused()) {
return true;
}
diff --git a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
index d8e7c77..fd2a909 100644
--- a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
+++ b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
@@ -659,7 +659,6 @@
private boolean mIsPIPMenu;
private boolean mIsFocused;
private boolean mShouldMagnify;
- private boolean mIgnoreDuetoRecentsAnimation;
private final Region mTouchableRegionInScreen = new Region();
private final Region mTouchableRegionInWindow = new Region();
private WindowInfo mWindowInfo;
@@ -692,10 +691,6 @@
instance.mIsFocused = windowState != null && windowState.isFocused();
instance.mShouldMagnify = windowState == null || windowState.shouldMagnify();
- final RecentsAnimationController controller = service.getRecentsAnimationController();
- instance.mIgnoreDuetoRecentsAnimation = windowState != null && controller != null
- && controller.shouldIgnoreForAccessibility(windowState);
-
final Rect windowFrame = new Rect(inputWindowHandle.frame);
getTouchableRegionInWindow(instance.mShouldMagnify, inputWindowHandle.touchableRegion,
instance.mTouchableRegionInWindow, windowFrame, magnificationInverseMatrix,
@@ -793,13 +788,6 @@
}
/**
- * @return true if it's running the recent animation but not the target app.
- */
- public boolean ignoreRecentsAnimationForAccessibility() {
- return mIgnoreDuetoRecentsAnimation;
- }
-
- /**
* @return true if this window is the trusted overlay.
*/
public boolean isTrustedOverlay() {
@@ -909,7 +897,6 @@
+ ", privateFlag=0x" + Integer.toHexString(mPrivateFlags)
+ ", focused=" + mIsFocused
+ ", shouldMagnify=" + mShouldMagnify
- + ", ignoreDuetoRecentsAnimation=" + mIgnoreDuetoRecentsAnimation
+ ", isTrustedOverlay=" + isTrustedOverlay()
+ ", regionInScreen=" + mTouchableRegionInScreen
+ ", touchableRegion=" + mTouchableRegionInWindow
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index fb2bf39..2ce1aa42 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -1275,10 +1275,8 @@
final ActivityRecord r = info.mLastLaunchedActivity;
final long lastTopLossTime = r.topResumedStateLossTime;
final WindowManagerService wm = mSupervisor.mService.mWindowManager;
- final Object controller = wm.getRecentsAnimationController();
mLoggerHandler.postDelayed(() -> {
- if (lastTopLossTime != r.topResumedStateLossTime
- || controller != wm.getRecentsAnimationController()) {
+ if (lastTopLossTime != r.topResumedStateLossTime) {
// Skip if the animation was finished in a short time.
return;
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 530c03f..6bf70f0 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -109,8 +109,6 @@
import static android.os.Process.SYSTEM_UID;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.Display.INVALID_DISPLAY;
-import static android.view.Surface.ROTATION_270;
-import static android.view.Surface.ROTATION_90;
import static android.view.WindowManager.ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15;
import static android.view.WindowManager.ENABLE_ACTIVITY_EMBEDDING_FOR_ANDROID_15;
import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
@@ -228,7 +226,6 @@
import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE;
import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE;
import static com.android.server.wm.ActivityTaskManagerService.getInputDispatchingTimeoutMillisLocked;
-import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode;
import static com.android.server.wm.IdentifierProto.HASH_CODE;
import static com.android.server.wm.IdentifierProto.TITLE;
import static com.android.server.wm.IdentifierProto.USER_ID;
@@ -341,7 +338,6 @@
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationDefinition;
import android.view.RemoteAnimationTarget;
-import android.view.Surface.Rotation;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
import android.view.WindowInsets;
@@ -640,12 +636,6 @@
private SizeConfigurationBuckets mSizeConfigurations;
- /**
- * The precomputed display insets for resolving configuration. It will be non-null if
- * {@link #shouldCreateCompatDisplayInsets} returns {@code true}.
- */
- private CompatDisplayInsets mCompatDisplayInsets;
-
@VisibleForTesting
final TaskFragment.ConfigOverrideHint mResolveConfigHint;
@@ -795,22 +785,6 @@
@NonNull
final AppCompatController mAppCompatController;
- /**
- * The scale to fit at least one side of the activity to its parent. If the activity uses
- * 1920x1080, and the actually size on the screen is 960x540, then the scale is 0.5.
- */
- private float mSizeCompatScale = 1f;
-
- /**
- * The bounds in global coordinates for activity in size compatibility mode.
- * @see ActivityRecord#hasSizeCompatBounds()
- */
- private Rect mSizeCompatBounds;
-
- // Whether this activity is in size compatibility mode because its bounds don't fit in parent
- // naturally.
- private boolean mInSizeCompatModeForBounds = false;
-
// Whether the activity is eligible to be letterboxed for fixed orientation with respect to its
// requested orientation, even when it's letterbox for another reason (e.g., size compat mode)
// and therefore #isLetterboxedForFixedOrientationAndAspectRatio returns false.
@@ -1260,10 +1234,6 @@
if (mPendingRelaunchCount != 0) {
pw.print(prefix); pw.print("mPendingRelaunchCount="); pw.println(mPendingRelaunchCount);
}
- if (mSizeCompatScale != 1f || mSizeCompatBounds != null) {
- pw.println(prefix + "mSizeCompatScale=" + mSizeCompatScale + " mSizeCompatBounds="
- + mSizeCompatBounds);
- }
if (mRemovingFromDisplay) {
pw.println(prefix + "mRemovingFromDisplay=" + mRemovingFromDisplay);
}
@@ -6442,7 +6412,7 @@
mTaskSupervisor.mStoppingActivities.remove(this);
if (getDisplayArea().allResumedActivitiesComplete()) {
// Construct the compat environment at a relatively stable state if needed.
- updateCompatDisplayInsets();
+ mAppCompatController.getAppCompatSizeCompatModePolicy().updateAppCompatDisplayInsets();
mRootWindowContainer.executeAppTransitionForAllDisplay();
}
@@ -8074,8 +8044,8 @@
if (getRequestedConfigurationOrientation(false, requestedOrientation)
!= getRequestedConfigurationOrientation(false /*forDisplay */)) {
// Do not change the requested configuration now, because this will be done when setting
- // the orientation below with the new mCompatDisplayInsets
- clearSizeCompatModeAttributes();
+ // the orientation below with the new mAppCompatDisplayInsets
+ mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatModeAttributes();
}
ProtoLog.v(WM_DEBUG_ORIENTATION,
"Setting requested orientation %s for %s",
@@ -8207,19 +8177,8 @@
}
@Nullable
- CompatDisplayInsets getCompatDisplayInsets() {
- if (mAppCompatController.getTransparentPolicy().isRunning()) {
- return mAppCompatController.getTransparentPolicy().getInheritedCompatDisplayInsets();
- }
- return mCompatDisplayInsets;
- }
-
- /**
- * @return The {@code true} if the current instance has {@link mCompatDisplayInsets} without
- * considering the inheritance implemented in {@link #getCompatDisplayInsets()}
- */
- boolean hasCompatDisplayInsetsWithoutInheritance() {
- return mCompatDisplayInsets != null;
+ AppCompatDisplayInsets getAppCompatDisplayInsets() {
+ return mAppCompatController.getAppCompatSizeCompatModePolicy().getAppCompatDisplayInsets();
}
/**
@@ -8227,10 +8186,12 @@
* density than its parent or its bounds don't fit in parent naturally.
*/
boolean inSizeCompatMode() {
- if (mInSizeCompatModeForBounds) {
+ final AppCompatSizeCompatModePolicy scmPolicy = mAppCompatController
+ .getAppCompatSizeCompatModePolicy();
+ if (scmPolicy.isInSizeCompatModeForBounds()) {
return true;
}
- if (getCompatDisplayInsets() == null || !shouldCreateCompatDisplayInsets()
+ if (getAppCompatDisplayInsets() == null || !shouldCreateAppCompatDisplayInsets()
// The orientation is different from parent when transforming.
|| isFixedRotationTransforming()) {
return false;
@@ -8256,13 +8217,13 @@
* Indicates the activity will keep the bounds and screen configuration when it was first
* launched, no matter how its parent changes.
*
- * <p>If {@true}, then {@link CompatDisplayInsets} will be created in {@link
+ * <p>If {@true}, then {@link AppCompatDisplayInsets} will be created in {@link
* #resolveOverrideConfiguration} to "freeze" activity bounds and insets.
*
* @return {@code true} if this activity is declared as non-resizable and fixed orientation or
* aspect ratio.
*/
- boolean shouldCreateCompatDisplayInsets() {
+ boolean shouldCreateAppCompatDisplayInsets() {
if (mAppCompatController.getAppCompatAspectRatioOverrides().hasFullscreenOverride()) {
// If the user has forced the applications aspect ratio to be fullscreen, don't use size
// compatibility mode in any situation. The user has been warned and therefore accepts
@@ -8284,7 +8245,7 @@
final TaskDisplayArea tda = getTaskDisplayArea();
if (inMultiWindowMode() || (tda != null && tda.inFreeformWindowingMode())) {
final ActivityRecord root = task != null ? task.getRootActivity() : null;
- if (root != null && root != this && !root.shouldCreateCompatDisplayInsets()) {
+ if (root != null && root != this && !root.shouldCreateAppCompatDisplayInsets()) {
// If the root activity doesn't use size compatibility mode, the activities above
// are forced to be the same for consistent visual appearance.
return false;
@@ -8324,69 +8285,7 @@
@Override
boolean hasSizeCompatBounds() {
- return mSizeCompatBounds != null;
- }
-
- // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer.
- private void updateCompatDisplayInsets() {
- if (getCompatDisplayInsets() != null || !shouldCreateCompatDisplayInsets()) {
- // The override configuration is set only once in size compatibility mode.
- return;
- }
-
- Configuration overrideConfig = getRequestedOverrideConfiguration();
- final Configuration fullConfig = getConfiguration();
-
- // Ensure the screen related fields are set. It is used to prevent activity relaunch
- // when moving between displays. For screenWidthDp and screenWidthDp, because they
- // are relative to bounds and density, they will be calculated in
- // {@link Task#computeConfigResourceOverrides} and the result will also be
- // relatively fixed.
- overrideConfig.colorMode = fullConfig.colorMode;
- overrideConfig.densityDpi = fullConfig.densityDpi;
- // The smallest screen width is the short side of screen bounds. Because the bounds
- // and density won't be changed, smallestScreenWidthDp is also fixed.
- overrideConfig.smallestScreenWidthDp = fullConfig.smallestScreenWidthDp;
- if (ActivityInfo.isFixedOrientation(getOverrideOrientation())) {
- // lock rotation too. When in size-compat, onConfigurationChanged will watch for and
- // apply runtime rotation changes.
- overrideConfig.windowConfiguration.setRotation(
- fullConfig.windowConfiguration.getRotation());
- }
-
- final Rect letterboxedContainerBounds = mAppCompatController
- .getAppCompatAspectRatioPolicy().getLetterboxedContainerBounds();
-
- // The role of CompatDisplayInsets is like the override bounds.
- mCompatDisplayInsets =
- new CompatDisplayInsets(
- mDisplayContent, this, letterboxedContainerBounds,
- mResolveConfigHint.mUseOverrideInsetsForConfig);
- }
-
- private void clearSizeCompatModeAttributes() {
- mInSizeCompatModeForBounds = false;
- final float lastSizeCompatScale = mSizeCompatScale;
- mSizeCompatScale = 1f;
- if (mSizeCompatScale != lastSizeCompatScale) {
- forAllWindows(WindowState::updateGlobalScale, false /* traverseTopToBottom */);
- }
- mSizeCompatBounds = null;
- mCompatDisplayInsets = null;
- mAppCompatController.getTransparentPolicy().clearInheritedCompatDisplayInsets();
- }
-
- @VisibleForTesting
- void clearSizeCompatMode() {
- clearSizeCompatModeAttributes();
- // Clear config override in #updateCompatDisplayInsets().
- final int activityType = getActivityType();
- final Configuration overrideConfig = getRequestedOverrideConfiguration();
- overrideConfig.unset();
- // Keep the activity type which was set when attaching to a task to prevent leaving it
- // undefined.
- overrideConfig.windowConfiguration.setActivityType(activityType);
- onRequestedOverrideConfigurationChanged(overrideConfig);
+ return mAppCompatController.getAppCompatSizeCompatModePolicy().hasSizeCompatBounds();
}
@Override
@@ -8404,7 +8303,9 @@
@Override
float getCompatScale() {
- return hasSizeCompatBounds() ? mSizeCompatScale : super.getCompatScale();
+ // We need to invoke {#getCompatScale()} only if the CompatScale is not available.
+ return mAppCompatController.getAppCompatSizeCompatModePolicy()
+ .getCompatScaleIfAvailable(ActivityRecord.super::getCompatScale);
}
@Override
@@ -8471,9 +8372,12 @@
.hasFullscreenOverride()) {
resolveAspectRatioRestriction(newParentConfiguration);
}
- final CompatDisplayInsets compatDisplayInsets = getCompatDisplayInsets();
- if (compatDisplayInsets != null) {
- resolveSizeCompatModeConfiguration(newParentConfiguration, compatDisplayInsets);
+ final AppCompatDisplayInsets appCompatDisplayInsets = getAppCompatDisplayInsets();
+ final AppCompatSizeCompatModePolicy scmPolicy =
+ mAppCompatController.getAppCompatSizeCompatModePolicy();
+ if (appCompatDisplayInsets != null) {
+ scmPolicy.resolveSizeCompatModeConfiguration(newParentConfiguration,
+ appCompatDisplayInsets, mTmpBounds);
} else if (inMultiWindowMode() && !isFixedOrientationLetterboxAllowed) {
// We ignore activities' requested orientation in multi-window modes. They may be
// taken into consideration in resolveFixedOrientationConfiguration call above.
@@ -8491,13 +8395,14 @@
if (!Flags.immersiveAppRepositioning()
&& !mAppCompatController.getAppCompatAspectRatioPolicy()
.isLetterboxedForFixedOrientationAndAspectRatio()
- && !mInSizeCompatModeForBounds
+ && !scmPolicy.isInSizeCompatModeForBounds()
&& !mAppCompatController.getAppCompatAspectRatioOverrides()
.hasFullscreenOverride()) {
resolveAspectRatioRestriction(newParentConfiguration);
}
- if (isFixedOrientationLetterboxAllowed || compatDisplayInsets != null
+ if (isFixedOrientationLetterboxAllowed
+ || scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()
// In fullscreen, can be letterboxed for aspect ratio.
|| !inMultiWindowMode()) {
updateResolvedBoundsPosition(newParentConfiguration);
@@ -8505,8 +8410,8 @@
boolean isIgnoreOrientationRequest = mDisplayContent != null
&& mDisplayContent.getIgnoreOrientationRequest();
- if (compatDisplayInsets == null
- // for size compat mode set in updateCompatDisplayInsets
+ if (!scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()
+ // for size compat mode set in updateAppCompatDisplayInsets
// Fixed orientation letterboxing is possible on both large screen devices
// with ignoreOrientationRequest enabled and on phones in split screen even with
// ignoreOrientationRequest disabled.
@@ -8533,7 +8438,7 @@
getResolvedOverrideConfiguration().seq = mConfigurationSeq;
// Sandbox max bounds by setting it to the activity bounds, if activity is letterboxed, or
- // has or will have mCompatDisplayInsets for size compat. Also forces an activity to be
+ // has or will have mAppCompatDisplayInsets for size compat. Also forces an activity to be
// sandboxed or not depending upon the configuration settings.
if (providesMaxBounds()) {
mTmpBounds.set(resolvedConfig.windowConfiguration.getBounds());
@@ -8554,8 +8459,8 @@
info.neverSandboxDisplayApis(sConstrainDisplayApisConfig),
info.alwaysSandboxDisplayApis(sConstrainDisplayApisConfig),
!matchParentBounds(),
- compatDisplayInsets != null,
- shouldCreateCompatDisplayInsets());
+ scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance(),
+ shouldCreateAppCompatDisplayInsets());
}
resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds);
}
@@ -8567,7 +8472,8 @@
resolvedConfig,
mOptOutEdgeToEdge,
hasFixedRotationTransform(),
- getCompatDisplayInsets() != null);
+ getAppCompatDisplayInsets() != null,
+ task);
mResolveConfigHint.resetTmpOverrides();
logAppCompatState();
@@ -8577,7 +8483,7 @@
return Rect.copyOrNull(mResolveConfigHint.mParentAppBoundsOverride);
}
- private void computeConfigByResolveHint(@NonNull Configuration resolvedConfig,
+ void computeConfigByResolveHint(@NonNull Configuration resolvedConfig,
@NonNull Configuration parentConfig) {
task.computeConfigResourceOverrides(resolvedConfig, parentConfig, mResolveConfigHint);
// Reset the temp info which should only take effect for the specified computation.
@@ -8629,7 +8535,9 @@
if (mAppCompatController.getTransparentPolicy().isRunning()) {
return mAppCompatController.getTransparentPolicy().getInheritedAppCompatState();
}
- if (mInSizeCompatModeForBounds) {
+ final AppCompatSizeCompatModePolicy scmPolicy = mAppCompatController
+ .getAppCompatSizeCompatModePolicy();
+ if (scmPolicy.isInSizeCompatModeForBounds()) {
return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
}
// Letterbox for fixed orientation. This check returns true only when an activity is
@@ -8665,8 +8573,9 @@
if (resolvedBounds.isEmpty()) {
return;
}
- final Rect screenResolvedBounds =
- mSizeCompatBounds != null ? mSizeCompatBounds : resolvedBounds;
+ final AppCompatSizeCompatModePolicy scmPolicy =
+ mAppCompatController.getAppCompatSizeCompatModePolicy();
+ final Rect screenResolvedBounds = scmPolicy.replaceResolvedBoundsIfNeeded(resolvedBounds);
final Rect parentAppBounds = mResolveConfigHint.mParentAppBoundsOverride;
final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds();
final float screenResolvedBoundsWidth = screenResolvedBounds.width();
@@ -8696,7 +8605,7 @@
offsetX = Math.max(0, (int) Math.ceil((appWidth
- screenResolvedBoundsWidth) * positionMultiplier)
// This is added to make sure that insets added inside
- // CompatDisplayInsets#getContainerBounds() do not break the alignment
+ // AppCompatDisplayInsets#getContainerBounds() do not break the alignment
// provided by the positionMultiplier
- screenResolvedBounds.left + parentAppBounds.left);
}
@@ -8717,19 +8626,15 @@
offsetY = Math.max(0, (int) Math.ceil((appHeight
- screenResolvedBoundsHeight) * positionMultiplier)
// This is added to make sure that insets added inside
- // CompatDisplayInsets#getContainerBounds() do not break the alignment
+ // AppCompatDisplayInsets#getContainerBounds() do not break the alignment
// provided by the positionMultiplier
- screenResolvedBounds.top + parentAppBounds.top);
}
}
-
- if (mSizeCompatBounds != null) {
- mSizeCompatBounds.offset(offsetX , offsetY);
- final int dy = mSizeCompatBounds.top - resolvedBounds.top;
- final int dx = mSizeCompatBounds.left - resolvedBounds.left;
- offsetBounds(resolvedConfig, dx, dy);
- } else {
- offsetBounds(resolvedConfig, offsetX, offsetY);
+ // If in SCM, apply offset to resolved bounds relative to size compat bounds. If
+ // not, apply directly to resolved bounds.
+ if (!scmPolicy.applyOffsetIfNeeded(resolvedBounds, resolvedConfig, offsetX, offsetY)) {
+ AppCompatUtils.offsetBounds(resolvedConfig, offsetX, offsetY);
}
// If the top is aligned with parentAppBounds add the vertical insets back so that the app
@@ -8737,9 +8642,7 @@
if (resolvedConfig.windowConfiguration.getAppBounds().top == parentAppBounds.top
&& !isImmersiveMode) {
resolvedConfig.windowConfiguration.getBounds().top = parentBounds.top;
- if (mSizeCompatBounds != null) {
- mSizeCompatBounds.top = parentBounds.top;
- }
+ scmPolicy.alignToTopIfNeeded(parentBounds);
}
// Since bounds has changed, the configuration needs to be computed accordingly.
@@ -8749,13 +8652,7 @@
// easier to resolve the relative position in parent container. However, if the activity is
// scaled, the position should follow the scale because the configuration will be sent to
// the client which is expected to be in a scaled environment.
- if (mSizeCompatScale != 1f) {
- final int screenPosX = resolvedBounds.left;
- final int screenPosY = resolvedBounds.top;
- final int dx = (int) (screenPosX / mSizeCompatScale + 0.5f) - screenPosX;
- final int dy = (int) (screenPosY / mSizeCompatScale + 0.5f) - screenPosY;
- offsetBounds(resolvedConfig, dx, dy);
- }
+ scmPolicy.applySizeCompatScaleIfNeeded(resolvedBounds, resolvedConfig);
}
boolean isImmersiveMode(@NonNull Rect parentBounds) {
@@ -8777,7 +8674,9 @@
@NonNull Rect getScreenResolvedBounds() {
final Configuration resolvedConfig = getResolvedOverrideConfiguration();
final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
- return mSizeCompatBounds != null ? mSizeCompatBounds : resolvedBounds;
+ final AppCompatSizeCompatModePolicy scmPolicy =
+ mAppCompatController.getAppCompatSizeCompatModePolicy();
+ return scmPolicy.replaceResolvedBoundsIfNeeded(resolvedBounds);
}
void recomputeConfiguration() {
@@ -8928,10 +8827,12 @@
|| orientationRespectedWithInsets)) {
return;
}
- final CompatDisplayInsets compatDisplayInsets = getCompatDisplayInsets();
+ final AppCompatDisplayInsets mAppCompatDisplayInsets = getAppCompatDisplayInsets();
+ final AppCompatSizeCompatModePolicy scmPolicy =
+ mAppCompatController.getAppCompatSizeCompatModePolicy();
- if (compatDisplayInsets != null
- && !compatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) {
+ if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()
+ && !mAppCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) {
// App prefers to keep its original size.
// If the size compat is from previous fixed orientation letterboxing, we may want to
// have fixed orientation letterbox again, otherwise it will show the size compat
@@ -8983,8 +8884,8 @@
.applyDesiredAspectRatio(newParentConfig, parentBounds, resolvedBounds,
containingBoundsWithInsets, containingBounds);
- if (compatDisplayInsets != null) {
- compatDisplayInsets.getBoundsByRotation(mTmpBounds,
+ if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()) {
+ mAppCompatDisplayInsets.getBoundsByRotation(mTmpBounds,
newParentConfig.windowConfiguration.getRotation());
if (resolvedBounds.width() != mTmpBounds.width()
|| resolvedBounds.height() != mTmpBounds.height()) {
@@ -9007,7 +8908,7 @@
// Calculate app bounds using fixed orientation bounds because they will be needed later
// for comparison with size compat app bounds in {@link resolveSizeCompatModeConfiguration}.
- mResolveConfigHint.mTmpCompatInsets = compatDisplayInsets;
+ mResolveConfigHint.mTmpCompatInsets = mAppCompatDisplayInsets;
computeConfigByResolveHint(getResolvedOverrideConfiguration(), newParentConfig);
mAppCompatController.getAppCompatAspectRatioPolicy()
.setLetterboxBoundsForFixedOrientationAndAspectRatio(new Rect(resolvedBounds));
@@ -9044,246 +8945,15 @@
}
}
- /**
- * Resolves consistent screen configuration for orientation and rotation changes without
- * inheriting the parent bounds.
- */
- private void resolveSizeCompatModeConfiguration(Configuration newParentConfiguration,
- @NonNull CompatDisplayInsets compatDisplayInsets) {
- final Configuration resolvedConfig = getResolvedOverrideConfiguration();
- final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
-
- // When an activity needs to be letterboxed because of fixed orientation, use fixed
- // orientation bounds (stored in resolved bounds) instead of parent bounds since the
- // activity will be displayed within them even if it is in size compat mode. They should be
- // saved here before resolved bounds are overridden below.
- final boolean useResolvedBounds = Flags.immersiveAppRepositioning()
- ? mAppCompatController.getAppCompatAspectRatioPolicy()
- .isAspectRatioApplied()
- : mAppCompatController.getAppCompatAspectRatioPolicy()
- .isLetterboxedForFixedOrientationAndAspectRatio();
- final Rect containerBounds = useResolvedBounds
- ? new Rect(resolvedBounds)
- : newParentConfiguration.windowConfiguration.getBounds();
- final Rect containerAppBounds = useResolvedBounds
- ? new Rect(resolvedConfig.windowConfiguration.getAppBounds())
- : mResolveConfigHint.mParentAppBoundsOverride;
-
- final int requestedOrientation = getRequestedConfigurationOrientation();
- final boolean orientationRequested = requestedOrientation != ORIENTATION_UNDEFINED;
- final int parentOrientation = mResolveConfigHint.mUseOverrideInsetsForConfig
- ? mResolveConfigHint.mTmpOverrideConfigOrientation
- : newParentConfiguration.orientation;
- final int orientation = orientationRequested
- ? requestedOrientation
- // We should use the original orientation of the activity when possible to avoid
- // forcing the activity in the opposite orientation.
- : compatDisplayInsets.mOriginalRequestedOrientation != ORIENTATION_UNDEFINED
- ? compatDisplayInsets.mOriginalRequestedOrientation
- : parentOrientation;
- int rotation = newParentConfiguration.windowConfiguration.getRotation();
- final boolean isFixedToUserRotation = mDisplayContent == null
- || mDisplayContent.getDisplayRotation().isFixedToUserRotation();
- if (!isFixedToUserRotation && !compatDisplayInsets.mIsFloating) {
- // Use parent rotation because the original display can be rotated.
- resolvedConfig.windowConfiguration.setRotation(rotation);
- } else {
- final int overrideRotation = resolvedConfig.windowConfiguration.getRotation();
- if (overrideRotation != ROTATION_UNDEFINED) {
- rotation = overrideRotation;
- }
- }
-
- // Use compat insets to lock width and height. We should not use the parent width and height
- // because apps in compat mode should have a constant width and height. The compat insets
- // are locked when the app is first launched and are never changed after that, so we can
- // rely on them to contain the original and unchanging width and height of the app.
- final Rect containingAppBounds = new Rect();
- final Rect containingBounds = mTmpBounds;
- compatDisplayInsets.getContainerBounds(containingAppBounds, containingBounds, rotation,
- orientation, orientationRequested, isFixedToUserRotation);
- resolvedBounds.set(containingBounds);
- // The size of floating task is fixed (only swap), so the aspect ratio is already correct.
- if (!compatDisplayInsets.mIsFloating) {
- mAppCompatController.getAppCompatAspectRatioPolicy()
- .applyAspectRatioForLetterbox(resolvedBounds, containingAppBounds,
- containingBounds);
- }
-
- // Use resolvedBounds to compute other override configurations such as appBounds. The bounds
- // are calculated in compat container space. The actual position on screen will be applied
- // later, so the calculation is simpler that doesn't need to involve offset from parent.
- mResolveConfigHint.mTmpCompatInsets = compatDisplayInsets;
- computeConfigByResolveHint(resolvedConfig, newParentConfiguration);
- // Use current screen layout as source because the size of app is independent to parent.
- resolvedConfig.screenLayout = computeScreenLayout(
- getConfiguration().screenLayout, resolvedConfig.screenWidthDp,
- resolvedConfig.screenHeightDp);
-
- // Use parent orientation if it cannot be decided by bounds, so the activity can fit inside
- // the parent bounds appropriately.
- if (resolvedConfig.screenWidthDp == resolvedConfig.screenHeightDp) {
- resolvedConfig.orientation = parentOrientation;
- }
-
- // Below figure is an example that puts an activity which was launched in a larger container
- // into a smaller container.
- // The outermost rectangle is the real display bounds.
- // "@" is the container app bounds (parent bounds or fixed orientation bounds)
- // "#" is the {@code resolvedBounds} that applies to application.
- // "*" is the {@code mSizeCompatBounds} that used to show on screen if scaled.
- // ------------------------------
- // | |
- // | @@@@*********@@@@### |
- // | @ * * @ # |
- // | @ * * @ # |
- // | @ * * @ # |
- // | @@@@*********@@@@ # |
- // ---------#--------------#-----
- // # #
- // ################
- // The application is still layouted in "#" since it was launched, and it will be visually
- // scaled and positioned to "*".
-
- final Rect resolvedAppBounds = resolvedConfig.windowConfiguration.getAppBounds();
-
- // Calculates the scale the size compatibility bounds into the region which is available
- // to application.
- final float lastSizeCompatScale = mSizeCompatScale;
- updateSizeCompatScale(resolvedAppBounds, containerAppBounds);
-
- final int containerTopInset = containerAppBounds.top - containerBounds.top;
- final boolean topNotAligned =
- containerTopInset != resolvedAppBounds.top - resolvedBounds.top;
- if (mSizeCompatScale != 1f || topNotAligned) {
- if (mSizeCompatBounds == null) {
- mSizeCompatBounds = new Rect();
- }
- mSizeCompatBounds.set(resolvedAppBounds);
- mSizeCompatBounds.offsetTo(0, 0);
- mSizeCompatBounds.scale(mSizeCompatScale);
- // The insets are included in height, e.g. the area of real cutout shouldn't be scaled.
- mSizeCompatBounds.bottom += containerTopInset;
- } else {
- mSizeCompatBounds = null;
- }
- if (mSizeCompatScale != lastSizeCompatScale) {
- forAllWindows(WindowState::updateGlobalScale, false /* traverseTopToBottom */);
- }
-
- // The position will be later adjusted in updateResolvedBoundsPosition.
- // Above coordinates are in "@" space, now place "*" and "#" to screen space.
- final boolean fillContainer = resolvedBounds.equals(containingBounds);
- final int screenPosX = fillContainer ? containerBounds.left : containerAppBounds.left;
- final int screenPosY = fillContainer ? containerBounds.top : containerAppBounds.top;
-
- if (screenPosX != 0 || screenPosY != 0) {
- if (mSizeCompatBounds != null) {
- mSizeCompatBounds.offset(screenPosX, screenPosY);
- }
- // Add the global coordinates and remove the local coordinates.
- final int dx = screenPosX - resolvedBounds.left;
- final int dy = screenPosY - resolvedBounds.top;
- offsetBounds(resolvedConfig, dx, dy);
- }
-
- mInSizeCompatModeForBounds =
- isInSizeCompatModeForBounds(resolvedAppBounds, containerAppBounds);
- }
-
- void updateSizeCompatScale(Rect resolvedAppBounds, Rect containerAppBounds) {
- mSizeCompatScale = mAppCompatController.getTransparentPolicy()
- .findOpaqueNotFinishingActivityBelow()
- .map(activityRecord -> activityRecord.mSizeCompatScale)
- .orElseGet(() -> calculateSizeCompatScale(resolvedAppBounds, containerAppBounds));
- }
-
- private float calculateSizeCompatScale(Rect resolvedAppBounds, Rect containerAppBounds) {
- final int contentW = resolvedAppBounds.width();
- final int contentH = resolvedAppBounds.height();
- final int viewportW = containerAppBounds.width();
- final int viewportH = containerAppBounds.height();
- // Allow an application to be up-scaled if its window is smaller than its
- // original container or if it's a freeform window in desktop mode.
- boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH)
- || (canEnterDesktopMode(mAtmService.mContext)
- && getWindowingMode() == WINDOWING_MODE_FREEFORM);
- return shouldAllowUpscaling ? Math.min(
- (float) viewportW / contentW, (float) viewportH / contentH) : 1f;
- }
-
- private boolean isInSizeCompatModeForBounds(final Rect appBounds, final Rect containerBounds) {
- if (mAppCompatController.getTransparentPolicy().isRunning()) {
- // To avoid wrong app behaviour, we decided to disable SCM when a translucent activity
- // is letterboxed.
- return false;
- }
- final int appWidth = appBounds.width();
- final int appHeight = appBounds.height();
- final int containerAppWidth = containerBounds.width();
- final int containerAppHeight = containerBounds.height();
-
- if (containerAppWidth == appWidth && containerAppHeight == appHeight) {
- // Matched the container bounds.
- return false;
- }
- if (containerAppWidth > appWidth && containerAppHeight > appHeight) {
- // Both sides are smaller than the container.
- return true;
- }
- if (containerAppWidth < appWidth || containerAppHeight < appHeight) {
- // One side is larger than the container.
- return true;
- }
-
- // The rest of the condition is that only one side is smaller than the container, but it
- // still needs to exclude the cases where the size is limited by the fixed aspect ratio.
- final float maxAspectRatio = getMaxAspectRatio();
- if (maxAspectRatio > 0) {
- final float aspectRatio = (0.5f + Math.max(appWidth, appHeight))
- / Math.min(appWidth, appHeight);
- if (aspectRatio >= maxAspectRatio) {
- // The current size has reached the max aspect ratio.
- return false;
- }
- }
- final float minAspectRatio = getMinAspectRatio();
- if (minAspectRatio > 0) {
- // The activity should have at least the min aspect ratio, so this checks if the
- // container still has available space to provide larger aspect ratio.
- final float containerAspectRatio =
- (0.5f + Math.max(containerAppWidth, containerAppHeight))
- / Math.min(containerAppWidth, containerAppHeight);
- if (containerAspectRatio <= minAspectRatio) {
- // The long side has reached the parent.
- return false;
- }
- }
- return true;
- }
-
- /** @return The horizontal / vertical offset of putting the content in the center of viewport.*/
- private static int getCenterOffset(int viewportDim, int contentDim) {
- return (int) ((viewportDim - contentDim + 1) * 0.5f);
- }
-
- private static void offsetBounds(Configuration inOutConfig, int offsetX, int offsetY) {
- inOutConfig.windowConfiguration.getBounds().offset(offsetX, offsetY);
- inOutConfig.windowConfiguration.getAppBounds().offset(offsetX, offsetY);
- }
-
@Override
public Rect getBounds() {
// TODO(b/268458693): Refactor configuration inheritance in case of translucent activities
final Rect superBounds = super.getBounds();
+ final AppCompatSizeCompatModePolicy scmPolicy =
+ mAppCompatController.getAppCompatSizeCompatModePolicy();
return mAppCompatController.getTransparentPolicy().findOpaqueNotFinishingActivityBelow()
.map(ActivityRecord::getBounds)
- .orElseGet(() -> {
- if (mSizeCompatBounds != null) {
- return mSizeCompatBounds;
- }
- return superBounds;
- });
+ .orElseGet(() -> scmPolicy.getAppSizeCompatBoundsIfAvailable(superBounds));
}
@Override
@@ -9305,13 +8975,13 @@
if (info.alwaysSandboxDisplayApis(sConstrainDisplayApisConfig)) {
return true;
}
- // Max bounds should be sandboxed when an activity should have compatDisplayInsets, and it
- // will keep the same bounds and screen configuration when it was first launched regardless
- // how its parent window changes, so that the sandbox API will provide a consistent result.
- if (getCompatDisplayInsets() != null || shouldCreateCompatDisplayInsets()) {
+ // Max bounds should be sandboxed when an activity should have mAppCompatDisplayInsets,
+ // and it will keep the same bounds and screen configuration when it was first launched
+ // regardless how its parent window changes, so that the sandbox API will provide a
+ // consistent result.
+ if (getAppCompatDisplayInsets() != null || shouldCreateAppCompatDisplayInsets()) {
return true;
}
-
// No need to sandbox for resizable apps in (including in multi-window) because
// resizableActivity=true indicates that they support multi-window. Likewise, do not sandbox
// for activities in letterbox since the activity has declared it can handle resizing.
@@ -9362,7 +9032,7 @@
mTransitionController.collect(this);
}
}
- if (getCompatDisplayInsets() != null) {
+ if (getAppCompatDisplayInsets() != null) {
Configuration overrideConfig = getRequestedOverrideConfiguration();
// Adapt to changes in orientation locking. The app is still non-resizable, but
// it can change which orientation is fixed. If the fixed orientation changes,
@@ -9442,9 +9112,9 @@
if (mVisibleRequested) {
// It may toggle the UI for user to restart the size compatibility mode activity.
display.handleActivitySizeCompatModeIfNeeded(this);
- } else if (getCompatDisplayInsets() != null && !visibleIgnoringKeyguard
+ } else if (getAppCompatDisplayInsets() != null && !visibleIgnoringKeyguard
&& (app == null || !app.hasVisibleActivities())) {
- // visibleIgnoringKeyguard is checked to avoid clearing mCompatDisplayInsets during
+ // visibleIgnoringKeyguard is checked to avoid clearing mAppCompatDisplayInsets during
// displays change. Displays are turned off during the change so mVisibleRequested
// can be false.
// The override changes can only be obtained from display, because we don't have the
@@ -9607,14 +9277,14 @@
// Calling from here rather than from onConfigurationChanged because it's possible that
// onConfigurationChanged was called before mVisibleRequested became true and
- // mCompatDisplayInsets may not be called again when mVisibleRequested changes. And we
- // don't want to save mCompatDisplayInsets in onConfigurationChanged without visibility
+ // mAppCompatDisplayInsets may not be called again when mVisibleRequested changes. And we
+ // don't want to save mAppCompatDisplayInsets in onConfigurationChanged without visibility
// check to avoid remembering obsolete configuration which can lead to unnecessary
// size-compat mode.
if (mVisibleRequested) {
// Calling from here rather than resolveOverrideConfiguration to ensure that this is
// called after full config is updated in ConfigurationContainer#onConfigurationChanged.
- updateCompatDisplayInsets();
+ mAppCompatController.getAppCompatSizeCompatModePolicy().updateAppCompatDisplayInsets();
}
// Short circuit: if the two full configurations are equal (the common case), then there is
@@ -9954,7 +9624,7 @@
// Reset the existing override configuration so it can be updated according to the latest
// configuration.
- clearSizeCompatMode();
+ mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
if (!attachedToProcess()) {
return;
@@ -10440,202 +10110,6 @@
proto.end(token);
}
- /**
- * The precomputed insets of the display in each rotation. This is used to make the size
- * compatibility mode activity compute the configuration without relying on its current display.
- */
- static class CompatDisplayInsets {
- /** The original rotation the compat insets were computed in. */
- final @Rotation int mOriginalRotation;
- /** The original requested orientation for the activity. */
- final @Configuration.Orientation int mOriginalRequestedOrientation;
- /** The container width on rotation 0. */
- private final int mWidth;
- /** The container height on rotation 0. */
- private final int mHeight;
- /** Whether the {@link Task} windowingMode represents a floating window*/
- final boolean mIsFloating;
- /**
- * Whether is letterboxed because of fixed orientation or aspect ratio when
- * the unresizable activity is first shown.
- */
- final boolean mIsInFixedOrientationOrAspectRatioLetterbox;
- /**
- * The nonDecorInsets for each rotation. Includes the navigation bar and cutout insets. It
- * is used to compute the appBounds.
- */
- final Rect[] mNonDecorInsets = new Rect[4];
- /**
- * The stableInsets for each rotation. Includes the status bar inset and the
- * nonDecorInsets. It is used to compute {@link Configuration#screenWidthDp} and
- * {@link Configuration#screenHeightDp}.
- */
- final Rect[] mStableInsets = new Rect[4];
-
- /** Constructs the environment to simulate the bounds behavior of the given container. */
- CompatDisplayInsets(DisplayContent display, ActivityRecord container,
- @Nullable Rect letterboxedContainerBounds, boolean useOverrideInsets) {
- mOriginalRotation = display.getRotation();
- mIsFloating = container.getWindowConfiguration().tasksAreFloating();
- mOriginalRequestedOrientation = container.getRequestedConfigurationOrientation();
- if (mIsFloating) {
- final Rect containerBounds = container.getWindowConfiguration().getBounds();
- mWidth = containerBounds.width();
- mHeight = containerBounds.height();
- // For apps in freeform, the task bounds are the parent bounds from the app's
- // perspective. No insets because within a window.
- final Rect emptyRect = new Rect();
- for (int rotation = 0; rotation < 4; rotation++) {
- mNonDecorInsets[rotation] = emptyRect;
- mStableInsets[rotation] = emptyRect;
- }
- mIsInFixedOrientationOrAspectRatioLetterbox = false;
- return;
- }
-
- final Task task = container.getTask();
-
- mIsInFixedOrientationOrAspectRatioLetterbox = letterboxedContainerBounds != null;
-
- // Store the bounds of the Task for the non-resizable activity to use in size compat
- // mode so that the activity will not be resized regardless the windowing mode it is
- // currently in.
- // When an activity needs to be letterboxed because of fixed orientation or aspect
- // ratio, use resolved bounds instead of task bounds since the activity will be
- // displayed within these even if it is in size compat mode.
- final Rect filledContainerBounds = mIsInFixedOrientationOrAspectRatioLetterbox
- ? letterboxedContainerBounds
- : task != null ? task.getBounds() : display.getBounds();
- final boolean useActivityRotation = container.hasFixedRotationTransform()
- && mIsInFixedOrientationOrAspectRatioLetterbox;
- final int filledContainerRotation = useActivityRotation
- ? container.getWindowConfiguration().getRotation()
- : display.getConfiguration().windowConfiguration.getRotation();
- final Point dimensions = getRotationZeroDimensions(
- filledContainerBounds, filledContainerRotation);
- mWidth = dimensions.x;
- mHeight = dimensions.y;
-
- // Bounds of the filled container if it doesn't fill the display.
- final Rect unfilledContainerBounds =
- filledContainerBounds.equals(display.getBounds()) ? null : new Rect();
- final DisplayPolicy policy = display.getDisplayPolicy();
- for (int rotation = 0; rotation < 4; rotation++) {
- mNonDecorInsets[rotation] = new Rect();
- mStableInsets[rotation] = new Rect();
- final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
- final int dw = rotated ? display.mBaseDisplayHeight : display.mBaseDisplayWidth;
- final int dh = rotated ? display.mBaseDisplayWidth : display.mBaseDisplayHeight;
- final DisplayPolicy.DecorInsets.Info decorInfo =
- policy.getDecorInsetsInfo(rotation, dw, dh);
- if (useOverrideInsets) {
- mStableInsets[rotation].set(decorInfo.mOverrideConfigInsets);
- mNonDecorInsets[rotation].set(decorInfo.mOverrideNonDecorInsets);
- } else {
- mStableInsets[rotation].set(decorInfo.mConfigInsets);
- mNonDecorInsets[rotation].set(decorInfo.mNonDecorInsets);
- }
-
- if (unfilledContainerBounds == null) {
- continue;
- }
- // The insets is based on the display, but the container may be smaller than the
- // display, so update the insets to exclude parts that are not intersected with the
- // container.
- unfilledContainerBounds.set(filledContainerBounds);
- display.rotateBounds(
- filledContainerRotation,
- rotation,
- unfilledContainerBounds);
- updateInsetsForBounds(unfilledContainerBounds, dw, dh, mNonDecorInsets[rotation]);
- updateInsetsForBounds(unfilledContainerBounds, dw, dh, mStableInsets[rotation]);
- }
- }
-
- /**
- * Gets the width and height of the {@code container} when it is not rotated, so that after
- * the display is rotated, we can calculate the bounds by rotating the dimensions.
- * @see #getBoundsByRotation
- */
- private static Point getRotationZeroDimensions(final Rect bounds, int rotation) {
- final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
- final int width = bounds.width();
- final int height = bounds.height();
- return rotated ? new Point(height, width) : new Point(width, height);
- }
-
- /**
- * Updates the display insets to exclude the parts that are not intersected with the given
- * bounds.
- */
- private static void updateInsetsForBounds(Rect bounds, int displayWidth, int displayHeight,
- Rect inset) {
- inset.left = Math.max(0, inset.left - bounds.left);
- inset.top = Math.max(0, inset.top - bounds.top);
- inset.right = Math.max(0, bounds.right - displayWidth + inset.right);
- inset.bottom = Math.max(0, bounds.bottom - displayHeight + inset.bottom);
- }
-
- void getBoundsByRotation(Rect outBounds, int rotation) {
- final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
- final int dw = rotated ? mHeight : mWidth;
- final int dh = rotated ? mWidth : mHeight;
- outBounds.set(0, 0, dw, dh);
- }
-
- void getFrameByOrientation(Rect outBounds, int orientation) {
- final int longSide = Math.max(mWidth, mHeight);
- final int shortSide = Math.min(mWidth, mHeight);
- final boolean isLandscape = orientation == ORIENTATION_LANDSCAPE;
- outBounds.set(0, 0, isLandscape ? longSide : shortSide,
- isLandscape ? shortSide : longSide);
- }
-
- // TODO(b/267151420): Explore removing getContainerBounds() from CompatDisplayInsets.
- /** Gets the horizontal centered container bounds for size compatibility mode. */
- void getContainerBounds(Rect outAppBounds, Rect outBounds, int rotation, int orientation,
- boolean orientationRequested, boolean isFixedToUserRotation) {
- getFrameByOrientation(outBounds, orientation);
- if (mIsFloating) {
- outAppBounds.set(outBounds);
- return;
- }
-
- getBoundsByRotation(outAppBounds, rotation);
- final int dW = outAppBounds.width();
- final int dH = outAppBounds.height();
- final boolean isOrientationMismatched =
- ((outBounds.width() > outBounds.height()) != (dW > dH));
-
- if (isOrientationMismatched && isFixedToUserRotation && orientationRequested) {
- // The orientation is mismatched but the display cannot rotate. The bounds will fit
- // to the short side of container.
- if (orientation == ORIENTATION_LANDSCAPE) {
- outBounds.bottom = (int) ((float) dW * dW / dH);
- outBounds.right = dW;
- } else {
- outBounds.bottom = dH;
- outBounds.right = (int) ((float) dH * dH / dW);
- }
- outBounds.offset(getCenterOffset(mWidth, outBounds.width()), 0 /* dy */);
- }
- outAppBounds.set(outBounds);
-
- if (isOrientationMismatched) {
- // One side of container is smaller than the requested size, then it will be scaled
- // and the final position will be calculated according to the parent container and
- // scale, so the original size shouldn't be shrunk by insets.
- final Rect insets = mNonDecorInsets[rotation];
- outBounds.offset(insets.left, insets.top);
- outAppBounds.offset(insets.left, insets.top);
- } else if (rotation != ROTATION_UNDEFINED) {
- // Ensure the app bounds won't overlap with insets.
- TaskFragment.intersectWithInsetsIfFits(outAppBounds, outBounds,
- mNonDecorInsets[rotation]);
- }
- }
- }
-
private static class AppSaturationInfo {
float[] mMatrix = new float[9];
float[] mTranslation = new float[3];
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 2f74a9d..2d8e3db 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -438,13 +438,10 @@
/** It is set from keyguard-going-away to set-keyguard-shown. */
static final int DEMOTE_TOP_REASON_DURING_UNLOCKING = 1;
- /** It is set if legacy recents animation is running. */
- static final int DEMOTE_TOP_REASON_ANIMATING_RECENTS = 1 << 1;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DEMOTE_TOP_REASON_DURING_UNLOCKING,
- DEMOTE_TOP_REASON_ANIMATING_RECENTS,
})
@interface DemoteTopReason {}
@@ -1777,19 +1774,15 @@
@Override
public void preloadRecentsActivity(Intent intent) {
enforceTaskPermission("preloadRecentsActivity()");
- final int callingPid = Binder.getCallingPid();
- final int callingUid = Binder.getCallingUid();
final long origId = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
final ComponentName recentsComponent = mRecentTasks.getRecentsComponent();
final String recentsFeatureId = mRecentTasks.getRecentsComponentFeatureId();
final int recentsUid = mRecentTasks.getRecentsComponentUid();
- final WindowProcessController caller = getProcessController(callingPid, callingUid);
-
final RecentsAnimation anim = new RecentsAnimation(this, mTaskSupervisor,
- getActivityStartController(), mWindowManager, intent, recentsComponent,
- recentsFeatureId, recentsUid, caller);
+ getActivityStartController(), intent, recentsComponent,
+ recentsFeatureId, recentsUid);
anim.preloadRecentsActivity();
}
} finally {
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index cd795ae..f245efd 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -220,7 +220,7 @@
float getFixedOrientationLetterboxAspectRatio(@NonNull Configuration parentConfiguration) {
return shouldUseSplitScreenAspectRatio(parentConfiguration)
? getSplitScreenAspectRatio()
- : mActivityRecord.shouldCreateCompatDisplayInsets()
+ : mActivityRecord.shouldCreateAppCompatDisplayInsets()
? getDefaultMinAspectRatioForUnresizableApps()
: getDefaultMinAspectRatio();
}
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
index aeaaffd..d8abf69 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
@@ -217,7 +217,7 @@
*/
boolean isCameraCompatSplitScreenAspectRatioAllowed() {
return mAppCompatConfiguration.isCameraCompatSplitScreenAspectRatioEnabled()
- && !mActivityRecord.shouldCreateCompatDisplayInsets();
+ && !mActivityRecord.shouldCreateAppCompatDisplayInsets();
}
@FreeformCameraCompatMode
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index 3c3b773..173362c 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -46,6 +46,8 @@
private final AppCompatDeviceStateQuery mAppCompatDeviceStateQuery;
@NonNull
private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy;
+ @NonNull
+ private final AppCompatSizeCompatModePolicy mAppCompatSizeCompatModePolicy;
AppCompatController(@NonNull WindowManagerService wmService,
@NonNull ActivityRecord activityRecord) {
@@ -67,6 +69,8 @@
wmService.mAppCompatConfiguration);
mDesktopAppCompatAspectRatioPolicy = new DesktopAppCompatAspectRatioPolicy(activityRecord,
mAppCompatOverrides, mTransparentPolicy, wmService.mAppCompatConfiguration);
+ mAppCompatSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(mActivityRecord,
+ mAppCompatOverrides);
}
@NonNull
@@ -152,9 +156,15 @@
return mAppCompatOverrides.getAppCompatLetterboxOverrides();
}
+ @NonNull
+ AppCompatSizeCompatModePolicy getAppCompatSizeCompatModePolicy() {
+ return mAppCompatSizeCompatModePolicy;
+ }
+
void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
getTransparentPolicy().dump(pw, prefix);
getAppCompatLetterboxPolicy().dump(pw, prefix);
+ getAppCompatSizeCompatModePolicy().dump(pw, prefix);
}
}
diff --git a/services/core/java/com/android/server/wm/AppCompatDisplayInsets.java b/services/core/java/com/android/server/wm/AppCompatDisplayInsets.java
new file mode 100644
index 0000000..743bfb9
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatDisplayInsets.java
@@ -0,0 +1,234 @@
+/*
+ * 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.server.wm;
+
+
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Surface;
+
+/**
+ * The precomputed insets of the display in each rotation. This is used to make the size
+ * compatibility mode activity compute the configuration without relying on its current display.
+ */
+class AppCompatDisplayInsets {
+ /** The original rotation the compat insets were computed in. */
+ final @Surface.Rotation int mOriginalRotation;
+ /** The original requested orientation for the activity. */
+ final @Configuration.Orientation int mOriginalRequestedOrientation;
+ /** The container width on rotation 0. */
+ private final int mWidth;
+ /** The container height on rotation 0. */
+ private final int mHeight;
+ /** Whether the {@link Task} windowingMode represents a floating window*/
+ final boolean mIsFloating;
+ /**
+ * Whether is letterboxed because of fixed orientation or aspect ratio when
+ * the unresizable activity is first shown.
+ */
+ final boolean mIsInFixedOrientationOrAspectRatioLetterbox;
+ /**
+ * The nonDecorInsets for each rotation. Includes the navigation bar and cutout insets. It
+ * is used to compute the appBounds.
+ */
+ final Rect[] mNonDecorInsets = new Rect[4];
+ /**
+ * The stableInsets for each rotation. Includes the status bar inset and the
+ * nonDecorInsets. It is used to compute {@link Configuration#screenWidthDp} and
+ * {@link Configuration#screenHeightDp}.
+ */
+ final Rect[] mStableInsets = new Rect[4];
+
+ /** Constructs the environment to simulate the bounds behavior of the given container. */
+ AppCompatDisplayInsets(@NonNull DisplayContent display, @NonNull ActivityRecord container,
+ @Nullable Rect letterboxedContainerBounds, boolean useOverrideInsets) {
+ mOriginalRotation = display.getRotation();
+ mIsFloating = container.getWindowConfiguration().tasksAreFloating();
+ mOriginalRequestedOrientation = container.getRequestedConfigurationOrientation();
+ if (mIsFloating) {
+ final Rect containerBounds = container.getWindowConfiguration().getBounds();
+ mWidth = containerBounds.width();
+ mHeight = containerBounds.height();
+ // For apps in freeform, the task bounds are the parent bounds from the app's
+ // perspective. No insets because within a window.
+ final Rect emptyRect = new Rect();
+ for (int rotation = 0; rotation < 4; rotation++) {
+ mNonDecorInsets[rotation] = emptyRect;
+ mStableInsets[rotation] = emptyRect;
+ }
+ mIsInFixedOrientationOrAspectRatioLetterbox = false;
+ return;
+ }
+
+ final Task task = container.getTask();
+
+ mIsInFixedOrientationOrAspectRatioLetterbox = letterboxedContainerBounds != null;
+
+ // Store the bounds of the Task for the non-resizable activity to use in size compat
+ // mode so that the activity will not be resized regardless the windowing mode it is
+ // currently in.
+ // When an activity needs to be letterboxed because of fixed orientation or aspect
+ // ratio, use resolved bounds instead of task bounds since the activity will be
+ // displayed within these even if it is in size compat mode.
+ final Rect filledContainerBounds = mIsInFixedOrientationOrAspectRatioLetterbox
+ ? letterboxedContainerBounds
+ : task != null ? task.getBounds() : display.getBounds();
+ final boolean useActivityRotation = container.hasFixedRotationTransform()
+ && mIsInFixedOrientationOrAspectRatioLetterbox;
+ final int filledContainerRotation = useActivityRotation
+ ? container.getWindowConfiguration().getRotation()
+ : display.getConfiguration().windowConfiguration.getRotation();
+ final Point dimensions = getRotationZeroDimensions(
+ filledContainerBounds, filledContainerRotation);
+ mWidth = dimensions.x;
+ mHeight = dimensions.y;
+
+ // Bounds of the filled container if it doesn't fill the display.
+ final Rect unfilledContainerBounds =
+ filledContainerBounds.equals(display.getBounds()) ? null : new Rect();
+ final DisplayPolicy policy = display.getDisplayPolicy();
+ for (int rotation = 0; rotation < 4; rotation++) {
+ mNonDecorInsets[rotation] = new Rect();
+ mStableInsets[rotation] = new Rect();
+ final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+ final int dw = rotated ? display.mBaseDisplayHeight : display.mBaseDisplayWidth;
+ final int dh = rotated ? display.mBaseDisplayWidth : display.mBaseDisplayHeight;
+ final DisplayPolicy.DecorInsets.Info decorInfo =
+ policy.getDecorInsetsInfo(rotation, dw, dh);
+ if (useOverrideInsets) {
+ mStableInsets[rotation].set(decorInfo.mOverrideConfigInsets);
+ mNonDecorInsets[rotation].set(decorInfo.mOverrideNonDecorInsets);
+ } else {
+ mStableInsets[rotation].set(decorInfo.mConfigInsets);
+ mNonDecorInsets[rotation].set(decorInfo.mNonDecorInsets);
+ }
+
+ if (unfilledContainerBounds == null) {
+ continue;
+ }
+ // The insets is based on the display, but the container may be smaller than the
+ // display, so update the insets to exclude parts that are not intersected with the
+ // container.
+ unfilledContainerBounds.set(filledContainerBounds);
+ display.rotateBounds(
+ filledContainerRotation,
+ rotation,
+ unfilledContainerBounds);
+ updateInsetsForBounds(unfilledContainerBounds, dw, dh, mNonDecorInsets[rotation]);
+ updateInsetsForBounds(unfilledContainerBounds, dw, dh, mStableInsets[rotation]);
+ }
+ }
+
+ /**
+ * Gets the width and height of the {@code container} when it is not rotated, so that after
+ * the display is rotated, we can calculate the bounds by rotating the dimensions.
+ * @see #getBoundsByRotation
+ */
+ @NonNull
+ private static Point getRotationZeroDimensions(final @NonNull Rect bounds,
+ @Surface.Rotation int rotation) {
+ final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+ final int width = bounds.width();
+ final int height = bounds.height();
+ return rotated ? new Point(height, width) : new Point(width, height);
+ }
+
+ /**
+ * Updates the display insets to exclude the parts that are not intersected with the given
+ * bounds.
+ */
+ private static void updateInsetsForBounds(@NonNull Rect bounds, int displayWidth,
+ int displayHeight, @NonNull Rect inset) {
+ inset.left = Math.max(0, inset.left - bounds.left);
+ inset.top = Math.max(0, inset.top - bounds.top);
+ inset.right = Math.max(0, bounds.right - displayWidth + inset.right);
+ inset.bottom = Math.max(0, bounds.bottom - displayHeight + inset.bottom);
+ }
+
+ void getBoundsByRotation(@NonNull Rect outBounds, @Surface.Rotation int rotation) {
+ final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+ final int dw = rotated ? mHeight : mWidth;
+ final int dh = rotated ? mWidth : mHeight;
+ outBounds.set(0, 0, dw, dh);
+ }
+
+ void getFrameByOrientation(@NonNull Rect outBounds,
+ @Configuration.Orientation int orientation) {
+ final int longSide = Math.max(mWidth, mHeight);
+ final int shortSide = Math.min(mWidth, mHeight);
+ final boolean isLandscape = orientation == ORIENTATION_LANDSCAPE;
+ outBounds.set(0, 0, isLandscape ? longSide : shortSide,
+ isLandscape ? shortSide : longSide);
+ }
+
+ /** Gets the horizontal centered container bounds for size compatibility mode. */
+ void getContainerBounds(@NonNull Rect outAppBounds, @NonNull Rect outBounds,
+ @Surface.Rotation int rotation, @Configuration.Orientation int orientation,
+ boolean orientationRequested, boolean isFixedToUserRotation) {
+ getFrameByOrientation(outBounds, orientation);
+ if (mIsFloating) {
+ outAppBounds.set(outBounds);
+ return;
+ }
+
+ getBoundsByRotation(outAppBounds, rotation);
+ final int dW = outAppBounds.width();
+ final int dH = outAppBounds.height();
+ final boolean isOrientationMismatched =
+ ((outBounds.width() > outBounds.height()) != (dW > dH));
+
+ if (isOrientationMismatched && isFixedToUserRotation && orientationRequested) {
+ // The orientation is mismatched but the display cannot rotate. The bounds will fit
+ // to the short side of container.
+ if (orientation == ORIENTATION_LANDSCAPE) {
+ outBounds.bottom = (int) ((float) dW * dW / dH);
+ outBounds.right = dW;
+ } else {
+ outBounds.bottom = dH;
+ outBounds.right = (int) ((float) dH * dH / dW);
+ }
+ outBounds.offset(getCenterOffset(mWidth, outBounds.width()), 0 /* dy */);
+ }
+ outAppBounds.set(outBounds);
+
+ if (isOrientationMismatched) {
+ // One side of container is smaller than the requested size, then it will be scaled
+ // and the final position will be calculated according to the parent container and
+ // scale, so the original size shouldn't be shrunk by insets.
+ final Rect insets = mNonDecorInsets[rotation];
+ outBounds.offset(insets.left, insets.top);
+ outAppBounds.offset(insets.left, insets.top);
+ } else if (rotation != ROTATION_UNDEFINED) {
+ // Ensure the app bounds won't overlap with insets.
+ TaskFragment.intersectWithInsetsIfFits(outAppBounds, outBounds,
+ mNonDecorInsets[rotation]);
+ }
+ }
+
+ /** @return The horizontal / vertical offset of putting the content in the center of viewport.*/
+ private static int getCenterOffset(int viewportDim, int contentDim) {
+ return (int) ((viewportDim - contentDim + 1) * 0.5f);
+ }
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
new file mode 100644
index 0000000..3be266e
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
@@ -0,0 +1,439 @@
+/*
+ * 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.server.wm;
+
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+
+import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+
+import com.android.window.flags.Flags;
+
+import java.io.PrintWriter;
+import java.util.function.DoubleSupplier;
+
+/**
+ * Encapsulate logic related to the SizeCompatMode.
+ */
+class AppCompatSizeCompatModePolicy {
+
+ @NonNull
+ private final ActivityRecord mActivityRecord;
+ @NonNull
+ private final AppCompatOverrides mAppCompatOverrides;
+
+ // Whether this activity is in size compatibility mode because its bounds don't fit in parent
+ // naturally.
+ private boolean mInSizeCompatModeForBounds = false;
+ /**
+ * The scale to fit at least one side of the activity to its parent. If the activity uses
+ * 1920x1080, and the actually size on the screen is 960x540, then the scale is 0.5.
+ */
+ private float mSizeCompatScale = 1f;
+
+ /**
+ * The bounds in global coordinates for activity in size compatibility mode.
+ * @see #hasSizeCompatBounds()
+ */
+ private Rect mSizeCompatBounds;
+
+ /**
+ * The precomputed display insets for resolving configuration. It will be non-null if
+ * {@link #shouldCreateAppCompatDisplayInsets} returns {@code true}.
+ */
+ @Nullable
+ private AppCompatDisplayInsets mAppCompatDisplayInsets;
+
+ AppCompatSizeCompatModePolicy(@NonNull ActivityRecord activityRecord,
+ @NonNull AppCompatOverrides appCompatOverrides) {
+ mActivityRecord = activityRecord;
+ mAppCompatOverrides = appCompatOverrides;
+ }
+
+ boolean isInSizeCompatModeForBounds() {
+ return mInSizeCompatModeForBounds;
+ }
+
+ void setInSizeCompatModeForBounds(boolean inSizeCompatModeForBounds) {
+ mInSizeCompatModeForBounds = inSizeCompatModeForBounds;
+ }
+
+ boolean hasSizeCompatBounds() {
+ return mSizeCompatBounds != null;
+ }
+
+ /**
+ * @return The {@code true} if the current instance has {@link mAppCompatDisplayInsets} without
+ * considering the inheritance implemented in {@link #getAppCompatDisplayInsets()}
+ */
+ boolean hasAppCompatDisplayInsetsWithoutInheritance() {
+ return mAppCompatDisplayInsets != null;
+ }
+
+ @Nullable
+ AppCompatDisplayInsets getAppCompatDisplayInsets() {
+ final TransparentPolicy transparentPolicy = mActivityRecord.mAppCompatController
+ .getTransparentPolicy();
+ if (transparentPolicy.isRunning()) {
+ return transparentPolicy.getInheritedAppCompatDisplayInsets();
+ }
+ return mAppCompatDisplayInsets;
+ }
+
+ float getCompatScaleIfAvailable(@NonNull DoubleSupplier scaleWhenNotAvailable) {
+ return hasSizeCompatBounds() ? mSizeCompatScale
+ : (float) scaleWhenNotAvailable.getAsDouble();
+ }
+
+ @NonNull
+ Rect getAppSizeCompatBoundsIfAvailable(@NonNull Rect boundsWhenNotAvailable) {
+ return hasSizeCompatBounds() ? mSizeCompatBounds : boundsWhenNotAvailable;
+ }
+
+ @NonNull
+ Rect replaceResolvedBoundsIfNeeded(@NonNull Rect resolvedBounds) {
+ return hasSizeCompatBounds() ? mSizeCompatBounds : resolvedBounds;
+ }
+
+ boolean applyOffsetIfNeeded(@NonNull Rect resolvedBounds,
+ @NonNull Configuration resolvedConfig, int offsetX, int offsetY) {
+ if (hasSizeCompatBounds()) {
+ mSizeCompatBounds.offset(offsetX , offsetY);
+ final int dy = mSizeCompatBounds.top - resolvedBounds.top;
+ final int dx = mSizeCompatBounds.left - resolvedBounds.left;
+ AppCompatUtils.offsetBounds(resolvedConfig, dx, dy);
+ return true;
+ }
+ return false;
+ }
+
+ void alignToTopIfNeeded(@NonNull Rect parentBounds) {
+ if (hasSizeCompatBounds()) {
+ mSizeCompatBounds.top = parentBounds.top;
+ }
+ }
+
+ void applySizeCompatScaleIfNeeded(@NonNull Rect resolvedBounds,
+ @NonNull Configuration resolvedConfig) {
+ if (mSizeCompatScale != 1f) {
+ final int screenPosX = resolvedBounds.left;
+ final int screenPosY = resolvedBounds.top;
+ final int dx = (int) (screenPosX / mSizeCompatScale + 0.5f) - screenPosX;
+ final int dy = (int) (screenPosY / mSizeCompatScale + 0.5f) - screenPosY;
+ AppCompatUtils.offsetBounds(resolvedConfig, dx, dy);
+ }
+ }
+
+ void updateSizeCompatScale(@NonNull Rect resolvedAppBounds, @NonNull Rect containerAppBounds) {
+ mSizeCompatScale = mActivityRecord.mAppCompatController.getTransparentPolicy()
+ .findOpaqueNotFinishingActivityBelow()
+ .map(activityRecord -> mSizeCompatScale)
+ .orElseGet(() -> calculateSizeCompatScale(resolvedAppBounds, containerAppBounds));
+ }
+
+ void clearSizeCompatModeAttributes() {
+ mInSizeCompatModeForBounds = false;
+ final float lastSizeCompatScale = mSizeCompatScale;
+ mSizeCompatScale = 1f;
+ if (mSizeCompatScale != lastSizeCompatScale) {
+ mActivityRecord.forAllWindows(WindowState::updateGlobalScale,
+ false /* traverseTopToBottom */);
+ }
+ mSizeCompatBounds = null;
+ mAppCompatDisplayInsets = null;
+ mActivityRecord.mAppCompatController.getTransparentPolicy()
+ .clearInheritedAppCompatDisplayInsets();
+ }
+
+ void clearSizeCompatMode() {
+ clearSizeCompatModeAttributes();
+ // Clear config override in #updateAppCompatDisplayInsets().
+ final int activityType = mActivityRecord.getActivityType();
+ final Configuration overrideConfig = mActivityRecord.getRequestedOverrideConfiguration();
+ overrideConfig.unset();
+ // Keep the activity type which was set when attaching to a task to prevent leaving it
+ // undefined.
+ overrideConfig.windowConfiguration.setActivityType(activityType);
+ mActivityRecord.onRequestedOverrideConfigurationChanged(overrideConfig);
+ }
+
+ void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
+ if (mSizeCompatScale != 1f || hasSizeCompatBounds()) {
+ pw.println(prefix + "mSizeCompatScale=" + mSizeCompatScale + " mSizeCompatBounds="
+ + mSizeCompatBounds);
+ }
+ }
+
+ /**
+ * Resolves consistent screen configuration for orientation and rotation changes without
+ * inheriting the parent bounds.
+ */
+ void resolveSizeCompatModeConfiguration(@NonNull Configuration newParentConfiguration,
+ @NonNull AppCompatDisplayInsets appCompatDisplayInsets, @NonNull Rect tmpBounds) {
+ final Configuration resolvedConfig = mActivityRecord.getResolvedOverrideConfiguration();
+ final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
+
+ // When an activity needs to be letterboxed because of fixed orientation, use fixed
+ // orientation bounds (stored in resolved bounds) instead of parent bounds since the
+ // activity will be displayed within them even if it is in size compat mode. They should be
+ // saved here before resolved bounds are overridden below.
+ final AppCompatAspectRatioPolicy aspectRatioPolicy = mActivityRecord.mAppCompatController
+ .getAppCompatAspectRatioPolicy();
+ final boolean useResolvedBounds = Flags.immersiveAppRepositioning()
+ ? aspectRatioPolicy.isAspectRatioApplied()
+ : aspectRatioPolicy.isLetterboxedForFixedOrientationAndAspectRatio();
+ final Rect containerBounds = useResolvedBounds
+ ? new Rect(resolvedBounds)
+ : newParentConfiguration.windowConfiguration.getBounds();
+ final Rect containerAppBounds = useResolvedBounds
+ ? new Rect(resolvedConfig.windowConfiguration.getAppBounds())
+ : mActivityRecord.mResolveConfigHint.mParentAppBoundsOverride;
+
+ final int requestedOrientation = mActivityRecord.getRequestedConfigurationOrientation();
+ final boolean orientationRequested = requestedOrientation != ORIENTATION_UNDEFINED;
+ final int parentOrientation = mActivityRecord.mResolveConfigHint.mUseOverrideInsetsForConfig
+ ? mActivityRecord.mResolveConfigHint.mTmpOverrideConfigOrientation
+ : newParentConfiguration.orientation;
+ final int orientation = orientationRequested
+ ? requestedOrientation
+ // We should use the original orientation of the activity when possible to avoid
+ // forcing the activity in the opposite orientation.
+ : appCompatDisplayInsets.mOriginalRequestedOrientation != ORIENTATION_UNDEFINED
+ ? appCompatDisplayInsets.mOriginalRequestedOrientation
+ : parentOrientation;
+ int rotation = newParentConfiguration.windowConfiguration.getRotation();
+ final boolean isFixedToUserRotation = mActivityRecord.mDisplayContent == null
+ || mActivityRecord.mDisplayContent.getDisplayRotation().isFixedToUserRotation();
+ if (!isFixedToUserRotation && !appCompatDisplayInsets.mIsFloating) {
+ // Use parent rotation because the original display can be rotated.
+ resolvedConfig.windowConfiguration.setRotation(rotation);
+ } else {
+ final int overrideRotation = resolvedConfig.windowConfiguration.getRotation();
+ if (overrideRotation != ROTATION_UNDEFINED) {
+ rotation = overrideRotation;
+ }
+ }
+
+ // Use compat insets to lock width and height. We should not use the parent width and height
+ // because apps in compat mode should have a constant width and height. The compat insets
+ // are locked when the app is first launched and are never changed after that, so we can
+ // rely on them to contain the original and unchanging width and height of the app.
+ final Rect containingAppBounds = new Rect();
+ final Rect containingBounds = tmpBounds;
+ appCompatDisplayInsets.getContainerBounds(containingAppBounds, containingBounds, rotation,
+ orientation, orientationRequested, isFixedToUserRotation);
+ resolvedBounds.set(containingBounds);
+ // The size of floating task is fixed (only swap), so the aspect ratio is already correct.
+ if (!appCompatDisplayInsets.mIsFloating) {
+ mActivityRecord.mAppCompatController.getAppCompatAspectRatioPolicy()
+ .applyAspectRatioForLetterbox(resolvedBounds, containingAppBounds,
+ containingBounds);
+ }
+
+ // Use resolvedBounds to compute other override configurations such as appBounds. The bounds
+ // are calculated in compat container space. The actual position on screen will be applied
+ // later, so the calculation is simpler that doesn't need to involve offset from parent.
+ mActivityRecord.mResolveConfigHint.mTmpCompatInsets = appCompatDisplayInsets;
+ mActivityRecord.computeConfigByResolveHint(resolvedConfig, newParentConfiguration);
+ // Use current screen layout as source because the size of app is independent to parent.
+ resolvedConfig.screenLayout = ActivityRecord.computeScreenLayout(
+ mActivityRecord.getConfiguration().screenLayout, resolvedConfig.screenWidthDp,
+ resolvedConfig.screenHeightDp);
+
+ // Use parent orientation if it cannot be decided by bounds, so the activity can fit inside
+ // the parent bounds appropriately.
+ if (resolvedConfig.screenWidthDp == resolvedConfig.screenHeightDp) {
+ resolvedConfig.orientation = parentOrientation;
+ }
+
+ // Below figure is an example that puts an activity which was launched in a larger container
+ // into a smaller container.
+ // The outermost rectangle is the real display bounds.
+ // "@" is the container app bounds (parent bounds or fixed orientation bounds)
+ // "#" is the {@code resolvedBounds} that applies to application.
+ // "*" is the {@code mSizeCompatBounds} that used to show on screen if scaled.
+ // ------------------------------
+ // | |
+ // | @@@@*********@@@@### |
+ // | @ * * @ # |
+ // | @ * * @ # |
+ // | @ * * @ # |
+ // | @@@@*********@@@@ # |
+ // ---------#--------------#-----
+ // # #
+ // ################
+ // The application is still layouted in "#" since it was launched, and it will be visually
+ // scaled and positioned to "*".
+
+ final Rect resolvedAppBounds = resolvedConfig.windowConfiguration.getAppBounds();
+ // Calculates the scale the size compatibility bounds into the region which is available
+ // to application.
+ final float lastSizeCompatScale = mSizeCompatScale;
+ updateSizeCompatScale(resolvedAppBounds, containerAppBounds);
+
+ final int containerTopInset = containerAppBounds.top - containerBounds.top;
+ final boolean topNotAligned =
+ containerTopInset != resolvedAppBounds.top - resolvedBounds.top;
+ if (mSizeCompatScale != 1f || topNotAligned) {
+ if (mSizeCompatBounds == null) {
+ mSizeCompatBounds = new Rect();
+ }
+ mSizeCompatBounds.set(resolvedAppBounds);
+ mSizeCompatBounds.offsetTo(0, 0);
+ mSizeCompatBounds.scale(mSizeCompatScale);
+ // The insets are included in height, e.g. the area of real cutout shouldn't be scaled.
+ mSizeCompatBounds.bottom += containerTopInset;
+ } else {
+ mSizeCompatBounds = null;
+ }
+ if (mSizeCompatScale != lastSizeCompatScale) {
+ mActivityRecord.forAllWindows(WindowState::updateGlobalScale,
+ false /* traverseTopToBottom */);
+ }
+
+ // The position will be later adjusted in updateResolvedBoundsPosition.
+ // Above coordinates are in "@" space, now place "*" and "#" to screen space.
+ final boolean fillContainer = resolvedBounds.equals(containingBounds);
+ final int screenPosX = fillContainer ? containerBounds.left : containerAppBounds.left;
+ final int screenPosY = fillContainer ? containerBounds.top : containerAppBounds.top;
+
+ if (screenPosX != 0 || screenPosY != 0) {
+ if (hasSizeCompatBounds()) {
+ mSizeCompatBounds.offset(screenPosX, screenPosY);
+ }
+ // Add the global coordinates and remove the local coordinates.
+ final int dx = screenPosX - resolvedBounds.left;
+ final int dy = screenPosY - resolvedBounds.top;
+ AppCompatUtils.offsetBounds(resolvedConfig, dx, dy);
+ }
+
+ mInSizeCompatModeForBounds = isInSizeCompatModeForBounds(resolvedAppBounds,
+ containerAppBounds);
+ }
+
+ // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer.
+ void updateAppCompatDisplayInsets() {
+ if (getAppCompatDisplayInsets() != null
+ || !mActivityRecord.shouldCreateAppCompatDisplayInsets()) {
+ // The override configuration is set only once in size compatibility mode.
+ return;
+ }
+
+ Configuration overrideConfig = mActivityRecord.getRequestedOverrideConfiguration();
+ final Configuration fullConfig = mActivityRecord.getConfiguration();
+
+ // Ensure the screen related fields are set. It is used to prevent activity relaunch
+ // when moving between displays. For screenWidthDp and screenWidthDp, because they
+ // are relative to bounds and density, they will be calculated in
+ // {@link Task#computeConfigResourceOverrides} and the result will also be
+ // relatively fixed.
+ overrideConfig.colorMode = fullConfig.colorMode;
+ overrideConfig.densityDpi = fullConfig.densityDpi;
+ // The smallest screen width is the short side of screen bounds. Because the bounds
+ // and density won't be changed, smallestScreenWidthDp is also fixed.
+ overrideConfig.smallestScreenWidthDp = fullConfig.smallestScreenWidthDp;
+ if (ActivityInfo.isFixedOrientation(mActivityRecord.getOverrideOrientation())) {
+ // lock rotation too. When in size-compat, onConfigurationChanged will watch for and
+ // apply runtime rotation changes.
+ overrideConfig.windowConfiguration.setRotation(
+ fullConfig.windowConfiguration.getRotation());
+ }
+
+ final Rect letterboxedContainerBounds = mActivityRecord.mAppCompatController
+ .getAppCompatAspectRatioPolicy().getLetterboxedContainerBounds();
+
+ // The role of AppCompatDisplayInsets is like the override bounds.
+ mAppCompatDisplayInsets =
+ new AppCompatDisplayInsets(mActivityRecord.mDisplayContent, mActivityRecord,
+ letterboxedContainerBounds, mActivityRecord.mResolveConfigHint
+ .mUseOverrideInsetsForConfig);
+ }
+
+
+ private boolean isInSizeCompatModeForBounds(final @NonNull Rect appBounds,
+ final @NonNull Rect containerBounds) {
+ if (mActivityRecord.mAppCompatController.getTransparentPolicy().isRunning()) {
+ // To avoid wrong app behaviour, we decided to disable SCM when a translucent activity
+ // is letterboxed.
+ return false;
+ }
+ final int appWidth = appBounds.width();
+ final int appHeight = appBounds.height();
+ final int containerAppWidth = containerBounds.width();
+ final int containerAppHeight = containerBounds.height();
+
+ if (containerAppWidth == appWidth && containerAppHeight == appHeight) {
+ // Matched the container bounds.
+ return false;
+ }
+ if (containerAppWidth > appWidth && containerAppHeight > appHeight) {
+ // Both sides are smaller than the container.
+ return true;
+ }
+ if (containerAppWidth < appWidth || containerAppHeight < appHeight) {
+ // One side is larger than the container.
+ return true;
+ }
+
+ // The rest of the condition is that only one side is smaller than the container, but it
+ // still needs to exclude the cases where the size is limited by the fixed aspect ratio.
+ final float maxAspectRatio = mActivityRecord.getMaxAspectRatio();
+ if (maxAspectRatio > 0) {
+ final float aspectRatio = (0.5f + Math.max(appWidth, appHeight))
+ / Math.min(appWidth, appHeight);
+ if (aspectRatio >= maxAspectRatio) {
+ // The current size has reached the max aspect ratio.
+ return false;
+ }
+ }
+ final float minAspectRatio = mActivityRecord.getMinAspectRatio();
+ if (minAspectRatio > 0) {
+ // The activity should have at least the min aspect ratio, so this checks if the
+ // container still has available space to provide larger aspect ratio.
+ final float containerAspectRatio =
+ (0.5f + Math.max(containerAppWidth, containerAppHeight))
+ / Math.min(containerAppWidth, containerAppHeight);
+ if (containerAspectRatio <= minAspectRatio) {
+ // The long side has reached the parent.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private float calculateSizeCompatScale(@NonNull Rect resolvedAppBounds,
+ @NonNull Rect containerAppBounds) {
+ final int contentW = resolvedAppBounds.width();
+ final int contentH = resolvedAppBounds.height();
+ final int viewportW = containerAppBounds.width();
+ final int viewportH = containerAppBounds.height();
+ // Allow an application to be up-scaled if its window is smaller than its
+ // original container or if it's a freeform window in desktop mode.
+ boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH)
+ || (canEnterDesktopMode(mActivityRecord.mAtmService.mContext)
+ && mActivityRecord.getWindowingMode() == WINDOWING_MODE_FREEFORM);
+ return shouldAllowUpscaling ? Math.min(
+ (float) viewportW / contentW, (float) viewportH / contentH) : 1f;
+ }
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java
index e3ff851..69421d0 100644
--- a/services/core/java/com/android/server/wm/AppCompatUtils.java
+++ b/services/core/java/com/android/server/wm/AppCompatUtils.java
@@ -251,6 +251,11 @@
}
}
+ static void offsetBounds(@NonNull Configuration inOutConfig, int offsetX, int offsetY) {
+ inOutConfig.windowConfiguration.getBounds().offset(offsetX, offsetY);
+ inOutConfig.windowConfiguration.getAppBounds().offset(offsetX, offsetY);
+ }
+
private static void clearAppCompatTaskInfo(@NonNull AppCompatTaskInfo info) {
info.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index bc7e84a..90d33fb 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -134,8 +134,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.TransitionAnimation;
-import com.android.internal.protolog.common.LogLevel;
import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.common.LogLevel;
import com.android.internal.util.DumpUtils.Dump;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.internal.util.function.pooled.PooledPredicate;
@@ -402,8 +402,7 @@
mRemoteAnimationController.goodToGo(transit);
} else if ((isTaskOpenTransitOld(transit) || transit == TRANSIT_OLD_WALLPAPER_CLOSE)
&& topOpeningAnim != null) {
- if (mDisplayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()
- && mService.getRecentsAnimationController() == null) {
+ if (mDisplayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()) {
final NavBarFadeAnimationController controller =
new NavBarFadeAnimationController(mDisplayContent);
// For remote animation case, the nav bar fades out and in is controlled by the
@@ -471,11 +470,9 @@
}
private boolean needsBoosting() {
- final boolean recentsAnimRunning = mService.getRecentsAnimationController() != null;
return !mNextAppTransitionRequests.isEmpty()
|| mAppTransitionState == APP_STATE_READY
- || mAppTransitionState == APP_STATE_RUNNING
- || recentsAnimRunning;
+ || mAppTransitionState == APP_STATE_RUNNING;
}
void registerListenerLocked(AppTransitionListener listener) {
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index 5a0cbf3..06bdc04 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -1032,12 +1032,8 @@
private void applyAnimations(ArraySet<ActivityRecord> openingApps,
ArraySet<ActivityRecord> closingApps, @TransitionOldType int transit,
LayoutParams animLp, boolean voiceInteraction) {
- final RecentsAnimationController rac = mService.getRecentsAnimationController();
if (transit == WindowManager.TRANSIT_OLD_UNSET
|| (openingApps.isEmpty() && closingApps.isEmpty())) {
- if (rac != null) {
- rac.sendTasksAppeared();
- }
return;
}
@@ -1075,9 +1071,6 @@
voiceInteraction);
applyAnimations(closingWcs, closingApps, transit, false /* visible */, animLp,
voiceInteraction);
- if (rac != null) {
- rac.sendTasksAppeared();
- }
for (int i = 0; i < openingApps.size(); ++i) {
openingApps.valueAtUnchecked(i).mOverrideTaskTransition = false;
diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java
index eb85c1a..f0a6e9e 100644
--- a/services/core/java/com/android/server/wm/AsyncRotationController.java
+++ b/services/core/java/com/android/server/wm/AsyncRotationController.java
@@ -174,11 +174,6 @@
mNavBarToken = w.mToken;
// Do not animate movable navigation bar (e.g. 3-buttons mode).
if (navigationBarCanMove) return;
- // Or when the navigation bar is currently controlled by recents animation.
- final RecentsAnimationController recents = mService.getRecentsAnimationController();
- if (recents != null && recents.isNavigationBarAttachedToApp()) {
- return;
- }
} else if (navigationBarCanMove || mTransitionOp == OP_CHANGE_MAY_SEAMLESS
|| mDisplayContent.mTransitionController.mNavigationBarAttachedToApp) {
action = Operation.ACTION_SEAMLESS;
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index 9be3f43..670a61d 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -195,12 +195,14 @@
* screenWidthDp, screenHeightDp, smallestScreenWidthDp, and orientation.
* All overrides to those fields should be in this method.
*
+ * Task is only needed for split-screen to apply an offset special handling.
+ *
* TODO: Consider integrate this with computeConfigByResolveHint()
*/
static void applySizeOverrideIfNeeded(DisplayContent displayContent, ApplicationInfo appInfo,
Configuration newParentConfiguration, Configuration inOutConfig,
boolean optsOutEdgeToEdge, boolean hasFixedRotationTransform,
- boolean hasCompatDisplayInsets) {
+ boolean hasCompatDisplayInsets, Task task) {
if (displayContent == null) {
return;
}
@@ -223,13 +225,16 @@
}
if (!optsOutEdgeToEdge && (!useOverrideInsetsForConfig
|| hasCompatDisplayInsets
- || isFloating
|| rotation == ROTATION_UNDEFINED)) {
// If the insets configuration decoupled logic is not enabled for the app, or the app
// already has a compat override, or the context doesn't contain enough info to
// calculate the override, skip the override.
return;
}
+ if (isFloating) {
+ // Floating window won't have any insets affect configuration. Skip the override.
+ return;
+ }
// Make sure the orientation related fields will be updated by the override insets, because
// fixed rotation has assigned the fields from display's configuration.
if (hasFixedRotationTransform) {
@@ -255,17 +260,17 @@
inOutConfig.windowConfiguration.setAppBounds(
newParentConfiguration.windowConfiguration.getBounds());
outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
- if (inOutConfig.windowConfiguration.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
- final DisplayPolicy.DecorInsets.Info decor =
- displayContent.getDisplayPolicy().getDecorInsetsInfo(rotation, dw, dh);
- if (outAppBounds.contains(decor.mOverrideNonDecorFrame)) {
- outAppBounds.intersect(decor.mOverrideNonDecorFrame);
+ if (task != null) {
+ task = task.getCreatedByOrganizerTask();
+ if (task != null && (task.mOffsetYForInsets != 0 || task.mOffsetXForInsets != 0)) {
+ outAppBounds.offset(task.mOffsetXForInsets, task.mOffsetYForInsets);
}
- } else {
- // TODO(b/358509380): Handle other windowing mode like split screen and freeform
- // cases correctly.
- outAppBounds.inset(displayContent.getDisplayPolicy()
- .getDecorInsetsInfo(rotation, dw, dh).mOverrideNonDecorInsets);
+ }
+ final DisplayPolicy.DecorInsets.Info decor =
+ displayContent.getDisplayPolicy().getDecorInsetsInfo(rotation, dw, dh);
+ outAppBounds.intersectUnchecked(decor.mOverrideNonDecorFrame);
+ if (task != null && (task.mOffsetYForInsets != 0 || task.mOffsetXForInsets != 0)) {
+ outAppBounds.offset(-task.mOffsetXForInsets, -task.mOffsetYForInsets);
}
}
float density = inOutConfig.densityDpi;
diff --git a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
index b936556..ff1742b 100644
--- a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
@@ -104,7 +104,7 @@
* resizability.
*/
private float getFixedOrientationLetterboxAspectRatio(@NonNull Task task) {
- return mActivityRecord.shouldCreateCompatDisplayInsets()
+ return mActivityRecord.shouldCreateAppCompatDisplayInsets()
? getDefaultMinAspectRatioForUnresizableApps(task)
: getDefaultMinAspectRatio(task);
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 9bf2555..0597ed7 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1936,7 +1936,6 @@
return false;
}
if (mLastWallpaperVisible && r.windowsCanBeWallpaperTarget()
- && mFixedRotationTransitionListener.mAnimatingRecents == null
&& !mTransitionController.isTransientLaunch(r)) {
// Use normal rotation animation for orientation change of visible wallpaper if recents
// animation is not running (it may be swiping to home).
@@ -1962,9 +1961,7 @@
/** Returns {@code true} if the top activity is transformed with the new rotation of display. */
boolean hasTopFixedRotationLaunchingApp() {
- return mFixedRotationLaunchingApp != null
- // Ignore animating recents because it hasn't really become the top.
- && mFixedRotationLaunchingApp != mFixedRotationTransitionListener.mAnimatingRecents;
+ return mFixedRotationLaunchingApp != null;
}
/** It usually means whether the recents activity is launching with a different rotation. */
@@ -1991,8 +1988,7 @@
mWmService.mDisplayNotificationController.dispatchFixedRotationStarted(this, rotation);
// Delay the hide animation to avoid blinking by clicking navigation bar that may
// toggle fixed rotation in a short time.
- final boolean shouldDebounce = r == mFixedRotationTransitionListener.mAnimatingRecents
- || mTransitionController.isTransientLaunch(r);
+ final boolean shouldDebounce = mTransitionController.isTransientLaunch(r);
startAsyncRotation(shouldDebounce);
} else if (mFixedRotationLaunchingApp != null && r == null) {
mWmService.mDisplayNotificationController.dispatchFixedRotationFinished(this);
@@ -2024,12 +2020,9 @@
// the heavy operations. This also benefits that the states of multiple activities
// are handled together.
r.linkFixedRotationTransform(prevRotatedLaunchingApp);
- if (r != mFixedRotationTransitionListener.mAnimatingRecents) {
- // Only update the record for normal activity so the display orientation can be
- // updated when the transition is done if it becomes the top. And the case of
- // recents can be handled when the recents animation is finished.
- setFixedRotationLaunchingAppUnchecked(r, rotation);
- }
+ // Only update the record for normal activity so the display orientation can be
+ // updated when the transition is done if it becomes the top.
+ setFixedRotationLaunchingAppUnchecked(r, rotation);
return;
}
@@ -5899,18 +5892,13 @@
final Region local = Region.obtain();
final int[] remainingLeftRight =
{mSystemGestureExclusionLimit, mSystemGestureExclusionLimit};
- final RecentsAnimationController recentsAnimationController =
- mWmService.getRecentsAnimationController();
// Traverse all windows top down to assemble the gesture exclusion rects.
// For each window, we only take the rects that fall within its touchable region.
forAllWindows(w -> {
- final boolean ignoreRecentsAnimationTarget = recentsAnimationController != null
- && recentsAnimationController.shouldApplyInputConsumer(w.getActivityRecord());
if (!w.canReceiveTouchInput() || !w.isVisible()
|| (w.getAttrs().flags & FLAG_NOT_TOUCHABLE) != 0
- || unhandled.isEmpty()
- || ignoreRecentsAnimationTarget) {
+ || unhandled.isEmpty()) {
return;
}
@@ -6122,16 +6110,7 @@
void getKeepClearAreas(Set<Rect> outRestricted, Set<Rect> outUnrestricted) {
final Matrix tmpMatrix = new Matrix();
final float[] tmpFloat9 = new float[9];
- final RecentsAnimationController recentsAnimationController =
- mWmService.getRecentsAnimationController();
forAllWindows(w -> {
- // Skip the window if it is part of Recents animation
- final boolean ignoreRecentsAnimationTarget = recentsAnimationController != null
- && recentsAnimationController.shouldApplyInputConsumer(w.getActivityRecord());
- if (ignoreRecentsAnimationTarget) {
- return false; // continue traversal
- }
-
if (w.isVisible() && !w.inPinnedWindowingMode()) {
w.getKeepClearAreas(outRestricted, outUnrestricted, tmpMatrix, tmpFloat9);
@@ -6306,14 +6285,6 @@
}
boolean updateDisplayOverrideConfigurationLocked() {
- // Preemptively cancel the running recents animation -- SysUI can't currently handle this
- // case properly since the signals it receives all happen post-change
- final RecentsAnimationController recentsAnimationController =
- mWmService.getRecentsAnimationController();
- if (recentsAnimationController != null) {
- recentsAnimationController.cancelAnimationForDisplayChange();
- }
-
Configuration values = new Configuration();
computeScreenConfiguration(values);
@@ -6914,79 +6885,11 @@
/** The entry for proceeding to handle {@link #mFixedRotationLaunchingApp}. */
class FixedRotationTransitionListener extends WindowManagerInternal.AppTransitionListener {
- /**
- * The animating activity which shows the recents task list. It is set between
- * {@link RecentsAnimationController#initialize} and
- * {@link RecentsAnimationController#cleanupAnimation}.
- */
- private ActivityRecord mAnimatingRecents;
-
- /** Whether {@link #mAnimatingRecents} is going to be the top activity. */
- private boolean mRecentsWillBeTop;
-
FixedRotationTransitionListener(int displayId) {
super(displayId);
}
/**
- * If the recents activity has a fixed orientation which is different from the current top
- * activity, it will be rotated before being shown so we avoid a screen rotation animation
- * when showing the Recents view.
- */
- void onStartRecentsAnimation(@NonNull ActivityRecord r) {
- mAnimatingRecents = r;
- if (r.isVisible() && mFocusedApp != null && !mFocusedApp.occludesParent()) {
- // The recents activity has shown with the orientation determined by the top
- // activity, keep its current orientation to avoid flicking by the configuration
- // change of visible activity.
- return;
- }
- rotateInDifferentOrientationIfNeeded(r);
- if (r.hasFixedRotationTransform()) {
- // Set the record so we can recognize it to continue to update display orientation
- // if the recents activity becomes the top later.
- setFixedRotationLaunchingApp(r, r.getWindowConfiguration().getRotation());
- }
- }
-
- /**
- * If {@link #mAnimatingRecents} still has fixed rotation, it should be moved to top so we
- * don't clear {@link #mFixedRotationLaunchingApp} that will be handled by transition.
- */
- void onFinishRecentsAnimation() {
- final ActivityRecord animatingRecents = mAnimatingRecents;
- final boolean recentsWillBeTop = mRecentsWillBeTop;
- mAnimatingRecents = null;
- mRecentsWillBeTop = false;
- if (recentsWillBeTop) {
- // The recents activity will be the top, such as staying at recents list or
- // returning to home (if home and recents are the same activity).
- return;
- }
-
- if (animatingRecents != null && animatingRecents == mFixedRotationLaunchingApp
- && animatingRecents.isVisible() && animatingRecents != topRunningActivity()) {
- // The recents activity should be going to be invisible (switch to another app or
- // return to original top). Only clear the top launching record without finishing
- // the transform immediately because it won't affect display orientation. And before
- // the visibility is committed, the recents activity may perform relayout which may
- // cause unexpected configuration change if the rotated configuration is restored.
- // The transform will be finished when the transition is done.
- setFixedRotationLaunchingAppUnchecked(null);
- } else {
- // If there is already a launching activity that is not the recents, before its
- // transition is completed, the recents animation may be started. So if the recents
- // activity won't be the top, the display orientation should be updated according
- // to the current top activity.
- continueUpdateOrientationForDiffOrienLaunchingApp();
- }
- }
-
- void notifyRecentsWillBeTop() {
- mRecentsWillBeTop = true;
- }
-
- /**
* Returns {@code true} if the transient launch (e.g. recents animation) requested a fixed
* orientation, then the rotation change should be deferred.
*/
@@ -6996,8 +6899,6 @@
if (hasFixedRotationTransientLaunch()) {
source = mFixedRotationLaunchingApp;
}
- } else if (mAnimatingRecents != null && !hasTopFixedRotationLaunchingApp()) {
- source = mAnimatingRecents;
}
if (source == null || source.getRequestedConfigurationOrientation(
true /* forDisplay */) == ORIENTATION_UNDEFINED) {
@@ -7010,19 +6911,7 @@
@Override
public void onAppTransitionFinishedLocked(IBinder token) {
final ActivityRecord r = ActivityRecord.forTokenLocked(token);
- // Ignore the animating recents so the fixed rotation transform won't be switched twice
- // by finishing the recents animation and moving it to top. That also avoids flickering
- // due to wait for previous activity to be paused if it supports PiP that ignores the
- // effect of resume-while-pausing.
- if (r == null || r == mAnimatingRecents) {
- return;
- }
- if (mAnimatingRecents != null && mRecentsWillBeTop) {
- // The activity is not the recents and it should be moved to back later, so it is
- // better to keep its current appearance for the next transition. Otherwise the
- // display orientation may be updated too early and the layout procedures at the
- // end of finishing recents animation is skipped. That causes flickering because
- // the surface of closing app hasn't updated to invisible.
+ if (r == null) {
return;
}
if (mFixedRotationLaunchingApp == null) {
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index a5da5e7..5200e82 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -611,16 +611,6 @@
mDisplayRotationCoordinator.onDefaultDisplayRotationChanged(rotation);
}
- // Preemptively cancel the running recents animation -- SysUI can't currently handle this
- // case properly since the signals it receives all happen post-change. We do this earlier
- // in the rotation flow, since DisplayContent.updateDisplayOverrideConfigurationLocked seems
- // to happen too late.
- final RecentsAnimationController recentsAnimationController =
- mService.getRecentsAnimationController();
- if (recentsAnimationController != null) {
- recentsAnimationController.cancelAnimationForDisplayChange();
- }
-
ProtoLog.v(WM_DEBUG_ORIENTATION,
"Display id=%d rotation changed to %d from %d, lastOrientation=%d",
displayId, rotation, oldRotation, lastOrientation);
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index b8869f1..f23dafe 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -413,17 +413,12 @@
// Request focus for the recents animation input consumer if an input consumer should
// be applied for the window.
if (recentsAnimationInputConsumer != null && focus != null) {
- final RecentsAnimationController recentsAnimationController =
- mService.getRecentsAnimationController();
// Apply recents input consumer when the focusing window is in recents animation.
- final boolean shouldApplyRecentsInputConsumer = (recentsAnimationController != null
- && recentsAnimationController.shouldApplyInputConsumer(focus.mActivityRecord))
- // Shell transitions doesn't use RecentsAnimationController but we still
- // have carryover legacy logic that relies on the consumer.
- || (getWeak(mActiveRecentsActivity) != null && focus.inTransition()
+ final boolean shouldApplyRecentsInputConsumer =
+ getWeak(mActiveRecentsActivity) != null && focus.inTransition()
// only take focus from the recents activity to avoid intercepting
// events before the gesture officially starts.
- && focus.isActivityTypeHomeOrRecents());
+ && focus.isActivityTypeHomeOrRecents();
if (shouldApplyRecentsInputConsumer) {
if (mInputFocus != recentsAnimationInputConsumer.mWindowHandle.token) {
requestFocus(recentsAnimationInputConsumer.mWindowHandle.token,
@@ -629,24 +624,6 @@
return;
}
- // This only works for legacy transitions.
- final RecentsAnimationController recentsAnimationController =
- mService.getRecentsAnimationController();
- final boolean shouldApplyRecentsInputConsumer = recentsAnimationController != null
- && recentsAnimationController.shouldApplyInputConsumer(w.mActivityRecord);
- if (mAddRecentsAnimationInputConsumerHandle && shouldApplyRecentsInputConsumer) {
- if (recentsAnimationController.updateInputConsumerForApp(
- mRecentsAnimationInputConsumer.mWindowHandle)) {
- final DisplayArea targetDA =
- recentsAnimationController.getTargetAppDisplayArea();
- if (targetDA != null) {
- mRecentsAnimationInputConsumer.reparent(mInputTransaction, targetDA);
- mRecentsAnimationInputConsumer.show(mInputTransaction, MAX_VALUE - 2);
- mAddRecentsAnimationInputConsumerHandle = false;
- }
- }
- }
-
if (w.inPinnedWindowingMode()) {
if (mAddPipInputConsumerHandle) {
final Task rootTask = w.getTask().getRootTask();
diff --git a/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java b/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java
index 1a895ea..403d3bd 100644
--- a/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java
+++ b/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java
@@ -91,7 +91,6 @@
return (transit == TRANSIT_OLD_TASK_OPEN || transit == TRANSIT_OLD_TASK_TO_FRONT
|| transit == TRANSIT_OLD_WALLPAPER_CLOSE)
&& displayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()
- && service.getRecentsAnimationController() == null
&& displayContent.getAsyncRotationController() == null;
}
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index c592caf..c06efc7 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -17,82 +17,54 @@
package com.android.server.wm;
import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY;
-import static android.app.ActivityManager.START_TASK_TO_FRONT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
-import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS;
import static com.android.server.wm.ActivityRecord.State.STOPPED;
import static com.android.server.wm.ActivityRecord.State.STOPPING;
-import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
-import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
-import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_TOP;
-import static com.android.server.wm.TaskDisplayArea.getRootTaskAbove;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.Intent;
-import android.os.RemoteException;
-import android.os.Trace;
import android.util.Slog;
-import android.view.IRecentsAnimationRunner;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.internal.util.function.pooled.PooledPredicate;
-import com.android.server.wm.ActivityMetricsLogger.LaunchingState;
-import com.android.server.wm.RecentsAnimationController.RecentsAnimationCallbacks;
-import com.android.server.wm.TaskDisplayArea.OnRootTaskOrderChangedListener;
/**
* Manages the recents animation, including the reordering of the root tasks for the transition and
* cleanup. See {@link com.android.server.wm.RecentsAnimationController}.
*/
-class RecentsAnimation implements RecentsAnimationCallbacks, OnRootTaskOrderChangedListener {
+class RecentsAnimation {
private static final String TAG = RecentsAnimation.class.getSimpleName();
- private final ActivityTaskManagerService mService;
private final ActivityTaskSupervisor mTaskSupervisor;
private final ActivityStartController mActivityStartController;
- private final WindowManagerService mWindowManager;
private final TaskDisplayArea mDefaultTaskDisplayArea;
private final Intent mTargetIntent;
private final ComponentName mRecentsComponent;
private final @Nullable String mRecentsFeatureId;
private final int mRecentsUid;
- private final @Nullable WindowProcessController mCaller;
private final int mUserId;
private final int mTargetActivityType;
- /**
- * The activity which has been launched behind. We need to remember the activity because the
- * target root task may have other activities, then we are able to restore the launch-behind
- * state for the exact activity.
- */
- private ActivityRecord mLaunchedTargetActivity;
-
- // The root task to restore the target root task behind when the animation is finished
- private Task mRestoreTargetBehindRootTask;
-
RecentsAnimation(ActivityTaskManagerService atm, ActivityTaskSupervisor taskSupervisor,
- ActivityStartController activityStartController, WindowManagerService wm,
+ ActivityStartController activityStartController,
Intent targetIntent, ComponentName recentsComponent, @Nullable String recentsFeatureId,
- int recentsUid, @Nullable WindowProcessController caller) {
- mService = atm;
+ int recentsUid) {
mTaskSupervisor = taskSupervisor;
- mDefaultTaskDisplayArea = mService.mRootWindowContainer.getDefaultTaskDisplayArea();
+ mDefaultTaskDisplayArea = atm.mRootWindowContainer.getDefaultTaskDisplayArea();
mActivityStartController = activityStartController;
- mWindowManager = wm;
mTargetIntent = targetIntent;
mRecentsComponent = recentsComponent;
mRecentsFeatureId = recentsFeatureId;
mRecentsUid = recentsUid;
- mCaller = caller;
mUserId = atm.getCurrentUserId();
mTargetActivityType = targetIntent.getComponent() != null
&& recentsComponent.equals(targetIntent.getComponent())
@@ -171,310 +143,6 @@
}
}
- void startRecentsActivity(IRecentsAnimationRunner recentsAnimationRunner, long eventTime) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "startRecentsActivity(): intent=%s", mTargetIntent);
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "RecentsAnimation#startRecentsActivity");
-
- // Cancel any existing recents animation running synchronously (do not hold the
- // WM lock) before starting the newly requested recents animation as they can not coexist
- if (mWindowManager.getRecentsAnimationController() != null) {
- mWindowManager.getRecentsAnimationController().forceCancelAnimation(
- REORDER_MOVE_TO_ORIGINAL_POSITION, "startRecentsActivity");
- }
-
- // If the activity is associated with the root recents task, then try and get that first
- Task targetRootTask = mDefaultTaskDisplayArea.getRootTask(WINDOWING_MODE_UNDEFINED,
- mTargetActivityType);
- ActivityRecord targetActivity = getTargetActivity(targetRootTask);
- final boolean hasExistingActivity = targetActivity != null;
- if (hasExistingActivity) {
- mRestoreTargetBehindRootTask = getRootTaskAbove(targetRootTask);
- if (mRestoreTargetBehindRootTask == null
- && targetRootTask.getTopMostTask() == targetActivity.getTask()) {
- notifyAnimationCancelBeforeStart(recentsAnimationRunner);
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "No root task above target root task=%s", targetRootTask);
- return;
- }
- }
-
- // Send launch hint if we are actually launching the target. If it's already visible
- // (shouldn't happen in general) we don't need to send it.
- if (targetActivity == null || !targetActivity.isVisibleRequested()) {
- mService.mRootWindowContainer.startPowerModeLaunchIfNeeded(
- true /* forceSend */, targetActivity);
- }
-
- final LaunchingState launchingState =
- mTaskSupervisor.getActivityMetricsLogger().notifyActivityLaunching(mTargetIntent);
-
- setProcessAnimating(true);
-
- mService.deferWindowLayout();
- try {
- if (hasExistingActivity) {
- // Move the recents activity into place for the animation if it is not top most
- mDefaultTaskDisplayArea.moveRootTaskBehindBottomMostVisibleRootTask(targetRootTask);
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "Moved rootTask=%s behind rootTask=%s",
- targetRootTask, getRootTaskAbove(targetRootTask));
-
- // If there are multiple tasks in the target root task (ie. the root home task,
- // with 3p and default launchers coexisting), then move the task to the top as a
- // part of moving the root task to the front
- final Task task = targetActivity.getTask();
- if (targetRootTask.getTopMostTask() != task) {
- targetRootTask.positionChildAtTop(task);
- }
- } else {
- // No recents activity, create the new recents activity bottom most
- startRecentsActivityInBackground("startRecentsActivity_noTargetActivity");
-
- // Move the recents activity into place for the animation
- targetRootTask = mDefaultTaskDisplayArea.getRootTask(WINDOWING_MODE_UNDEFINED,
- mTargetActivityType);
- targetActivity = getTargetActivity(targetRootTask);
- mDefaultTaskDisplayArea.moveRootTaskBehindBottomMostVisibleRootTask(targetRootTask);
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "Moved rootTask=%s behind rootTask=%s",
- targetRootTask, getRootTaskAbove(targetRootTask));
-
- mWindowManager.prepareAppTransitionNone();
- mWindowManager.executeAppTransition();
-
- // TODO: Maybe wait for app to draw in this particular case?
-
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "Started intent=%s", mTargetIntent);
- }
-
- // Mark the target activity as launch-behind to bump its visibility for the
- // duration of the gesture that is driven by the recents component
- targetActivity.mLaunchTaskBehind = true;
- mLaunchedTargetActivity = targetActivity;
- // TODO(b/156772625): Evaluate to send new intents vs. replacing the intent extras.
- targetActivity.intent.replaceExtras(mTargetIntent);
-
- // Fetch all the surface controls and pass them to the client to get the animation
- // started
- mWindowManager.initializeRecentsAnimation(mTargetActivityType, recentsAnimationRunner,
- this, mDefaultTaskDisplayArea.getDisplayId(),
- mTaskSupervisor.mRecentTasks.getRecentTaskIds(), targetActivity);
-
- // If we updated the launch-behind state, update the visibility of the activities after
- // we fetch the visible tasks to be controlled by the animation
- mService.mRootWindowContainer.ensureActivitiesVisible();
-
- ActivityOptions options = null;
- if (eventTime > 0) {
- options = ActivityOptions.makeBasic();
- options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
- }
- mTaskSupervisor.getActivityMetricsLogger().notifyActivityLaunched(launchingState,
- START_TASK_TO_FRONT, !hasExistingActivity, targetActivity, options);
-
- // Register for root task order changes
- mDefaultTaskDisplayArea.registerRootTaskOrderChangedListener(this);
- } catch (Exception e) {
- Slog.e(TAG, "Failed to start recents activity", e);
- throw e;
- } finally {
- mService.continueWindowLayout();
- Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- }
- }
-
- private void finishAnimation(@RecentsAnimationController.ReorderMode int reorderMode,
- boolean sendUserLeaveHint) {
- synchronized (mService.mGlobalLock) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "onAnimationFinished(): controller=%s reorderMode=%d",
- mWindowManager.getRecentsAnimationController(), reorderMode);
-
- // Unregister for root task order changes
- mDefaultTaskDisplayArea.unregisterRootTaskOrderChangedListener(this);
-
- final RecentsAnimationController controller =
- mWindowManager.getRecentsAnimationController();
- if (controller == null) return;
-
- // Just to be sure end the launch hint in case the target activity was never launched.
- // However, if we're keeping the activity and making it visible, we can leave it on.
- if (reorderMode != REORDER_KEEP_IN_PLACE) {
- mService.endPowerMode(ActivityTaskManagerService.POWER_MODE_REASON_START_ACTIVITY);
- }
-
- // Once the target is shown, prevent spurious background app switches
- if (reorderMode == REORDER_MOVE_TO_TOP) {
- mService.stopAppSwitches();
- }
-
- inSurfaceTransaction(() -> {
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
- "RecentsAnimation#onAnimationFinished_inSurfaceTransaction");
- mService.deferWindowLayout();
- try {
- mWindowManager.cleanupRecentsAnimation(reorderMode);
-
- final Task targetRootTask = mDefaultTaskDisplayArea.getRootTask(
- WINDOWING_MODE_UNDEFINED, mTargetActivityType);
- // Prefer to use the original target activity instead of top activity because
- // we may have moved another task to top (starting 3p launcher).
- final ActivityRecord targetActivity = targetRootTask != null
- ? targetRootTask.isInTask(mLaunchedTargetActivity)
- : null;
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "onAnimationFinished(): targetRootTask=%s targetActivity=%s "
- + "mRestoreTargetBehindRootTask=%s",
- targetRootTask, targetActivity, mRestoreTargetBehindRootTask);
- if (targetActivity == null) {
- return;
- }
-
- // Restore the launched-behind state
- targetActivity.mLaunchTaskBehind = false;
-
- if (reorderMode == REORDER_MOVE_TO_TOP) {
- // Bring the target root task to the front
- mTaskSupervisor.mNoAnimActivities.add(targetActivity);
-
- if (sendUserLeaveHint) {
- // Setting this allows the previous app to PiP.
- mTaskSupervisor.mUserLeaving = true;
- targetRootTask.moveTaskToFront(targetActivity.getTask(),
- true /* noAnimation */, null /* activityOptions */,
- targetActivity.appTimeTracker,
- "RecentsAnimation.onAnimationFinished()");
- } else {
- targetRootTask.moveToFront("RecentsAnimation.onAnimationFinished()");
- }
-
- if (WM_DEBUG_RECENTS_ANIMATIONS.isLogToAny()) {
- final Task topRootTask = getTopNonAlwaysOnTopRootTask();
- if (topRootTask != targetRootTask) {
- ProtoLog.w(WM_DEBUG_RECENTS_ANIMATIONS,
- "Expected target rootTask=%s"
- + " to be top most but found rootTask=%s",
- targetRootTask, topRootTask);
- }
- }
- } else if (reorderMode == REORDER_MOVE_TO_ORIGINAL_POSITION){
- // Restore the target root task to its previous position
- final TaskDisplayArea taskDisplayArea = targetActivity.getDisplayArea();
- taskDisplayArea.moveRootTaskBehindRootTask(targetRootTask,
- mRestoreTargetBehindRootTask);
- if (WM_DEBUG_RECENTS_ANIMATIONS.isLogToAny()) {
- final Task aboveTargetRootTask = getRootTaskAbove(targetRootTask);
- if (mRestoreTargetBehindRootTask != null
- && aboveTargetRootTask != mRestoreTargetBehindRootTask) {
- ProtoLog.w(WM_DEBUG_RECENTS_ANIMATIONS,
- "Expected target rootTask=%s to restored behind "
- + "rootTask=%s but it is behind rootTask=%s",
- targetRootTask, mRestoreTargetBehindRootTask,
- aboveTargetRootTask);
- }
- }
- } else {
- // If there is no recents screenshot animation, we can update the visibility
- // of target root task immediately because it is visually invisible and the
- // launch-behind state is restored. That also prevents the next transition
- // type being disturbed if the visibility is updated after setting the next
- // transition (the target activity will be one of closing apps).
- if (!controller.shouldDeferCancelWithScreenshot()
- && !targetRootTask.isFocusedRootTaskOnDisplay()) {
- targetRootTask.ensureActivitiesVisible(null /* starting */);
- }
- // Keep target root task in place, nothing changes, so ignore the transition
- // logic below
- return;
- }
-
- mWindowManager.prepareAppTransitionNone();
- mService.mRootWindowContainer.ensureActivitiesVisible();
- mService.mRootWindowContainer.resumeFocusedTasksTopActivities();
-
- // No reason to wait for the pausing activity in this case, as the hiding of
- // surfaces needs to be done immediately.
- mWindowManager.executeAppTransition();
-
- final Task rootTask = targetRootTask.getRootTask();
- // Client state may have changed during the recents animation, so force
- // send task info so the client can synchronize its state.
- rootTask.dispatchTaskInfoChangedIfNeeded(true /* force */);
- } catch (Exception e) {
- Slog.e(TAG, "Failed to clean up recents activity", e);
- throw e;
- } finally {
- mTaskSupervisor.mUserLeaving = false;
- mService.continueWindowLayout();
- // Make sure the surfaces are updated with the latest state. Sometimes the
- // surface placement may be skipped if display configuration is changed (i.e.
- // {@link DisplayContent#mWaitingForConfig} is true).
- if (mWindowManager.mRoot.isLayoutNeeded()) {
- mWindowManager.mRoot.performSurfacePlacement();
- }
- setProcessAnimating(false);
- Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- }
- });
- }
- }
-
- // No-op wrapper to keep legacy code.
- private static void inSurfaceTransaction(Runnable exec) {
- exec.run();
- }
-
- /** Gives the owner of recents animation higher priority. */
- private void setProcessAnimating(boolean animating) {
- if (mCaller == null) return;
- // Apply the top-app scheduling group to who runs the animation.
- mCaller.setRunningRecentsAnimation(animating);
- int demoteReasons = mService.mDemoteTopAppReasons;
- if (animating) {
- demoteReasons |= ActivityTaskManagerService.DEMOTE_TOP_REASON_ANIMATING_RECENTS;
- } else {
- demoteReasons &= ~ActivityTaskManagerService.DEMOTE_TOP_REASON_ANIMATING_RECENTS;
- }
- mService.mDemoteTopAppReasons = demoteReasons;
- // Make the demotion of the real top app take effect. No need to restore top app state for
- // finishing recents because addToStopping -> scheduleIdle -> activityIdleInternal ->
- // trimApplications will have a full update.
- if (animating && mService.mTopApp != null) {
- mService.mTopApp.scheduleUpdateOomAdj();
- }
- }
-
- @Override
- public void onAnimationFinished(@RecentsAnimationController.ReorderMode int reorderMode,
- boolean sendUserLeaveHint) {
- finishAnimation(reorderMode, sendUserLeaveHint);
- }
-
- @Override
- public void onRootTaskOrderChanged(Task rootTask) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "onRootTaskOrderChanged(): rootTask=%s", rootTask);
- if (mDefaultTaskDisplayArea.getRootTask(t -> t == rootTask) == null
- || !rootTask.shouldBeVisible(null)) {
- // The root task is not visible, so ignore this change
- return;
- }
- final RecentsAnimationController controller =
- mWindowManager.getRecentsAnimationController();
- if (controller == null) {
- return;
- }
-
- // We defer canceling the recents animation until the next app transition in the following
- // cases:
- // 1) The next launching task is not being animated by the recents animation
- // 2) The next task is home activity. (i.e. pressing home key to back home in recents).
- if ((!controller.isAnimatingTask(rootTask.getTopMostTask())
- || controller.isTargetApp(rootTask.getTopNonFinishingActivity()))
- && controller.shouldDeferCancelUntilNextTransition()) {
- // Always prepare an app transition since we rely on the transition callbacks to cleanup
- mWindowManager.prepareAppTransitionNone();
- controller.setCancelOnNextTransitionStart();
- }
- }
-
private void startRecentsActivityInBackground(String reason) {
final ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchActivityType(mTargetActivityType);
@@ -492,26 +160,6 @@
}
/**
- * Called only when the animation should be canceled prior to starting.
- */
- static void notifyAnimationCancelBeforeStart(IRecentsAnimationRunner recentsAnimationRunner) {
- try {
- recentsAnimationRunner.onAnimationCanceled(null /* taskIds */,
- null /* taskSnapshots */);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to cancel recents animation before start", e);
- }
- }
-
- /**
- * @return The top root task that is not always-on-top.
- */
- private Task getTopNonAlwaysOnTopRootTask() {
- return mDefaultTaskDisplayArea.getRootTask(task ->
- !task.getWindowConfiguration().isAlwaysOnTop());
- }
-
- /**
* @return the top activity in the {@param targetRootTask} matching the {@param component},
* or just the top activity of the top task if no task matches the component.
*/
diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java
deleted file mode 100644
index 6f94713..0000000
--- a/services/core/java/com/android/server/wm/RecentsAnimationController.java
+++ /dev/null
@@ -1,1382 +0,0 @@
-/*
- * 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.server.wm;
-
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-import static android.view.RemoteAnimationTarget.MODE_OPENING;
-import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION;
-import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-
-import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS;
-import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
-import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM;
-import static com.android.server.wm.AnimationAdapterProto.REMOTE;
-import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
-import static com.android.server.wm.WindowManagerInternal.AppTransitionListener;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.app.WindowConfiguration;
-import android.graphics.GraphicBuffer;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.hardware.HardwareBuffer;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.IBinder.DeathRecipient;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-import android.util.IntArray;
-import android.util.Slog;
-import android.util.SparseBooleanArray;
-import android.util.proto.ProtoOutputStream;
-import android.view.IRecentsAnimationController;
-import android.view.IRecentsAnimationRunner;
-import android.view.InputWindowHandle;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-import android.view.SurfaceSession;
-import android.view.WindowInsets.Type;
-import android.window.PictureInPictureSurfaceTransaction;
-import android.window.TaskSnapshot;
-import android.window.WindowAnimationState;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.os.IResultReceiver;
-import com.android.internal.protolog.ProtoLog;
-import com.android.server.LocalServices;
-import com.android.server.inputmethod.InputMethodManagerInternal;
-import com.android.server.statusbar.StatusBarManagerInternal;
-import com.android.server.wm.SurfaceAnimator.AnimationType;
-import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
-import com.android.server.wm.utils.InsetUtils;
-
-import com.google.android.collect.Sets;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.stream.Collectors;
-
-/**
- * Controls a single instance of the remote driven recents animation. In particular, this allows
- * the calling SystemUI to animate the visible task windows as a part of the transition. The remote
- * runner is provided an animation controller which allows it to take screenshots and to notify
- * window manager when the animation is completed. In addition, window manager may also notify the
- * app if it requires the animation to be canceled at any time (ie. due to timeout, etc.)
- */
-public class RecentsAnimationController implements DeathRecipient {
- private static final String TAG = RecentsAnimationController.class.getSimpleName();
- private static final long FAILSAFE_DELAY = 1000;
-
- // Constant for a yet-to-be-calculated {@link RemoteAnimationTarget#Mode} state
- private static final int MODE_UNKNOWN = -1;
-
- public static final int REORDER_KEEP_IN_PLACE = 0;
- public static final int REORDER_MOVE_TO_TOP = 1;
- public static final int REORDER_MOVE_TO_ORIGINAL_POSITION = 2;
-
- @IntDef(prefix = { "REORDER_MODE_" }, value = {
- REORDER_KEEP_IN_PLACE,
- REORDER_MOVE_TO_TOP,
- REORDER_MOVE_TO_ORIGINAL_POSITION
- })
- public @interface ReorderMode {}
-
- private final WindowManagerService mService;
- @VisibleForTesting
- final StatusBarManagerInternal mStatusBar;
- private IRecentsAnimationRunner mRunner;
- private final RecentsAnimationCallbacks mCallbacks;
- private final ArrayList<TaskAnimationAdapter> mPendingAnimations = new ArrayList<>();
- private final IntArray mPendingNewTaskTargets = new IntArray(0);
-
- private final ArrayList<WallpaperAnimationAdapter> mPendingWallpaperAnimations =
- new ArrayList<>();
- private final int mDisplayId;
- private boolean mWillFinishToHome = false;
- private final Runnable mFailsafeRunnable = this::onFailsafe;
-
- // The recents component app token that is shown behind the visible tasks
- private ActivityRecord mTargetActivityRecord;
- private DisplayContent mDisplayContent;
- private int mTargetActivityType;
-
- // We start the RecentsAnimationController in a pending-start state since we need to wait for
- // the wallpaper/activity to draw before we can give control to the handler to start animating
- // the visible task surfaces
- private boolean mPendingStart = true;
-
- // Set when the animation has been canceled
- private boolean mCanceled;
-
- // Whether or not the input consumer is enabled. The input consumer must be both registered and
- // enabled for it to start intercepting touch events.
- private boolean mInputConsumerEnabled;
-
- private final Rect mTmpRect = new Rect();
-
- private boolean mLinkedToDeathOfRunner;
-
- // Whether to try to defer canceling from a root task order change until the next transition
- private boolean mRequestDeferCancelUntilNextTransition;
- // Whether to actually defer canceling until the next transition
- private boolean mCancelOnNextTransitionStart;
- // Whether to take a screenshot when handling a deferred cancel
- private boolean mCancelDeferredWithScreenshot;
- // The reorder mode to apply after the cleanupScreenshot() callback
- private int mPendingCancelWithScreenshotReorderMode = REORDER_MOVE_TO_ORIGINAL_POSITION;
-
- @VisibleForTesting
- boolean mIsAddingTaskToTargets;
- private boolean mNavigationBarAttachedToApp;
- private ActivityRecord mNavBarAttachedApp;
-
- private final ArrayList<RemoteAnimationTarget> mPendingTaskAppears = new ArrayList<>();
-
- /**
- * An app transition listener to cancel the recents animation only after the app transition
- * starts or is canceled.
- */
- final AppTransitionListener mAppTransitionListener = new AppTransitionListener() {
- @Override
- public int onAppTransitionStartingLocked(long statusBarAnimationStartTime,
- long statusBarAnimationDuration) {
- continueDeferredCancel();
- return 0;
- }
-
- @Override
- public void onAppTransitionCancelledLocked(boolean keyguardGoingAwayCancelled) {
- continueDeferredCancel();
- }
-
- private void continueDeferredCancel() {
- mDisplayContent.mAppTransition.unregisterListener(this);
- if (mCanceled) {
- return;
- }
-
- if (mCancelOnNextTransitionStart) {
- mCancelOnNextTransitionStart = false;
- cancelAnimationWithScreenshot(mCancelDeferredWithScreenshot);
- }
- }
- };
-
- public interface RecentsAnimationCallbacks {
- /** Callback when recents animation is finished. */
- void onAnimationFinished(@ReorderMode int reorderMode, boolean sendUserLeaveHint);
- }
-
- private final IRecentsAnimationController mController =
- new IRecentsAnimationController.Stub() {
-
- @Override
- public TaskSnapshot screenshotTask(int taskId) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "screenshotTask(%d): mCanceled=%b", taskId, mCanceled);
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mService.getWindowManagerLock()) {
- if (mCanceled) {
- return null;
- }
- for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- final TaskAnimationAdapter adapter = mPendingAnimations.get(i);
- final Task task = adapter.mTask;
- if (task.mTaskId == taskId) {
- final TaskSnapshotController snapshotController =
- mService.mTaskSnapshotController;
- final ArraySet<Task> tasks = Sets.newArraySet(task);
- snapshotController.snapshotTasks(tasks);
- snapshotController.addSkipClosingAppSnapshotTasks(tasks);
- return snapshotController.getSnapshot(taskId, task.mUserId,
- false /* restoreFromDisk */, false /* isLowResolution */);
- }
- }
- return null;
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void setFinishTaskTransaction(int taskId,
- PictureInPictureSurfaceTransaction finishTransaction,
- SurfaceControl overlay) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "setFinishTaskTransaction(%d): transaction=%s", taskId, finishTransaction);
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mService.getWindowManagerLock()) {
- for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i);
- if (taskAdapter.mTask.mTaskId == taskId) {
- taskAdapter.mFinishTransaction = finishTransaction;
- taskAdapter.mFinishOverlay = overlay;
- break;
- }
- }
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void finish(boolean moveHomeToTop, boolean sendUserLeaveHint,
- IResultReceiver finishCb) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "finish(%b): mCanceled=%b", moveHomeToTop, mCanceled);
- final long token = Binder.clearCallingIdentity();
- try {
- // Note, the callback will handle its own synchronization, do not lock on WM lock
- // prior to calling the callback
- mCallbacks.onAnimationFinished(moveHomeToTop
- ? REORDER_MOVE_TO_TOP
- : REORDER_MOVE_TO_ORIGINAL_POSITION, sendUserLeaveHint);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- if (finishCb != null) {
- try {
- finishCb.send(0, new Bundle());
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to report animation finished", e);
- }
- }
- }
-
- @Override
- public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars)
- throws RemoteException {
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mService.getWindowManagerLock()) {
- for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- final Task task = mPendingAnimations.get(i).mTask;
- if (task.getActivityType() != mTargetActivityType) {
- task.setCanAffectSystemUiFlags(behindSystemBars);
- }
- }
- InputMethodManagerInternal.get().maybeFinishStylusHandwriting();
- mService.mWindowPlacerLocked.requestTraversal();
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void setInputConsumerEnabled(boolean enabled) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "setInputConsumerEnabled(%s): mCanceled=%b", enabled, mCanceled);
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mService.getWindowManagerLock()) {
- if (mCanceled) {
- return;
- }
- mInputConsumerEnabled = enabled;
- final InputMonitor inputMonitor = mDisplayContent.getInputMonitor();
- inputMonitor.updateInputWindowsLw(true /*force*/);
- mService.scheduleAnimationLocked();
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) {
- synchronized (mService.mGlobalLock) {
- setDeferredCancel(defer, screenshot);
- }
- }
-
- @Override
- public void cleanupScreenshot() {
- final long token = Binder.clearCallingIdentity();
- try {
- // Note, the callback will handle its own synchronization, do not lock on WM lock
- // prior to calling the callback
- continueDeferredCancelAnimation();
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void setWillFinishToHome(boolean willFinishToHome) {
- synchronized (mService.getWindowManagerLock()) {
- RecentsAnimationController.this.setWillFinishToHome(willFinishToHome);
- }
- }
-
- @Override
- public boolean removeTask(int taskId) {
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mService.getWindowManagerLock()) {
- return removeTaskInternal(taskId);
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void detachNavigationBarFromApp(boolean moveHomeToTop) {
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mService.getWindowManagerLock()) {
- restoreNavigationBarFromApp(
- moveHomeToTop || mIsAddingTaskToTargets /* animate */);
- mService.mWindowPlacerLocked.requestTraversal();
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void animateNavigationBarToApp(long duration) {
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mService.getWindowManagerLock()) {
- animateNavigationBarForAppLaunch(duration);
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void handOffAnimation(
- RemoteAnimationTarget[] targets, WindowAnimationState[] states) {
- // unused legacy implementation
- }
- };
-
- /**
- * @param remoteAnimationRunner The remote runner which should be notified when the animation is
- * ready to start or has been canceled
- * @param callbacks Callbacks to be made when the animation finishes
- */
- RecentsAnimationController(WindowManagerService service,
- IRecentsAnimationRunner remoteAnimationRunner, RecentsAnimationCallbacks callbacks,
- int displayId) {
- mService = service;
- mRunner = remoteAnimationRunner;
- mCallbacks = callbacks;
- mDisplayId = displayId;
- mStatusBar = LocalServices.getService(StatusBarManagerInternal.class);
- mDisplayContent = service.mRoot.getDisplayContent(displayId);
- }
-
- /**
- * Initializes the recents animation controller. This is a separate call from the constructor
- * because it may call cancelAnimation() which needs to properly clean up the controller
- * in the window manager.
- */
- public void initialize(int targetActivityType, SparseBooleanArray recentTaskIds,
- ActivityRecord targetActivity) {
- mTargetActivityType = targetActivityType;
- mDisplayContent.mAppTransition.registerListenerLocked(mAppTransitionListener);
-
- // Make leashes for each of the visible/target tasks and add it to the recents animation to
- // be started
- // TODO(b/153090560): Support Recents on multiple task display areas
- final ArrayList<Task> visibleTasks = mDisplayContent.getDefaultTaskDisplayArea()
- .getVisibleTasks();
- final Task targetRootTask = mDisplayContent.getDefaultTaskDisplayArea()
- .getRootTask(WINDOWING_MODE_UNDEFINED, targetActivityType);
- if (targetRootTask != null) {
- targetRootTask.forAllLeafTasks(t -> {
- if (!visibleTasks.contains(t)) {
- visibleTasks.add(t);
- }
- }, true /* traverseTopToBottom */);
- }
-
- final int taskCount = visibleTasks.size();
- for (int i = taskCount - 1; i >= 0; i--) {
- final Task task = visibleTasks.get(i);
- if (skipAnimation(task)) {
- continue;
- }
- addAnimation(task, !recentTaskIds.get(task.mTaskId), false /* hidden */,
- (type, anim) -> task.forAllWindows(win -> {
- win.onAnimationFinished(type, anim);
- }, true /* traverseTopToBottom */));
- }
-
- // Skip the animation if there is nothing to animate
- if (mPendingAnimations.isEmpty()) {
- cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "initialize-noVisibleTasks");
- return;
- }
-
- try {
- linkToDeathOfRunner();
- } catch (RemoteException e) {
- cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "initialize-failedToLinkToDeath");
- return;
- }
-
- attachNavigationBarToApp();
-
- // Adjust the wallpaper visibility for the showing target activity
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "setHomeApp(%s)", targetActivity.getName());
- mTargetActivityRecord = targetActivity;
- if (targetActivity.windowsCanBeWallpaperTarget()) {
- mDisplayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
- mDisplayContent.setLayoutNeeded();
- }
-
- mService.mWindowPlacerLocked.performSurfacePlacement();
-
- mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(targetActivity);
-
- // Notify that the animation has started
- if (mStatusBar != null) {
- mStatusBar.onRecentsAnimationStateChanged(true /* running */);
- }
- }
-
- /**
- * Return whether the given window should still be considered interesting for the all-drawn
- * state. This is only interesting for the target app, which may have child windows that are
- * not actually visible and should not be considered interesting and waited upon.
- */
- protected boolean isInterestingForAllDrawn(WindowState window) {
- if (isTargetApp(window.getActivityRecord())) {
- if (window.getWindowType() != TYPE_BASE_APPLICATION
- && window.getAttrs().alpha == 0f) {
- // If there is a cihld window that is alpha 0, then ignore that window
- return false;
- }
- }
- // By default all windows are still interesting for all drawn purposes
- return true;
- }
-
- /**
- * Whether a task should be filtered from the recents animation. This can be true for tasks
- * being displayed outside of recents.
- */
- private boolean skipAnimation(Task task) {
- final WindowConfiguration config = task.getWindowConfiguration();
- return task.isAlwaysOnTop() || config.tasksAreFloating();
- }
-
- @VisibleForTesting
- TaskAnimationAdapter addAnimation(Task task, boolean isRecentTaskInvisible) {
- return addAnimation(task, isRecentTaskInvisible, false /* hidden */,
- null /* finishedCallback */);
- }
-
- @VisibleForTesting
- TaskAnimationAdapter addAnimation(Task task, boolean isRecentTaskInvisible, boolean hidden,
- OnAnimationFinishedCallback finishedCallback) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "addAnimation(%s)", task.getName());
- final TaskAnimationAdapter taskAdapter = new TaskAnimationAdapter(task,
- isRecentTaskInvisible);
- task.startAnimation(task.getPendingTransaction(), taskAdapter, hidden,
- ANIMATION_TYPE_RECENTS, finishedCallback);
- task.commitPendingTransaction();
- mPendingAnimations.add(taskAdapter);
- return taskAdapter;
- }
-
- @VisibleForTesting
- void removeAnimation(TaskAnimationAdapter taskAdapter) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "removeAnimation(%d)", taskAdapter.mTask.mTaskId);
- taskAdapter.onRemove();
- mPendingAnimations.remove(taskAdapter);
- }
-
- @VisibleForTesting
- void removeWallpaperAnimation(WallpaperAnimationAdapter wallpaperAdapter) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "removeWallpaperAnimation()");
- wallpaperAdapter.getLeashFinishedCallback().onAnimationFinished(
- wallpaperAdapter.getLastAnimationType(), wallpaperAdapter);
- mPendingWallpaperAnimations.remove(wallpaperAdapter);
- }
-
- void startAnimation() {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "startAnimation(): mPendingStart=%b mCanceled=%b", mPendingStart, mCanceled);
- if (!mPendingStart || mCanceled) {
- // Skip starting if we've already started or canceled the animation
- return;
- }
- try {
- // Create the app targets
- final RemoteAnimationTarget[] appTargets = createAppAnimations();
-
- // Skip the animation if there is nothing to animate
- if (appTargets.length == 0) {
- cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "startAnimation-noAppWindows");
- return;
- }
-
- // Create the wallpaper targets
- final RemoteAnimationTarget[] wallpaperTargets = createWallpaperAnimations();
-
- mPendingStart = false;
-
- final Rect contentInsets;
- final WindowState targetAppMainWindow = getTargetAppMainWindow();
- if (targetAppMainWindow != null) {
- contentInsets = targetAppMainWindow
- .getInsetsStateWithVisibilityOverride()
- .calculateInsets(mTargetActivityRecord.getBounds(), Type.systemBars(),
- false /* ignoreVisibility */).toRect();
- } else {
- // If the window for the activity had not yet been created, use the display insets.
- mService.getStableInsets(mDisplayId, mTmpRect);
- contentInsets = mTmpRect;
- }
- mRunner.onAnimationStart(mController, appTargets, wallpaperTargets, contentInsets,
- null, new Bundle());
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "startAnimation(): Notify animation start: %s",
- mPendingAnimations.stream()
- .map(anim->anim.mTask.mTaskId).collect(Collectors.toList()));
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to start recents animation", e);
- }
-
- if (mTargetActivityRecord != null) {
- final ArrayMap<WindowContainer, Integer> reasons = new ArrayMap<>(1);
- reasons.put(mTargetActivityRecord, APP_TRANSITION_RECENTS_ANIM);
- mService.mAtmService.mTaskSupervisor.getActivityMetricsLogger()
- .notifyTransitionStarting(reasons);
- }
- }
-
- boolean isNavigationBarAttachedToApp() {
- return mNavigationBarAttachedToApp;
- }
-
- @VisibleForTesting
- WindowState getNavigationBarWindow() {
- return mDisplayContent.getDisplayPolicy().getNavigationBar();
- }
-
- private void attachNavigationBarToApp() {
- if (!mDisplayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()
- // Skip the case where the nav bar is controlled by fade rotation.
- || mDisplayContent.getAsyncRotationController() != null) {
- return;
- }
- for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- final TaskAnimationAdapter adapter = mPendingAnimations.get(i);
- final Task task = adapter.mTask;
- if (task.isActivityTypeHomeOrRecents()) {
- continue;
- }
- mNavBarAttachedApp = task.getTopVisibleActivity();
- break;
- }
-
- final WindowState navWindow = getNavigationBarWindow();
- if (mNavBarAttachedApp == null || navWindow == null || navWindow.mToken == null) {
- return;
- }
- mNavigationBarAttachedToApp = true;
- navWindow.mToken.cancelAnimation();
- final SurfaceControl.Transaction t = navWindow.mToken.getPendingTransaction();
- final SurfaceControl navSurfaceControl = navWindow.mToken.getSurfaceControl();
- navWindow.setSurfaceTranslationY(-mNavBarAttachedApp.getBounds().top);
- t.reparent(navSurfaceControl, mNavBarAttachedApp.getSurfaceControl());
- t.show(navSurfaceControl);
-
- final WindowContainer imeContainer = mDisplayContent.getImeContainer();
- if (imeContainer.isVisible()) {
- t.setRelativeLayer(navSurfaceControl, imeContainer.getSurfaceControl(), 1);
- } else {
- // Place the nav bar on top of anything else in the top activity.
- t.setLayer(navSurfaceControl, Integer.MAX_VALUE);
- }
- if (mStatusBar != null) {
- mStatusBar.setNavigationBarLumaSamplingEnabled(mDisplayId, false);
- }
- }
-
- @VisibleForTesting
- void restoreNavigationBarFromApp(boolean animate) {
- if (!mNavigationBarAttachedToApp) {
- return;
- }
- mNavigationBarAttachedToApp = false;
-
- if (mStatusBar != null) {
- mStatusBar.setNavigationBarLumaSamplingEnabled(mDisplayId, true);
- }
-
- final WindowState navWindow = getNavigationBarWindow();
- if (navWindow == null) {
- return;
- }
- navWindow.setSurfaceTranslationY(0);
-
- final WindowToken navToken = navWindow.mToken;
- if (navToken == null) {
- return;
- }
- final SurfaceControl.Transaction t = mDisplayContent.getPendingTransaction();
- final WindowContainer parent = navToken.getParent();
- t.setLayer(navToken.getSurfaceControl(), navToken.getLastLayer());
-
- if (animate) {
- final NavBarFadeAnimationController controller =
- new NavBarFadeAnimationController(mDisplayContent);
- controller.fadeWindowToken(true);
- } else {
- // Reparent the SurfaceControl of nav bar token back.
- t.reparent(navToken.getSurfaceControl(), parent.getSurfaceControl());
- }
- }
-
- void animateNavigationBarForAppLaunch(long duration) {
- if (!mDisplayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()
- // Skip the case where the nav bar is controlled by fade rotation.
- || mDisplayContent.getAsyncRotationController() != null
- || mNavigationBarAttachedToApp
- || mNavBarAttachedApp == null) {
- return;
- }
-
- final NavBarFadeAnimationController controller =
- new NavBarFadeAnimationController(mDisplayContent);
- controller.fadeOutAndInSequentially(duration, null /* fadeOutParent */,
- mNavBarAttachedApp.getSurfaceControl());
- }
-
- void addTaskToTargets(Task task, OnAnimationFinishedCallback finishedCallback) {
- if (mRunner != null) {
- mIsAddingTaskToTargets = task != null;
- mNavBarAttachedApp = task == null ? null : task.getTopVisibleActivity();
- // No need to send task appeared when the task target already exists, or when the
- // task is being managed as a multi-window mode outside of recents (e.g. bubbles).
- if (isAnimatingTask(task) || skipAnimation(task)) {
- return;
- }
- collectTaskRemoteAnimations(task, MODE_OPENING, finishedCallback);
- }
- }
-
- void sendTasksAppeared() {
- if (mPendingTaskAppears.isEmpty() || mRunner == null) return;
- try {
- final RemoteAnimationTarget[] targets = mPendingTaskAppears.toArray(
- new RemoteAnimationTarget[0]);
- mRunner.onTasksAppeared(targets);
- mPendingTaskAppears.clear();
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to report task appeared", e);
- }
- }
-
- private void collectTaskRemoteAnimations(Task task, int mode,
- OnAnimationFinishedCallback finishedCallback) {
- final SparseBooleanArray recentTaskIds =
- mService.mAtmService.getRecentTasks().getRecentTaskIds();
-
- // The target must be built off the root task (the leaf task surface would be cropped
- // within the root surface). However, recents only tracks leaf task ids, so we'll traverse
- // and create animation target for all visible leaf tasks.
- task.forAllLeafTasks(leafTask -> {
- if (!leafTask.shouldBeVisible(null /* starting */)) {
- return;
- }
- final int taskId = leafTask.mTaskId;
- TaskAnimationAdapter adapter = addAnimation(leafTask,
- !recentTaskIds.get(taskId), true /* hidden */, finishedCallback);
- mPendingNewTaskTargets.add(taskId);
- final RemoteAnimationTarget target =
- adapter.createRemoteAnimationTarget(taskId, mode);
- if (target != null) {
- mPendingTaskAppears.add(target);
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "collectTaskRemoteAnimations, target: %s", target);
- }
- }, false /* traverseTopToBottom */);
- }
-
- private boolean removeTaskInternal(int taskId) {
- boolean result = false;
- for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- // Only allows when task target has became visible to user, to prevent
- // the flickering during remove animation and task visible.
- final TaskAnimationAdapter target = mPendingAnimations.get(i);
- if (target.mTask.mTaskId == taskId && target.mTask.isOnTop()) {
- removeAnimation(target);
- final int taskIndex = mPendingNewTaskTargets.indexOf(taskId);
- if (taskIndex != -1) {
- mPendingNewTaskTargets.remove(taskIndex);
- }
- result = true;
- break;
- }
- }
- return result;
- }
-
- private RemoteAnimationTarget[] createAppAnimations() {
- final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>();
- for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i);
- final RemoteAnimationTarget target =
- taskAdapter.createRemoteAnimationTarget(INVALID_TASK_ID, MODE_UNKNOWN);
- if (target != null) {
- targets.add(target);
- } else {
- removeAnimation(taskAdapter);
- }
- }
- return targets.toArray(new RemoteAnimationTarget[targets.size()]);
- }
-
- private RemoteAnimationTarget[] createWallpaperAnimations() {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "createWallpaperAnimations()");
- return WallpaperAnimationAdapter.startWallpaperAnimations(mDisplayContent, 0L, 0L,
- adapter -> {
- synchronized (mService.mGlobalLock) {
- // If the wallpaper animation is canceled, continue with the recents
- // animation
- mPendingWallpaperAnimations.remove(adapter);
- }
- }, mPendingWallpaperAnimations);
- }
-
- void forceCancelAnimation(@ReorderMode int reorderMode, String reason) {
- if (!mCanceled) {
- cancelAnimation(reorderMode, reason);
- } else {
- continueDeferredCancelAnimation();
- }
- }
-
- void cancelAnimation(@ReorderMode int reorderMode, String reason) {
- cancelAnimation(reorderMode, false /*screenshot */, reason);
- }
-
- void cancelAnimationWithScreenshot(boolean screenshot) {
- cancelAnimation(REORDER_KEEP_IN_PLACE, screenshot, "rootTaskOrderChanged");
- }
-
- /**
- * Cancels the running animation when starting home, providing a snapshot for the runner to
- * properly handle the cancellation. This call uses the provided hint to determine how to
- * finish the animation.
- */
- public void cancelAnimationForHomeStart() {
- final int reorderMode = mTargetActivityType == ACTIVITY_TYPE_HOME && mWillFinishToHome
- ? REORDER_MOVE_TO_TOP
- : REORDER_KEEP_IN_PLACE;
- cancelAnimation(reorderMode, true /* screenshot */, "cancelAnimationForHomeStart");
- }
-
- /**
- * Cancels the running animation when there is a display change, providing a snapshot for the
- * runner to properly handle the cancellation. This call uses the provided hint to determine
- * how to finish the animation.
- */
- public void cancelAnimationForDisplayChange() {
- cancelAnimation(mWillFinishToHome ? REORDER_MOVE_TO_TOP : REORDER_MOVE_TO_ORIGINAL_POSITION,
- true /* screenshot */, "cancelAnimationForDisplayChange");
- }
-
- private void cancelAnimation(@ReorderMode int reorderMode, boolean screenshot, String reason) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "cancelAnimation(): reason=%s", reason);
- synchronized (mService.getWindowManagerLock()) {
- if (mCanceled) {
- // We've already canceled the animation
- return;
- }
- mService.mH.removeCallbacks(mFailsafeRunnable);
- mCanceled = true;
-
- if (screenshot && !mPendingAnimations.isEmpty()) {
- final ArrayMap<Task, TaskSnapshot> snapshotMap = screenshotRecentTasks();
- mPendingCancelWithScreenshotReorderMode = reorderMode;
-
- if (!snapshotMap.isEmpty()) {
- try {
- int[] taskIds = new int[snapshotMap.size()];
- TaskSnapshot[] snapshots = new TaskSnapshot[snapshotMap.size()];
- for (int i = snapshotMap.size() - 1; i >= 0; i--) {
- taskIds[i] = snapshotMap.keyAt(i).mTaskId;
- snapshots[i] = snapshotMap.valueAt(i);
- }
- mRunner.onAnimationCanceled(taskIds, snapshots);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to cancel recents animation", e);
- }
- // Schedule a new failsafe for if the runner doesn't clean up the screenshot
- scheduleFailsafe();
- return;
- }
- // Fallback to a normal cancel since we couldn't screenshot
- }
-
- // Notify the runner and clean up the animation immediately
- // Note: In the fallback case, this can trigger multiple onAnimationCancel() calls
- // to the runner if we this actually triggers cancel twice on the caller
- try {
- mRunner.onAnimationCanceled(null /* taskIds */, null /* taskSnapshots */);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to cancel recents animation", e);
- }
- mCallbacks.onAnimationFinished(reorderMode, false /* sendUserLeaveHint */);
- }
- }
-
- @VisibleForTesting
- void continueDeferredCancelAnimation() {
- mCallbacks.onAnimationFinished(mPendingCancelWithScreenshotReorderMode,
- false /* sendUserLeaveHint */);
- }
-
- @VisibleForTesting
- void setWillFinishToHome(boolean willFinishToHome) {
- mWillFinishToHome = willFinishToHome;
- }
-
- /**
- * Cancel recents animation when the next app transition starts.
- * <p>
- * When we cancel the recents animation due to a root task order change, we can't just cancel it
- * immediately as it would lead to a flicker in Launcher if we just remove the task from the
- * leash. Instead we screenshot the previous task and replace the child of the leash with the
- * screenshot, so that Launcher can still control the leash lifecycle & make the next app
- * transition animate smoothly without flickering.
- */
- void setCancelOnNextTransitionStart() {
- mCancelOnNextTransitionStart = true;
- }
-
- /**
- * Requests that we attempt to defer the cancel until the next app transition if we are
- * canceling from a root task order change. If {@param screenshot} is specified, then the
- * system will replace the contents of the leash with a screenshot, which must be cleaned up
- * when the runner calls cleanUpScreenshot().
- */
- void setDeferredCancel(boolean defer, boolean screenshot) {
- mRequestDeferCancelUntilNextTransition = defer;
- mCancelDeferredWithScreenshot = screenshot;
- }
-
- /**
- * @return Whether we should defer the cancel from a root task order change until the next app
- * transition.
- */
- boolean shouldDeferCancelUntilNextTransition() {
- return mRequestDeferCancelUntilNextTransition;
- }
-
- /**
- * @return Whether we should both defer the cancel from a root task order change until the next
- * app transition, and also that the deferred cancel should replace the contents of the leash
- * with a screenshot.
- */
- boolean shouldDeferCancelWithScreenshot() {
- return mRequestDeferCancelUntilNextTransition && mCancelDeferredWithScreenshot;
- }
-
- private ArrayMap<Task, TaskSnapshot> screenshotRecentTasks() {
- final TaskSnapshotController snapshotController = mService.mTaskSnapshotController;
- final ArrayMap<Task, TaskSnapshot> snapshotMap = new ArrayMap<>();
- for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- final TaskAnimationAdapter adapter = mPendingAnimations.get(i);
- final Task task = adapter.mTask;
- if (task.isActivityTypeHome()) continue;
- snapshotController.recordSnapshot(task);
- final TaskSnapshot snapshot = snapshotController.getSnapshot(task.mTaskId, task.mUserId,
- false /* restoreFromDisk */, false /* isLowResolution */);
- if (snapshot != null) {
- snapshotMap.put(task, snapshot);
- // Defer until the runner calls back to cleanupScreenshot()
- adapter.setSnapshotOverlay(snapshot);
- }
- }
- snapshotController.addSkipClosingAppSnapshotTasks(snapshotMap.keySet());
- return snapshotMap;
- }
-
- void cleanupAnimation(@ReorderMode int reorderMode) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "cleanupAnimation(): Notify animation finished mPendingAnimations=%d "
- + "reorderMode=%d",
- mPendingAnimations.size(), reorderMode);
- if (reorderMode != REORDER_MOVE_TO_ORIGINAL_POSITION
- && mTargetActivityRecord != mDisplayContent.topRunningActivity()) {
- // Notify the state at the beginning because the removeAnimation may notify the
- // transition is finished. This is a signal that there will be a next transition.
- mDisplayContent.mFixedRotationTransitionListener.notifyRecentsWillBeTop();
- }
- for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i);
- if (reorderMode == REORDER_MOVE_TO_TOP || reorderMode == REORDER_KEEP_IN_PLACE) {
- taskAdapter.mTask.dontAnimateDimExit();
- }
- removeAnimation(taskAdapter);
- taskAdapter.onCleanup();
- }
- // Should already be empty, but clean-up pending task-appears in-case they weren't sent.
- mPendingNewTaskTargets.clear();
- mPendingTaskAppears.clear();
-
- for (int i = mPendingWallpaperAnimations.size() - 1; i >= 0; i--) {
- final WallpaperAnimationAdapter wallpaperAdapter = mPendingWallpaperAnimations.get(i);
- removeWallpaperAnimation(wallpaperAdapter);
- }
-
- restoreNavigationBarFromApp(
- reorderMode == REORDER_MOVE_TO_TOP || mIsAddingTaskToTargets /* animate */);
-
- // Clear any pending failsafe runnables
- mService.mH.removeCallbacks(mFailsafeRunnable);
- mDisplayContent.mAppTransition.unregisterListener(mAppTransitionListener);
-
- // Clear references to the runner
- unlinkToDeathOfRunner();
- mRunner = null;
- mCanceled = true;
-
- // Restore IME icon only when moving the original app task to front from recents, in case
- // IME icon may missing if the moving task has already been the current focused task.
- if (reorderMode == REORDER_MOVE_TO_ORIGINAL_POSITION && !mIsAddingTaskToTargets) {
- InputMethodManagerInternal.get().updateImeWindowStatus(
- false /* disableImeIcon */, mDisplayId);
- }
-
- // Update the input windows after the animation is complete
- final InputMonitor inputMonitor = mDisplayContent.getInputMonitor();
- inputMonitor.updateInputWindowsLw(true /*force*/);
-
- // We have deferred all notifications to the target app as a part of the recents animation,
- // so if we are actually transitioning there, notify again here
- if (mTargetActivityRecord != null) {
- if (reorderMode == REORDER_MOVE_TO_TOP || reorderMode == REORDER_KEEP_IN_PLACE) {
- mDisplayContent.mAppTransition.notifyAppTransitionFinishedLocked(
- mTargetActivityRecord.token);
- }
- }
- mDisplayContent.mFixedRotationTransitionListener.onFinishRecentsAnimation();
-
- // Notify that the animation has ended
- if (mStatusBar != null) {
- mStatusBar.onRecentsAnimationStateChanged(false /* running */);
- }
- }
-
- void scheduleFailsafe() {
- mService.mH.postDelayed(mFailsafeRunnable, FAILSAFE_DELAY);
- }
-
- void onFailsafe() {
- forceCancelAnimation(
- mWillFinishToHome ? REORDER_MOVE_TO_TOP : REORDER_MOVE_TO_ORIGINAL_POSITION,
- "onFailsafe");
- }
-
- private void linkToDeathOfRunner() throws RemoteException {
- if (!mLinkedToDeathOfRunner) {
- mRunner.asBinder().linkToDeath(this, 0);
- mLinkedToDeathOfRunner = true;
- }
- }
-
- private void unlinkToDeathOfRunner() {
- if (mLinkedToDeathOfRunner) {
- mRunner.asBinder().unlinkToDeath(this, 0);
- mLinkedToDeathOfRunner = false;
- }
- }
-
- @Override
- public void binderDied() {
- forceCancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "binderDied");
-
- synchronized (mService.getWindowManagerLock()) {
- // Clear associated input consumers on runner death
- final InputMonitor inputMonitor = mDisplayContent.getInputMonitor();
- final InputConsumerImpl consumer = inputMonitor.getInputConsumer(
- INPUT_CONSUMER_RECENTS_ANIMATION);
- if (consumer != null) {
- inputMonitor.destroyInputConsumer(consumer.mToken);
- }
- }
- }
-
- void checkAnimationReady(WallpaperController wallpaperController) {
- if (mPendingStart) {
- final boolean wallpaperReady = !isTargetOverWallpaper()
- || (wallpaperController.getWallpaperTarget() != null
- && wallpaperController.wallpaperTransitionReady());
- if (wallpaperReady) {
- mService.getRecentsAnimationController().startAnimation();
- }
- }
- }
-
- boolean isWallpaperVisible(WindowState w) {
- return w != null && w.mAttrs.type == TYPE_BASE_APPLICATION &&
- ((w.mActivityRecord != null && mTargetActivityRecord == w.mActivityRecord)
- || isAnimatingTask(w.getTask()))
- && isTargetOverWallpaper() && w.isOnScreen();
- }
-
- /**
- * @return Whether to use the input consumer to override app input to route home/recents.
- */
- boolean shouldApplyInputConsumer(ActivityRecord activity) {
- // Only apply the input consumer if it is enabled, it is not the target (home/recents)
- // being revealed with the transition, and we are actively animating the app as a part of
- // the animation
- return mInputConsumerEnabled && activity != null
- && !isTargetApp(activity) && isAnimatingApp(activity);
- }
-
- boolean updateInputConsumerForApp(InputWindowHandle inputWindowHandle) {
- // Update the input consumer touchable region to match the target app main window
- final WindowState targetAppMainWindow = getTargetAppMainWindow();
- if (targetAppMainWindow != null) {
- targetAppMainWindow.getBounds(mTmpRect);
- inputWindowHandle.touchableRegion.set(mTmpRect);
- return true;
- }
- return false;
- }
-
- boolean isTargetApp(ActivityRecord activity) {
- return mTargetActivityRecord != null && activity == mTargetActivityRecord;
- }
-
- private boolean isTargetOverWallpaper() {
- if (mTargetActivityRecord == null) {
- return false;
- }
- return mTargetActivityRecord.windowsCanBeWallpaperTarget();
- }
-
- WindowState getTargetAppMainWindow() {
- if (mTargetActivityRecord == null) {
- return null;
- }
- return mTargetActivityRecord.findMainWindow();
- }
-
- DisplayArea getTargetAppDisplayArea() {
- if (mTargetActivityRecord == null) {
- return null;
- }
- return mTargetActivityRecord.getDisplayArea();
- }
-
- boolean isAnimatingTask(Task task) {
- for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- if (task == mPendingAnimations.get(i).mTask) {
- return true;
- }
- }
- return false;
- }
-
- boolean isAnimatingWallpaper(WallpaperWindowToken token) {
- for (int i = mPendingWallpaperAnimations.size() - 1; i >= 0; i--) {
- if (token == mPendingWallpaperAnimations.get(i).getToken()) {
- return true;
- }
- }
- return false;
- }
-
- private boolean isAnimatingApp(ActivityRecord activity) {
- for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- if (activity.isDescendantOf(mPendingAnimations.get(i).mTask)) {
- return true;
- }
- }
- return false;
- }
-
- boolean shouldIgnoreForAccessibility(WindowState windowState) {
- final Task task = windowState.getTask();
- return task != null && isAnimatingTask(task) && !isTargetApp(windowState.mActivityRecord);
- }
-
- /**
- * If the animation target ActivityRecord has a fixed rotation ({@link
- * WindowToken#hasFixedRotationTransform()}, the provided wallpaper will be rotated accordingly.
- *
- * This avoids any screen rotation animation when animating to the Recents view.
- */
- void linkFixedRotationTransformIfNeeded(@NonNull WindowToken wallpaper) {
- if (mTargetActivityRecord == null) {
- return;
- }
- wallpaper.linkFixedRotationTransform(mTargetActivityRecord);
- }
-
- @VisibleForTesting
- class TaskAnimationAdapter implements AnimationAdapter {
-
- private final Task mTask;
- private SurfaceControl mCapturedLeash;
- private OnAnimationFinishedCallback mCapturedFinishCallback;
- private @AnimationType int mLastAnimationType;
- private final boolean mIsRecentTaskInvisible;
- private RemoteAnimationTarget mTarget;
- private final Rect mBounds = new Rect();
- // The bounds of the target relative to its parent.
- private final Rect mLocalBounds = new Rect();
- // The final surface transaction when animation is finished.
- private PictureInPictureSurfaceTransaction mFinishTransaction;
- // An overlay used to mask the content as an app goes into PIP
- private SurfaceControl mFinishOverlay;
- // An overlay used for canceling the animation with a screenshot
- private SurfaceControl mSnapshotOverlay;
-
- TaskAnimationAdapter(Task task, boolean isRecentTaskInvisible) {
- mTask = task;
- mIsRecentTaskInvisible = isRecentTaskInvisible;
- mBounds.set(mTask.getBounds());
-
- mLocalBounds.set(mBounds);
- Point tmpPos = new Point();
- mTask.getRelativePosition(tmpPos);
- mLocalBounds.offsetTo(tmpPos.x, tmpPos.y);
- }
-
- /**
- * @param overrideTaskId overrides the target's taskId. It may differ from mTaskId and thus
- * can differ from taskInfo. This mismatch is needed, however, in
- * some cases where we are animating root tasks but need need leaf
- * ids for identification. If this is INVALID (-1), then mTaskId
- * will be used.
- * @param overrideMode overrides the target's mode. If this is -1, the mode will be
- * calculated relative to going to the target activity (ie. OPENING if
- * this is the target task, CLOSING otherwise).
- */
- RemoteAnimationTarget createRemoteAnimationTarget(int overrideTaskId, int overrideMode) {
- ActivityRecord topApp = mTask.getTopRealVisibleActivity();
- if (topApp == null) {
- topApp = mTask.getTopVisibleActivity();
- }
- final WindowState mainWindow = topApp != null
- ? topApp.findMainWindow()
- : null;
- if (mainWindow == null) {
- return null;
- }
- final Rect insets = mainWindow.getInsetsStateWithVisibilityOverride().calculateInsets(
- mBounds, Type.systemBars(), false /* ignoreVisibility */).toRect();
- InsetUtils.addInsets(insets, mainWindow.mActivityRecord.getLetterboxInsets());
- final int mode = overrideMode != MODE_UNKNOWN
- ? overrideMode
- : topApp.getActivityType() == mTargetActivityType
- ? MODE_OPENING
- : MODE_CLOSING;
- if (overrideTaskId < 0) {
- overrideTaskId = mTask.mTaskId;
- }
- mTarget = new RemoteAnimationTarget(overrideTaskId, mode, mCapturedLeash,
- !topApp.fillsParent(), new Rect(),
- insets, mTask.getPrefixOrderIndex(), new Point(mBounds.left, mBounds.top),
- mLocalBounds, mBounds, mTask.getWindowConfiguration(),
- mIsRecentTaskInvisible, null, null, mTask.getTaskInfo(),
- topApp.checkEnterPictureInPictureAppOpsState());
-
- final ActivityRecord topActivity = mTask.getTopNonFinishingActivity();
- if (topActivity != null && topActivity.mStartingData != null
- && topActivity.mStartingData.hasImeSurface()) {
- mTarget.setWillShowImeOnTarget(true);
- }
- return mTarget;
- }
-
- void setSnapshotOverlay(TaskSnapshot snapshot) {
- // Create a surface control for the snapshot and reparent it to the leash
- final HardwareBuffer buffer = snapshot.getHardwareBuffer();
- if (buffer == null) {
- return;
- }
-
- final SurfaceSession session = new SurfaceSession();
- mSnapshotOverlay = mService.mSurfaceControlFactory.apply(session)
- .setName("RecentTaskScreenshotSurface")
- .setCallsite("TaskAnimationAdapter.setSnapshotOverlay")
- .setFormat(buffer.getFormat())
- .setParent(mCapturedLeash)
- .setBLASTLayer()
- .build();
-
- final float scale = 1.0f * mTask.getBounds().width() / buffer.getWidth();
- mTask.getPendingTransaction()
- .setBuffer(mSnapshotOverlay, GraphicBuffer.createFromHardwareBuffer(buffer))
- .setColorSpace(mSnapshotOverlay, snapshot.getColorSpace())
- .setLayer(mSnapshotOverlay, Integer.MAX_VALUE)
- .setMatrix(mSnapshotOverlay, scale, 0, 0, scale)
- .show(mSnapshotOverlay)
- .apply();
- }
-
- void onRemove() {
- if (mSnapshotOverlay != null) {
- // Clean up the snapshot overlay if necessary
- mTask.getPendingTransaction()
- .remove(mSnapshotOverlay)
- .apply();
- mSnapshotOverlay = null;
- }
- mTask.setCanAffectSystemUiFlags(true);
- mCapturedFinishCallback.onAnimationFinished(mLastAnimationType, this);
- }
-
- void onCleanup() {
- final Transaction pendingTransaction = mTask.getPendingTransaction();
- if (mFinishTransaction != null) {
- // Reparent the overlay
- if (mFinishOverlay != null) {
- pendingTransaction.reparent(mFinishOverlay, mTask.mSurfaceControl);
- }
-
- // Transfer the transform from the leash to the task
- PictureInPictureSurfaceTransaction.apply(mFinishTransaction,
- mTask.mSurfaceControl, pendingTransaction);
- mTask.setLastRecentsAnimationTransaction(mFinishTransaction, mFinishOverlay);
- if (mDisplayContent.isFixedRotationLaunchingApp(mTargetActivityRecord)) {
- // The transaction is needed for position when rotating the display.
- mDisplayContent.mPinnedTaskController.setEnterPipTransaction(
- mFinishTransaction);
- }
- // In the case where we are transferring the transform to the task in preparation
- // for entering PIP, we disable the task being able to affect sysui flags otherwise
- // it may cause a flash
- if (mTask.getActivityType() != mTargetActivityType
- && mFinishTransaction.getShouldDisableCanAffectSystemUiFlags()) {
- mTask.setCanAffectSystemUiFlags(false);
- }
- mFinishTransaction = null;
- mFinishOverlay = null;
- pendingTransaction.apply();
- } else if (!mTask.isAttached()) {
- // Apply the task's pending transaction in case it is detached and its transaction
- // is not reachable.
- pendingTransaction.apply();
- }
- }
-
- @VisibleForTesting
- public SurfaceControl getSnapshotOverlay() {
- return mSnapshotOverlay;
- }
-
- @Override
- public boolean getShowWallpaper() {
- return false;
- }
-
- @Override
- public void startAnimation(SurfaceControl animationLeash, Transaction t,
- @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) {
- // Restore position and root task crop until client has a chance to modify it.
- t.setPosition(animationLeash, mLocalBounds.left, mLocalBounds.top);
- mTmpRect.set(mLocalBounds);
- mTmpRect.offsetTo(0, 0);
- t.setWindowCrop(animationLeash, mTmpRect);
- mCapturedLeash = animationLeash;
- mCapturedFinishCallback = finishCallback;
- mLastAnimationType = type;
- }
-
- @Override
- public void onAnimationCancelled(SurfaceControl animationLeash) {
- // Cancel the animation immediately if any single task animator is canceled
- cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "taskAnimationAdapterCanceled");
- }
-
- @Override
- public long getDurationHint() {
- return 0;
- }
-
- @Override
- public long getStatusBarTransitionsStartTime() {
- return SystemClock.uptimeMillis();
- }
-
- @Override
- public void dump(PrintWriter pw, String prefix) {
- pw.print(prefix); pw.println("task=" + mTask);
- if (mTarget != null) {
- pw.print(prefix); pw.println("Target:");
- mTarget.dump(pw, prefix + " ");
- } else {
- pw.print(prefix); pw.println("Target: null");
- }
- pw.println("mIsRecentTaskInvisible=" + mIsRecentTaskInvisible);
- pw.println("mLocalBounds=" + mLocalBounds);
- pw.println("mFinishTransaction=" + mFinishTransaction);
- pw.println("mBounds=" + mBounds);
- pw.println("mIsRecentTaskInvisible=" + mIsRecentTaskInvisible);
- }
-
- @Override
- public void dumpDebug(ProtoOutputStream proto) {
- final long token = proto.start(REMOTE);
- if (mTarget != null) {
- mTarget.dumpDebug(proto, TARGET);
- }
- proto.end(token);
- }
- }
-
- public void dump(PrintWriter pw, String prefix) {
- final String innerPrefix = prefix + " ";
- pw.print(prefix); pw.println(RecentsAnimationController.class.getSimpleName() + ":");
- pw.print(innerPrefix); pw.println("mPendingStart=" + mPendingStart);
- pw.print(innerPrefix); pw.println("mPendingAnimations=" + mPendingAnimations.size());
- pw.print(innerPrefix); pw.println("mCanceled=" + mCanceled);
- pw.print(innerPrefix); pw.println("mInputConsumerEnabled=" + mInputConsumerEnabled);
- pw.print(innerPrefix); pw.println("mTargetActivityRecord=" + mTargetActivityRecord);
- pw.print(innerPrefix); pw.println("isTargetOverWallpaper=" + isTargetOverWallpaper());
- pw.print(innerPrefix); pw.println("mRequestDeferCancelUntilNextTransition="
- + mRequestDeferCancelUntilNextTransition);
- pw.print(innerPrefix); pw.println("mCancelOnNextTransitionStart="
- + mCancelOnNextTransitionStart);
- pw.print(innerPrefix); pw.println("mCancelDeferredWithScreenshot="
- + mCancelDeferredWithScreenshot);
- pw.print(innerPrefix); pw.println("mPendingCancelWithScreenshotReorderMode="
- + mPendingCancelWithScreenshotReorderMode);
- }
-}
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 862f84d..a6d9659 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -804,12 +804,6 @@
checkAppTransitionReady(surfacePlacer);
- // Defer starting the recents animation until the wallpaper has drawn
- final RecentsAnimationController recentsAnimationController =
- mWmService.getRecentsAnimationController();
- if (recentsAnimationController != null) {
- recentsAnimationController.checkAnimationReady(defaultDisplay.mWallpaperController);
- }
mWmService.mAtmService.mBackNavigationController
.checkAnimationReady(defaultDisplay.mWallpaperController);
@@ -1471,9 +1465,6 @@
// Updates the extra information of the intent.
if (fromHomeKey) {
homeIntent.putExtra(WindowManagerPolicy.EXTRA_FROM_HOME_KEY, true);
- if (mWindowManager.getRecentsAnimationController() != null) {
- mWindowManager.getRecentsAnimationController().cancelAnimationForHomeStart();
- }
}
homeIntent.putExtra(WindowManagerPolicy.EXTRA_START_REASON, reason);
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 92813a8..d6bd55f 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -61,7 +61,6 @@
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LOCKTASK;
-import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS;
import static com.android.server.wm.ActivityRecord.State.PAUSED;
@@ -168,7 +167,6 @@
import android.view.RemoteAnimationAdapter;
import android.view.SurfaceControl;
import android.view.WindowManager;
-import android.view.WindowManager.TransitionOldType;
import android.window.ITaskOrganizer;
import android.window.PictureInPictureSurfaceTransaction;
import android.window.StartingWindowInfo;
@@ -499,6 +497,13 @@
*/
boolean mIsTrimmableFromRecents;
+ /**
+ * Bounds offset should be applied when calculating compatible configuration for apps targeting
+ * SDK level 34 or before.
+ */
+ int mOffsetXForInsets;
+ int mOffsetYForInsets;
+
private final AnimatingActivityRegistry mAnimatingActivityRegistry =
new AnimatingActivityRegistry();
@@ -1278,8 +1283,8 @@
EventLogTags.writeWmTaskMoved(mTaskId, getRootTaskId(), getDisplayId(), toTop ? 1 : 0,
position);
final TaskDisplayArea taskDisplayArea = getDisplayArea();
- if (taskDisplayArea != null && isLeafTask()) {
- taskDisplayArea.onLeafTaskMoved(this, toTop, toBottom);
+ if (taskDisplayArea != null) {
+ taskDisplayArea.onTaskMoved(this, toTop, toBottom);
}
if (isPersistable) {
mLastTimeMoved = System.currentTimeMillis();
@@ -2950,14 +2955,6 @@
if (isOrganized()) {
return false;
}
- // Don't animate while the task runs recents animation but only if we are in the mode
- // where we cancel with deferred screenshot, which means that the controller has
- // transformed the task.
- final RecentsAnimationController controller = mWmService.getRecentsAnimationController();
- if (controller != null && controller.isAnimatingTask(this)
- && controller.shouldDeferCancelUntilNextTransition()) {
- return false;
- }
return true;
}
@@ -3275,30 +3272,6 @@
}
@Override
- protected void applyAnimationUnchecked(WindowManager.LayoutParams lp, boolean enter,
- @TransitionOldType int transit, boolean isVoiceInteraction,
- @Nullable ArrayList<WindowContainer> sources) {
- final RecentsAnimationController control = mWmService.getRecentsAnimationController();
- if (control != null) {
- // We let the transition to be controlled by RecentsAnimation, and callback task's
- // RemoteAnimationTarget for remote runner to animate.
- if (enter && !isActivityTypeHomeOrRecents()) {
- ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS,
- "applyAnimationUnchecked, control: %s, task: %s, transit: %s",
- control, asTask(), AppTransition.appTransitionOldToString(transit));
- final int size = sources != null ? sources.size() : 0;
- control.addTaskToTargets(this, (type, anim) -> {
- for (int i = 0; i < size; ++i) {
- sources.get(i).onAnimationFinished(type, anim);
- }
- });
- }
- } else {
- super.applyAnimationUnchecked(lp, enter, transit, isVoiceInteraction, sources);
- }
- }
-
- @Override
void dump(PrintWriter pw, String prefix, boolean dumpAll) {
super.dump(pw, prefix, dumpAll);
mAnimatingActivityRegistry.dump(pw, "AnimatingApps:", prefix);
@@ -5874,6 +5847,10 @@
super.dumpInner(prefix, pw, dumpAll, dumpPackage);
if (mCreatedByOrganizer) {
pw.println(prefix + " mCreatedByOrganizer=true");
+ if (mOffsetXForInsets != 0 || mOffsetYForInsets != 0) {
+ pw.println(prefix + " mOffsetXForInsets=" + mOffsetXForInsets
+ + " mOffsetYForInsets=" + mOffsetYForInsets);
+ }
}
if (mLastNonFullscreenBounds != null) {
pw.print(prefix); pw.print(" mLastNonFullscreenBounds=");
@@ -6130,9 +6107,7 @@
if (canBeLaunchedOnDisplay(newParent.getDisplayId())) {
reparent(newParent, onTop ? POSITION_TOP : POSITION_BOTTOM);
- if (isLeafTask()) {
- newParent.onLeafTaskMoved(this, onTop, !onTop);
- }
+ newParent.onTaskMoved(this, onTop, !onTop);
} else {
Slog.w(TAG, "Task=" + this + " can't reparent to " + newParent);
}
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index d9e88e1..638e92f 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -37,6 +37,7 @@
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import android.annotation.ColorInt;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.WindowConfiguration;
@@ -142,13 +143,6 @@
* current focused root task.
*/
Task mLastFocusedRootTask;
- /**
- * All of the root tasks on this display. Order matters, topmost root task is in front of all
- * other root tasks, bottommost behind. Accessed directly by ActivityManager package classes.
- * Any calls changing the list should also call {@link #onRootTaskOrderChanged(Task)}.
- */
- private ArrayList<OnRootTaskOrderChangedListener> mRootTaskOrderChangedCallbacks =
- new ArrayList<>();
/**
* The task display area is removed from the system and we are just waiting for all activities
@@ -331,7 +325,6 @@
mAtmService.mTaskSupervisor.updateTopResumedActivityIfNeeded("addChildTask");
mAtmService.updateSleepIfNeededLocked();
- onRootTaskOrderChanged(task);
}
@Override
@@ -423,10 +416,6 @@
// Update the top resumed activity because the preferred top focusable task may be changed.
mAtmService.mTaskSupervisor.updateTopResumedActivityIfNeeded("positionChildTaskAt");
-
- if (mChildren.indexOf(child) != oldPosition) {
- onRootTaskOrderChanged(child);
- }
}
void onLeafTaskRemoved(int taskId) {
@@ -435,7 +424,19 @@
}
}
- void onLeafTaskMoved(Task t, boolean toTop, boolean toBottom) {
+ void onTaskMoved(@NonNull Task t, boolean toTop, boolean toBottom) {
+ if (toBottom && !t.isLeafTask()) {
+ // Return early when a non-leaf task moved to bottom, to prevent sending duplicated
+ // leaf task movement callback if the leaf task is moved along with its parent tasks.
+ // Unless, we also track the task id, like `mLastLeafTaskToFrontId`.
+ return;
+ }
+
+ final Task topLeafTask = t.getTopLeafTask();
+ onLeafTaskMoved(topLeafTask, toTop, toBottom);
+ }
+
+ void onLeafTaskMoved(@NonNull Task t, boolean toTop, boolean toBottom) {
if (toBottom) {
mAtmService.getTaskChangeNotificationController().notifyTaskMovedToBack(
t.getTaskInfo());
@@ -831,7 +832,6 @@
mLaunchAdjacentFlagRootTask = null;
}
mDisplayContent.releaseSelfIfNeeded();
- onRootTaskOrderChanged(rootTask);
}
/**
@@ -1730,35 +1730,6 @@
return mRemoved;
}
- /**
- * Adds a listener to be notified whenever the root task order in the display changes. Currently
- * only used by the {@link RecentsAnimation} to determine whether to interrupt and cancel the
- * current animation when the system state changes.
- */
- void registerRootTaskOrderChangedListener(OnRootTaskOrderChangedListener listener) {
- if (!mRootTaskOrderChangedCallbacks.contains(listener)) {
- mRootTaskOrderChangedCallbacks.add(listener);
- }
- }
-
- /**
- * Removes a previously registered root task order change listener.
- */
- void unregisterRootTaskOrderChangedListener(OnRootTaskOrderChangedListener listener) {
- mRootTaskOrderChangedCallbacks.remove(listener);
- }
-
- /**
- * Notifies of a root task order change
- *
- * @param rootTask The root task which triggered the order change
- */
- void onRootTaskOrderChanged(Task rootTask) {
- for (int i = mRootTaskOrderChangedCallbacks.size() - 1; i >= 0; i--) {
- mRootTaskOrderChangedCallbacks.get(i).onRootTaskOrderChanged(rootTask);
- }
- }
-
@Override
boolean canCreateRemoteAnimationTarget() {
// In the legacy transition system, promoting animation target from TaskFragment to
@@ -1773,13 +1744,6 @@
return mDisplayContent.isHomeSupported() && mCanHostHomeTask;
}
- /**
- * Callback for when the order of the root tasks in the display changes.
- */
- interface OnRootTaskOrderChangedListener {
- void onRootTaskOrderChanged(Task rootTask);
- }
-
void ensureActivitiesVisible(ActivityRecord starting, boolean notifyClients) {
mAtmService.mTaskSupervisor.beginActivityVisibilityUpdate();
try {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 2fbabc5..c8139fa 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2224,7 +2224,7 @@
static class ConfigOverrideHint {
@Nullable DisplayInfo mTmpOverrideDisplayInfo;
- @Nullable ActivityRecord.CompatDisplayInsets mTmpCompatInsets;
+ @Nullable AppCompatDisplayInsets mTmpCompatInsets;
@Nullable Rect mParentAppBoundsOverride;
int mTmpOverrideConfigOrientation;
boolean mUseOverrideInsetsForConfig;
@@ -2294,7 +2294,7 @@
void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
@NonNull Configuration parentConfig, @Nullable ConfigOverrideHint overrideHint) {
DisplayInfo overrideDisplayInfo = null;
- ActivityRecord.CompatDisplayInsets compatInsets = null;
+ AppCompatDisplayInsets compatInsets = null;
boolean useOverrideInsetsForConfig = false;
if (overrideHint != null) {
overrideDisplayInfo = overrideHint.mTmpOverrideDisplayInfo;
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 1d2b693..9bbf102 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -52,8 +52,8 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLog;
import com.android.internal.protolog.ProtoLogGroup;
+import com.android.internal.protolog.ProtoLog;
import com.android.server.FgThread;
import com.android.window.flags.Flags;
diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java
index f2615f7..f1941af 100644
--- a/services/core/java/com/android/server/wm/TransparentPolicy.java
+++ b/services/core/java/com/android/server/wm/TransparentPolicy.java
@@ -95,7 +95,7 @@
}
final boolean wasStarted = mTransparentPolicyState.isRunning();
mTransparentPolicyState.reset();
- // In case mActivityRecord.hasCompatDisplayInsetsWithoutOverride() we don't apply the
+ // In case mActivityRecord.hasAppCompatDisplayInsetsWithoutOverride() we don't apply the
// opaque activity constraints because we're expecting the activity is already letterboxed.
final ActivityRecord firstOpaqueActivity = mActivityRecord.getTask().getActivity(
FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE /* callback */,
@@ -159,12 +159,12 @@
return mTransparentPolicyState.mInheritedOrientation;
}
- ActivityRecord.CompatDisplayInsets getInheritedCompatDisplayInsets() {
- return mTransparentPolicyState.mInheritedCompatDisplayInsets;
+ AppCompatDisplayInsets getInheritedAppCompatDisplayInsets() {
+ return mTransparentPolicyState.mInheritedAppCompatDisplayInsets;
}
- void clearInheritedCompatDisplayInsets() {
- mTransparentPolicyState.clearInheritedCompatDisplayInsets();
+ void clearInheritedAppCompatDisplayInsets() {
+ mTransparentPolicyState.clearInheritedAppCompatDisplayInsets();
}
/**
@@ -201,8 +201,10 @@
// never has letterbox.
return true;
}
+ final AppCompatSizeCompatModePolicy scmPolicy = mActivityRecord.mAppCompatController
+ .getAppCompatSizeCompatModePolicy();
if (mActivityRecord.getTask() == null || mActivityRecord.fillsParent()
- || mActivityRecord.hasCompatDisplayInsetsWithoutInheritance()) {
+ || scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()) {
return true;
}
return false;
@@ -239,9 +241,9 @@
// The app compat state for the opaque activity if any
private int mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
- // The CompatDisplayInsets of the opaque activity beneath the translucent one.
+ // The AppCompatDisplayInsets of the opaque activity beneath the translucent one.
@Nullable
- private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets;
+ private AppCompatDisplayInsets mInheritedAppCompatDisplayInsets;
@Nullable
private ActivityRecord mFirstOpaqueActivity;
@@ -303,7 +305,7 @@
}
mInheritedOrientation = opaqueActivity.getRequestedConfigurationOrientation();
mInheritedAppCompatState = opaqueActivity.getAppCompatState();
- mInheritedCompatDisplayInsets = opaqueActivity.getCompatDisplayInsets();
+ mInheritedAppCompatDisplayInsets = opaqueActivity.getAppCompatDisplayInsets();
}
private void reset() {
@@ -315,7 +317,7 @@
mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
- mInheritedCompatDisplayInsets = null;
+ mInheritedAppCompatDisplayInsets = null;
if (mFirstOpaqueActivity != null) {
mFirstOpaqueActivity.mAppCompatController.getTransparentPolicy()
.mDestroyListeners.remove(mActivityRecord.mAppCompatController
@@ -340,8 +342,8 @@
|| !mActivityRecord.handlesOrientationChangeFromDescendant(orientation);
}
- private void clearInheritedCompatDisplayInsets() {
- mInheritedCompatDisplayInsets = null;
+ private void clearInheritedAppCompatDisplayInsets() {
+ mInheritedAppCompatDisplayInsets = null;
}
/**
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 4536f24..06010bb 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -172,8 +172,8 @@
&& animatingContainer.getAnimation() != null
&& animatingContainer.getAnimation().getShowWallpaper();
final boolean hasWallpaper = w.hasWallpaper() || animationWallpaper;
- if (isRecentsTransitionTarget(w) || isBackNavigationTarget(w)) {
- if (DEBUG_WALLPAPER) Slog.v(TAG, "Found recents animation wallpaper target: " + w);
+ if (isBackNavigationTarget(w)) {
+ if (DEBUG_WALLPAPER) Slog.v(TAG, "Found back animation wallpaper target: " + w);
mFindResults.setWallpaperTarget(w);
return true;
} else if (hasWallpaper
@@ -199,15 +199,6 @@
return false;
};
- private boolean isRecentsTransitionTarget(WindowState w) {
- if (w.mTransitionController.isShellTransitionsEnabled()) {
- return false;
- }
- // The window is either the recents activity or is in the task animating by the recents.
- final RecentsAnimationController controller = mService.getRecentsAnimationController();
- return controller != null && controller.isWallpaperVisible(w);
- }
-
private boolean isBackNavigationTarget(WindowState w) {
// The window is in animating by back navigation and set to show wallpaper.
return mService.mAtmService.mBackNavigationController.isWallpaperVisible(w);
@@ -928,12 +919,6 @@
Slog.v(TAG, "*** WALLPAPER DRAW TIMEOUT");
}
- // If there was a pending recents animation, start the animation anyways (it's better
- // to not see the wallpaper than for the animation to not start)
- if (mService.getRecentsAnimationController() != null) {
- mService.getRecentsAnimationController().startAnimation();
- }
-
// If there was a pending back navigation animation that would show wallpaper, start
// the animation due to it was skipped in previous surface placement.
mService.mAtmService.mBackNavigationController.startAnimation();
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index 384d111..89ad564 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -161,15 +161,7 @@
mDisplayContent.mWallpaperController.getWallpaperTarget();
if (visible && wallpaperTarget != null) {
- final RecentsAnimationController recentsAnimationController =
- mWmService.getRecentsAnimationController();
- if (recentsAnimationController != null
- && recentsAnimationController.isAnimatingTask(wallpaperTarget.getTask())) {
- // If the Recents animation is running, and the wallpaper target is the animating
- // task we want the wallpaper to be rotated in the same orientation as the
- // RecentsAnimation's target (e.g the launcher)
- recentsAnimationController.linkFixedRotationTransformIfNeeded(this);
- } else if ((wallpaperTarget.mActivityRecord == null
+ if ((wallpaperTarget.mActivityRecord == null
// Ignore invisible activity because it may be moving to background.
|| wallpaperTarget.mActivityRecord.isVisibleRequested())
&& wallpaperTarget.mToken.hasFixedRotationTransform()) {
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 9ae881b..a980b77 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -19,6 +19,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
@@ -2997,12 +2998,18 @@
// Make sure display isn't a part of the transition already - needed for legacy transitions.
if (mDisplayContent.inTransition()) return false;
- if (!ActivityTaskManagerService.isPip2ExperimentEnabled()) {
- // Screenshots are turned off when PiP is undergoing changes.
- return !inPinnedWindowingMode() && getParent() != null
- && !getParent().inPinnedWindowingMode();
- }
- return true;
+ // Screenshots are turned off when PiP is undergoing changes.
+ return ActivityTaskManagerService.isPip2ExperimentEnabled() || !isPipChange();
+ }
+
+ /** Returns true if WC is pinned and undergoing changes. */
+ private boolean isPipChange() {
+ final boolean isExitingPip = this.asTaskFragment() != null
+ && mTransitionController.getWindowingModeAtStart(this) == WINDOWING_MODE_PINNED
+ && !inPinnedWindowingMode();
+
+ return isExitingPip || inPinnedWindowingMode()
+ || (getParent() != null && getParent().inPinnedWindowingMode());
}
/**
diff --git a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java b/services/core/java/com/android/server/wm/WindowContainerThumbnail.java
index 57fc4c7..ad88062 100644
--- a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java
+++ b/services/core/java/com/android/server/wm/WindowContainerThumbnail.java
@@ -121,13 +121,6 @@
ANIMATION_TYPE_RECENTS);
}
- /**
- * Start animation with existing adapter.
- */
- void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden) {
- mSurfaceAnimator.startAnimation(t, anim, hidden, ANIMATION_TYPE_RECENTS);
- }
-
private void onAnimationFinished(@AnimationType int type, AnimationAdapter anim) {
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index d8df645..4b91e27 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -246,7 +246,6 @@
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
-import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.util.TimeUtils;
import android.util.TypedValue;
@@ -266,7 +265,6 @@
import android.view.IInputFilter;
import android.view.IOnKeyguardExitResult;
import android.view.IPinnedTaskListener;
-import android.view.IRecentsAnimationRunner;
import android.view.IRotationWatcher;
import android.view.IScrollCaptureResponseListener;
import android.view.ISystemGestureExclusionListener;
@@ -681,7 +679,6 @@
private final SparseIntArray mOrientationMapping = new SparseIntArray();
final AccessibilityController mAccessibilityController;
- private RecentsAnimationController mRecentsAnimationController;
Watermark mWatermark;
StrictModeFlash mStrictModeFlash;
@@ -1159,17 +1156,12 @@
return;
}
- // While running a recents animation, this will get called early because we show the
- // recents animation target activity immediately when the animation starts. Defer the
- // mLaunchTaskBehind updates until recents animation finishes.
- if (atoken.mLaunchTaskBehind && !isRecentsAnimationTarget(atoken)) {
+ if (atoken.mLaunchTaskBehind) {
mAtmService.mTaskSupervisor.scheduleLaunchTaskBehindComplete(atoken.token);
atoken.mLaunchTaskBehind = false;
} else {
atoken.updateReportedVisibilityLocked();
- // We should also defer sending the finished callback until the recents animation
- // successfully finishes.
- if (atoken.mEnteringAnimation && !isRecentsAnimationTarget(atoken)) {
+ if (atoken.mEnteringAnimation) {
atoken.mEnteringAnimation = false;
if (atoken.attachedToProcess()) {
try {
@@ -3168,7 +3160,7 @@
}
// TODO(multi-display): remove when no default display use case.
- // (i.e. KeyguardController / RecentsAnimation)
+ // (i.e. KeyguardController)
public void executeAppTransition() {
if (!checkCallingPermission(MANAGE_APP_TOKENS, "executeAppTransition()")) {
throw new SecurityException("Requires MANAGE_APP_TOKENS permission");
@@ -3176,57 +3168,6 @@
getDefaultDisplayContentLocked().executeAppTransition();
}
- void initializeRecentsAnimation(int targetActivityType,
- IRecentsAnimationRunner recentsAnimationRunner,
- RecentsAnimationController.RecentsAnimationCallbacks callbacks, int displayId,
- SparseBooleanArray recentTaskIds, ActivityRecord targetActivity) {
- mRecentsAnimationController = new RecentsAnimationController(this, recentsAnimationRunner,
- callbacks, displayId);
- mRoot.getDisplayContent(displayId).mAppTransition.updateBooster();
- mRecentsAnimationController.initialize(targetActivityType, recentTaskIds, targetActivity);
- }
-
- @VisibleForTesting
- void setRecentsAnimationController(RecentsAnimationController controller) {
- mRecentsAnimationController = controller;
- }
-
- RecentsAnimationController getRecentsAnimationController() {
- return mRecentsAnimationController;
- }
-
- void cancelRecentsAnimation(
- @RecentsAnimationController.ReorderMode int reorderMode, String reason) {
- if (mRecentsAnimationController != null) {
- // This call will call through to cleanupAnimation() below after the animation is
- // canceled
- mRecentsAnimationController.cancelAnimation(reorderMode, reason);
- }
- }
-
-
- void cleanupRecentsAnimation(@RecentsAnimationController.ReorderMode int reorderMode) {
- if (mRecentsAnimationController != null) {
- final RecentsAnimationController controller = mRecentsAnimationController;
- mRecentsAnimationController = null;
- controller.cleanupAnimation(reorderMode);
- // TODO(multi-display): currently only default display support recents animation.
- final DisplayContent dc = getDefaultDisplayContentLocked();
- if (dc.mAppTransition.isTransitionSet()) {
- dc.mSkipAppTransitionAnimation = true;
- }
- dc.forAllWindowContainers((wc) -> {
- if (wc.isAnimating(TRANSITION, ANIMATION_TYPE_APP_TRANSITION)) {
- wc.cancelAnimation();
- }
- });
- }
- }
-
- boolean isRecentsAnimationTarget(ActivityRecord r) {
- return mRecentsAnimationController != null && mRecentsAnimationController.isTargetApp(r);
- }
-
boolean isValidPictureInPictureAspectRatio(DisplayContent displayContent, float aspectRatio) {
return displayContent.getPinnedTaskController().isValidPictureInPictureAspectRatio(
aspectRatio);
@@ -3258,11 +3199,6 @@
}
@Override
- public void triggerAnimationFailsafe() {
- mH.sendEmptyMessage(H.ANIMATION_FAILSAFE);
- }
-
- @Override
public void onKeyguardShowingAndNotOccludedChanged() {
mH.sendEmptyMessage(H.RECOMPUTE_FOCUS);
dispatchKeyguardLockedState();
@@ -5652,7 +5588,6 @@
public static final int UPDATE_ANIMATION_SCALE = 51;
public static final int WINDOW_HIDE_TIMEOUT = 52;
public static final int SET_HAS_OVERLAY_UI = 58;
- public static final int ANIMATION_FAILSAFE = 60;
public static final int RECOMPUTE_FOCUS = 61;
public static final int ON_POINTER_DOWN_OUTSIDE_FOCUS = 62;
public static final int WINDOW_STATE_BLAST_SYNC_TIMEOUT = 64;
@@ -5887,14 +5822,6 @@
mAmInternal.setHasOverlayUi(msg.arg1, msg.arg2 == 1);
break;
}
- case ANIMATION_FAILSAFE: {
- synchronized (mGlobalLock) {
- if (mRecentsAnimationController != null) {
- mRecentsAnimationController.scheduleFailsafe();
- }
- }
- break;
- }
case RECOMPUTE_FOCUS: {
synchronized (mGlobalLock) {
updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL,
@@ -7036,10 +6963,6 @@
pw.print(" window="); pw.print(mWindowAnimationScaleSetting);
pw.print(" transition="); pw.print(mTransitionAnimationScaleSetting);
pw.print(" animator="); pw.println(mAnimatorDurationScaleSetting);
- if (mRecentsAnimationController != null) {
- pw.print(" mRecentsAnimationController="); pw.println(mRecentsAnimationController);
- mRecentsAnimationController.dump(pw, " ");
- }
}
}
@@ -9075,17 +8998,6 @@
}
clearPointerDownOutsideFocusRunnable();
- if (mRecentsAnimationController != null
- && mRecentsAnimationController.getTargetAppMainWindow() == t) {
- // If there is an active recents animation and touched window is the target,
- // then ignore the touch. The target already handles touches using its own
- // input monitor and we don't want to trigger any lifecycle changes from
- // focusing another window.
- // TODO(b/186770026): We should remove this once we support multiple resumed
- // activities while in overview
- return;
- }
-
final WindowState w = t.getWindowState();
if (w != null) {
final Task task = w.getTask();
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 9d40b16..e1e64ee 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -18,9 +18,12 @@
import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
import static android.app.ActivityManager.isStartResultSuccessful;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
+import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
+import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION;
import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS;
@@ -818,6 +821,31 @@
final Configuration c =
new Configuration(container.getRequestedOverrideConfiguration());
c.setTo(change.getConfiguration(), configMask, windowMask);
+ if (container.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW
+ && (change.getConfigSetMask() & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) {
+ // Special handling for split screen window got offset. The insets calculation
+ // for configuration should be stable regardless of the offset. Set offset to
+ // the task level to be applied when calculate compat override for apps
+ // targeting SDK level 34 or before.
+ final Task task = container.asTask();
+ if (task != null) {
+ if (c.screenWidthDp != SCREEN_WIDTH_DP_UNDEFINED
+ && c.screenHeightDp != SCREEN_HEIGHT_DP_UNDEFINED) {
+ final Rect oldBounds = container.getRequestedOverrideBounds();
+ final Rect newBounds =
+ change.getConfiguration().windowConfiguration.getBounds();
+ if (oldBounds.width() == newBounds.width()
+ && oldBounds.height() == newBounds.height()) {
+ task.mOffsetXForInsets = oldBounds.left - newBounds.left;
+ task.mOffsetYForInsets = oldBounds.top - newBounds.top;
+ } else {
+ task.mOffsetXForInsets = task.mOffsetYForInsets = 0;
+ }
+ } else {
+ task.mOffsetXForInsets = task.mOffsetYForInsets = 0;
+ }
+ }
+ }
container.onRequestedOverrideConfigurationChanged(c);
}
effects |= TRANSACT_EFFECTS_CLIENT_CONFIG;
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 2bae0a8..d96ebc6 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -284,14 +284,11 @@
static final int ANIMATING_REASON_REMOTE_ANIMATION = 1;
/** It is set for wakefulness transition. */
static final int ANIMATING_REASON_WAKEFULNESS_CHANGE = 1 << 1;
- /** Whether the legacy {@link RecentsAnimation} is running. */
- static final int ANIMATING_REASON_LEGACY_RECENT_ANIMATION = 1 << 2;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
ANIMATING_REASON_REMOTE_ANIMATION,
ANIMATING_REASON_WAKEFULNESS_CHANGE,
- ANIMATING_REASON_LEGACY_RECENT_ANIMATION,
})
@interface AnimatingReason {}
@@ -1689,7 +1686,8 @@
resolvedConfig,
false /* optsOutEdgeToEdge */,
false /* hasFixedRotationTransform */,
- false /* hasCompatDisplayInsets */);
+ false /* hasCompatDisplayInsets */,
+ null /* task */);
}
void dispatchConfiguration(@NonNull Configuration config) {
@@ -2016,14 +2014,6 @@
return mStoppedState == STOPPED_STATE_FIRST_LAUNCH;
}
- void setRunningRecentsAnimation(boolean running) {
- if (running) {
- addAnimatingReason(ANIMATING_REASON_LEGACY_RECENT_ANIMATION);
- } else {
- removeAnimatingReason(ANIMATING_REASON_LEGACY_RECENT_ANIMATION);
- }
- }
-
void setRunningRemoteAnimation(boolean running) {
if (running) {
addAnimatingReason(ANIMATING_REASON_REMOTE_ANIMATION);
@@ -2118,9 +2108,6 @@
if ((animatingReasons & ANIMATING_REASON_WAKEFULNESS_CHANGE) != 0) {
pw.print("wakefulness|");
}
- if ((animatingReasons & ANIMATING_REASON_LEGACY_RECENT_ANIMATION) != 0) {
- pw.print("legacy-recents");
- }
pw.println();
}
if (mUseFifoUiScheduling) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 4568f2e..81bce18 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1971,13 +1971,9 @@
* it must be drawn before allDrawn can become true.
*/
boolean isInteresting() {
- final RecentsAnimationController recentsAnimationController =
- mWmService.getRecentsAnimationController();
return mActivityRecord != null
&& (!mActivityRecord.isFreezingScreen() || !mAppFreezing)
- && mViewVisibility == View.VISIBLE
- && (recentsAnimationController == null
- || recentsAnimationController.isInterestingForAllDrawn(this));
+ && mViewVisibility == View.VISIBLE;
}
/**
@@ -3002,7 +2998,8 @@
resolvedConfig,
(mAttrs.privateFlags & PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE) != 0,
false /* hasFixedRotationTransform */,
- false /* hasCompatDisplayInsets */);
+ false /* hasCompatDisplayInsets */,
+ null /* task */);
}
/**
diff --git a/services/core/java/com/android/server/wm/WindowTracingDataSource.java b/services/core/java/com/android/server/wm/WindowTracingDataSource.java
index 6984f0d..73ecbb4 100644
--- a/services/core/java/com/android/server/wm/WindowTracingDataSource.java
+++ b/services/core/java/com/android/server/wm/WindowTracingDataSource.java
@@ -33,8 +33,8 @@
import android.util.proto.ProtoInputStream;
import java.io.IOException;
+import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
public final class WindowTracingDataSource extends DataSource<WindowTracingDataSource.Instance,
WindowTracingDataSource.TlsState, Void> {
@@ -76,15 +76,11 @@
private static final String TAG = "WindowTracingDataSource";
@NonNull
- private final Consumer<Config> mOnStartCallback;
- @NonNull
- private final Consumer<Config> mOnStopCallback;
+ private final WeakReference<WindowTracingPerfetto> mWindowTracing;
- public WindowTracingDataSource(@NonNull Consumer<Config> onStart,
- @NonNull Consumer<Config> onStop) {
+ public WindowTracingDataSource(WindowTracingPerfetto windowTracing) {
super(DATA_SOURCE_NAME);
- mOnStartCallback = onStart;
- mOnStopCallback = onStop;
+ mWindowTracing = new WeakReference<>(windowTracing);
Producer.init(InitArguments.DEFAULTS);
DataSourceParams params =
@@ -102,12 +98,18 @@
return new Instance(this, instanceIndex, config != null ? config : CONFIG_DEFAULT) {
@Override
protected void onStart(StartCallbackArguments args) {
- mOnStartCallback.accept(mConfig);
+ WindowTracingPerfetto windowTracing = mWindowTracing.get();
+ if (windowTracing != null) {
+ windowTracing.onStart(mConfig);
+ }
}
@Override
protected void onStop(StopCallbackArguments args) {
- mOnStopCallback.accept(mConfig);
+ WindowTracingPerfetto windowTracing = mWindowTracing.get();
+ if (windowTracing != null) {
+ windowTracing.onStop(mConfig);
+ }
}
};
}
diff --git a/services/core/java/com/android/server/wm/WindowTracingPerfetto.java b/services/core/java/com/android/server/wm/WindowTracingPerfetto.java
index cf948ca..22d6c86 100644
--- a/services/core/java/com/android/server/wm/WindowTracingPerfetto.java
+++ b/services/core/java/com/android/server/wm/WindowTracingPerfetto.java
@@ -35,8 +35,7 @@
private final AtomicInteger mCountSessionsOnFrame = new AtomicInteger();
private final AtomicInteger mCountSessionsOnTransaction = new AtomicInteger();
- private final WindowTracingDataSource mDataSource = new WindowTracingDataSource(
- this::onStart, this::onStop);
+ private final WindowTracingDataSource mDataSource = new WindowTracingDataSource(this);
WindowTracingPerfetto(WindowManagerService service, Choreographer choreographer) {
this(service, choreographer, service.mGlobalLock);
@@ -156,7 +155,7 @@
return mCountSessionsOnTransaction.get() > 0;
}
- private void onStart(WindowTracingDataSource.Config config) {
+ void onStart(WindowTracingDataSource.Config config) {
if (config.mLogFrequency == WindowTracingLogFrequency.FRAME) {
mCountSessionsOnFrame.incrementAndGet();
} else if (config.mLogFrequency == WindowTracingLogFrequency.TRANSACTION) {
@@ -168,7 +167,7 @@
log(WHERE_START_TRACING);
}
- private void onStop(WindowTracingDataSource.Config config) {
+ void onStop(WindowTracingDataSource.Config config) {
if (config.mLogFrequency == WindowTracingLogFrequency.FRAME) {
mCountSessionsOnFrame.decrementAndGet();
} else if (config.mLogFrequency == WindowTracingLogFrequency.TRANSACTION) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index d5013517..1290fb7 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -23501,6 +23501,8 @@
MANAGE_DEVICE_POLICY_ACROSS_USERS);
CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_WIPE_DATA,
MANAGE_DEVICE_POLICY_ACROSS_USERS);
+ CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_CONTENT_PROTECTION,
+ MANAGE_DEVICE_POLICY_ACROSS_USERS);
// These permissions may grant access to user data and therefore must be protected with
// MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL for cross-user calls.
@@ -24136,15 +24138,13 @@
@Override
public @ContentProtectionPolicy int getContentProtectionPolicy(
- ComponentName who, String callerPackageName) {
+ ComponentName who, String callerPackageName, int userId) {
if (!android.view.contentprotection.flags.Flags.manageDevicePolicyEnabled()) {
return CONTENT_PROTECTION_DISABLED;
}
CallerIdentity caller = getCallerIdentity(who, callerPackageName);
- int userId = caller.getUserId();
enforceCanQuery(MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, callerPackageName, userId);
-
Integer policy =
mDevicePolicyEngine.getResolvedPolicy(PolicyDefinition.CONTENT_PROTECTION, userId);
if (policy == null) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index bbf2ecb..026fcc4 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -2080,6 +2080,31 @@
}
/**
+ * Tests that the DisplayInfo is updated correctly with a render frame rate even if it not
+ * a divisor of the peak refresh rate.
+ */
+ @Test
+ public void testDisplayInfoRenderFrameRateNonPeakDivisor() {
+ DisplayManagerService displayManager =
+ new DisplayManagerService(mContext, mShortMockedInjector);
+ DisplayManagerService.BinderService displayManagerBinderService =
+ displayManager.new BinderService();
+ registerDefaultDisplays(displayManager);
+ displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+ FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+ new float[]{120f}, new float[]{240f});
+ int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+ displayDevice);
+ DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+ assertEquals(120f, displayInfo.getRefreshRate(), 0.01f);
+
+ updateRenderFrameRate(displayManager, displayDevice, 80f);
+ displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+ assertEquals(80f, displayInfo.getRefreshRate(), 0.01f);
+ }
+
+ /**
* Tests that the mode reflects the render frame rate is in compat mode
*/
@Test
@@ -3348,13 +3373,26 @@
}
private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager,
-
float[] refreshRates) {
return createFakeDisplayDevice(displayManager, refreshRates, Display.TYPE_UNKNOWN);
}
private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager,
float[] refreshRates,
+ float[] vsyncRates) {
+ return createFakeDisplayDevice(displayManager, refreshRates, vsyncRates,
+ Display.TYPE_UNKNOWN);
+ }
+
+ private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager,
+ float[] refreshRates,
+ int displayType) {
+ return createFakeDisplayDevice(displayManager, refreshRates, refreshRates, displayType);
+ }
+
+ private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager,
+ float[] refreshRates,
+ float[] vsyncRates,
int displayType) {
FakeDisplayDevice displayDevice = new FakeDisplayDevice();
DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
@@ -3363,7 +3401,8 @@
displayDeviceInfo.supportedModes = new Display.Mode[refreshRates.length];
for (int i = 0; i < refreshRates.length; i++) {
displayDeviceInfo.supportedModes[i] =
- new Display.Mode(i + 1, width, height, refreshRates[i]);
+ new Display.Mode(i + 1, width, height, refreshRates[i], vsyncRates[i],
+ new float[0], new int[0]);
}
displayDeviceInfo.modeId = 1;
displayDeviceInfo.type = displayType;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 5840cb9..2166cb7 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -2213,6 +2213,20 @@
/* ignoreAnimationLimits= */ anyBoolean());
}
+ @Test
+ public void testManualBrightnessModeSavesBrightness() {
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Initialize
+
+ Settings.System.putInt(mContext.getContentResolver(),
+ Settings.System.SCREEN_BRIGHTNESS_MODE,
+ Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
+ advanceTime(1);
+
+ verify(mHolder.brightnessSetting).saveIfNeeded();
+ }
+
/**
* Creates a mock and registers it to {@link LocalServices}.
*/
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
index e610a32..809e13c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
@@ -28,8 +28,8 @@
import static android.app.ActivityManager.PROCESS_STATE_TOP;
import static android.app.ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND;
import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
-import static android.os.PowerExemptionManager.REASON_DENIED;
import static android.content.ContentResolver.SCHEME_CONTENT;
+import static android.os.PowerExemptionManager.REASON_DENIED;
import static android.os.UserHandle.USER_ALL;
import static android.util.DebugUtils.valueToString;
@@ -68,11 +68,9 @@
import static org.mockito.Mockito.after;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -84,7 +82,6 @@
import android.app.BackgroundStartPrivileges;
import android.app.BroadcastOptions;
import android.app.ForegroundServiceDelegationOptions;
-import android.app.IApplicationThread;
import android.app.IUidObserver;
import android.app.Notification;
import android.app.NotificationChannel;
@@ -129,7 +126,7 @@
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.sdksandbox.flags.Flags;
import com.android.server.LocalServices;
-import com.android.server.am.ActivityManagerService.StickyBroadcast;
+import com.android.server.am.BroadcastController.StickyBroadcast;
import com.android.server.am.ProcessList.IsolatedUidRange;
import com.android.server.am.ProcessList.IsolatedUidRangeAllocator;
import com.android.server.am.UidObserverController.ChangeRecord;
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java
index ee96c2a..3dd2f24a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java
@@ -22,6 +22,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -42,6 +43,8 @@
import android.platform.test.annotations.Presubmit;
import android.text.TextUtils;
+import com.android.internal.os.Clock;
+import com.android.internal.os.MonotonicClock;
import com.android.server.LocalServices;
import com.android.server.ServiceThread;
import com.android.server.appop.AppOpsService;
@@ -121,11 +124,18 @@
LocalServices.removeServiceForTest(PackageManagerInternal.class);
LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
+ mAppStartInfoTracker.mMonotonicClock = new MonotonicClock(
+ Clock.SYSTEM_CLOCK.elapsedRealtime(), Clock.SYSTEM_CLOCK);
mAppStartInfoTracker.clearProcessStartInfo(true);
mAppStartInfoTracker.mAppStartInfoLoaded.set(true);
mAppStartInfoTracker.mAppStartInfoHistoryListSize =
mAppStartInfoTracker.APP_START_INFO_HISTORY_LIST_SIZE;
doNothing().when(mAppStartInfoTracker).schedulePersistProcessStartInfo(anyBoolean());
+
+ mAppStartInfoTracker.mProcStartStoreDir = new File(mContext.getFilesDir(),
+ AppStartInfoTracker.APP_START_STORE_DIR);
+ mAppStartInfoTracker.mProcStartInfoFile = new File(mAppStartInfoTracker.mProcStartStoreDir,
+ AppStartInfoTracker.APP_START_INFO_FILE);
}
@After
@@ -135,11 +145,8 @@
@Test
public void testApplicationStartInfo() throws Exception {
- mAppStartInfoTracker.mProcStartStoreDir = new File(mContext.getFilesDir(),
- AppStartInfoTracker.APP_START_STORE_DIR);
+ // Make sure we can write to the file.
assertTrue(FileUtils.createDir(mAppStartInfoTracker.mProcStartStoreDir));
- mAppStartInfoTracker.mProcStartInfoFile = new File(mAppStartInfoTracker.mProcStartStoreDir,
- AppStartInfoTracker.APP_START_INFO_FILE);
final long appStartTimestampIntentStarted = 1000000;
final long appStartTimestampActivityLaunchFinished = 2000000;
@@ -482,6 +489,79 @@
verifyInProgressRecordsSize(AppStartInfoTracker.MAX_IN_PROGRESS_RECORDS);
}
+ /**
+ * Test to make sure that records are returned in correct order, from most recently added at
+ * index 0 to least recently added at index size - 1.
+ */
+ @Test
+ public void testHistoricalRecordsOrdering() throws Exception {
+ // Clear old records
+ mAppStartInfoTracker.clearProcessStartInfo(false);
+
+ // Add some records with timestamps 0 decreasing as clock increases.
+ ProcessRecord app = makeProcessRecord(
+ APP_1_PID_1, // pid
+ APP_1_UID, // uid
+ APP_1_UID, // packageUid
+ null, // definingUid
+ APP_1_PROCESS_NAME, // processName
+ APP_1_PACKAGE_NAME); // packageName
+
+ mAppStartInfoTracker.handleProcessBroadcastStart(3, app, buildIntent(COMPONENT),
+ false /* isAlarm */);
+ mAppStartInfoTracker.handleProcessBroadcastStart(2, app, buildIntent(COMPONENT),
+ false /* isAlarm */);
+ mAppStartInfoTracker.handleProcessBroadcastStart(1, app, buildIntent(COMPONENT),
+ false /* isAlarm */);
+
+ // Get records
+ ArrayList<ApplicationStartInfo> list = new ArrayList<ApplicationStartInfo>();
+ mAppStartInfoTracker.getStartInfo(null, APP_1_UID, 0, 0, list);
+
+ // Confirm that records are in correct order, with index 0 representing the most recently
+ // added record and index size - 1 representing the least recently added one.
+ assertEquals(3, list.size());
+ assertEquals(1L, list.get(0).getStartupTimestamps().get(0).longValue());
+ assertEquals(2L, list.get(1).getStartupTimestamps().get(0).longValue());
+ assertEquals(3L, list.get(2).getStartupTimestamps().get(0).longValue());
+ }
+
+ /**
+ * Test to make sure that persist and restore correctly maintains the state of the monotonic
+ * clock.
+ */
+ @Test
+ public void testPersistAndRestoreMonotonicClock() {
+ // Make sure we can write to the file.
+ assertTrue(FileUtils.createDir(mAppStartInfoTracker.mProcStartStoreDir));
+
+ // No need to persist records for this test, clear any that may be there.
+ mAppStartInfoTracker.clearProcessStartInfo(false);
+
+ // Set clock with an arbitrary 5 minute offset, just needs to be longer than it would take
+ // for code to run.
+ mAppStartInfoTracker.mMonotonicClock = new MonotonicClock(5 * 60 * 1000,
+ Clock.SYSTEM_CLOCK);
+
+ // Record the current time.
+ long originalMonotonicTime = mAppStartInfoTracker.mMonotonicClock.monotonicTime();
+
+ // Now persist the process start info. Records were cleared above so this should just
+ // persist the monotonic time.
+ mAppStartInfoTracker.persistProcessStartInfo();
+
+ // Null out the clock to make sure its set on load.
+ mAppStartInfoTracker.mMonotonicClock = null;
+ assertNull(mAppStartInfoTracker.mMonotonicClock);
+
+ // Now load from disk.
+ mAppStartInfoTracker.loadExistingProcessStartInfo();
+
+ // Confirm clock has been set and that its current time is greater than the previous one.
+ assertNotNull(mAppStartInfoTracker.mMonotonicClock);
+ assertTrue(mAppStartInfoTracker.mMonotonicClock.monotonicTime() > originalMonotonicTime);
+ }
+
private static <T> void setFieldValue(Class clazz, Object obj, String fieldName, T val) {
try {
Field field = clazz.getDeclaredField(fieldName);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 0ba74c6..100b548 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -1176,6 +1176,17 @@
verifyPendingRecords(greenQueue, List.of(screenOff, screenOn));
verifyPendingRecords(redQueue, List.of(screenOff));
verifyPendingRecords(blueQueue, List.of(screenOff, screenOn));
+
+ final BroadcastRecord screenOffRecord = makeBroadcastRecord(screenOff, screenOnOffOptions,
+ List.of(greenReceiver, redReceiver, blueReceiver), false);
+ screenOffRecord.setDeliveryState(2, BroadcastRecord.DELIVERY_DEFERRED,
+ "testDeliveryGroupPolicy_prioritized_diffReceivers");
+ mImpl.enqueueBroadcastLocked(screenOffRecord);
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+ List.of(greenReceiver, blueReceiver), false));
+ verifyPendingRecords(greenQueue, List.of(screenOff, screenOn));
+ verifyPendingRecords(redQueue, List.of(screenOff));
+ verifyPendingRecords(blueQueue, List.of(screenOn));
}
/**
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 37d87c4e..1cba3c5 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -52,6 +52,7 @@
import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.content.pm.UserInfo;
+import android.content.res.Resources;
import android.multiuser.Flags;
import android.os.PowerManager;
import android.os.ServiceSpecificException;
@@ -152,6 +153,7 @@
private File mTestDir;
private Context mSpiedContext;
+ private Resources mSpyResources;
private @Mock PackageManagerService mMockPms;
private @Mock UserDataPreparer mMockUserDataPreparer;
@@ -193,6 +195,13 @@
doNothing().when(mSpiedContext).sendBroadcastAsUser(any(), any(), any());
mockIsLowRamDevice(false);
+ // Called when getting boot user. config_bootToHeadlessSystemUser is false by default.
+ mSpyResources = spy(mSpiedContext.getResources());
+ when(mSpiedContext.getResources()).thenReturn(mSpyResources);
+ doReturn(false)
+ .when(mSpyResources)
+ .getBoolean(com.android.internal.R.bool.config_bootToHeadlessSystemUser);
+
// Must construct UserManagerService in the UiThread
mTestDir = new File(mRealContext.getDataDir(), "umstest");
mTestDir.mkdirs();
@@ -849,6 +858,16 @@
USER_TYPE_PROFILE_PRIVATE, 0, mainUser, null));
}
+ @Test
+ public void testGetBootUser_enableBootToHeadlessSystemUser() {
+ setSystemUserHeadless(true);
+ doReturn(true)
+ .when(mSpyResources)
+ .getBoolean(com.android.internal.R.bool.config_bootToHeadlessSystemUser);
+
+ assertThat(mUms.getBootUser()).isEqualTo(UserHandle.USER_SYSTEM);
+ }
+
/**
* Returns true if the user's XML file has Default restrictions
* @param userId Id of the user.
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
index bbab0ee..7b635d4 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
@@ -38,6 +38,7 @@
import android.hardware.power.stats.StateResidencyResult;
import android.os.Handler;
import android.os.Looper;
+import android.os.connectivity.WifiActivityEnergyInfo;
import android.platform.test.ravenwood.RavenwoodRule;
import android.power.PowerStatsInternal;
import android.util.IntArray;
@@ -88,6 +89,33 @@
}
@Test
+ public void testUpdateWifiState() {
+ WifiActivityEnergyInfo firstInfo = new WifiActivityEnergyInfo(1111,
+ WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 11, 22, 33, 44);
+
+ WifiActivityEnergyInfo delta = mBatteryExternalStatsWorker.extractDeltaLocked(firstInfo);
+
+ assertEquals(1111, delta.getTimeSinceBootMillis());
+ assertEquals(WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, delta.getStackState());
+ assertEquals(0, delta.getControllerTxDurationMillis());
+ assertEquals(0, delta.getControllerRxDurationMillis());
+ assertEquals(0, delta.getControllerScanDurationMillis());
+ assertEquals(0, delta.getControllerIdleDurationMillis());
+
+ WifiActivityEnergyInfo secondInfo = new WifiActivityEnergyInfo(91111,
+ WifiActivityEnergyInfo.STACK_STATE_STATE_IDLE, 811, 722, 633, 544);
+
+ delta = mBatteryExternalStatsWorker.extractDeltaLocked(secondInfo);
+
+ assertEquals(91111, delta.getTimeSinceBootMillis());
+ assertEquals(WifiActivityEnergyInfo.STACK_STATE_STATE_IDLE, delta.getStackState());
+ assertEquals(800, delta.getControllerTxDurationMillis());
+ assertEquals(700, delta.getControllerRxDurationMillis());
+ assertEquals(600, delta.getControllerScanDurationMillis());
+ assertEquals(500, delta.getControllerIdleDurationMillis());
+ }
+
+ @Test
public void testTargetedEnergyConsumerQuerying() {
final int numCpuClusters = 4;
final int numDisplays = 5;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
index 9083a1e..1904145 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
@@ -1319,7 +1319,6 @@
final AccessibilityWindow window = Mockito.mock(AccessibilityWindow.class);
when(window.getWindowInfo()).thenReturn(windowInfo);
- when(window.ignoreRecentsAnimationForAccessibility()).thenReturn(false);
when(window.isFocused()).thenAnswer(invocation -> windowInfo.focused);
when(window.isTouchable()).thenReturn(true);
when(window.getType()).thenReturn(windowInfo.type);
diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
index e45ab31..beed0a3d 100644
--- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
@@ -111,6 +111,9 @@
private static final AudioDeviceAttributes DEVICE_SPEAKER_OUT = new AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "");
+ /** Choose a default stream volume value which does not depend on min/max. */
+ private static final int DEFAULT_STREAM_VOLUME = 2;
+
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@@ -144,6 +147,8 @@
private TestLooper mTestLooper;
+ private boolean mIsAutomotive;
+
public static final int[] BASIC_VOLUME_BEHAVIORS = {
AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE,
AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL,
@@ -232,9 +237,10 @@
|| packageManager.hasSystemFeature(PackageManager.FEATURE_TELEVISION));
final boolean isSingleVolume = mContext.getResources().getBoolean(
Resources.getSystem().getIdentifier("config_single_volume", "bool", "android"));
- final boolean automotiveHardened = mContext.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_AUTOMOTIVE) && autoPublicVolumeApiHardening();
- assumeFalse("Skipping test for fixed, TV, single volume and auto devices",
+ mIsAutomotive = mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_AUTOMOTIVE);
+ final boolean automotiveHardened = mIsAutomotive && autoPublicVolumeApiHardening();
+ assumeFalse("Skipping test for fixed, TV, single volume and auto hardened devices",
useFixedVolume || isTelevision || isSingleVolume || automotiveHardened);
InstrumentationRegistry.getInstrumentation().getUiAutomation()
@@ -249,15 +255,14 @@
{STREAM_MUSIC, STREAM_NOTIFICATION, STREAM_RING, STREAM_ALARM, STREAM_SYSTEM,
STREAM_VOICE_CALL, STREAM_ACCESSIBILITY};
for (int streamType : usedStreamTypes) {
- final int streamVolume = (mAm.getStreamMinVolume(streamType) + mAm.getStreamMaxVolume(
- streamType)) / 2;
-
- mAudioService.setStreamVolume(streamType, streamVolume, /*flags=*/0,
+ mAudioService.setStreamVolume(streamType, DEFAULT_STREAM_VOLUME, /*flags=*/0,
mContext.getOpPackageName());
}
- mAudioService.setRingerModeInternal(RINGER_MODE_NORMAL, mContext.getOpPackageName());
- mAudioService.setRingerModeExternal(RINGER_MODE_NORMAL, mContext.getOpPackageName());
+ if (!mIsAutomotive) {
+ mAudioService.setRingerModeInternal(RINGER_MODE_NORMAL, mContext.getOpPackageName());
+ mAudioService.setRingerModeExternal(RINGER_MODE_NORMAL, mContext.getOpPackageName());
+ }
}
private AudioVolumeGroup getStreamTypeVolumeGroup(int streamType) {
@@ -297,6 +302,7 @@
@Test
public void setStreamRingVolume0_setsRingerModeVibrate() throws Exception {
+ assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive);
mAudioService.setStreamVolume(STREAM_RING, 0, /*flags=*/0,
mContext.getOpPackageName());
mTestLooper.dispatchAll();
@@ -462,6 +468,7 @@
@Test
public void flagAllowRingerModes_onSystemStreams_changesMode() throws Exception {
+ assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive);
mAudioService.setStreamVolume(STREAM_SYSTEM,
mAudioService.getStreamMinVolume(STREAM_SYSTEM), /*flags=*/0,
mContext.getOpPackageName());
@@ -476,6 +483,7 @@
@Test
public void flagAllowRingerModesAbsent_onNonSystemStreams_noModeChange() throws Exception {
+ assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive);
mAudioService.setStreamVolume(STREAM_MUSIC,
mAudioService.getStreamMinVolume(STREAM_MUSIC), /*flags=*/0,
mContext.getOpPackageName());
@@ -544,17 +552,23 @@
mAudioService.setDeviceVolume(volMin, usbDevice, mContext.getOpPackageName());
mTestLooper.dispatchAll();
- assertEquals(mAudioService.getDeviceVolume(volMin, usbDevice,
- mContext.getOpPackageName()), volMin);
+ if (!mIsAutomotive) {
+ // there is a min/max index mismatch in automotive
+ assertEquals(mAudioService.getDeviceVolume(volMin, usbDevice,
+ mContext.getOpPackageName()), volMin);
+ }
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
- STREAM_MUSIC, minIndex, AudioSystem.DEVICE_OUT_USB_DEVICE);
+ eq(STREAM_MUSIC), anyInt(), eq(AudioSystem.DEVICE_OUT_USB_DEVICE));
mAudioService.setDeviceVolume(volMid, usbDevice, mContext.getOpPackageName());
mTestLooper.dispatchAll();
- assertEquals(mAudioService.getDeviceVolume(volMid, usbDevice,
- mContext.getOpPackageName()), volMid);
+ if (!mIsAutomotive) {
+ // there is a min/max index mismatch in automotive
+ assertEquals(mAudioService.getDeviceVolume(volMid, usbDevice,
+ mContext.getOpPackageName()), volMid);
+ }
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
- STREAM_MUSIC, midIndex, AudioSystem.DEVICE_OUT_USB_DEVICE);
+ eq(STREAM_MUSIC), anyInt(), eq(AudioSystem.DEVICE_OUT_USB_DEVICE));
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index dd2b845..6f10370 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -5538,6 +5538,49 @@
}
@Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void removeAndAddAutomaticZenRule_ifChangingComponent_isAllowedAndDoesNotRestore() {
+ // Start with a rule.
+ mZenModeHelper.mConfig.automaticRules.clear();
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+ .setOwner(new ComponentName("first", "owner"))
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALL)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+
+ // User customizes it.
+ AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI,
+ "userUpdate", SYSTEM_UID);
+
+ // App deletes it. It's preserved for a possible restoration.
+ mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it",
+ CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+ assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1);
+
+ // App adds it again, but this time with a different owner!
+ AutomaticZenRule readdingWithDifferentOwner = new AutomaticZenRule.Builder(rule)
+ .setOwner(new ComponentName("second", "owner"))
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .build();
+ String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+ readdingWithDifferentOwner, ORIGIN_APP, "add it again", CUSTOM_PKG_UID);
+
+ // Verify that the rule was NOT restored:
+ assertThat(newRuleId).isNotEqualTo(ruleId);
+ AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+ assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
+ assertThat(finalRule.getOwner()).isEqualTo(new ComponentName("second", "owner"));
+
+ // Also, we discarded the "deleted rule" since we found it but decided not to use it.
+ assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0);
+ }
+
+ @Test
@EnableFlags(FLAG_MODES_API)
public void removeAutomaticZenRule_preservedForRestoringByPackageAndConditionId() {
mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS,
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index ea825c7..0d8b720 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -705,7 +705,7 @@
assertEquals(ORIENTATION_PORTRAIT, activity.getConfiguration().orientation);
// Clear size compat.
- activity.clearSizeCompatMode();
+ activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
activity.ensureActivityConfiguration();
mDisplayContent.sendNewConfiguration();
@@ -1629,10 +1629,10 @@
@Test
public void testCompleteResume_updateCompatDisplayInsets() {
final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
- doReturn(true).when(activity).shouldCreateCompatDisplayInsets();
+ doReturn(true).when(activity).shouldCreateAppCompatDisplayInsets();
activity.setState(RESUMED, "test");
activity.completeResumeLocked();
- assertNotNull(activity.getCompatDisplayInsets());
+ assertNotNull(activity.getAppCompatDisplayInsets());
}
/**
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index c788f3b..a7a08b2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -209,7 +209,7 @@
}
void setShouldCreateCompatDisplayInsets(boolean enabled) {
- doReturn(enabled).when(mActivityStack.top()).shouldCreateCompatDisplayInsets();
+ doReturn(enabled).when(mActivityStack.top()).shouldCreateAppCompatDisplayInsets();
}
void setTopActivityInSizeCompatMode(boolean inScm) {
@@ -499,7 +499,7 @@
activity.setRequestedOrientation(screenOrientation);
}
// Make sure to use the provided configuration to construct the size compat fields.
- activity.clearSizeCompatMode();
+ activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
activity.ensureActivityConfiguration();
// Make sure the display configuration reflects the change of activity.
if (activity.mDisplayContent.updateOrientation()) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 58e919d..f2ea1c9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1732,25 +1732,6 @@
assertFalse(mDisplayContent.hasTopFixedRotationLaunchingApp());
}
- @SetupWindows(addWindows = W_ACTIVITY)
- @Test
- public void testRotateSeamlesslyWithFixedRotation() {
- final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation();
- final ActivityRecord app = mAppWindow.mActivityRecord;
- mDisplayContent.setFixedRotationLaunchingAppUnchecked(app);
- mAppWindow.mAttrs.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
-
- // Use seamless rotation if the top app is rotated.
- assertTrue(displayRotation.shouldRotateSeamlessly(ROTATION_0 /* oldRotation */,
- ROTATION_90 /* newRotation */, false /* forceUpdate */));
-
- mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(app);
-
- // Use normal rotation because animating recents is an intermediate state.
- assertFalse(displayRotation.shouldRotateSeamlessly(ROTATION_0 /* oldRotation */,
- ROTATION_90 /* newRotation */, false /* forceUpdate */));
- }
-
@Test
public void testFixedRotationWithPip() {
final DisplayContent displayContent = mDefaultDisplay;
@@ -1828,49 +1809,6 @@
assertFalse(mDisplayContent.hasTopFixedRotationLaunchingApp());
}
- @Test
- public void testRecentsNotRotatingWithFixedRotation() {
- unblockDisplayRotation(mDisplayContent);
- final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation();
- // Skip freezing so the unrelated conditions in updateRotationUnchecked won't disturb.
- doNothing().when(mWm).startFreezingDisplay(anyInt(), anyInt(), any(), anyInt());
-
- final ActivityRecord activity = createActivityRecord(mDisplayContent);
- final ActivityRecord recentsActivity = createActivityRecord(mDisplayContent);
- recentsActivity.setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
- doReturn(mock(RecentsAnimationController.class)).when(mWm).getRecentsAnimationController();
-
- // Do not rotate if the recents animation is animating on top.
- mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity);
- displayRotation.setRotation((displayRotation.getRotation() + 1) % 4);
- assertFalse(displayRotation.updateRotationUnchecked(false));
-
- // Rotation can be updated if the recents animation is finished.
- mDisplayContent.mFixedRotationTransitionListener.onFinishRecentsAnimation();
- assertTrue(displayRotation.updateRotationUnchecked(false));
-
- // Rotation can be updated if the policy is not ok to animate (e.g. going to sleep).
- mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity);
- displayRotation.setRotation((displayRotation.getRotation() + 1) % 4);
- ((TestWindowManagerPolicy) mWm.mPolicy).mOkToAnimate = false;
- assertTrue(displayRotation.updateRotationUnchecked(false));
-
- // Rotation can be updated if the recents animation is animating but it is not on top, e.g.
- // switching activities in different orientations by quickstep gesture.
- mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity);
- mDisplayContent.setFixedRotationLaunchingAppUnchecked(activity);
- displayRotation.setRotation((displayRotation.getRotation() + 1) % 4);
- assertTrue(displayRotation.updateRotationUnchecked(false));
-
- // The recents activity should not apply fixed rotation if the top activity is not opaque.
- mDisplayContent.mFocusedApp = activity;
- doReturn(false).when(mDisplayContent.mFocusedApp).occludesParent();
- doReturn(ROTATION_90).when(mDisplayContent).rotationForActivityInDifferentOrientation(
- eq(recentsActivity));
- mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity);
- assertFalse(recentsActivity.hasFixedRotationTransform());
- }
-
@EnableFlags(com.android.window.flags.Flags.FLAG_RESPECT_NON_TOP_VISIBLE_FIXED_ORIENTATION)
@Test
public void testRespectNonTopVisibleFixedOrientation() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
deleted file mode 100644
index 63e3e5c..0000000
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
+++ /dev/null
@@ -1,834 +0,0 @@
-/*
- * 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.server.wm;
-
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
-import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
-import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.atLeast;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
-import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
-import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
-import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_TOP;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_TOKEN_TRANSFORM;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-
-import android.content.pm.ActivityInfo;
-import android.content.res.Configuration;
-import android.os.Binder;
-import android.os.IBinder;
-import android.os.IInterface;
-import android.platform.test.annotations.Presubmit;
-import android.util.SparseBooleanArray;
-import android.view.IRecentsAnimationRunner;
-import android.view.SurfaceControl;
-import android.view.WindowManager.LayoutParams;
-import android.window.TaskSnapshot;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
-
-import com.google.common.truth.Truth;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.ArrayList;
-
-/**
- * Build/Install/Run:
- * atest WmTests:RecentsAnimationControllerTest
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class RecentsAnimationControllerTest extends WindowTestsBase {
-
- @Mock SurfaceControl mMockLeash;
- @Mock SurfaceControl.Transaction mMockTransaction;
- @Mock OnAnimationFinishedCallback mFinishedCallback;
- @Mock IRecentsAnimationRunner mMockRunner;
- @Mock RecentsAnimationController.RecentsAnimationCallbacks mAnimationCallbacks;
- @Mock TaskSnapshot mMockTaskSnapshot;
- private RecentsAnimationController mController;
- private Task mRootHomeTask;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- doNothing().when(mWm.mRoot).performSurfacePlacement();
- when(mMockRunner.asBinder()).thenReturn(new Binder());
- mController = spy(new RecentsAnimationController(mWm, mMockRunner, mAnimationCallbacks,
- DEFAULT_DISPLAY));
- mRootHomeTask = mDefaultDisplay.getDefaultTaskDisplayArea().getRootHomeTask();
- assertNotNull(mRootHomeTask);
- }
-
- @Test
- public void testRemovedBeforeStarted_expectCanceled() throws Exception {
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- AnimationAdapter adapter = mController.addAnimation(activity.getTask(),
- false /* isRecentTaskInvisible */);
- adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_RECENTS,
- mFinishedCallback);
-
- // The activity doesn't contain window so the animation target cannot be created.
- mController.startAnimation();
-
- // Verify that the finish callback to reparent the leash is called
- verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_RECENTS), eq(adapter));
- // Verify the animation canceled callback to the app was made
- verify(mMockRunner).onAnimationCanceled(null /* taskIds */, null /* taskSnapshots */);
- verifyNoMoreInteractionsExceptAsBinder(mMockRunner);
- }
-
- @Test
- public void testCancelAfterRemove_expectIgnored() {
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- AnimationAdapter adapter = mController.addAnimation(activity.getTask(),
- false /* isRecentTaskInvisible */);
- adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_RECENTS,
- mFinishedCallback);
-
- // Remove the app window so that the animation target can not be created
- activity.removeImmediately();
- mController.startAnimation();
- mController.cleanupAnimation(REORDER_KEEP_IN_PLACE);
- try {
- mController.cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "test");
- } catch (Exception e) {
- fail("Unexpected failure when canceling animation after finishing it");
- }
- }
-
- @Test
- public void testIncludedApps_expectTargetAndVisible() {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord homeActivity = createHomeActivity();
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final ActivityRecord hiddenActivity = createActivityRecord(mDefaultDisplay);
- hiddenActivity.setVisible(false);
- mDefaultDisplay.getConfiguration().windowConfiguration.setRotation(
- mDefaultDisplay.getRotation());
- initializeRecentsAnimationController(mController, homeActivity);
-
- // Ensure that we are animating the target activity as well
- assertTrue(mController.isAnimatingTask(homeActivity.getTask()));
- assertTrue(mController.isAnimatingTask(activity.getTask()));
- assertFalse(mController.isAnimatingTask(hiddenActivity.getTask()));
- }
-
- @Test
- public void testLaunchAndStartRecents_expectTargetAndVisible() throws Exception {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord homeActivity = createHomeActivity();
- final Task task = createTask(mDefaultDisplay);
- // Emulate that activity1 has just launched activity2, but app transition has not yet been
- // executed.
- final ActivityRecord activity1 = createActivityRecord(task);
- activity1.setVisible(true);
- activity1.setVisibleRequested(false);
- activity1.addWindow(createWindowState(new LayoutParams(TYPE_BASE_APPLICATION), activity1));
-
- final ActivityRecord activity2 = createActivityRecord(task);
- activity2.setVisible(false);
- activity2.setVisibleRequested(true);
-
- mDefaultDisplay.getConfiguration().windowConfiguration.setRotation(
- mDefaultDisplay.getRotation());
- initializeRecentsAnimationController(mController, homeActivity);
- mController.startAnimation();
- verify(mMockRunner, never()).onAnimationCanceled(null /* taskIds */,
- null /* taskSnapshots */);
- }
-
- @Test
- public void testWallpaperIncluded_expectTarget() throws Exception {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord homeActivity = createHomeActivity();
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1");
- activity.addWindow(win1);
- final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm,
- mock(IBinder.class), true, mDefaultDisplay, true /* ownerCanManageAppTokens */);
- spyOn(mDefaultDisplay.mWallpaperController);
- doReturn(true).when(mDefaultDisplay.mWallpaperController).isWallpaperVisible();
-
- mDefaultDisplay.getConfiguration().windowConfiguration.setRotation(
- mDefaultDisplay.getRotation());
- initializeRecentsAnimationController(mController, homeActivity);
- mController.startAnimation();
-
- // Ensure that we are animating the app and wallpaper target
- assertTrue(mController.isAnimatingTask(activity.getTask()));
- assertTrue(mController.isAnimatingWallpaper(wallpaperWindowToken));
- }
-
- @Test
- public void testWallpaperAnimatorCanceled_expectAnimationKeepsRunning() throws Exception {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord homeActivity = createHomeActivity();
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1");
- activity.addWindow(win1);
- final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm,
- mock(IBinder.class), true, mDefaultDisplay, true /* ownerCanManageAppTokens */);
- spyOn(mDefaultDisplay.mWallpaperController);
- doReturn(true).when(mDefaultDisplay.mWallpaperController).isWallpaperVisible();
-
- mDefaultDisplay.getConfiguration().windowConfiguration.setRotation(
- mDefaultDisplay.getRotation());
- initializeRecentsAnimationController(mController, homeActivity);
- mController.startAnimation();
-
- // Cancel the animation and ensure the controller is still running
- wallpaperWindowToken.cancelAnimation();
- assertTrue(mController.isAnimatingTask(activity.getTask()));
- assertFalse(mController.isAnimatingWallpaper(wallpaperWindowToken));
- verify(mMockRunner, never()).onAnimationCanceled(null /* taskIds */,
- null /* taskSnapshots */);
- }
-
- @Test
- public void testFinish_expectTargetAndWallpaperAdaptersRemoved() {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord homeActivity = createHomeActivity();
- final WindowState hwin1 = createWindow(null, TYPE_BASE_APPLICATION, homeActivity, "hwin1");
- homeActivity.addWindow(hwin1);
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1");
- activity.addWindow(win1);
- final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm,
- mock(IBinder.class), true, mDefaultDisplay, true /* ownerCanManageAppTokens */);
- spyOn(mDefaultDisplay.mWallpaperController);
- doReturn(true).when(mDefaultDisplay.mWallpaperController).isWallpaperVisible();
-
- // Start and finish the animation
- initializeRecentsAnimationController(mController, homeActivity);
- mController.startAnimation();
-
- assertTrue(mController.isAnimatingTask(homeActivity.getTask()));
- assertTrue(mController.isAnimatingTask(activity.getTask()));
-
- // Reset at this point since we may remove adapters that couldn't be created
- clearInvocations(mController);
- mController.cleanupAnimation(REORDER_MOVE_TO_TOP);
-
- // Ensure that we remove the task (home & app) and wallpaper adapters
- verify(mController, times(2)).removeAnimation(any());
- verify(mController, times(1)).removeWallpaperAnimation(any());
- }
-
- @Test
- public void testDeferCancelAnimation() throws Exception {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1");
- activity.addWindow(win1);
- assertEquals(activity.getTask().getTopVisibleActivity(), activity);
- assertEquals(activity.findMainWindow(), win1);
-
- mController.addAnimation(activity.getTask(), false /* isRecentTaskInvisible */);
- assertTrue(mController.isAnimatingTask(activity.getTask()));
-
- mController.setDeferredCancel(true /* deferred */, false /* screenshot */);
- mController.cancelAnimationWithScreenshot(false /* screenshot */);
- verify(mMockRunner).onAnimationCanceled(null /* taskIds */, null /* taskSnapshots */);
-
- // Simulate the app transition finishing
- mController.mAppTransitionListener.onAppTransitionStartingLocked(0, 0);
- verify(mAnimationCallbacks).onAnimationFinished(REORDER_KEEP_IN_PLACE, false);
- }
-
- @Test
- public void testDeferCancelAnimationWithScreenShot() throws Exception {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1");
- activity.addWindow(win1);
- assertEquals(activity.getTask().getTopVisibleActivity(), activity);
- assertEquals(activity.findMainWindow(), win1);
-
- RecentsAnimationController.TaskAnimationAdapter adapter = mController.addAnimation(
- activity.getTask(), false /* isRecentTaskInvisible */);
- assertTrue(mController.isAnimatingTask(activity.getTask()));
-
- spyOn(mWm.mTaskSnapshotController);
- doReturn(mMockTaskSnapshot).when(mWm.mTaskSnapshotController).getSnapshot(anyInt(),
- anyInt(), eq(false) /* restoreFromDisk */, eq(false) /* isLowResolution */);
- mController.setDeferredCancel(true /* deferred */, true /* screenshot */);
- mController.cancelAnimationWithScreenshot(true /* screenshot */);
- verify(mMockRunner).onAnimationCanceled(any(int[].class) /* taskIds */,
- any(TaskSnapshot[].class) /* taskSnapshots */);
-
- // Continue the animation (simulating a call to cleanupScreenshot())
- mController.continueDeferredCancelAnimation();
- verify(mAnimationCallbacks).onAnimationFinished(REORDER_KEEP_IN_PLACE, false);
- }
-
- @Test
- public void testShouldAnimateWhenNoCancelWithDeferredScreenshot() {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1");
- activity.addWindow(win1);
- assertEquals(activity.getTask().getTopVisibleActivity(), activity);
- assertEquals(activity.findMainWindow(), win1);
-
- mController.addAnimation(activity.getTask(), false /* isRecentTaskInvisible */);
- assertTrue(mController.isAnimatingTask(activity.getTask()));
-
- // Assume activity transition should animate when no
- // IRecentsAnimationController#setDeferCancelUntilNextTransition called.
- assertFalse(mController.shouldDeferCancelWithScreenshot());
- assertTrue(activity.shouldAnimate());
- }
-
- @Test
- public void testBinderDiedAfterCancelWithDeferredScreenshot() throws Exception {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord homeActivity = createHomeActivity();
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1");
- activity.addWindow(win1);
-
- initializeRecentsAnimationController(mController, homeActivity);
- mController.setWillFinishToHome(true);
-
- // Verify cancel is called with a snapshot and that we've created an overlay
- spyOn(mWm.mTaskSnapshotController);
- doReturn(mMockTaskSnapshot).when(mWm.mTaskSnapshotController).getSnapshot(anyInt(),
- anyInt(), eq(false) /* restoreFromDisk */, eq(false) /* isLowResolution */);
- mController.cancelAnimationWithScreenshot(true /* screenshot */);
- verify(mMockRunner).onAnimationCanceled(any(), any());
-
- // Simulate process crashing and ensure the animation is still canceled
- mController.binderDied();
- verify(mAnimationCallbacks).onAnimationFinished(REORDER_KEEP_IN_PLACE, false);
- }
-
- @Test
- public void testRecentViewInFixedPortraitWhenTopAppInLandscape() {
- makeDisplayPortrait(mDefaultDisplay);
- unblockDisplayRotation(mDefaultDisplay);
- mWm.setRecentsAnimationController(mController);
-
- final ActivityRecord homeActivity = createHomeActivity();
- homeActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
-
- final ActivityRecord landActivity = createActivityRecord(mDefaultDisplay);
- landActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, landActivity, "win1");
- landActivity.addWindow(win1);
-
- assertEquals(landActivity.getTask().getTopVisibleActivity(), landActivity);
- assertEquals(landActivity.findMainWindow(), win1);
-
- // Ensure that the display is in Landscape
- landActivity.onDescendantOrientationChanged(landActivity);
- assertEquals(Configuration.ORIENTATION_LANDSCAPE,
- mDefaultDisplay.getConfiguration().orientation);
-
- initializeRecentsAnimationController(mController, homeActivity);
-
- assertTrue(mDefaultDisplay.isFixedRotationLaunchingApp(homeActivity));
-
- // Check that the home app is in portrait
- assertEquals(Configuration.ORIENTATION_PORTRAIT,
- homeActivity.getConfiguration().orientation);
-
- // Home activity won't become top (return to landActivity), so the top rotated record should
- // be cleared.
- mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION);
- assertFalse(mDefaultDisplay.isFixedRotationLaunchingApp(homeActivity));
- assertFalse(mDefaultDisplay.hasTopFixedRotationLaunchingApp());
- // The transform should keep until the transition is done, so the restored configuration
- // won't be sent to activity and cause unnecessary configuration change.
- assertTrue(homeActivity.hasFixedRotationTransform());
-
- // In real case the transition will be executed from RecentsAnimation#finishAnimation.
- mDefaultDisplay.mFixedRotationTransitionListener.onAppTransitionFinishedLocked(
- homeActivity.token);
- assertFalse(homeActivity.hasFixedRotationTransform());
- }
-
- private ActivityRecord prepareFixedRotationLaunchingAppWithRecentsAnim() {
- final ActivityRecord homeActivity = createHomeActivity();
- homeActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- // Add a window so it can be animated by the recents.
- final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win");
- activity.addWindow(win);
- // Assume an activity is launching to different rotation.
- mDefaultDisplay.setFixedRotationLaunchingApp(activity,
- (mDefaultDisplay.getRotation() + 1) % 4);
-
- assertTrue(activity.hasFixedRotationTransform());
- assertTrue(mDefaultDisplay.isFixedRotationLaunchingApp(activity));
-
- // Before the transition is done, the recents animation is triggered.
- initializeRecentsAnimationController(mController, homeActivity);
- assertFalse(homeActivity.hasFixedRotationTransform());
- assertTrue(mController.isAnimatingTask(activity.getTask()));
-
- return activity;
- }
-
- @Test
- public void testClearFixedRotationLaunchingAppAfterCleanupAnimation() {
- final ActivityRecord activity = prepareFixedRotationLaunchingAppWithRecentsAnim();
-
- // Simulate giving up the swipe up gesture to keep the original activity as top.
- mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION);
- // The rotation transform should be cleared after updating orientation with display.
- assertTopFixedRotationLaunchingAppCleared(activity);
-
- // Simulate swiping up recents (home) in different rotation.
- final ActivityRecord home = mDefaultDisplay.getDefaultTaskDisplayArea().getHomeActivity();
- startRecentsInDifferentRotation(home);
-
- // If the recents activity becomes the top running activity (e.g. the original top activity
- // is either finishing or moved to back during recents animation), the display orientation
- // will be determined by it so the fixed rotation must be cleared.
- activity.finishing = true;
- mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION);
- assertTopFixedRotationLaunchingAppCleared(home);
-
- startRecentsInDifferentRotation(home);
- // Assume recents activity becomes invisible for some reason (e.g. screen off).
- home.setVisible(false);
- mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION);
- // Although there won't be a transition finish callback, the fixed rotation must be cleared.
- assertTopFixedRotationLaunchingAppCleared(home);
- }
-
- @Test
- public void testKeepFixedRotationWhenMovingRecentsToTop() {
- final ActivityRecord activity = prepareFixedRotationLaunchingAppWithRecentsAnim();
- // Assume a transition animation has started running before recents animation. Then the
- // activity will receive onAnimationFinished that notifies app transition finished when
- // removing the recents animation of task.
- activity.getTask().getAnimationSources().add(activity);
-
- // Simulate swiping to home/recents before the transition is done.
- mController.cleanupAnimation(REORDER_MOVE_TO_TOP);
- // The rotation transform should be preserved. In real case, it will be cleared by the next
- // move-to-top transition.
- assertTrue(activity.hasFixedRotationTransform());
- }
-
- @Test
- public void testCheckRotationAfterCleanup() {
- mWm.setRecentsAnimationController(mController);
- spyOn(mDisplayContent.mFixedRotationTransitionListener);
- final ActivityRecord recents = mock(ActivityRecord.class);
- recents.setOverrideOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
- doReturn(ORIENTATION_PORTRAIT).when(recents)
- .getRequestedConfigurationOrientation(anyBoolean());
- mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recents);
-
- // Rotation update is skipped while the recents animation is running.
- final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation();
- final int topOrientation = DisplayContentTests.getRotatedOrientation(mDefaultDisplay);
- assertFalse(displayRotation.updateOrientation(topOrientation, false /* forceUpdate */));
- assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSET, displayRotation.getLastOrientation());
- final int prevRotation = mDisplayContent.getRotation();
- mWm.cleanupRecentsAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION);
-
- // In real case, it is called from RecentsAnimation#finishAnimation -> continueWindowLayout
- // -> handleAppTransitionReady -> add FINISH_LAYOUT_REDO_CONFIG, and DisplayContent#
- // applySurfaceChangesTransaction will call updateOrientation for FINISH_LAYOUT_REDO_CONFIG.
- assertTrue(displayRotation.updateOrientation(topOrientation, false /* forceUpdate */));
- // The display should be updated to the changed orientation after the animation is finished.
- assertNotEquals(displayRotation.getRotation(), prevRotation);
- }
-
- @Test
- public void testWallpaperHasFixedRotationApplied() {
- makeDisplayPortrait(mDefaultDisplay);
- unblockDisplayRotation(mDefaultDisplay);
- mWm.setRecentsAnimationController(mController);
-
- // Create a portrait home activity, a wallpaper and a landscape activity displayed on top.
- final ActivityRecord homeActivity = createHomeActivity();
- homeActivity.setOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
-
- final WindowState homeWindow = createWindow(null, TYPE_BASE_APPLICATION, homeActivity,
- "homeWindow");
- makeWindowVisible(homeWindow);
- homeActivity.addWindow(homeWindow);
- homeWindow.getAttrs().flags |= FLAG_SHOW_WALLPAPER;
-
- // Landscape application
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState applicationWindow = createWindow(null, TYPE_BASE_APPLICATION, activity,
- "applicationWindow");
- activity.addWindow(applicationWindow);
- activity.setOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
-
- // Wallpaper
- final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm,
- mock(IBinder.class), true, mDefaultDisplay, true /* ownerCanManageAppTokens */);
- final WindowState wallpaperWindow = createWindow(null, TYPE_WALLPAPER, wallpaperWindowToken,
- "wallpaperWindow");
-
- // Make sure the landscape activity is on top and the display is in landscape
- activity.moveFocusableActivityToTop("test");
- mDefaultDisplay.getConfiguration().windowConfiguration.setRotation(
- mDefaultDisplay.getRotation());
-
- spyOn(mDefaultDisplay.mWallpaperController);
- doReturn(true).when(mDefaultDisplay.mWallpaperController).isWallpaperVisible();
-
- // Start the recents animation
- initializeRecentsAnimationController(mController, homeActivity);
-
- mDefaultDisplay.mWallpaperController.adjustWallpaperWindows();
-
- // Check preconditions
- ArrayList<WallpaperWindowToken> wallpapers = new ArrayList<>(1);
- mDefaultDisplay.forAllWallpaperWindows(wallpapers::add);
-
- Truth.assertThat(wallpapers).hasSize(1);
- Truth.assertThat(wallpapers.get(0).getTopChild()).isEqualTo(wallpaperWindow);
-
- // Actual check
- assertEquals(Configuration.ORIENTATION_PORTRAIT,
- wallpapers.get(0).getConfiguration().orientation);
-
- mController.cleanupAnimation(REORDER_MOVE_TO_TOP);
- // The transform state should keep because we expect to listen the signal from the
- // transition executed by moving the task to front.
- assertTrue(homeActivity.hasFixedRotationTransform());
- assertTrue(mDefaultDisplay.isFixedRotationLaunchingApp(homeActivity));
-
- mDefaultDisplay.mFixedRotationTransitionListener.onAppTransitionFinishedLocked(
- homeActivity.token);
- // Wallpaper's transform state should be cleared with home.
- assertFalse(homeActivity.hasFixedRotationTransform());
- assertFalse(wallpaperWindowToken.hasFixedRotationTransform());
- }
-
- @Test
- public void testIsAnimatingByRecents() {
- final ActivityRecord homeActivity = createHomeActivity();
- final Task rootTask = createTask(mDefaultDisplay);
- final Task childTask = createTaskInRootTask(rootTask, 0 /* userId */);
- final Task leafTask = createTaskInRootTask(childTask, 0 /* userId */);
- spyOn(leafTask);
- doReturn(true).when(leafTask).isVisible();
-
- initializeRecentsAnimationController(mController, homeActivity);
-
- // Verify RecentsAnimationController will animate visible leaf task by default.
- verify(mController).addAnimation(eq(leafTask), anyBoolean(), anyBoolean(), any());
- assertTrue(leafTask.isAnimatingByRecents());
-
- // Make sure isAnimatingByRecents will also return true when it called by the parent task.
- assertTrue(rootTask.isAnimatingByRecents());
- assertTrue(childTask.isAnimatingByRecents());
- }
-
- @Test
- public void testRestoreNavBarWhenEnteringRecents_expectAnimation() {
- setupForShouldAttachNavBarDuringTransition();
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final ActivityRecord homeActivity = createHomeActivity();
- initializeRecentsAnimationController(mController, homeActivity);
-
- final WindowToken navToken = mDefaultDisplay.getDisplayPolicy().getNavigationBar().mToken;
- final SurfaceControl.Transaction transaction = navToken.getPendingTransaction();
-
- verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled(
- eq(mDefaultDisplay.mDisplayId), eq(false));
- verify(transaction).reparent(navToken.getSurfaceControl(), activity.getSurfaceControl());
- verify(transaction).setLayer(navToken.getSurfaceControl(), Integer.MAX_VALUE);
- assertTrue(mController.isNavigationBarAttachedToApp());
-
- mController.cleanupAnimation(REORDER_MOVE_TO_TOP);
- verify(mController).restoreNavigationBarFromApp(eq(true));
- verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled(
- eq(mDefaultDisplay.mDisplayId), eq(true));
- verify(transaction).setLayer(navToken.getSurfaceControl(), 0);
- assertFalse(mController.isNavigationBarAttachedToApp());
- assertTrue(navToken.isAnimating(ANIMATION_TYPE_TOKEN_TRANSFORM));
- }
-
- @Test
- public void testRestoreNavBarWhenBackToApp_expectNoAnimation() {
- setupForShouldAttachNavBarDuringTransition();
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final ActivityRecord homeActivity = createHomeActivity();
- initializeRecentsAnimationController(mController, homeActivity);
-
- final WindowToken navToken = mDefaultDisplay.getDisplayPolicy().getNavigationBar().mToken;
- final SurfaceControl.Transaction transaction = navToken.getPendingTransaction();
-
- verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled(
- eq(mDefaultDisplay.mDisplayId), eq(false));
- verify(transaction).reparent(navToken.getSurfaceControl(), activity.getSurfaceControl());
- verify(transaction).setLayer(navToken.getSurfaceControl(), Integer.MAX_VALUE);
- assertTrue(mController.isNavigationBarAttachedToApp());
-
- final WindowContainer parent = navToken.getParent();
-
- mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION);
- verify(mController).restoreNavigationBarFromApp(eq(false));
- verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled(
- eq(mDefaultDisplay.mDisplayId), eq(true));
- verify(transaction).setLayer(navToken.getSurfaceControl(), 0);
- verify(transaction).reparent(navToken.getSurfaceControl(), parent.getSurfaceControl());
- assertFalse(mController.isNavigationBarAttachedToApp());
- assertFalse(navToken.isAnimating(ANIMATION_TYPE_TOKEN_TRANSFORM));
- }
-
- @Test
- public void testAddTaskToTargets_expectAnimation() {
- setupForShouldAttachNavBarDuringTransition();
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final ActivityRecord homeActivity = createHomeActivity();
- initializeRecentsAnimationController(mController, homeActivity);
-
- final WindowToken navToken = mDefaultDisplay.getDisplayPolicy().getNavigationBar().mToken;
- final SurfaceControl.Transaction transaction = navToken.getPendingTransaction();
-
- verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled(
- eq(mDefaultDisplay.mDisplayId), eq(false));
- verify(transaction).reparent(navToken.getSurfaceControl(), activity.getSurfaceControl());
- verify(transaction).setLayer(navToken.getSurfaceControl(), Integer.MAX_VALUE);
- assertTrue(mController.isNavigationBarAttachedToApp());
-
- final WindowContainer parent = navToken.getParent();
-
- mController.addTaskToTargets(createTask(mDefaultDisplay), (type, anim) -> {});
- mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION);
- verify(mController).restoreNavigationBarFromApp(eq(true));
- verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled(
- eq(mDefaultDisplay.mDisplayId), eq(true));
- verify(transaction).setLayer(navToken.getSurfaceControl(), 0);
- assertFalse(mController.isNavigationBarAttachedToApp());
- assertTrue(navToken.isAnimating(ANIMATION_TYPE_TOKEN_TRANSFORM));
- }
-
- @Test
- public void testNotAttachNavigationBar_controlledByFadeRotationAnimation() {
- setupForShouldAttachNavBarDuringTransition();
- AsyncRotationController mockController =
- mock(AsyncRotationController.class);
- doReturn(mockController).when(mDefaultDisplay).getAsyncRotationController();
- final ActivityRecord homeActivity = createHomeActivity();
- initializeRecentsAnimationController(mController, homeActivity);
- assertFalse(mController.isNavigationBarAttachedToApp());
- }
-
- @Test
- public void testAttachNavBarInSplitScreenMode() {
- setupForShouldAttachNavBarDuringTransition();
- TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm);
- final ActivityRecord primary = createActivityRecordWithParentTask(
- organizer.createTaskToPrimary(true));
- final ActivityRecord secondary = createActivityRecordWithParentTask(
- organizer.createTaskToSecondary(true));
- final ActivityRecord homeActivity = createHomeActivity();
- homeActivity.setVisibility(true);
- initializeRecentsAnimationController(mController, homeActivity);
-
- WindowState navWindow = mController.getNavigationBarWindow();
- final WindowToken navToken = navWindow.mToken;
- final SurfaceControl.Transaction transaction = navToken.getPendingTransaction();
-
- verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled(
- eq(mDefaultDisplay.mDisplayId), eq(false));
- verify(navWindow).setSurfaceTranslationY(-secondary.getBounds().top);
- verify(transaction).reparent(navToken.getSurfaceControl(), secondary.getSurfaceControl());
- assertTrue(mController.isNavigationBarAttachedToApp());
- reset(navWindow);
-
- mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION);
- final WindowContainer parent = navToken.getParent();
- verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled(
- eq(mDefaultDisplay.mDisplayId), eq(true));
- verify(navWindow).setSurfaceTranslationY(0);
- verify(transaction).reparent(navToken.getSurfaceControl(), parent.getSurfaceControl());
- verify(mController).restoreNavigationBarFromApp(eq(false));
- assertFalse(mController.isNavigationBarAttachedToApp());
- }
-
- @Test
- public void testCleanupAnimation_expectExitAnimationDone() {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord homeActivity = createHomeActivity();
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1");
- activity.addWindow(win1);
-
- initializeRecentsAnimationController(mController, homeActivity);
- mController.startAnimation();
-
- spyOn(win1);
- spyOn(win1.mWinAnimator);
- // Simulate when the window is exiting and cleanupAnimation invoked
- // (e.g. screen off during RecentsAnimation animating), will expect the window receives
- // onExitAnimationDone to destroy the surface when the removal is allowed.
- win1.mWinAnimator.mSurfaceControl = mock(SurfaceControl.class);
- win1.mHasSurface = true;
- win1.mAnimatingExit = true;
- win1.mRemoveOnExit = true;
- win1.mWindowRemovalAllowed = true;
- mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION);
- verify(win1).onAnimationFinished(eq(ANIMATION_TYPE_RECENTS), any());
- verify(win1).onExitAnimationDone();
- verify(win1).destroySurface(eq(false), eq(false));
- assertFalse(win1.mAnimatingExit);
- assertFalse(win1.mHasSurface);
- }
-
- @Test
- public void testCancelForRotation_ReorderToTop() throws Exception {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1");
- activity.addWindow(win1);
-
- mController.addAnimation(activity.getTask(), false /* isRecentTaskInvisible */);
- mController.setWillFinishToHome(true);
- mController.cancelAnimationForDisplayChange();
-
- verify(mMockRunner).onAnimationCanceled(any(), any());
- verify(mAnimationCallbacks).onAnimationFinished(REORDER_MOVE_TO_TOP, false);
- }
-
- @Test
- public void testCancelForRotation_ReorderToOriginalPosition() throws Exception {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1");
- activity.addWindow(win1);
-
- mController.addAnimation(activity.getTask(), false /* isRecentTaskInvisible */);
- mController.setWillFinishToHome(false);
- mController.cancelAnimationForDisplayChange();
-
- verify(mMockRunner).onAnimationCanceled(any(), any());
- verify(mAnimationCallbacks).onAnimationFinished(REORDER_MOVE_TO_ORIGINAL_POSITION, false);
- }
-
- @Test
- public void testCancelForStartHome() throws Exception {
- mWm.setRecentsAnimationController(mController);
- final ActivityRecord homeActivity = createHomeActivity();
- final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1");
- activity.addWindow(win1);
-
- initializeRecentsAnimationController(mController, homeActivity);
- mController.setWillFinishToHome(true);
-
- // Verify cancel is called with a snapshot and that we've created an overlay
- spyOn(mWm.mTaskSnapshotController);
- doReturn(mMockTaskSnapshot).when(mWm.mTaskSnapshotController).getSnapshot(anyInt(),
- anyInt(), eq(false) /* restoreFromDisk */, eq(false) /* isLowResolution */);
- mController.cancelAnimationForHomeStart();
- verify(mMockRunner).onAnimationCanceled(any(), any());
-
- // Continue the animation (simulating a call to cleanupScreenshot())
- mController.continueDeferredCancelAnimation();
- verify(mAnimationCallbacks).onAnimationFinished(REORDER_MOVE_TO_TOP, false);
-
- // Assume home was moved to front so will-be-top callback should not be called.
- homeActivity.moveFocusableActivityToTop("test");
- spyOn(mDefaultDisplay.mFixedRotationTransitionListener);
- mController.cleanupAnimation(REORDER_MOVE_TO_TOP);
- verify(mDefaultDisplay.mFixedRotationTransitionListener, never()).notifyRecentsWillBeTop();
- }
-
- private ActivityRecord createHomeActivity() {
- final ActivityRecord homeActivity = new ActivityBuilder(mWm.mAtmService)
- .setParentTask(mRootHomeTask)
- .setCreateTask(true)
- .build();
- // Avoid {@link RecentsAnimationController.TaskAnimationAdapter#createRemoteAnimationTarget}
- // returning null when calling {@link RecentsAnimationController#createAppAnimations}.
- homeActivity.setVisibility(true);
- return homeActivity;
- }
-
- private void startRecentsInDifferentRotation(ActivityRecord recentsActivity) {
- final DisplayContent displayContent = recentsActivity.mDisplayContent;
- displayContent.setFixedRotationLaunchingApp(recentsActivity,
- (displayContent.getRotation() + 1) % 4);
- mController = new RecentsAnimationController(mWm, mMockRunner, mAnimationCallbacks,
- displayContent.getDisplayId());
- initializeRecentsAnimationController(mController, recentsActivity);
- assertTrue(recentsActivity.hasFixedRotationTransform());
- }
-
- private static void assertTopFixedRotationLaunchingAppCleared(ActivityRecord activity) {
- assertFalse(activity.hasFixedRotationTransform());
- assertFalse(activity.mDisplayContent.hasTopFixedRotationLaunchingApp());
- }
-
- private void setupForShouldAttachNavBarDuringTransition() {
- final WindowState navBar = spy(createWindow(null, TYPE_NAVIGATION_BAR, "NavigationBar"));
- mDefaultDisplay.getDisplayPolicy().addWindowLw(navBar, navBar.mAttrs);
- mWm.setRecentsAnimationController(mController);
- doReturn(navBar).when(mController).getNavigationBarWindow();
- final DisplayPolicy displayPolicy = spy(mDefaultDisplay.getDisplayPolicy());
- doReturn(displayPolicy).when(mDefaultDisplay).getDisplayPolicy();
- doReturn(true).when(displayPolicy).shouldAttachNavBarToAppDuringTransition();
- }
-
- private static void initializeRecentsAnimationController(RecentsAnimationController controller,
- ActivityRecord activity) {
- controller.initialize(activity.getActivityType(), new SparseBooleanArray(), activity);
- }
-
- private static void verifyNoMoreInteractionsExceptAsBinder(IInterface binder) {
- verify(binder, atLeast(0)).asBinder();
- verifyNoMoreInteractions(binder);
- }
-}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
index f93ffb8..1e8c3b2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -728,25 +728,6 @@
}
}
- @Test
- public void testNonAppTarget_notSendNavBar_controlledByRecents() throws Exception {
- final RecentsAnimationController mockController =
- mock(RecentsAnimationController.class);
- doReturn(mockController).when(mWm).getRecentsAnimationController();
- final int transit = TRANSIT_OLD_TASK_OPEN;
- setupForNonAppTargetNavBar(transit, true);
-
- final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor =
- ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
- verify(mMockRunner).onAnimationStart(eq(transit),
- any(), any(), nonAppsCaptor.capture(), any());
- for (int i = 0; i < nonAppsCaptor.getValue().length; i++) {
- if (nonAppsCaptor.getValue()[0].windowType == TYPE_NAVIGATION_BAR) {
- fail("Non-app animation target must not contain navbar");
- }
- }
- }
-
@android.platform.test.annotations.RequiresFlagsDisabled(
com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY)
@SetupWindows(addWindows = W_INPUT_METHOD)
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
index e019a41..9007733 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
@@ -1280,51 +1280,6 @@
}
@Test
- public void testRootTaskOrderChangedOnRemoveRootTask() {
- final Task task = new TaskBuilder(mSupervisor).build();
- RootTaskOrderChangedListener listener = new RootTaskOrderChangedListener();
- mDefaultTaskDisplayArea.registerRootTaskOrderChangedListener(listener);
- try {
- mDefaultTaskDisplayArea.removeRootTask(task);
- } finally {
- mDefaultTaskDisplayArea.unregisterRootTaskOrderChangedListener(listener);
- }
- assertTrue(listener.mChanged);
- }
-
- @Test
- public void testRootTaskOrderChangedOnAddPositionRootTask() {
- final Task task = new TaskBuilder(mSupervisor).build();
- mDefaultTaskDisplayArea.removeRootTask(task);
-
- RootTaskOrderChangedListener listener = new RootTaskOrderChangedListener();
- mDefaultTaskDisplayArea.registerRootTaskOrderChangedListener(listener);
- try {
- task.mReparenting = true;
- mDefaultTaskDisplayArea.addChild(task, 0);
- } finally {
- mDefaultTaskDisplayArea.unregisterRootTaskOrderChangedListener(listener);
- }
- assertTrue(listener.mChanged);
- }
-
- @Test
- public void testRootTaskOrderChangedOnPositionRootTask() {
- RootTaskOrderChangedListener listener = new RootTaskOrderChangedListener();
- try {
- final Task fullscreenRootTask1 = createTaskForShouldBeVisibleTest(
- mDefaultTaskDisplayArea, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
- true /* onTop */);
- mDefaultTaskDisplayArea.registerRootTaskOrderChangedListener(listener);
- mDefaultTaskDisplayArea.positionChildAt(POSITION_BOTTOM, fullscreenRootTask1,
- false /*includingParents*/);
- } finally {
- mDefaultTaskDisplayArea.unregisterRootTaskOrderChangedListener(listener);
- }
- assertTrue(listener.mChanged);
- }
-
- @Test
public void testNavigateUpTo() {
final ActivityStartController controller = mock(ActivityStartController.class);
final ActivityStarter starter = new ActivityStarter(controller,
@@ -1451,11 +1406,6 @@
anyBoolean());
}
- private boolean isAssistantOnTop() {
- return mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_assistantOnTopOfDream);
- }
-
private void verifyShouldSleepActivities(boolean focusedRootTask,
boolean keyguardGoingAway, boolean displaySleeping, boolean isDefaultDisplay,
boolean expected) {
@@ -1471,14 +1421,4 @@
assertEquals(expected, task.shouldSleepActivities());
}
-
- private static class RootTaskOrderChangedListener
- implements TaskDisplayArea.OnRootTaskOrderChangedListener {
- public boolean mChanged = false;
-
- @Override
- public void onRootTaskOrderChanged(Task rootTask) {
- mChanged = true;
- }
- }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 1e1055b..f743401 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -613,7 +613,7 @@
assertFalse(mActivity.mDisplayContent.shouldImeAttachedToApp());
// Recompute the natural configuration without resolving size compat configuration.
- mActivity.clearSizeCompatMode();
+ mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
mActivity.onConfigurationChanged(mTask.getConfiguration());
// It should keep non-attachable because the resolved bounds will be computed according to
// the aspect ratio that won't match its parent bounds.
@@ -706,7 +706,7 @@
/ originalBounds.width()));
// Recompute the natural configuration in the new display.
- mActivity.clearSizeCompatMode();
+ mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
mActivity.ensureActivityConfiguration();
// Because the display cannot rotate, the portrait activity will fit the short side of
// display with keeping portrait bounds [200, 0 - 700, 1000] in center.
@@ -957,12 +957,12 @@
.setResizeMode(ActivityInfo.RESIZE_MODE_UNRESIZEABLE)
.setScreenOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
.build();
- assertTrue(activity.shouldCreateCompatDisplayInsets());
+ assertTrue(activity.shouldCreateAppCompatDisplayInsets());
// The non-resizable activity should not be size compat because it is on a resizable task
// in multi-window mode.
mTask.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM);
- assertFalse(activity.shouldCreateCompatDisplayInsets());
+ assertFalse(activity.shouldCreateAppCompatDisplayInsets());
// Activity should not be sandboxed.
assertMaxBoundsInheritDisplayAreaBounds();
@@ -971,7 +971,7 @@
mTask.mDisplayContent.getDefaultTaskDisplayArea()
.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM);
mTask.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
- assertFalse(activity.shouldCreateCompatDisplayInsets());
+ assertFalse(activity.shouldCreateAppCompatDisplayInsets());
// Activity should not be sandboxed.
assertMaxBoundsInheritDisplayAreaBounds();
}
@@ -986,7 +986,7 @@
// Create an activity on the same task.
final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */true,
RESIZE_MODE_UNRESIZEABLE, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- assertFalse(activity.shouldCreateCompatDisplayInsets());
+ assertFalse(activity.shouldCreateAppCompatDisplayInsets());
}
@Test
@@ -999,7 +999,7 @@
// Create an activity on the same task.
final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */false,
RESIZE_MODE_UNRESIZEABLE, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- assertTrue(activity.shouldCreateCompatDisplayInsets());
+ assertTrue(activity.shouldCreateAppCompatDisplayInsets());
}
@Test
@@ -1012,7 +1012,7 @@
// Create an activity on the same task.
final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */false,
RESIZE_MODE_RESIZEABLE, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- assertFalse(activity.shouldCreateCompatDisplayInsets());
+ assertFalse(activity.shouldCreateAppCompatDisplayInsets());
}
@Test
@@ -1026,7 +1026,7 @@
// Create an activity on the same task.
final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */false,
RESIZE_MODE_UNRESIZEABLE, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
- assertFalse(activity.shouldCreateCompatDisplayInsets());
+ assertFalse(activity.shouldCreateAppCompatDisplayInsets());
}
@Test
@@ -1040,7 +1040,7 @@
// Create an activity on the same task.
final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */false,
RESIZE_MODE_UNRESIZEABLE, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- assertFalse(activity.shouldCreateCompatDisplayInsets());
+ assertFalse(activity.shouldCreateAppCompatDisplayInsets());
}
@Test
@@ -1054,7 +1054,7 @@
// Create an activity on the same task.
final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */true,
RESIZE_MODE_RESIZEABLE, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- assertTrue(activity.shouldCreateCompatDisplayInsets());
+ assertTrue(activity.shouldCreateAppCompatDisplayInsets());
}
@Test
@@ -1068,7 +1068,7 @@
// Create an activity on the same task.
final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */true,
RESIZE_MODE_RESIZEABLE, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
- assertTrue(activity.shouldCreateCompatDisplayInsets());
+ assertTrue(activity.shouldCreateAppCompatDisplayInsets());
}
@Test
@@ -1090,7 +1090,7 @@
doReturn(USER_MIN_ASPECT_RATIO_FULLSCREEN)
.when(activity.mAppCompatController.getAppCompatAspectRatioOverrides())
.getUserMinAspectRatioOverrideCode();
- assertFalse(activity.shouldCreateCompatDisplayInsets());
+ assertFalse(activity.shouldCreateAppCompatDisplayInsets());
}
@Test
@@ -1110,7 +1110,7 @@
doReturn(true).when(
activity.mAppCompatController.getAppCompatAspectRatioOverrides())
.isSystemOverrideToFullscreenEnabled();
- assertFalse(activity.shouldCreateCompatDisplayInsets());
+ assertFalse(activity.shouldCreateAppCompatDisplayInsets());
}
@Test
@@ -1482,7 +1482,7 @@
// After changing the orientation to portrait the override should be applied.
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- activity.clearSizeCompatMode();
+ activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
// The per-package override forces the activity into a 3:2 aspect ratio
assertEquals(1200, activity.getBounds().height());
@@ -1511,7 +1511,7 @@
// After changing the orientation to portrait the override should be applied.
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- activity.clearSizeCompatMode();
+ activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
// The per-package override forces the activity into a 3:2 aspect ratio
assertEquals(1200, activity.getBounds().height());
@@ -1538,7 +1538,7 @@
// After changing the orientation to landscape the override shouldn't be applied.
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
- activity.clearSizeCompatMode();
+ activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
// The per-package override should have no effect
assertEquals(1200, activity.getBounds().height());
@@ -3054,7 +3054,10 @@
false /* deferPause */);
// App still in size compat, and the bounds don't change.
- verify(mActivity, never()).clearSizeCompatMode();
+ final AppCompatSizeCompatModePolicy scmPolicy = mActivity.mAppCompatController
+ .getAppCompatSizeCompatModePolicy();
+ spyOn(scmPolicy);
+ verify(scmPolicy, never()).clearSizeCompatMode();
assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
.isLetterboxedForFixedOrientationAndAspectRatio());
assertDownScaled();
@@ -3256,7 +3259,7 @@
// Activity max bounds are sandboxed since app may enter size compat mode.
assertActivityMaxBoundsSandboxed();
assertFalse(mActivity.inSizeCompatMode());
- assertTrue(mActivity.shouldCreateCompatDisplayInsets());
+ assertTrue(mActivity.shouldCreateAppCompatDisplayInsets());
// Resize display to half the width.
resizeDisplay(mActivity.getDisplayContent(), 500, 1000);
@@ -3896,7 +3899,7 @@
private void recomputeNaturalConfigurationOfUnresizableActivity() {
// Recompute the natural configuration of the non-resizable activity and the split screen.
- mActivity.clearSizeCompatMode();
+ mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
// Draw letterbox.
mActivity.setVisible(false);
@@ -4102,8 +4105,8 @@
// To force config to update again but with the same landscape orientation.
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
- assertTrue(activity.shouldCreateCompatDisplayInsets());
- assertNotNull(activity.getCompatDisplayInsets());
+ assertTrue(activity.shouldCreateAppCompatDisplayInsets());
+ assertNotNull(activity.getAppCompatDisplayInsets());
// Activity is not letterboxed for fixed orientation because orientation is respected
// with insets, and should not be in size compat mode
assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy()
@@ -4827,7 +4830,7 @@
assertEquals(origDensity, mActivity.getConfiguration().densityDpi);
// Activity should exit size compat with new density.
- mActivity.clearSizeCompatMode();
+ mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
assertFitted();
assertEquals(newDensity, mActivity.getConfiguration().densityDpi);
@@ -5013,7 +5016,7 @@
activity.setRequestedOrientation(screenOrientation);
}
// Make sure to use the provided configuration to construct the size compat fields.
- activity.clearSizeCompatMode();
+ activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
activity.ensureActivityConfiguration();
// Make sure the display configuration reflects the change of activity.
if (activity.mDisplayContent.updateOrientation()) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index 6fd5faf..b595383 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -34,6 +34,7 @@
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.wm.ActivityRecord.State.RESUMED;
@@ -50,6 +51,7 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -839,4 +841,16 @@
0 /* launchFlags */, pinnedTask /* candidateTask */);
assertNull(actualRootTask);
}
+
+ @Test
+ public void testMovedRootTaskToFront() {
+ final TaskDisplayArea tda = mDefaultDisplay.getDefaultTaskDisplayArea();
+ final Task rootTask = createTask(tda, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */, true /* createActivity */, true /* twoLevelTask */);
+ final Task leafTask = rootTask.getTopLeafTask();
+
+ clearInvocations(tda);
+ tda.onTaskMoved(rootTask, true /* toTop */, false /* toBottom */);
+ verify(tda).onLeafTaskMoved(eq(leafTask), anyBoolean(), anyBoolean());
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 0a592f2..55f74e9d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -867,8 +867,8 @@
// Without limiting to be inside the parent bounds, the out screen size should keep relative
// to the input bounds.
final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).build();
- final ActivityRecord.CompatDisplayInsets compatInsets =
- new ActivityRecord.CompatDisplayInsets(
+ final AppCompatDisplayInsets compatInsets =
+ new AppCompatDisplayInsets(
display, activity, /* letterboxedContainerBounds */ null,
/* useOverrideInsets */ false);
final TaskFragment.ConfigOverrideHint overrideHint = new TaskFragment.ConfigOverrideHint();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
index 29f6360..7a440e6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
@@ -322,9 +322,12 @@
a.checkTopActivityInSizeCompatMode(/* inScm */ true);
ta.launchTransparentActivityInTask();
- a.assertNotNullOnTopActivity(ActivityRecord::getCompatDisplayInsets);
- a.applyToTopActivity(ActivityRecord::clearSizeCompatMode);
- a.assertNullOnTopActivity(ActivityRecord::getCompatDisplayInsets);
+ a.assertNotNullOnTopActivity(ActivityRecord::getAppCompatDisplayInsets);
+ a.applyToTopActivity((top) -> {
+ top.mAppCompatController.getAppCompatSizeCompatModePolicy()
+ .clearSizeCompatMode();
+ });
+ a.assertNullOnTopActivity(ActivityRecord::getAppCompatDisplayInsets);
});
});
});
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index c65b76e..7652861 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -37,7 +37,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@@ -276,9 +275,8 @@
final DisplayContent dc = mDisplayContent;
final WindowState homeWin = createWallpaperTargetWindow(dc);
final WindowState appWin = createWindow(null, TYPE_BASE_APPLICATION, "app");
- final RecentsAnimationController recentsController = mock(RecentsAnimationController.class);
- doReturn(true).when(recentsController).isWallpaperVisible(eq(appWin));
- mWm.setRecentsAnimationController(recentsController);
+ appWin.mAttrs.flags |= FLAG_SHOW_WALLPAPER;
+ makeWindowVisible(appWin);
dc.mWallpaperController.adjustWallpaperWindows();
assertEquals(appWin, dc.mWallpaperController.getWallpaperTarget());
@@ -354,46 +352,6 @@
}
@Test
- public void testFixedRotationRecentsAnimatingTask() {
- final WindowState wallpaperWindow = createWallpaperWindow(mDisplayContent);
- final WallpaperWindowToken wallpaperToken = wallpaperWindow.mToken.asWallpaperToken();
- final WindowState appWin = createWindow(null, TYPE_BASE_APPLICATION, "app");
- makeWindowVisible(appWin);
- final ActivityRecord r = appWin.mActivityRecord;
- final RecentsAnimationController recentsController = mock(RecentsAnimationController.class);
- doReturn(true).when(recentsController).isWallpaperVisible(eq(appWin));
- mWm.setRecentsAnimationController(recentsController);
-
- r.applyFixedRotationTransform(mDisplayContent.getDisplayInfo(),
- mDisplayContent.mDisplayFrames, mDisplayContent.getConfiguration());
- // Invisible requested activity should not share its rotation transform.
- r.setVisibleRequested(false);
- mDisplayContent.mWallpaperController.adjustWallpaperWindows();
- assertFalse(wallpaperToken.hasFixedRotationTransform());
-
- // Wallpaper should link the transform of its target.
- r.setVisibleRequested(true);
- mDisplayContent.mWallpaperController.adjustWallpaperWindows();
- assertEquals(appWin, mDisplayContent.mWallpaperController.getWallpaperTarget());
- assertTrue(r.hasFixedRotationTransform());
- assertTrue(wallpaperToken.hasFixedRotationTransform());
-
- // The case with shell transition.
- registerTestTransitionPlayer();
- final Transition t = r.mTransitionController.createTransition(TRANSIT_OPEN);
- final ActivityRecord recents = mock(ActivityRecord.class);
- t.collect(r.getTask());
- r.mTransitionController.setTransientLaunch(recents, r.getTask());
- // The activity in restore-below task should not be the target if keyguard is not locked.
- mDisplayContent.mWallpaperController.adjustWallpaperWindows();
- assertNotEquals(appWin, mDisplayContent.mWallpaperController.getWallpaperTarget());
- // The activity in restore-below task should not be the target if keyguard is occluded.
- doReturn(true).when(mDisplayContent).isKeyguardLocked();
- mDisplayContent.mWallpaperController.adjustWallpaperWindows();
- assertNotEquals(appWin, mDisplayContent.mWallpaperController.getWallpaperTarget());
- }
-
- @Test
public void testWallpaperReportConfigChange() {
final WindowState wallpaperWindow = createWallpaperWindow(mDisplayContent);
createWallpaperTargetWindow(mDisplayContent);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index 0cb22ad..9bad2ec 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -44,7 +44,6 @@
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import android.Manifest;
@@ -197,21 +196,6 @@
}
@Test
- public void testSetRunningBothAnimations() {
- mWpc.setRunningRemoteAnimation(true);
- mWpc.setRunningRecentsAnimation(true);
-
- mWpc.setRunningRecentsAnimation(false);
- mWpc.setRunningRemoteAnimation(false);
- waitHandlerIdle(mAtm.mH);
-
- InOrder orderVerifier = Mockito.inOrder(mMockListener);
- orderVerifier.verify(mMockListener, times(1)).setRunningRemoteAnimation(eq(true));
- orderVerifier.verify(mMockListener, times(1)).setRunningRemoteAnimation(eq(false));
- orderVerifier.verifyNoMoreInteractions();
- }
-
- @Test
public void testConfigurationForSecondaryScreenDisplayArea() {
// By default, the process should not listen to any display area.
assertNull(mWpc.getDisplayArea());
diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
index 973ab84..d537bd7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
@@ -48,15 +48,10 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
import android.graphics.PixelFormat;
import android.graphics.Rect;
-import android.os.Binder;
import android.platform.test.annotations.Presubmit;
-import android.util.SparseBooleanArray;
-import android.view.IRecentsAnimationRunner;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.ScreenCapture;
@@ -493,43 +488,6 @@
}
@Test
- public void testAttachNavBarWhenEnteringRecents_expectNavBarHigherThanIme() {
- // create RecentsAnimationController
- IRecentsAnimationRunner mockRunner = mock(IRecentsAnimationRunner.class);
- when(mockRunner.asBinder()).thenReturn(new Binder());
- final int displayId = mDisplayContent.getDisplayId();
- RecentsAnimationController controller = new RecentsAnimationController(
- mWm, mockRunner, null, displayId);
- spyOn(controller);
- doReturn(mNavBarWindow).when(controller).getNavigationBarWindow();
- mWm.setRecentsAnimationController(controller);
-
- // set ime visible
- spyOn(mDisplayContent.mInputMethodWindow);
- doReturn(true).when(mDisplayContent.mInputMethodWindow).isVisible();
-
- DisplayPolicy policy = mDisplayContent.getDisplayPolicy();
- spyOn(policy);
- doReturn(true).when(policy).shouldAttachNavBarToAppDuringTransition();
-
- // create home activity
- Task rootHomeTask = mDisplayContent.getDefaultTaskDisplayArea().getRootHomeTask();
- final ActivityRecord homeActivity = new ActivityBuilder(mWm.mAtmService)
- .setParentTask(rootHomeTask)
- .setCreateTask(true)
- .build();
- homeActivity.setVisibility(true);
-
- // start recent animation
- controller.initialize(homeActivity.getActivityType(), new SparseBooleanArray(),
- homeActivity);
-
- mDisplayContent.assignChildLayers(mTransaction);
- assertZOrderGreaterThan(mTransaction, mNavBarWindow.mToken.getSurfaceControl(),
- mDisplayContent.getImeContainer().getSurfaceControl());
- }
-
- @Test
public void testPopupWindowAndParentIsImeTarget_expectHigherThanIme_inMultiWindow() {
// Simulate the app window is in multi windowing mode and being IME target
mAppWindow.getConfiguration().windowConfiguration.setWindowingMode(
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index dbe4f27..3e8b326 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -13928,7 +13928,10 @@
* {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
- @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.READ_BASIC_PHONE_STATE,
+ android.Manifest.permission.READ_PHONE_STATE
+ })
public void getCarrierRestrictionStatus(@NonNull Executor executor,
@NonNull @CarrierRestrictionStatus
Consumer<Integer> resultListener) {
diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
index e66a082..51154e5 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
@@ -24,6 +24,7 @@
import android.telephony.satellite.stub.ISatelliteListener;
import android.telephony.satellite.stub.SatelliteDatagram;
import android.telephony.satellite.stub.SystemSelectionSpecifier;
+import android.telephony.satellite.stub.SatelliteModemEnableRequestAttributes;
/**
* {@hide}
@@ -82,12 +83,7 @@
* is enabled, this may also disable the cellular modem, and if the satellite modem is disabled,
* this may also re-enable the cellular modem.
*
- * @param enableSatellite True to enable the satellite modem and false to disable.
- * @param enableDemoMode True to enable demo mode and false to disable.
- * @param isEmergency To specify the satellite is enabled for emergency session and false for
- * non emergency session. Note: it is possible that a emergency session started get converted
- * to a non emergency session and vice versa.
- * @param resultCallback The callback to receive the error code result of the operation.
+ * @param enableAttributes The enable parameters that will be applied to the satellite session
*
* Valid result codes returned:
* SatelliteResult:SATELLITE_RESULT_SUCCESS
@@ -99,8 +95,8 @@
* SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
* SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
*/
- void requestSatelliteEnabled(in boolean enableSatellite, in boolean enableDemoMode,
- in boolean isEmergency, in IIntegerConsumer resultCallback);
+ void requestSatelliteEnabled(in SatelliteModemEnableRequestAttributes enableAttributes,
+ in IIntegerConsumer resultCallback);
/**
* Request to get whether the satellite modem is enabled.
diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
index c50e469..4f47210 100644
--- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
@@ -89,12 +89,11 @@
}
@Override
- public void requestSatelliteEnabled(boolean enableSatellite, boolean enableDemoMode,
- boolean isEmergency, IIntegerConsumer resultCallback) throws RemoteException {
+ public void requestSatelliteEnabled(SatelliteModemEnableRequestAttributes enableAttributes,
+ IIntegerConsumer resultCallback) throws RemoteException {
executeMethodAsync(
() -> SatelliteImplBase.this
- .requestSatelliteEnabled(
- enableSatellite, enableDemoMode, isEmergency, resultCallback),
+ .requestSatelliteEnabled(enableAttributes, resultCallback),
"requestSatelliteEnabled");
}
@@ -325,11 +324,7 @@
* enabled, this may also disable the cellular modem, and if the satellite modem is disabled,
* this may also re-enable the cellular modem.
*
- * @param enableSatellite True to enable the satellite modem and false to disable.
- * @param enableDemoMode True to enable demo mode and false to disable.
- * @param isEmergency To specify the satellite is enabled for emergency session and false for
- * non emergency session. Note: it is possible that a emergency session started get converted
- * to a non emergency session and vice versa.
+ * @param enableAttributes The enable parameters that will be applied to the satellite session
* @param resultCallback The callback to receive the error code result of the operation.
*
* Valid result codes returned:
@@ -342,8 +337,8 @@
* SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
* SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
*/
- public void requestSatelliteEnabled(boolean enableSatellite, boolean enableDemoMode,
- boolean isEmergency, @NonNull IIntegerConsumer resultCallback) {
+ public void requestSatelliteEnabled(SatelliteModemEnableRequestAttributes enableAttributes,
+ @NonNull IIntegerConsumer resultCallback) {
// stub implementation
}
diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
index 8040610..cfc818b 100644
--- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
+++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
@@ -31,8 +31,7 @@
@RunWith(FlickerServiceJUnit4ClassRunner::class)
class CloseAppBackButton3ButtonLandscape :
CloseAppBackButton(NavBar.MODE_3BUTTON, Rotation.ROTATION_90) {
- // TODO: Missing CUJ (b/300078127)
- @ExpectedScenarios(["ENTIRE_TRACE"])
+ @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
@Test
override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
index aacccf4..6bf32a8 100644
--- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
+++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
@@ -31,8 +31,7 @@
@RunWith(FlickerServiceJUnit4ClassRunner::class)
class CloseAppBackButton3ButtonPortrait :
CloseAppBackButton(NavBar.MODE_3BUTTON, Rotation.ROTATION_0) {
- // TODO: Missing CUJ (b/300078127)
- @ExpectedScenarios(["ENTIRE_TRACE"])
+ @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
@Test
override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
index 74ee460..4b6ab77 100644
--- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
+++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
@@ -31,8 +31,7 @@
@RunWith(FlickerServiceJUnit4ClassRunner::class)
class CloseAppBackButtonGesturalNavLandscape :
CloseAppBackButton(NavBar.MODE_GESTURAL, Rotation.ROTATION_90) {
- // TODO: Missing CUJ (b/300078127)
- @ExpectedScenarios(["ENTIRE_TRACE"])
+ @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
@Test
override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
index 57463c3..7cc9db0 100644
--- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
+++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
@@ -31,8 +31,7 @@
@RunWith(FlickerServiceJUnit4ClassRunner::class)
class CloseAppBackButtonGesturalNavPortrait :
CloseAppBackButton(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) {
- // TODO: Missing CUJ (b/300078127)
- @ExpectedScenarios(["ENTIRE_TRACE"])
+ @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
@Test
override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index 8811e00..753cb1f 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -16,12 +16,17 @@
package com.android.server.wm.flicker.helpers
+import android.content.Context
+import android.graphics.Insets
import android.graphics.Rect
+import android.graphics.Region
import android.platform.uiautomator_helpers.DeviceHelpers
import android.tools.device.apphelpers.IStandardAppHelper
import android.tools.helpers.SYSTEMUI_PACKAGE
import android.tools.traces.parsers.WindowManagerStateHelper
import android.tools.traces.wm.WindowingMode
+import android.view.WindowInsets
+import android.view.WindowManager
import androidx.test.uiautomator.By
import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.UiDevice
@@ -70,9 +75,7 @@
// Start dragging a little under the top to prevent dragging the notification shade.
val startY = 10
- val displayRect =
- wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect
- ?: throw IllegalStateException("Default display is null")
+ val displayRect = getDisplayRect(wmHelper)
// The position we want to drag to
val endY = displayRect.centerY() / 2
@@ -81,18 +84,61 @@
device.drag(startX, startY, startX, endY, 100)
}
+ private fun getMaximizeButtonForTheApp(caption: UiObject2?): UiObject2 {
+ return caption
+ ?.children
+ ?.find { it.resourceName.endsWith(MAXIMIZE_BUTTON_VIEW) }
+ ?.children
+ ?.get(0)
+ ?: error("Unable to find resource $MAXIMIZE_BUTTON_VIEW\n")
+ }
+
/** Click maximise button on the app header for the given app. */
fun maximiseDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) {
val caption = getCaptionForTheApp(wmHelper, device)
- val maximizeButton =
- caption
- ?.children
- ?.find { it.resourceName.endsWith(MAXIMIZE_BUTTON_VIEW) }
- ?.children
- ?.get(0)
- maximizeButton?.click()
+ val maximizeButton = getMaximizeButtonForTheApp(caption)
+ maximizeButton.click()
wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
}
+
+ /** Open maximize menu and click snap resize button on the app header for the given app. */
+ fun snapResizeDesktopApp(
+ wmHelper: WindowManagerStateHelper,
+ device: UiDevice,
+ context: Context,
+ toLeft: Boolean
+ ) {
+ val caption = getCaptionForTheApp(wmHelper, device)
+ val maximizeButton = getMaximizeButtonForTheApp(caption)
+ maximizeButton?.longClick()
+ wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
+
+ val buttonResId = if (toLeft) SNAP_LEFT_BUTTON else SNAP_RIGHT_BUTTON
+ val maximizeMenu = getDesktopAppViewByRes(MAXIMIZE_MENU)
+
+ val snapResizeButton =
+ maximizeMenu
+ ?.wait(Until.findObject(By.res(SYSTEMUI_PACKAGE, buttonResId)), TIMEOUT.toMillis())
+ ?: error("Unable to find object with resource id $buttonResId")
+ snapResizeButton.click()
+
+ val displayRect = getDisplayRect(wmHelper)
+ val insets = getWindowInsets(
+ context, WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
+ displayRect.inset(insets)
+
+ val expectedWidth = displayRect.width() / 2
+ val expectedRect = Rect(displayRect).apply {
+ if (toLeft) right -= expectedWidth else left += expectedWidth
+ }
+
+ wmHelper
+ .StateSyncBuilder()
+ .withAppTransitionIdle()
+ .withSurfaceVisibleRegion(this, Region(expectedRect))
+ .waitForAndVerify()
+ }
+
/** Click close button on the app header for the given app. */
fun closeDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) {
val caption = getCaptionForTheApp(wmHelper, device)
@@ -112,8 +158,7 @@
if (
wmHelper.getWindow(innerHelper)?.windowingMode !=
WindowingMode.WINDOWING_MODE_FREEFORM.value
- )
- error("expected a freeform window with caption but window is not in freeform mode")
+ ) error("expected a freeform window with caption but window is not in freeform mode")
val captions =
device.wait(Until.findObjects(caption), TIMEOUT.toMillis())
?: error("Unable to find view $caption\n")
@@ -156,6 +201,30 @@
.waitForAndVerify()
}
+ /** Drag a window to a snap resize region, found at the left and right edges of the screen. */
+ fun dragToSnapResizeRegion(
+ wmHelper: WindowManagerStateHelper,
+ device: UiDevice,
+ isLeft: Boolean,
+ ) {
+ val windowRect = wmHelper.getWindowRegion(innerHelper).bounds
+ // Set start x-coordinate as center of app header.
+ val startX = windowRect.centerX()
+ val startY = windowRect.top
+
+ val displayRect = getDisplayRect(wmHelper)
+
+ val endX = if (isLeft) displayRect.left else displayRect.right
+ val endY = displayRect.centerY() / 2
+
+ // drag the window to snap resize
+ device.drag(startX, startY, endX, endY, /* steps= */ 100)
+ wmHelper
+ .StateSyncBuilder()
+ .withAppTransitionIdle()
+ .waitForAndVerify()
+ }
+
private fun getStartCoordinatesForCornerResize(
windowRect: Rect,
corner: Corners
@@ -179,9 +248,7 @@
private fun dragAppWindowToTopDragZone(wmHelper: WindowManagerStateHelper, device: UiDevice) {
val windowRect = wmHelper.getWindowRegion(innerHelper).bounds
- val displayRect =
- wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect
- ?: throw IllegalStateException("Default display is null")
+ val displayRect = getDisplayRect(wmHelper)
val startX = windowRect.centerX()
val endX = displayRect.centerX()
@@ -194,7 +261,8 @@
fun enterDesktopModeFromAppHandleMenu(
wmHelper: WindowManagerStateHelper,
- device: UiDevice) {
+ device: UiDevice
+ ) {
val windowRect = wmHelper.getWindowRegion(innerHelper).bounds
val startX = windowRect.centerX()
// Click a little under the top to prevent opening the notification shade.
@@ -204,7 +272,7 @@
device.click(startX, startY)
wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
- val pill = getAppHandlePillForWindow()
+ val pill = getDesktopAppViewByRes(PILL_CONTAINER)
val desktopModeButton =
pill
?.children
@@ -214,10 +282,13 @@
wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
}
- private fun getAppHandlePillForWindow(): UiObject2? {
- val pillContainer: BySelector = By.res(SYSTEMUI_PACKAGE, PILL_CONTAINER)
- return DeviceHelpers.waitForObj(pillContainer, TIMEOUT)
- }
+ private fun getDesktopAppViewByRes(viewResId: String): UiObject2? =
+ DeviceHelpers.waitForObj(By.res(SYSTEMUI_PACKAGE, viewResId), TIMEOUT)
+
+ private fun getDisplayRect(wmHelper: WindowManagerStateHelper): Rect =
+ wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect
+ ?: throw IllegalStateException("Default display is null")
+
/** Wait for transition to full screen to finish. */
private fun waitForTransitionToFullscreen(wmHelper: WindowManagerStateHelper) {
@@ -228,13 +299,23 @@
.waitForAndVerify()
}
+ private fun getWindowInsets(context: Context, typeMask: Int): Insets {
+ val wm: WindowManager = context.getSystemService(WindowManager::class.java)
+ ?: error("Unable to connect to WindowManager service")
+ val metricInsets = wm.currentWindowMetrics.windowInsets
+ return metricInsets.getInsetsIgnoringVisibility(typeMask)
+ }
+
private companion object {
- val TIMEOUT = Duration.ofSeconds(3)
- val CAPTION = "desktop_mode_caption"
- val MAXIMIZE_BUTTON_VIEW = "maximize_button_view"
- val CLOSE_BUTTON = "close_window"
- val PILL_CONTAINER = "windowing_pill"
- val DESKTOP_MODE_BUTTON = "desktop_button"
+ val TIMEOUT: Duration = Duration.ofSeconds(3)
+ const val CAPTION: String = "desktop_mode_caption"
+ const val MAXIMIZE_BUTTON_VIEW: String = "maximize_button_view"
+ const val MAXIMIZE_MENU: String = "maximize_menu"
+ const val CLOSE_BUTTON: String = "close_window"
+ const val PILL_CONTAINER: String = "windowing_pill"
+ const val DESKTOP_MODE_BUTTON: String = "desktop_button"
+ const val SNAP_LEFT_BUTTON: String = "maximize_menu_snap_left_button"
+ const val SNAP_RIGHT_BUTTON: String = "maximize_menu_snap_right_button"
val caption: BySelector
get() = By.res(SYSTEMUI_PACKAGE, CAPTION)
}
diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogTest.java
new file mode 100644
index 0000000..9d56a92
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.internal.protolog;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test class for {@link ProtoLog}. */
+@SuppressWarnings("ConstantConditions")
+@Presubmit
+@RunWith(JUnit4.class)
+public class ProtoLogTest {
+
+ @Test
+ public void canRunProtoLogInitMultipleTimes() {
+ ProtoLog.init(TEST_GROUP_1);
+ ProtoLog.init(TEST_GROUP_1);
+ ProtoLog.init(TEST_GROUP_2);
+ ProtoLog.init(TEST_GROUP_1, TEST_GROUP_2);
+
+ final var instance = ProtoLog.getSingleInstance();
+ Truth.assertThat(instance.getRegisteredGroups())
+ .containsExactly(TEST_GROUP_1, TEST_GROUP_2);
+ }
+
+ private static final IProtoLogGroup TEST_GROUP_1 = new ProtoLogGroup("TEST_TAG_1", 1);
+ private static final IProtoLogGroup TEST_GROUP_2 = new ProtoLogGroup("TEST_TAG_2", 2);
+
+ private static class ProtoLogGroup implements IProtoLogGroup {
+ private final boolean mEnabled;
+ private volatile boolean mLogToProto;
+ private volatile boolean mLogToLogcat;
+ private final String mTag;
+ private final int mId;
+
+ ProtoLogGroup(String tag, int id) {
+ this(true, true, false, tag, id);
+ }
+
+ ProtoLogGroup(
+ boolean enabled, boolean logToProto, boolean logToLogcat, String tag, int id) {
+ this.mEnabled = enabled;
+ this.mLogToProto = logToProto;
+ this.mLogToLogcat = logToLogcat;
+ this.mTag = tag;
+ this.mId = id;
+ }
+
+ @Override
+ public String name() {
+ return mTag;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ @Override
+ public boolean isLogToProto() {
+ return mLogToProto;
+ }
+
+ @Override
+ public boolean isLogToLogcat() {
+ return mLogToLogcat;
+ }
+
+ @Override
+ public boolean isLogToAny() {
+ return mLogToLogcat || mLogToProto;
+ }
+
+ @Override
+ public String getTag() {
+ return mTag;
+ }
+
+ @Override
+ public void setLogToProto(boolean logToProto) {
+ this.mLogToProto = logToProto;
+ }
+
+ @Override
+ public void setLogToLogcat(boolean logToLogcat) {
+ this.mLogToLogcat = logToLogcat;
+ }
+
+ @Override
+ public int getId() {
+ return mId;
+ }
+ }
+}
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index be63f82..498e431 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -306,6 +306,7 @@
OutputFormat output_format = OutputFormat::kApk;
std::unordered_set<std::string> extensions_to_not_compress;
std::optional<std::regex> regex_to_not_compress;
+ FeatureFlagValues feature_flag_values;
};
// A sampling of public framework resource IDs.
@@ -672,6 +673,13 @@
}
}
+ FeatureFlagsFilterOptions flags_filter_options;
+ flags_filter_options.flags_must_be_readonly = true;
+ FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options);
+ if (!flags_filter.Consume(context_, doc.get())) {
+ return 1;
+ }
+
error |= !FlattenXml(context_, *doc, dst_path, options_.keep_raw_values,
false /*utf16*/, options_.output_format, archive_writer);
}
@@ -1926,6 +1934,7 @@
static_cast<bool>(options_.generate_proguard_rules_path);
file_flattener_options.output_format = options_.output_format;
file_flattener_options.do_not_fail_on_missing_resources = options_.merge_only;
+ file_flattener_options.feature_flag_values = options_.feature_flag_values;
ResourceFileFlattener file_flattener(file_flattener_options, context_, keep_set);
if (!file_flattener.Flatten(table, writer)) {
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
index 4866d2c..c456e5c 100644
--- a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
@@ -30,12 +30,14 @@
"res/values/bools2.xml",
"res/values/ints.xml",
"res/values/strings.xml",
+ "res/layout/layout1.xml",
],
out: [
"values_bools.arsc.flat",
"values_bools2.arsc.flat",
"values_ints.arsc.flat",
"values_strings.arsc.flat",
+ "layout_layout1.xml.flat",
],
cmd: "$(location aapt2) compile $(in) -o $(genDir) " +
"--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
@@ -52,7 +54,10 @@
out: [
"resapp.apk",
],
- cmd: "$(location aapt2) link -o $(out) --manifest $(in)",
+ cmd: "$(location aapt2) link -o $(out) --manifest $(in) " +
+ "-I $(location :current_android_jar) " +
+ "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
+ tool_files: [":current_android_jar"],
}
genrule {
@@ -66,7 +71,10 @@
out: [
"resource-flagging-java/com/android/intenal/flaggedresources/R.java",
],
- cmd: "$(location aapt2) link -o $(genDir)/resapp.apk --java $(genDir)/resource-flagging-java --manifest $(in)",
+ cmd: "$(location aapt2) link -o $(genDir)/resapp.apk --java $(genDir)/resource-flagging-java --manifest $(in) " +
+ "-I $(location :current_android_jar) " +
+ "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
+ tool_files: [":current_android_jar"],
}
java_genrule {
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml
new file mode 100644
index 0000000..8b9ce13
--- /dev/null
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView android:id="@+id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ <TextView android:id="@+id/disabled_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:featureFlag="test.package.falseFlag" />
+ <TextView android:id="@+id/text2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:featureFlag="test.package.trueFlag" />
+</LinearLayout>
\ No newline at end of file
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
index 3e094fb..1ed0c8a 100644
--- a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <bool name="res1">true</bool>
- <bool name="res1" android:featureFlag="test.package.falseFlag">false</bool>
+ <bool name="bool1">true</bool>
+ <bool name="bool1" android:featureFlag="test.package.falseFlag">false</bool>
- <bool name="res2">false</bool>
- <bool name="res2" android:featureFlag="test.package.trueFlag">true</bool>
+ <bool name="bool2">false</bool>
+ <bool name="bool2" android:featureFlag="test.package.trueFlag">true</bool>
- <bool name="res3">false</bool>
+ <bool name="bool3">false</bool>
- <bool name="res4" android:featureFlag="test.package.falseFlag">true</bool>
+ <bool name="bool4" android:featureFlag="test.package.falseFlag">true</bool>
</resources>
\ No newline at end of file
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools2.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools2.xml
index e7563aa..248c45f 100644
--- a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools2.xml
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools2.xml
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <bool name="res3" android:featureFlag="test.package.trueFlag">true</bool>
+ <bool name="bool3" android:featureFlag="test.package.trueFlag">true</bool>
</resources>
\ No newline at end of file
diff --git a/tools/aapt2/link/FeatureFlagsFilter.cpp b/tools/aapt2/link/FeatureFlagsFilter.cpp
index 9d40db5..4e7c1b4 100644
--- a/tools/aapt2/link/FeatureFlagsFilter.cpp
+++ b/tools/aapt2/link/FeatureFlagsFilter.cpp
@@ -65,6 +65,13 @@
if (auto it = feature_flag_values_.find(flag_name); it != feature_flag_values_.end()) {
if (it->second.enabled.has_value()) {
+ if (options_.flags_must_be_readonly && !it->second.read_only) {
+ diagnostics_->Error(android::DiagMessage(node->line_number)
+ << "attribute 'android:featureFlag' has flag '" << flag_name
+ << "' which must be readonly but is not");
+ has_error_ = true;
+ return false;
+ }
if (options_.remove_disabled_elements) {
// Remove if flag==true && attr=="!flag" (negated) OR flag==false && attr=="flag"
return *it->second.enabled == negated;
diff --git a/tools/aapt2/link/FeatureFlagsFilter.h b/tools/aapt2/link/FeatureFlagsFilter.h
index 1d342a7..61e4c80 100644
--- a/tools/aapt2/link/FeatureFlagsFilter.h
+++ b/tools/aapt2/link/FeatureFlagsFilter.h
@@ -38,6 +38,10 @@
// If true, `Consume()` will return false (error) if a flag was found whose value in
// `feature_flag_values` is not defined (std::nullopt).
bool flags_must_have_value = true;
+
+ // If true, `Consume()` will return false (error) if a flag was found whose value in
+ // `feature_flag_values` is not readonly.
+ bool flags_must_be_readonly = false;
};
// Looks for the `android:featureFlag` attribute in each XML element, validates the flag names and
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index c901b58..3db37c2 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -84,7 +84,7 @@
std::string output;
DumpChunksToString(loaded_apk.get(), &output);
- ASSERT_EQ(output.find("res4"), std::string::npos);
+ ASSERT_EQ(output.find("bool4"), std::string::npos);
ASSERT_EQ(output.find("str1"), std::string::npos);
}
@@ -94,7 +94,7 @@
std::string r_contents;
::android::base::ReadFileToString(r_path, &r_contents);
- ASSERT_NE(r_contents.find("public static final int res4"), std::string::npos);
+ ASSERT_NE(r_contents.find("public static final int bool4"), std::string::npos);
ASSERT_NE(r_contents.find("public static final int str1"), std::string::npos);
}