Merge "Batch calls to the Metadata Syncer" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 0ca9789..dd919ca 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -93,6 +93,7 @@
"com.android.media.flags.performance-aconfig-java",
"com.android.media.flags.projection-aconfig-java",
"com.android.net.thread.platform.flags-aconfig-java",
+ "com.android.ranging.flags.ranging-aconfig-java",
"com.android.server.contextualsearch.flags-java",
"com.android.server.flags.services-aconfig-java",
"com.android.text.flags-aconfig-java",
@@ -1549,6 +1550,13 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+// Ranging
+java_aconfig_library {
+ name: "com.android.ranging.flags.ranging-aconfig-java",
+ aconfig_declarations: "ranging_aconfig_flags",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// System Server
aconfig_declarations {
name: "android.systemserver.flags-aconfig",
diff --git a/api/Android.bp b/api/Android.bp
index 533f9f6..3f2316f 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -102,6 +102,11 @@
"framework-crashrecovery",
],
default: [],
+ }) + select(release_flag("RELEASE_RANGING_STACK"), {
+ true: [
+ "framework-ranging",
+ ],
+ default: [],
}),
system_server_classpath: [
"service-art",
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index df45862..8447a7f 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -387,6 +387,12 @@
field public static final int DEVICE_INITIAL_SDK_INT;
}
+ public class Handler {
+ method @FlaggedApi("android.os.mainline_vcn_platform_api") public final boolean hasMessagesOrCallbacks();
+ method @FlaggedApi("android.os.mainline_vcn_platform_api") public final void removeCallbacksAndEqualMessages(@Nullable Object);
+ method @FlaggedApi("android.os.mainline_vcn_platform_api") public final void removeEqualMessages(int, @Nullable Object);
+ }
+
public class IpcDataCache<Query, Result> {
ctor public IpcDataCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.os.IpcDataCache.QueryHandler<Query,Result>);
method public void disableForCurrentProcess();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 7a8e829..b2a49e1 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3835,6 +3835,7 @@
field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String ON_DEVICE_INTELLIGENCE_SERVICE = "on_device_intelligence";
field public static final String PERMISSION_CONTROLLER_SERVICE = "permission_controller";
field public static final String PERMISSION_SERVICE = "permission";
+ field @FlaggedApi("com.android.ranging.flags.ranging_stack_enabled") public static final String RANGING_SERVICE = "ranging";
field public static final String REBOOT_READINESS_SERVICE = "reboot_readiness";
field public static final String ROLLBACK_SERVICE = "rollback";
field public static final String SAFETY_CENTER_SERVICE = "safety_center";
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 5db79fe..3bc3a93 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -255,6 +255,7 @@
import libcore.io.IoUtils;
import libcore.io.Os;
import libcore.net.event.NetworkEventDispatcher;
+import libcore.util.NativeAllocationRegistry;
import org.apache.harmony.dalvik.ddmc.DdmVmInternal;
@@ -1610,6 +1611,32 @@
}
@NeverCompile
+ private void dumpMemInfoNativeAllocations(PrintWriter pw) {
+ pw.println(" ");
+ pw.println(" Native Allocations");
+ printRow(pw, TWO_COUNT_COLUMN_HEADER, "", "Count", "", "Total(kB)");
+ printRow(pw, TWO_COUNT_COLUMN_HEADER, "", "------", "", "------");
+
+ for (NativeAllocationRegistry.Metrics m : NativeAllocationRegistry.getMetrics()) {
+ // group into 3 major categories: Bitmap, HardwareBuffer and Other
+ final String className = switch (m.getClassName()) {
+ case "android.graphics.Bitmap" -> "Bitmap";
+ case "android.hardware.HardwareBuffer" -> "HardwareBuffer";
+ default -> "Other";
+ };
+
+ if (m.getMallocedCount() != 0 || m.getMallocedBytes() != 0) {
+ printRow(pw, TWO_COUNT_COLUMNS, className + " (malloced):",
+ m.getMallocedCount(), "", m.getMallocedBytes() / 1024);
+ }
+ if (m.getNonmallocedCount() != 0 || m.getNonmallocedBytes() != 0) {
+ printRow(pw, TWO_COUNT_COLUMNS, className + " (nonmalloced):",
+ m.getNonmallocedCount(), "", m.getNonmallocedBytes() / 1024);
+ }
+ }
+ }
+
+ @NeverCompile
private void dumpMemInfo(PrintWriter pw, Debug.MemoryInfo memInfo, boolean checkin,
boolean dumpFullInfo, boolean dumpDalvik, boolean dumpSummaryOnly,
boolean dumpUnreachable, boolean dumpAllocatorStats) {
@@ -1707,6 +1734,10 @@
printRow(pw, TWO_COUNT_COLUMNS, "Death Recipients:", binderDeathObjectCount,
"WebViews:", webviewInstanceCount);
+ if (com.android.libcore.Flags.nativeMetrics()) {
+ dumpMemInfoNativeAllocations(pw);
+ }
+
// SQLite mem info
pw.println(" ");
pw.println(" SQL");
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index dbf9afd..ed6b851 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -16,6 +16,7 @@
package android.app;
+import static android.app.PropertyInvalidatedCache.createSystemCacheKey;
import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED;
import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_NOT_COLORED;
import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
@@ -817,7 +818,7 @@
private final static PropertyInvalidatedCache<HasSystemFeatureQuery, Boolean>
mHasSystemFeatureCache =
new PropertyInvalidatedCache<HasSystemFeatureQuery, Boolean>(
- 256, "cache_key.has_system_feature") {
+ 256, createSystemCacheKey("has_system_feature")) {
@Override
public Boolean recompute(HasSystemFeatureQuery query) {
try {
@@ -1127,7 +1128,7 @@
}
private static final String CACHE_KEY_PACKAGES_FOR_UID_PROPERTY =
- "cache_key.get_packages_for_uid";
+ createSystemCacheKey("get_packages_for_uid");
private static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult>
mGetPackagesForUidCache =
new PropertyInvalidatedCache<Integer, GetPackagesForUidResult>(
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 0c786cb..0e761fc 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
+import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -283,6 +284,12 @@
*/
/**
+ * The well-known key prefix.
+ * @hide
+ */
+ private static final String CACHE_KEY_PREFIX = "cache_key";
+
+ /**
* The module used for unit tests and cts tests. It is expected that no process in
* the system has permissions to write properties with this module.
* @hide
@@ -366,7 +373,44 @@
}
}
- return "cache_key." + module + "." + new String(suffix);
+ return CACHE_KEY_PREFIX + "." + module + "." + new String(suffix);
+ }
+
+ /**
+ * All legal keys start with one of the following strings.
+ */
+ private static final String[] sValidKeyPrefix = {
+ CACHE_KEY_PREFIX + "." + MODULE_SYSTEM + ".",
+ CACHE_KEY_PREFIX + "." + MODULE_BLUETOOTH + ".",
+ CACHE_KEY_PREFIX + "." + MODULE_TELEPHONY + ".",
+ CACHE_KEY_PREFIX + "." + MODULE_TEST + ".",
+ };
+
+ /**
+ * Verify that the property name conforms to the standard. Log a warning if this is not true.
+ * Note that this is done once in the cache constructor; it does not have to be very fast.
+ */
+ private void validateCacheKey(String name) {
+ if (Build.IS_USER) {
+ // Do not bother checking keys in user builds. The keys will have been tested in
+ // eng/userdebug builds already.
+ return;
+ }
+ for (int i = 0; i < sValidKeyPrefix.length; i++) {
+ if (name.startsWith(sValidKeyPrefix[i])) return;
+ }
+ Log.w(TAG, "invalid cache name: " + name);
+ }
+
+ /**
+ * Create a cache key for the system module. The parameter is the API name. This reduces
+ * some of the boilerplate in system caches. It is not needed in other modules because other
+ * modules must use the {@link IpcDataCache} interfaces.
+ * @hide
+ */
+ @NonNull
+ public static String createSystemCacheKey(@NonNull String api) {
+ return createPropertyName(MODULE_SYSTEM, api);
}
/**
@@ -561,6 +605,7 @@
public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName,
@NonNull String cacheName) {
mPropertyName = propertyName;
+ validateCacheKey(mPropertyName);
mCacheName = cacheName;
mMaxEntries = maxEntries;
mComputer = new DefaultComputer<>(this);
@@ -584,6 +629,7 @@
public PropertyInvalidatedCache(int maxEntries, @NonNull String module, @NonNull String api,
@NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) {
mPropertyName = createPropertyName(module, api);
+ validateCacheKey(mPropertyName);
mCacheName = cacheName;
mMaxEntries = maxEntries;
mComputer = computer;
diff --git a/core/java/android/app/compat/ChangeIdStateCache.java b/core/java/android/app/compat/ChangeIdStateCache.java
index 7948cec..db663f8 100644
--- a/core/java/android/app/compat/ChangeIdStateCache.java
+++ b/core/java/android/app/compat/ChangeIdStateCache.java
@@ -16,6 +16,8 @@
package android.app.compat;
+import static android.app.PropertyInvalidatedCache.createSystemCacheKey;
+
import android.annotation.NonNull;
import android.app.PropertyInvalidatedCache;
import android.content.Context;
@@ -31,7 +33,7 @@
*/
public final class ChangeIdStateCache
extends PropertyInvalidatedCache<ChangeIdStateQuery, Boolean> {
- private static final String CACHE_KEY = "cache_key.is_compat_change_enabled";
+ private static final String CACHE_KEY = createSystemCacheKey("is_compat_change_enabled");
private static final int MAX_ENTRIES = 2048;
private static boolean sDisabled = false;
private volatile IPlatformCompat mPlatformCompat;
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 12c5d07..91f7a8b 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4324,6 +4324,7 @@
SECURITY_STATE_SERVICE,
//@hide: ECM_ENHANCED_CONFIRMATION_SERVICE,
CONTACT_KEYS_SERVICE,
+ RANGING_SERVICE,
})
@Retention(RetentionPolicy.SOURCE)
@@ -6402,6 +6403,17 @@
/**
* Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.ranging.RangingManager}.
+ *
+ * @see #getSystemService(String)
+ * @hide
+ */
+ @FlaggedApi(com.android.ranging.flags.Flags.FLAG_RANGING_STACK_ENABLED)
+ @SystemApi
+ public static final String RANGING_SERVICE = "ranging";
+
+ /**
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.DreamManager} for controlling Dream states.
*
* @see #getSystemService(String)
diff --git a/core/java/android/content/pm/Android.bp b/core/java/android/content/pm/Android.bp
new file mode 100644
index 0000000..057b5da
--- /dev/null
+++ b/core/java/android/content/pm/Android.bp
@@ -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 {
+ default_team: "trendy_team_framework_android_packages",
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "framework-pm-sources",
+ srcs: [
+ "**/*.java",
+ "**/*.aidl",
+ ],
+ exclude_srcs: [
+ "dex/**/*.java",
+ "overlay/**/*.java",
+ "permission/**/*.java",
+ ],
+ visibility: ["//frameworks/base"],
+}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index d40b2e3..2162792 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -818,7 +818,7 @@
}
boolean hasConcurrentStreams =
- CameraManagerGlobal.get().cameraIdHasConcurrentStreamsLocked(cameraId,
+ CameraManagerGlobal.get().cameraIdHasConcurrentStreams(cameraId,
mContext.getDeviceId(), getDevicePolicyFromContext(mContext));
metadata.setHasMandatoryConcurrentStreams(hasConcurrentStreams);
@@ -2629,24 +2629,26 @@
* @return Whether the camera device was found in the set of combinations returned by
* getConcurrentCameraIds
*/
- public boolean cameraIdHasConcurrentStreamsLocked(String cameraId, int deviceId,
+ public boolean cameraIdHasConcurrentStreams(String cameraId, int deviceId,
int devicePolicy) {
- DeviceCameraInfo info = new DeviceCameraInfo(cameraId,
- devicePolicy == DEVICE_POLICY_DEFAULT ? DEVICE_ID_DEFAULT : deviceId);
- if (!mDeviceStatus.containsKey(info)) {
- // physical camera ids aren't advertised in concurrent camera id combinations.
- if (DEBUG) {
- Log.v(TAG, " physical camera id " + cameraId + " is hidden." +
- " Available logical camera ids : " + mDeviceStatus);
+ synchronized (mLock) {
+ DeviceCameraInfo info = new DeviceCameraInfo(cameraId,
+ devicePolicy == DEVICE_POLICY_DEFAULT ? DEVICE_ID_DEFAULT : deviceId);
+ if (!mDeviceStatus.containsKey(info)) {
+ // physical camera ids aren't advertised in concurrent camera id combinations.
+ if (DEBUG) {
+ Log.v(TAG, " physical camera id " + cameraId + " is hidden."
+ + " Available logical camera ids : " + mDeviceStatus);
+ }
+ return false;
+ }
+ for (Set<DeviceCameraInfo> comb : mConcurrentCameraIdCombinations) {
+ if (comb.contains(info)) {
+ return true;
+ }
}
return false;
}
- for (Set<DeviceCameraInfo> comb : mConcurrentCameraIdCombinations) {
- if (comb.contains(info)) {
- return true;
- }
- }
- return false;
}
public void setTorchMode(
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 9612a53..7185719 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -1445,7 +1445,7 @@
* system's display configuration.
*/
public static final String CACHE_KEY_DISPLAY_INFO_PROPERTY =
- "cache_key.display_info";
+ PropertyInvalidatedCache.createSystemCacheKey("display_info");
/**
* Invalidates the contents of the display info cache for all applications. Can only
diff --git a/core/java/android/hardware/fingerprint/FingerprintCallback.java b/core/java/android/hardware/fingerprint/FingerprintCallback.java
index e4fbe6e..24e9f9d 100644
--- a/core/java/android/hardware/fingerprint/FingerprintCallback.java
+++ b/core/java/android/hardware/fingerprint/FingerprintCallback.java
@@ -189,7 +189,7 @@
mEnrollmentCallback.onAcquired(acquireInfo == FINGERPRINT_ACQUIRED_GOOD);
}
final String msg = getAcquiredString(context, acquireInfo, vendorCode);
- if (msg == null) {
+ if (msg == null || msg.isEmpty()) {
return;
}
// emulate HAL 2.1 behavior and send real acquiredInfo
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 983bbc3..75683f6 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -138,3 +138,10 @@
description: "Controls whether external mouse vertical scrolling can be reversed"
bug: "352598211"
}
+
+flag {
+ name: "mouse_swap_primary_button"
+ namespace: "input"
+ description: "Controls whether the connected mice's primary buttons, left and right, can be swapped."
+ bug: "352598211"
+}
diff --git a/core/java/android/os/Handler.java b/core/java/android/os/Handler.java
index 80f39bf..d0828c3 100644
--- a/core/java/android/os/Handler.java
+++ b/core/java/android/os/Handler.java
@@ -16,8 +16,10 @@
package android.os;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.util.Log;
import android.util.Printer;
@@ -819,16 +821,25 @@
}
/**
+ * WARNING: This API is dangerous because if the implementation
+ * of equals() is broken, it would delete unrelated events. For example,
+ * if object.equals() always returns true, it'd remove all messages.
+ *
+ * For this reason, never expose this API to non-platform code. i.e.
+ * this shouldn't be exposed to SystemApi.PRIVILEGED_APPS.
+ *
* Remove any pending posts of messages with code 'what' and whose obj is
* 'object' that are in the message queue. If <var>object</var> is null,
* all messages will be removed.
- * <p>
- * Similar to {@link #removeMessages(int, Object)} but uses object equality
+ *
+ * <p>Similar to {@link #removeMessages(int, Object)} but uses object equality
* ({@link Object#equals(Object)}) instead of reference equality (==) in
* determining whether object is the message's obj'.
*
*@hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
public final void removeEqualMessages(int what, @Nullable Object object) {
mQueue.removeEqualMessages(this, what, disallowNullArgumentIfShared(object));
}
@@ -843,12 +854,25 @@
}
/**
+ * WARNING: This API is dangerous because if the implementation
+ * of equals() is broken, it would delete unrelated events. For example,
+ * if object.equals() always returns true, it'd remove all messages.
+ *
+ * For this reason, never expose this API to non-platform code. i.e.
+ * this shouldn't be exposed to SystemApi.PRIVILEGED_APPS.
+ *
* Remove any pending posts of callbacks and sent messages whose
* <var>obj</var> is <var>token</var>. If <var>token</var> is null,
* all callbacks and messages will be removed.
*
+ * <p>Similar to {@link #removeCallbacksAndMessages(Object)} but uses object
+ * equality ({@link Object#equals(Object)}) instead of reference equality (==) in
+ * determining whether object is the message's obj'.
+ *
*@hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
public final void removeCallbacksAndEqualMessages(@Nullable Object token) {
mQueue.removeCallbacksAndEqualMessages(this, disallowNullArgumentIfShared(token));
}
@@ -864,6 +888,8 @@
* Return whether there are any messages or callbacks currently scheduled on this handler.
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
public final boolean hasMessagesOrCallbacks() {
return mQueue.hasMessages(this);
}
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 026013c..e4c12b6 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -1144,9 +1144,10 @@
}
private static final String CACHE_KEY_IS_POWER_SAVE_MODE_PROPERTY =
- "cache_key.is_power_save_mode";
+ PropertyInvalidatedCache.createSystemCacheKey("is_power_save_mode");
- private static final String CACHE_KEY_IS_INTERACTIVE_PROPERTY = "cache_key.is_interactive";
+ private static final String CACHE_KEY_IS_INTERACTIVE_PROPERTY =
+ PropertyInvalidatedCache.createSystemCacheKey("is_interactive");
private static final int MAX_CACHE_ENTRIES = 1;
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 738d129..f670601 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -216,3 +216,11 @@
bug: "346294653"
is_exported: true
}
+
+flag {
+ name: "mainline_vcn_platform_api"
+ namespace: "vcn"
+ description: "Expose platform APIs to mainline VCN"
+ is_exported: true
+ bug: "366598445"
+}
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index 7e51cb0..e98397d 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -1796,7 +1796,8 @@
}
/** @hide */
- public static final String CACHE_KEY_PACKAGE_INFO = "cache_key.package_info";
+ public static final String CACHE_KEY_PACKAGE_INFO =
+ PropertyInvalidatedCache.createSystemCacheKey("package_info");
/** @hide */
private static final PropertyInvalidatedCache<PermissionQuery, Integer> sPermissionCache =
diff --git a/core/java/android/window/TransitionFilter.java b/core/java/android/window/TransitionFilter.java
index 3cfde87..8bb4c52 100644
--- a/core/java/android/window/TransitionFilter.java
+++ b/core/java/android/window/TransitionFilter.java
@@ -187,6 +187,7 @@
/** If non-null, requires the change to specifically have or not-have a custom animation. */
public Boolean mCustomAnimation = null;
+ public IBinder mTaskFragmentToken = null;
public Requirement() {
}
@@ -204,12 +205,19 @@
// 0: null, 1: false, 2: true
final int customAnimRaw = in.readInt();
mCustomAnimation = customAnimRaw == 0 ? null : Boolean.valueOf(customAnimRaw == 2);
+ mTaskFragmentToken = in.readStrongBinder();
}
/** Go through changes and find if at-least one change matches this filter */
boolean matches(@NonNull TransitionInfo info) {
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
+
+ if (mTaskFragmentToken != null
+ && !mTaskFragmentToken.equals(change.getTaskFragmentToken())) {
+ continue;
+ }
+
if (mMustBeIndependent && !TransitionInfo.isIndependent(change, info)) {
// Only look at independent animating windows.
continue;
@@ -313,6 +321,7 @@
dest.writeStrongBinder(mLaunchCookie);
int customAnimRaw = mCustomAnimation == null ? 0 : (mCustomAnimation ? 2 : 1);
dest.writeInt(customAnimRaw);
+ dest.writeStrongBinder(mTaskFragmentToken);
}
@NonNull
@@ -357,6 +366,9 @@
if (mCustomAnimation != null) {
out.append(" customAnim=").append(mCustomAnimation.booleanValue());
}
+ if (mTaskFragmentToken != null) {
+ out.append(" taskFragmentToken=").append(mTaskFragmentToken);
+ }
out.append("}");
return out.toString();
}
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index ec79f94..14505f5 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -49,6 +49,7 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
+import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.Surface;
@@ -681,6 +682,7 @@
private float mSnapshotLuma;
private ComponentName mActivityComponent = null;
private AnimationOptions mAnimationOptions = null;
+ private IBinder mTaskFragmentToken = null;
public Change(@Nullable WindowContainerToken container, @NonNull SurfaceControl leash) {
mContainer = container;
@@ -712,6 +714,7 @@
mSnapshotLuma = in.readFloat();
mActivityComponent = in.readTypedObject(ComponentName.CREATOR);
mAnimationOptions = in.readTypedObject(AnimationOptions.CREATOR);
+ mTaskFragmentToken = in.readStrongBinder();
}
private Change localRemoteCopy() {
@@ -737,6 +740,7 @@
out.mSnapshotLuma = mSnapshotLuma;
out.mActivityComponent = mActivityComponent;
out.mAnimationOptions = mAnimationOptions;
+ out.mTaskFragmentToken = mTaskFragmentToken;
return out;
}
@@ -854,6 +858,14 @@
mAnimationOptions = options;
}
+ /**
+ * Sets the client-defined TaskFragment token. Only set this if the window is a
+ * client-organized TaskFragment.
+ */
+ public void setTaskFragmentToken(@Nullable IBinder token) {
+ mTaskFragmentToken = token;
+ }
+
/** @return the container that is changing. May be null if non-remotable (eg. activity) */
@Nullable
public WindowContainerToken getContainer() {
@@ -1009,6 +1021,15 @@
return mAnimationOptions;
}
+ /**
+ * Returns the client-defined TaskFragment token. {@code null} if this window is not a
+ * client-organized TaskFragment.
+ */
+ @Nullable
+ public IBinder getTaskFragmentToken() {
+ return mTaskFragmentToken;
+ }
+
/** @hide */
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -1035,6 +1056,7 @@
dest.writeFloat(mSnapshotLuma);
dest.writeTypedObject(mActivityComponent, flags);
dest.writeTypedObject(mAnimationOptions, flags);
+ dest.writeStrongBinder(mTaskFragmentToken);
}
@NonNull
@@ -1110,6 +1132,9 @@
if (mAnimationOptions != null) {
sb.append(" opt=").append(mAnimationOptions);
}
+ if (mTaskFragmentToken != null) {
+ sb.append(" taskFragmentToken=").append(mTaskFragmentToken);
+ }
sb.append('}');
return sb.toString();
}
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index fe8b818..a14461a 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -17,7 +17,7 @@
<resources>
<!-- Determines whether the shell features all run on another thread. This is to be overrided
by the resources of the app using the Shell library. -->
- <bool name="config_enableShellMainThread">false</bool>
+ <bool name="config_enableShellMainThread">true</bool>
<!-- Determines whether to register the shell task organizer on init.
TODO(b/238217847): This config is temporary until we refactor the base WMComponent. -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 2f4d77b..584f272 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -75,6 +75,7 @@
import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator;
import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter;
import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository;
@@ -141,7 +142,7 @@
includes = {
WMShellBaseModule.class,
PipModule.class,
- ShellBackAnimationModule.class,
+ ShellBackAnimationModule.class
})
public abstract class WMShellModule {
@@ -247,6 +248,7 @@
AssistContentRequester assistContentRequester,
MultiInstanceHelper multiInstanceHelper,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler,
WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
@@ -272,6 +274,7 @@
assistContentRequester,
multiInstanceHelper,
desktopTasksLimiter,
+ windowDecorCaptionHandleRepository,
desktopActivityOrientationHandler,
windowDecorViewHostSupplier);
}
@@ -780,6 +783,12 @@
@WMSingleton
@Provides
+ static WindowDecorCaptionHandleRepository provideAppHandleRepository() {
+ return new WindowDecorCaptionHandleRepository();
+ }
+
+ @WMSingleton
+ @Provides
static AppHandleEducationController provideAppHandleEducationController(
AppHandleEducationFilter appHandleEducationFilter,
ShellTaskOrganizer shellTaskOrganizer,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt
new file mode 100644
index 0000000..7ae5370
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.Rect
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/** Repository to observe caption state. */
+class WindowDecorCaptionHandleRepository {
+ private val _captionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption)
+ /** Observer for app handle state changes. */
+ val captionStateFlow: StateFlow<CaptionState> = _captionStateFlow
+
+ /** Notifies [captionStateFlow] if there is a change to caption state. */
+ fun notifyCaptionChanged(captionState: CaptionState) {
+ _captionStateFlow.value = captionState
+ }
+}
+
+/**
+ * Represents the current status of the caption.
+ *
+ * It can be one of three options:
+ * * [AppHandle]: Indicating that there is at least one visible app handle on the screen.
+ * * [AppHeader]: Indicating that there is at least one visible app chip on the screen.
+ * * [NoCaption]: Signifying that no caption handle is currently visible on the device.
+ */
+sealed class CaptionState {
+ data class AppHandle(
+ val runningTaskInfo: RunningTaskInfo,
+ val isHandleMenuExpanded: Boolean,
+ val globalAppHandleBounds: Rect
+ ) : CaptionState()
+
+ data class AppHeader(
+ val runningTaskInfo: RunningTaskInfo,
+ val isHeaderMenuExpanded: Boolean,
+ val globalAppChipBounds: Rect
+ ) : CaptionState()
+
+ data object NoCaption : CaptionState()
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index 82fbfad..5710af6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -204,8 +204,7 @@
@NonNull
@Override
public Rect getFloatingBoundsOnScreen() {
- return !mPipBoundsState.getMotionBoundsState().getAnimatingToBounds().isEmpty()
- ? mPipBoundsState.getMotionBoundsState().getAnimatingToBounds() : getBounds();
+ return getBounds();
}
@NonNull
@@ -616,7 +615,7 @@
cancelPhysicsAnimation();
}
- setAnimatingToBounds(new Rect(
+ mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(new Rect(
(int) toX,
(int) toY,
(int) toX + getBounds().width(),
@@ -660,6 +659,9 @@
// All motion operations have actually finished.
mPipBoundsState.setBounds(
mPipBoundsState.getMotionBoundsState().getBoundsInMotion());
+ // Notifies the floating coordinator that we moved, so we return these bounds from
+ // {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}.
+ mFloatingContentCoordinator.onContentMoved(this);
mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded();
if (!mDismissalPending) {
// do not schedule resize if PiP is dismissing, which may cause app re-open to
@@ -674,16 +676,6 @@
}
/**
- * Notifies the floating coordinator that we're moving, and sets the animating to bounds so
- * we return these bounds from
- * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}.
- */
- private void setAnimatingToBounds(Rect bounds) {
- mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(bounds);
- mFloatingContentCoordinator.onContentMoved(this);
- }
-
- /**
* Directly resizes the PiP to the given {@param bounds}.
*/
private void resizePipUnchecked(Rect toBounds) {
@@ -712,7 +704,7 @@
// This is so all the proper callbacks are performed.
mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration,
TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND, null /* updateBoundsCallback */);
- setAnimatingToBounds(toBounds);
+ mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(toBounds);
}
/**
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 caac2f6..7ea0bd6 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
@@ -109,6 +109,7 @@
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
@@ -164,7 +165,9 @@
private final InputManager mInputManager;
private final InteractionJankMonitor mInteractionJankMonitor;
private final MultiInstanceHelper mMultiInstanceHelper;
+ private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter;
+ private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory;
private final WindowDecorViewHostSupplier mWindowDecorViewHostSupplier;
private boolean mTransitionDragActive;
@@ -234,6 +237,7 @@
AssistContentRequester assistContentRequester,
MultiInstanceHelper multiInstanceHelper,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
this(
@@ -259,10 +263,12 @@
new DesktopModeWindowDecoration.Factory(),
new InputMonitorFactory(),
SurfaceControl.Transaction::new,
+ new AppHeaderViewHolder.Factory(),
rootTaskDisplayAreaOrganizer,
new SparseArray<>(),
interactionJankMonitor,
desktopTasksLimiter,
+ windowDecorCaptionHandleRepository,
activityOrientationChangeHandler,
new TaskPositionerFactory());
}
@@ -291,10 +297,12 @@
DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
InputMonitorFactory inputMonitorFactory,
Supplier<SurfaceControl.Transaction> transactionFactory,
+ AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId,
InteractionJankMonitor interactionJankMonitor,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
TaskPositionerFactory taskPositionerFactory) {
mContext = context;
@@ -317,6 +325,7 @@
mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory;
mInputMonitorFactory = inputMonitorFactory;
mTransactionFactory = transactionFactory;
+ mAppHeaderViewHolderFactory = appHeaderViewHolderFactory;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mGenericLinksParser = genericLinksParser;
mInputManager = mContext.getSystemService(InputManager.class);
@@ -325,6 +334,7 @@
com.android.internal.R.string.config_systemUi);
mInteractionJankMonitor = interactionJankMonitor;
mDesktopTasksLimiter = desktopTasksLimiter;
+ mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository;
mActivityOrientationChangeHandler = activityOrientationChangeHandler;
mAssistContentRequester = assistContentRequester;
mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> {
@@ -1377,10 +1387,12 @@
mBgExecutor,
mMainChoreographer,
mSyncQueue,
+ mAppHeaderViewHolderFactory,
mRootTaskDisplayAreaOrganizer,
mGenericLinksParser,
mAssistContentRequester,
mMultiInstanceHelper,
+ mWindowDecorCaptionHandleRepository,
mWindowDecorViewHostSupplier);
mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 16036be..10e4a39 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -28,6 +28,7 @@
import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode;
import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
@@ -85,6 +86,8 @@
import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -165,6 +168,7 @@
private ExclusionRegionListener mExclusionRegionListener;
+ private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory;
private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private final MaximizeMenuFactory mMaximizeMenuFactory;
private final HandleMenuFactory mHandleMenuFactory;
@@ -181,6 +185,7 @@
private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu;
private final Runnable mCapturedLinkExpiredRunnable = this::onCapturedLinkExpired;
private final MultiInstanceHelper mMultiInstanceHelper;
+ private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
DesktopModeWindowDecoration(
Context context,
@@ -194,20 +199,24 @@
@ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
+ AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
AppToWebGenericLinksParser genericLinksParser,
AssistContentRequester assistContentRequester,
MultiInstanceHelper multiInstanceHelper,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
this (context, userContext, displayController, splitScreenController, taskOrganizer,
taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue,
- rootTaskDisplayAreaOrganizer, genericLinksParser, assistContentRequester,
+ appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser,
+ assistContentRequester,
SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
WindowContainerTransaction::new, SurfaceControl::new, new WindowManagerWrapper(
context.getSystemService(WindowManager.class)),
new SurfaceControlViewHostFactory() {}, windowDecorViewHostSupplier,
DefaultMaximizeMenuFactory.INSTANCE,
- DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper);
+ DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper,
+ windowDecorCaptionHandleRepository);
}
DesktopModeWindowDecoration(
@@ -222,6 +231,7 @@
@ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
+ AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
AppToWebGenericLinksParser genericLinksParser,
AssistContentRequester assistContentRequester,
@@ -234,7 +244,8 @@
WindowDecorViewHostSupplier windowDecorViewHostSupplier,
MaximizeMenuFactory maximizeMenuFactory,
HandleMenuFactory handleMenuFactory,
- MultiInstanceHelper multiInstanceHelper) {
+ MultiInstanceHelper multiInstanceHelper,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) {
super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface,
surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
windowContainerTransactionSupplier, surfaceControlSupplier,
@@ -244,6 +255,7 @@
mBgExecutor = bgExecutor;
mChoreographer = choreographer;
mSyncQueue = syncQueue;
+ mAppHeaderViewHolderFactory = appHeaderViewHolderFactory;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mGenericLinksParser = genericLinksParser;
mAssistContentRequester = assistContentRequester;
@@ -251,6 +263,7 @@
mHandleMenuFactory = handleMenuFactory;
mMultiInstanceHelper = multiInstanceHelper;
mWindowManagerWrapper = windowManagerWrapper;
+ mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository;
}
/**
@@ -383,6 +396,9 @@
if (mResult.mRootView == null) {
// This means something blocks the window decor from showing, e.g. the task is hidden.
// Nothing is set up in this case including the decoration surface.
+ if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ notifyNoCaptionHandle();
+ }
disposeStatusBarInputLayer();
Trace.endSection(); // DesktopModeWindowDecoration#relayout
return;
@@ -398,6 +414,9 @@
position.set(determineHandlePosition());
}
Trace.beginSection("DesktopModeWindowDecoration#relayout-bindData");
+ if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ notifyCaptionStateChanged();
+ }
mWindowDecorViewHolder.bindData(mTaskInfo,
position,
mResult.mCaptionWidth,
@@ -407,6 +426,7 @@
if (!mTaskInfo.isFocused) {
closeHandleMenu();
+ closeManageWindowsMenu();
closeMaximizeMenu();
}
updateDragResizeListener(oldDecorationSurface);
@@ -507,6 +527,67 @@
return taskInfo.isFreeform() && taskInfo.isResizeable;
}
+ private void notifyCaptionStateChanged() {
+ // TODO: b/366159408 - Ensure bounds sent with notification account for RTL mode.
+ if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) {
+ return;
+ }
+ if (!isCaptionVisible()) {
+ notifyNoCaptionHandle();
+ } else if (isAppHandle(mWindowDecorViewHolder)) {
+ // App handle is visible since `mWindowDecorViewHolder` is of type
+ // [AppHandleViewHolder].
+ final CaptionState captionState = new CaptionState.AppHandle(mTaskInfo,
+ isHandleMenuActive(), getCurrentAppHandleBounds());
+ mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
+ } else {
+ // App header is visible since `mWindowDecorViewHolder` is of type
+ // [AppHeaderViewHolder].
+ ((AppHeaderViewHolder) mWindowDecorViewHolder).runOnAppChipGlobalLayout(
+ () -> {
+ notifyAppChipStateChanged();
+ return Unit.INSTANCE;
+ });
+ }
+ }
+
+ private void notifyNoCaptionHandle() {
+ if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) {
+ return;
+ }
+ mWindowDecorCaptionHandleRepository.notifyCaptionChanged(
+ CaptionState.NoCaption.INSTANCE);
+ }
+
+ private Rect getCurrentAppHandleBounds() {
+ return new Rect(
+ mResult.mCaptionX,
+ /* top= */0,
+ mResult.mCaptionX + mResult.mCaptionWidth,
+ mResult.mCaptionHeight);
+ }
+
+ private void notifyAppChipStateChanged() {
+ final Rect appChipPositionInWindow =
+ ((AppHeaderViewHolder) mWindowDecorViewHolder).getAppChipLocationInWindow();
+ final Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds();
+ final Rect appChipGlobalPosition = new Rect(
+ taskBounds.left + appChipPositionInWindow.left,
+ taskBounds.top + appChipPositionInWindow.top,
+ taskBounds.left + appChipPositionInWindow.right,
+ taskBounds.top + appChipPositionInWindow.bottom);
+ final CaptionState captionState = new CaptionState.AppHeader(
+ mTaskInfo,
+ isHandleMenuActive(),
+ appChipGlobalPosition);
+
+ mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
+ }
+
+ private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
+ return taskInfo.isFreeform() && taskInfo.isResizeable;
+ }
+
private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
if (!isDragResizable(mTaskInfo, mContext) || !isMaximizeMenuActive()) {
return;
@@ -556,7 +637,7 @@
} else if (mRelayoutParams.mLayoutResId
== R.layout.desktop_mode_app_header) {
loadAppInfoIfNeeded();
- return new AppHeaderViewHolder(
+ return mAppHeaderViewHolderFactory.create(
mResult.mRootView,
mOnCaptionTouchListener,
mOnCaptionButtonClickListener,
@@ -994,7 +1075,7 @@
mAppIconBitmap,
mAppName,
mSplitScreenController,
- DesktopModeStatus.canEnterDesktopMode(mContext),
+ canEnterDesktopMode(mContext),
supportsMultiInstance,
shouldShowManageWindowsButton,
getBrowserLink(),
@@ -1027,6 +1108,9 @@
return Unit.INSTANCE;
}
);
+ if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ notifyCaptionStateChanged();
+ }
mMinimumInstancesFound = false;
}
@@ -1067,7 +1151,10 @@
}
void closeManageWindowsMenu() {
- mManageWindowsMenu.close();
+ if (mManageWindowsMenu != null) {
+ mManageWindowsMenu.close();
+ }
+ mManageWindowsMenu = null;
}
private void updateGenericLink() {
@@ -1089,11 +1176,15 @@
mWindowDecorViewHolder.onHandleMenuClosed();
mHandleMenu.close();
mHandleMenu = null;
+ if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ notifyCaptionStateChanged();
+ }
}
@Override
void releaseViews(WindowContainerTransaction wct) {
closeHandleMenu();
+ closeManageWindowsMenu();
closeMaximizeMenu();
super.releaseViews(wct);
}
@@ -1257,9 +1348,14 @@
public void close() {
closeDragResizeListener();
closeHandleMenu();
+ closeManageWindowsMenu();
mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId);
disposeResizeVeil();
disposeStatusBarInputLayer();
+ if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ notifyNoCaptionHandle();
+ }
+
super.close();
}
@@ -1367,10 +1463,12 @@
@ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
+ AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
AppToWebGenericLinksParser genericLinksParser,
AssistContentRequester assistContentRequester,
MultiInstanceHelper multiInstanceHelper,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
return new DesktopModeWindowDecoration(
context,
@@ -1384,10 +1482,12 @@
bgExecutor,
choreographer,
syncQueue,
+ appHeaderViewHolderFactory,
rootTaskDisplayAreaOrganizer,
genericLinksParser,
assistContentRequester,
multiInstanceHelper,
+ windowDecorCaptionHandleRepository,
windowDecorViewHostSupplier);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 033d695..4a8cabc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -22,12 +22,14 @@
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Point
+import android.graphics.Rect
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RoundRectShape
import android.view.View
import android.view.View.OnLongClickListener
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
@@ -62,7 +64,7 @@
* finer controls such as a close window button and an "app info" section to pull up additional
* controls.
*/
-internal class AppHeaderViewHolder(
+class AppHeaderViewHolder(
rootView: View,
onCaptionTouchListener: View.OnTouchListener,
onCaptionButtonClickListener: View.OnClickListener,
@@ -279,6 +281,34 @@
maximizeButtonView.startHoverAnimation()
}
+ fun runOnAppChipGlobalLayout(runnable: () -> Unit) {
+ if (openMenuButton.isAttachedToWindow) {
+ // App chip is already inflated.
+ runnable()
+ return
+ }
+ // Wait for app chip to be inflated before notifying repository.
+ openMenuButton.viewTreeObserver.addOnGlobalLayoutListener(object :
+ OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ runnable()
+ openMenuButton.viewTreeObserver.removeOnGlobalLayoutListener(this)
+ }
+ })
+ }
+
+ fun getAppChipLocationInWindow(): Rect {
+ val appChipBoundsInWindow = IntArray(2)
+ openMenuButton.getLocationInWindow(appChipBoundsInWindow)
+
+ return Rect(
+ /* left = */ appChipBoundsInWindow[0],
+ /* top = */ appChipBoundsInWindow[1],
+ /* right = */ appChipBoundsInWindow[0] + openMenuButton.width,
+ /* bottom = */ appChipBoundsInWindow[1] + openMenuButton.height
+ )
+ }
+
private fun getHeaderStyle(header: Header): HeaderStyle {
return HeaderStyle(
background = getHeaderBackground(header),
@@ -529,4 +559,26 @@
private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65%
private const val FOCUSED_OPACITY = 255
}
+
+ class Factory {
+ fun create(
+ rootView: View,
+ onCaptionTouchListener: View.OnTouchListener,
+ onCaptionButtonClickListener: View.OnClickListener,
+ onLongClickListener: OnLongClickListener,
+ onCaptionGenericMotionListener: View.OnGenericMotionListener,
+ appName: CharSequence,
+ appIconBitmap: Bitmap,
+ onMaximizeHoverAnimationFinishedListener: () -> Unit,
+ ): AppHeaderViewHolder = AppHeaderViewHolder(
+ rootView,
+ onCaptionTouchListener,
+ onCaptionButtonClickListener,
+ onLongClickListener,
+ onCaptionGenericMotionListener,
+ appName,
+ appIconBitmap,
+ onMaximizeHoverAnimationFinishedListener,
+ )
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
index 2341b09..5ea55b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
@@ -24,7 +24,7 @@
* Encapsulates the root [View] of a window decoration and its children to facilitate looking up
* children (via findViewById) and updating to the latest data from [RunningTaskInfo].
*/
-internal abstract class WindowDecorationViewHolder(rootView: View) {
+abstract class WindowDecorationViewHolder(rootView: View) {
val context: Context = rootView.context
/**
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/OWNERS
new file mode 100644
index 0000000..622f837
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 1168918
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/OWNERS
new file mode 100644
index 0000000..553540c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 929241
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/OWNERS
new file mode 100644
index 0000000..983e878
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 555586
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/OWNERS
new file mode 100644
index 0000000..5b05af9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 970984
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/OWNERS
new file mode 100644
index 0000000..553540c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 929241
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
new file mode 100644
index 0000000..e3caf2e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
@@ -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 com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class WindowDecorCaptionHandleRepositoryTest {
+ private lateinit var captionHandleRepository: WindowDecorCaptionHandleRepository
+
+ @Before
+ fun setUp() {
+ captionHandleRepository = WindowDecorCaptionHandleRepository()
+ }
+
+ @Test
+ fun initialState_noAction_returnsNoCaption() {
+ // Check the initial value of `captionStateFlow`.
+ assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption)
+ }
+
+ @Test
+ fun notifyCaptionChange_toAppHandleVisible_updatesStateWithCorrectData() {
+ val taskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, GMAIL_PACKAGE_NAME)
+ val appHandleCaptionState =
+ CaptionState.AppHandle(
+ taskInfo, false, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3))
+
+ captionHandleRepository.notifyCaptionChanged(appHandleCaptionState)
+
+ assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHandleCaptionState)
+ }
+
+ @Test
+ fun notifyCaptionChange_toAppChipVisible_updatesStateWithCorrectData() {
+ val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, GMAIL_PACKAGE_NAME)
+ val appHeaderCaptionState =
+ CaptionState.AppHeader(
+ taskInfo, true, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3))
+
+ captionHandleRepository.notifyCaptionChanged(appHeaderCaptionState)
+
+ assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHeaderCaptionState)
+ }
+
+ @Test
+ fun notifyCaptionChange_toNoCaption_updatesState() {
+ captionHandleRepository.notifyCaptionChanged(CaptionState.NoCaption)
+
+ assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption)
+ }
+
+ private fun createTaskInfo(
+ deviceWindowingMode: Int = WINDOWING_MODE_UNDEFINED,
+ runningTaskPackageName: String = LAUNCHER_PACKAGE_NAME
+ ): RunningTaskInfo =
+ RunningTaskInfo().apply {
+ configuration.windowConfiguration.apply { windowingMode = deviceWindowingMode }
+ topActivityInfo?.apply { packageName = runningTaskPackageName }
+ }
+
+ private companion object {
+ const val GMAIL_PACKAGE_NAME = "com.google.android.gm"
+ const val LAUNCHER_PACKAGE_NAME = "com.google.android.apps.nexuslauncher"
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/OWNERS
new file mode 100644
index 0000000..cb12401
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 1214056
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/OWNERS
new file mode 100644
index 0000000..553540c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 929241
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OWNERS
new file mode 100644
index 0000000..b66cfc3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 785166
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/OWNERS
new file mode 100644
index 0000000..ad3ca73
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 316251
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/OWNERS
new file mode 100644
index 0000000..ad3ca73
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 316251
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/OWNERS
new file mode 100644
index 0000000..aa019cc
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 1199235
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/OWNERS
new file mode 100644
index 0000000..9d926b2
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 928697
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/OWNERS
new file mode 100644
index 0000000..a24088a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 316275
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index fec9e3e..aea14b9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -332,6 +332,35 @@
}
@Test
+ public void testTransitionFilterTaskFragmentToken() {
+ final IBinder taskFragmentToken = new Binder();
+
+ TransitionFilter filter = new TransitionFilter();
+ filter.mRequirements =
+ new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()};
+ filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+ filter.mRequirements[0].mTaskFragmentToken = taskFragmentToken;
+
+ // Transition with the same token should match.
+ final TransitionInfo infoHasTaskFragmentToken = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN, taskFragmentToken).build();
+ assertTrue(filter.matches(infoHasTaskFragmentToken));
+
+ // Transition with a different token should not match.
+ final IBinder differentTaskFragmentToken = new Binder();
+ final TransitionInfo infoDifferentTaskFragmentToken =
+ new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN, differentTaskFragmentToken).build();
+ assertFalse(filter.matches(infoDifferentTaskFragmentToken));
+
+ // Transition without a token should not match.
+ final TransitionInfo infoNoTaskFragmentToken = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN, createTaskInfo(
+ 1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD)).build();
+ assertFalse(filter.matches(infoNoTaskFragmentToken));
+ }
+
+ @Test
public void testTransitionFilterMultiRequirement() {
// filter that requires at-least one opening and one closing app
TransitionFilter filter = new TransitionFilter();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
index b8939e6f..49ae182 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
@@ -20,8 +20,10 @@
import static org.mockito.Mockito.mock;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.ComponentName;
+import android.os.IBinder;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.TransitionInfo;
@@ -51,21 +53,24 @@
}
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
- @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo,
- ComponentName activityComponent) {
+ @TransitionInfo.ChangeFlags int flags,
+ @Nullable ActivityManager.RunningTaskInfo taskInfo,
+ @Nullable ComponentName activityComponent, @Nullable IBinder taskFragmentToken) {
final TransitionInfo.Change change = new TransitionInfo.Change(
taskInfo != null ? taskInfo.token : null, createMockSurface(true /* valid */));
change.setMode(mode);
change.setFlags(flags);
change.setTaskInfo(taskInfo);
change.setActivityComponent(activityComponent);
+ change.setTaskFragmentToken(taskFragmentToken);
return addChange(change);
}
/** Add a change to the TransitionInfo */
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
@TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) {
- return addChange(mode, flags, taskInfo, null /* activityComponent */);
+ return addChange(mode, flags, taskInfo, null /* activityComponent */,
+ null /* taskFragmentToken */);
}
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
@@ -76,13 +81,21 @@
/** Add a change to the TransitionInfo */
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
ComponentName activityComponent) {
- return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent);
+ return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent,
+ null /* taskFragmentToken */);
}
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) {
return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */);
}
+ /** Add a change with a TaskFragment token to the TransitionInfo */
+ public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
+ @Nullable IBinder taskFragmentToken) {
+ return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */,
+ null /* activityComponent */, taskFragmentToken);
+ }
+
public TransitionInfoBuilder addChange(TransitionInfo.Change change) {
change.setDisplayId(DISPLAY_ID, DISPLAY_ID);
mInfo.addChange(change);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/OWNERS
new file mode 100644
index 0000000..f5ba614
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 1267635
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 85bc7cc..ee2a41c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -88,6 +88,7 @@
import com.android.wm.shell.desktopmode.DesktopTasksController
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
import com.android.wm.shell.desktopmode.DesktopTasksLimiter
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
@@ -98,6 +99,7 @@
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
+import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder
import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier
import java.util.Optional
import java.util.function.Consumer
@@ -164,6 +166,7 @@
DesktopModeWindowDecorViewModel.InputMonitorFactory
@Mock private lateinit var mockShellController: ShellController
@Mock private lateinit var mockShellExecutor: ShellExecutor
+ @Mock private lateinit var mockAppHeaderViewHolderFactory: AppHeaderViewHolder.Factory
@Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock private lateinit var mockShellCommandHandler: ShellCommandHandler
@Mock private lateinit var mockWindowManager: IWindowManager
@@ -182,6 +185,7 @@
@Mock private lateinit var mockTaskPositionerFactory:
DesktopModeWindowDecorViewModel.TaskPositionerFactory
@Mock private lateinit var mockTaskPositioner: TaskPositioner
+ @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
@Mock private lateinit var mockWindowDecorViewHostSupplier: WindowDecorViewHostSupplier<*>
private lateinit var spyContext: TestableContext
@@ -236,10 +240,12 @@
mockDesktopModeWindowDecorFactory,
mockInputMonitorFactory,
transactionFactory,
+ mockAppHeaderViewHolderFactory,
mockRootTaskDisplayAreaOrganizer,
windowDecorByTaskIdSpy,
mockInteractionJankMonitor,
Optional.of(mockTasksLimiter),
+ mockCaptionHandleRepository,
Optional.of(mockActivityOrientationChangeHandler),
mockTaskPositionerFactory
)
@@ -1211,7 +1217,7 @@
whenever(
mockDesktopModeWindowDecorFactory.create(
any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
- any(), any(), any(), any(), any())
+ any(), any(), any(), any(), any(), any(), any())
).thenReturn(decoration)
decoration.mTaskInfo = task
whenever(decoration.isFocused).thenReturn(task.isFocused)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index dff42da..a1867f3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -19,9 +19,11 @@
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_UNDEFINED;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
+import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
@@ -38,6 +40,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -55,6 +58,7 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.PointF;
+import android.graphics.Rect;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemProperties;
@@ -68,11 +72,13 @@
import android.view.Choreographer;
import android.view.Display;
import android.view.GestureDetector;
+import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
+import android.view.WindowInsets;
import android.view.WindowManager;
import android.window.WindowContainerTransaction;
@@ -94,9 +100,12 @@
import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams;
+import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHost;
import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier;
@@ -153,6 +162,10 @@
@Mock
private SyncTransactionQueue mMockSyncQueue;
@Mock
+ private AppHeaderViewHolder.Factory mMockAppHeaderViewHolderFactory;
+ @Mock
+ private AppHeaderViewHolder mMockAppHeaderViewHolder;
+ @Mock
private RootTaskDisplayAreaOrganizer mMockRootTaskDisplayAreaOrganizer;
@Mock
private Supplier<SurfaceControl.Transaction> mMockTransactionSupplier;
@@ -192,6 +205,8 @@
private HandleMenuFactory mMockHandleMenuFactory;
@Mock
private MultiInstanceHelper mMockMultiInstanceHelper;
+ @Mock
+ private WindowDecorCaptionHandleRepository mMockCaptionHandleRepository;
@Captor
private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener;
@Captor
@@ -245,6 +260,8 @@
when(mMockWindowDecorViewHostSupplier.acquire(any(), eq(defaultDisplay)))
.thenReturn(mMockWindowDecorViewHost);
when(mMockWindowDecorViewHost.getSurfaceControl()).thenReturn(mock(SurfaceControl.class));
+ when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(),
+ any())).thenReturn(mMockAppHeaderViewHolder);
}
@After
@@ -838,6 +855,143 @@
assertFalse(decoration.isHandleMenuActive());
}
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ public void notifyCaptionStateChanged_flagDisabled_doNoNotify() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockCaptionHandleRepository, never()).notifyCaptionChanged(any());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ public void notifyCaptionStateChanged_inFullscreenMode_notifiesAppHandleVisible() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+ CaptionState.class);
+
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+ captionStateArgumentCaptor.capture());
+ assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+ CaptionState.AppHandle.class);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @Ignore("TODO(b/367235906): Due to MONITOR_INPUT permission error")
+ public void notifyCaptionStateChanged_inWindowingMode_notifiesAppHeaderVisible() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ when(mMockAppHeaderViewHolder.getAppChipLocationInWindow()).thenReturn(
+ new Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
+ taskInfo.isResizeable = false;
+ ArgumentCaptor<Function0<Unit>> runnableArgumentCaptor = ArgumentCaptor.forClass(
+ Function0.class);
+ ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+ CaptionState.class);
+
+ spyWindowDecor.relayout(taskInfo);
+ verify(mMockAppHeaderViewHolder, atLeastOnce()).runOnAppChipGlobalLayout(
+ runnableArgumentCaptor.capture());
+ runnableArgumentCaptor.getValue().invoke();
+
+ verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+ captionStateArgumentCaptor.capture());
+ assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+ CaptionState.AppHeader.class);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ public void notifyCaptionStateChanged_taskNotVisible_notifiesNoCaptionVisible() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ false);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED);
+ ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+ CaptionState.class);
+
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+ captionStateArgumentCaptor.capture());
+ assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+ CaptionState.NoCaption.class);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ public void notifyCaptionStateChanged_captionHandleExpanded_notifiesHandleMenuExpanded() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+ CaptionState.class);
+
+ spyWindowDecor.relayout(taskInfo);
+ createHandleMenu(spyWindowDecor);
+
+ verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+ captionStateArgumentCaptor.capture());
+ assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+ CaptionState.AppHandle.class);
+ assertThat(
+ ((CaptionState.AppHandle) captionStateArgumentCaptor.getValue())
+ .isHandleMenuExpanded()).isEqualTo(
+ true);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ public void notifyCaptionStateChanged_captionHandleClosed_notifiesHandleMenuClosed() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+ CaptionState.class);
+
+ spyWindowDecor.relayout(taskInfo);
+ createHandleMenu(spyWindowDecor);
+ spyWindowDecor.closeHandleMenu();
+
+ verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+ captionStateArgumentCaptor.capture());
+ assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+ CaptionState.AppHandle.class);
+ assertThat(
+ ((CaptionState.AppHandle) captionStateArgumentCaptor.getValue())
+ .isHandleMenuExpanded()).isEqualTo(
+ false);
+
+ }
+
private void verifyHandleMenuCreated(@Nullable Uri uri) {
verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(),
any(), anyBoolean(), anyBoolean(), anyBoolean(), eq(uri), anyInt(),
@@ -906,12 +1060,13 @@
final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
mContext, mMockDisplayController, mMockSplitScreenController,
mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor,
- mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
+ mMockChoreographer, mMockSyncQueue, mMockAppHeaderViewHolderFactory,
+ mMockRootTaskDisplayAreaOrganizer,
mMockGenericLinksParser, mMockAssistContentRequester, SurfaceControl.Builder::new,
mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new,
new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory,
mMockWindowDecorViewHostSupplier, maximizeMenuFactory, mMockHandleMenuFactory,
- mMockMultiInstanceHelper);
+ mMockMultiInstanceHelper, mMockCaptionHandleRepository);
windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener,
mMockTouchEventListener, mMockTouchEventListener);
windowDecor.setExclusionRegionListener(mMockExclusionRegionListener);
@@ -951,6 +1106,14 @@
!= 0;
}
+ private InsetsState createInsetsState(@WindowInsets.Type.InsetsType int type, boolean visible) {
+ final InsetsState state = new InsetsState();
+ final InsetsSource source = new InsetsSource(/* id= */0, type);
+ source.setVisible(visible);
+ state.addSource(source);
+ return state;
+ }
+
private static class TestTouchEventListener extends GestureDetector.SimpleOnGestureListener
implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
View.OnGenericMotionListener, DragDetector.MotionEventHandler {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/OWNERS
new file mode 100644
index 0000000..553540c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 929241
+# includes OWNERS from parent directories
\ No newline at end of file
diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java
index 6342489..306643d 100644
--- a/location/java/android/location/LocationManager.java
+++ b/location/java/android/location/LocationManager.java
@@ -451,7 +451,7 @@
private static final long MAX_SINGLE_LOCATION_TIMEOUT_MS = 30 * 1000;
private static final String CACHE_KEY_LOCATION_ENABLED_PROPERTY =
- "cache_key.location_enabled";
+ PropertyInvalidatedCache.createSystemCacheKey("location_enabled");
static ILocationManager getService() throws RemoteException {
try {
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index de85f1e..2804546 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -258,7 +258,7 @@
/**
* Mandatory String extra field in {@link #ACTION_TRANSACTION_DETECTED}
* Indicates the Secure Element on which the transaction occurred.
- * eSE1...eSEn for Embedded Secure Elements, SIM1...SIMn for UICC, etc.
+ * eSE1...eSEn for Embedded Secure Elements, SIM1...SIMn for UICC/EUICC, etc.
*/
public static final String EXTRA_SECURE_ELEMENT_NAME = "android.nfc.extra.SECURE_ELEMENT_NAME";
@@ -733,7 +733,7 @@
*
* @return List<String> containing secure elements on the device which supports
* off host card emulation. eSE for Embedded secure element,
- * SIM for UICC, eSIM for EUICC and so on.
+ * SIM for UICC/EUICC and so on.
* @hide
*/
public @NonNull List<String> getSupportedOffHostSecureElements() {
@@ -753,11 +753,6 @@
if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE)) {
offHostSE.add("eSE");
}
- if (Flags.enableCardEmulationEuicc()
- && callServiceReturn(
- () -> sCardEmulationService.isEuiccSupported(), false)) {
- offHostSE.add("eSIM");
- }
return offHostSE;
}
diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
index b28237c..2983875 100644
--- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -331,8 +331,6 @@
mOffHostName = "eSE1";
} else if (mOffHostName.equals("SIM")) {
mOffHostName = "SIM1";
- } else if (Flags.enableCardEmulationEuicc() && mOffHostName.equals("eSIM")) {
- mOffHostName = "eSIM1";
}
}
mStaticOffHostName = mOffHostName;
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index 83ad32c..4be082c 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -548,13 +548,11 @@
List<String> validSE = adapter.getSupportedOffHostSecureElements();
if ((offHostSecureElement.startsWith("eSE") && !validSE.contains("eSE"))
- || (offHostSecureElement.startsWith("SIM") && !validSE.contains("SIM"))
- || (offHostSecureElement.startsWith("eSIM") && !validSE.contains("eSIM"))) {
+ || (offHostSecureElement.startsWith("SIM") && !validSE.contains("SIM"))) {
return false;
}
- if (!offHostSecureElement.startsWith("eSE") && !offHostSecureElement.startsWith("SIM")
- && !(Flags.enableCardEmulationEuicc() && offHostSecureElement.startsWith("eSIM"))) {
+ if (!offHostSecureElement.startsWith("eSE") && !offHostSecureElement.startsWith("SIM")) {
return false;
}
@@ -562,8 +560,6 @@
offHostSecureElement = "eSE1";
} else if (offHostSecureElement.equals("SIM")) {
offHostSecureElement = "SIM1";
- } else if (Flags.enableCardEmulationEuicc() && offHostSecureElement.equals("eSIM")) {
- offHostSecureElement = "eSIM1";
}
final String offHostSecureElementV = new String(offHostSecureElement);
return callServiceReturn(() ->
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 0ae4da5..f64c305 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -2016,8 +2016,6 @@
if (!isValidMediaUri(name, value)) {
return false;
}
- // Invalidate any relevant cache files
- cacheFile.delete();
}
final boolean success;
@@ -2055,6 +2053,11 @@
return false;
}
+ if (cacheFile != null) {
+ // Invalidate any relevant cache files
+ cacheFile.delete();
+ }
+
if ((operation == MUTATION_OPERATION_INSERT || operation == MUTATION_OPERATION_UPDATE)
&& cacheFile != null && value != null) {
final Uri ringtoneUri = Uri.parse(value);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index cd16af7..bd7067b 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -860,6 +860,7 @@
resource_dirs: [],
kotlincflags: ["-Xjvm-default=all"],
optimize: {
+ optimize: false,
shrink_resources: false,
optimized_shrink_resources: false,
proguard_flags_files: ["proguard.flags"],
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index d86890b..437a4b3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -17,6 +17,7 @@
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD;
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;
@@ -1437,4 +1438,38 @@
// THEN vibrate is used
verify(mVibrator).performHapticFeedback(any(), eq(UdfpsController.LONG_PRESS));
}
+
+ @Test
+ public void onAcquiredCalbacks() {
+ runWithAllParams(
+ this::ultrasonicCallbackOnAcquired);
+ }
+
+ public void ultrasonicCallbackOnAcquired(TestParams testParams) throws RemoteException{
+ if (testParams.sensorProps.sensorType
+ == FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC) {
+ reset(mUdfpsView);
+
+ UdfpsController.Callback callbackMock = mock(UdfpsController.Callback.class);
+ mUdfpsController.addCallback(callbackMock);
+
+ // GIVEN UDFPS overlay is showing
+ mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
+ BiometricRequestConstants.REASON_AUTH_KEYGUARD,
+ mUdfpsOverlayControllerCallback);
+ mFgExecutor.runAllReady();
+
+ verify(mFingerprintManager).setUdfpsOverlayController(
+ mUdfpsOverlayControllerCaptor.capture());
+ mUdfpsOverlayControllerCaptor.getValue().onAcquired(0, FINGERPRINT_ACQUIRED_START);
+ mFgExecutor.runAllReady();
+
+ verify(callbackMock).onFingerDown();
+
+ mUdfpsOverlayControllerCaptor.getValue().onAcquired(0, FINGERPRINT_ACQUIRED_GOOD);
+ mFgExecutor.runAllReady();
+
+ verify(callbackMock).onFingerUp();
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
new file mode 100644
index 0000000..fa72d74
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.panels.ui.compose.selection
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MutableSelectionStateTest : SysuiTestCase() {
+ private val underTest = MutableSelectionState()
+
+ @Test
+ fun selectTile_isCorrectlySelected() {
+ assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
+
+ underTest.select(TEST_SPEC)
+ assertThat(underTest.isSelected(TEST_SPEC)).isTrue()
+
+ underTest.unSelect()
+ assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
+
+ val newSpec = TileSpec.create("newSpec")
+ underTest.select(TEST_SPEC)
+ underTest.select(newSpec)
+ assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
+ assertThat(underTest.isSelected(newSpec)).isTrue()
+ }
+
+ @Test
+ fun startResize_createsResizingState() {
+ assertThat(underTest.resizingState).isNull()
+
+ // Resizing starts but no tile is selected
+ underTest.onResizingDragStart(TileWidths(0, 0, 1)) {}
+ assertThat(underTest.resizingState).isNull()
+
+ // Resizing starts with a selected tile
+ underTest.select(TEST_SPEC)
+ underTest.onResizingDragStart(TileWidths(0, 0, 1)) {}
+
+ assertThat(underTest.resizingState).isNotNull()
+ }
+
+ @Test
+ fun endResize_clearsResizingState() {
+ val spec = TileSpec.create("testSpec")
+
+ // Resizing starts with a selected tile
+ underTest.select(spec)
+ underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+ assertThat(underTest.resizingState).isNotNull()
+
+ underTest.onResizingDragEnd()
+ assertThat(underTest.resizingState).isNull()
+ }
+
+ @Test
+ fun unselect_clearsResizingState() {
+ // Resizing starts with a selected tile
+ underTest.select(TEST_SPEC)
+ underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+ assertThat(underTest.resizingState).isNotNull()
+
+ underTest.unSelect()
+ assertThat(underTest.resizingState).isNull()
+ }
+
+ @Test
+ fun onResizingDrag_updatesResizingState() {
+ // Resizing starts with a selected tile
+ underTest.select(TEST_SPEC)
+ underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+ assertThat(underTest.resizingState).isNotNull()
+
+ underTest.onResizingDrag(5f)
+ assertThat(underTest.resizingState?.width).isEqualTo(5)
+
+ underTest.onResizingDrag(2f)
+ assertThat(underTest.resizingState?.width).isEqualTo(7)
+
+ underTest.onResizingDrag(-6f)
+ assertThat(underTest.resizingState?.width).isEqualTo(1)
+ }
+
+ @Test
+ fun onResizingDrag_receivesResizeCallback() {
+ var resized = false
+ val onResize: () -> Unit = { resized = !resized }
+
+ // Resizing starts with a selected tile
+ underTest.select(TEST_SPEC)
+ underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10), onResize)
+ assertThat(underTest.resizingState).isNotNull()
+
+ // Drag under the threshold
+ underTest.onResizingDrag(1f)
+ assertThat(resized).isFalse()
+
+ // Drag over the threshold
+ underTest.onResizingDrag(5f)
+ assertThat(resized).isTrue()
+
+ // Drag back under the threshold
+ underTest.onResizingDrag(-5f)
+ assertThat(resized).isFalse()
+ }
+
+ companion object {
+ private val TEST_SPEC = TileSpec.create("testSpec")
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingStateTest.kt
new file mode 100644
index 0000000..6e66783
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingStateTest.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.panels.ui.compose.selection
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ResizingStateTest : SysuiTestCase() {
+
+ @Test
+ fun drag_updatesStateCorrectly() {
+ var resized = false
+ val underTest =
+ ResizingState(TileWidths(base = 0, min = 0, max = 10)) { resized = !resized }
+
+ assertThat(underTest.width).isEqualTo(0)
+
+ underTest.onDrag(2f)
+ assertThat(underTest.width).isEqualTo(2)
+
+ underTest.onDrag(1f)
+ assertThat(underTest.width).isEqualTo(3)
+ assertThat(resized).isTrue()
+
+ underTest.onDrag(-1f)
+ assertThat(underTest.width).isEqualTo(2)
+ assertThat(resized).isFalse()
+ }
+
+ @Test
+ fun dragOutOfBounds_isClampedCorrectly() {
+ val underTest = ResizingState(TileWidths(base = 0, min = 0, max = 10)) {}
+
+ assertThat(underTest.width).isEqualTo(0)
+
+ underTest.onDrag(100f)
+ assertThat(underTest.width).isEqualTo(10)
+
+ underTest.onDrag(-200f)
+ assertThat(underTest.width).isEqualTo(0)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
index 3053672..2c8cc1a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
@@ -47,6 +47,7 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.time.SystemClock
+import com.google.common.truth.Truth.assertThat
import junit.framework.Assert
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -247,32 +248,35 @@
@Test
@EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testShowNotification_reorderNotAllowed_notPulsing_seenInShadeTrue() {
- whenever(mVSProvider.isReorderingAllowed).thenReturn(false)
- val hmp = createHeadsUpManagerPhone()
-
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- val row = mock<ExpandableNotificationRow>()
- whenever(row.showingPulsing()).thenReturn(false)
- notifEntry.row = row
-
- hmp.showNotification(notifEntry)
- Assert.assertTrue(notifEntry.isSeenInShade)
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testShowNotification_reorderAllowed_notPulsing_seenInShadeFalse() {
+ fun testShowNotification_removeWhenReorderingAllowedTrue() {
whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
val hmp = createHeadsUpManagerPhone()
val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- val row = mock<ExpandableNotificationRow>()
- whenever(row.showingPulsing()).thenReturn(false)
- notifEntry.row = row
-
hmp.showNotification(notifEntry)
- Assert.assertFalse(notifEntry.isSeenInShade)
+ assertThat(hmp.mEntriesToRemoveWhenReorderingAllowed.contains(notifEntry)).isTrue();
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testShowNotification_reorderNotAllowed_seenInShadeTrue() {
+ whenever(mVSProvider.isReorderingAllowed).thenReturn(false)
+ val hmp = createHeadsUpManagerPhone()
+
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ hmp.showNotification(notifEntry)
+ assertThat(notifEntry.isSeenInShade).isTrue();
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testShowNotification_reorderAllowed_seenInShadeFalse() {
+ whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
+ val hmp = createHeadsUpManagerPhone()
+
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ hmp.showNotification(notifEntry)
+ assertThat(notifEntry.isSeenInShade).isFalse();
}
@Test
diff --git a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
index 84f7a51..6c8db91 100644
--- a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
@@ -28,8 +28,4 @@
<!-- Overload default clock widget parameters -->
<dimen name="widget_big_font_size">100dp</dimen>
<dimen name="widget_label_font_size">18sp</dimen>
-
- <!-- New keyboard shortcut helper -->
- <dimen name="shortcut_helper_width">704dp</dimen>
- <dimen name="shortcut_helper_height">1208dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
deleted file mode 100644
index a15532f..0000000
--- a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/shortcut_helper_sheet_container"
- android:layout_gravity="center_horizontal|bottom"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <FrameLayout
- android:id="@+id/shortcut_helper_sheet"
- style="@style/ShortcutHelperBottomSheet"
- android:layout_width="@dimen/shortcut_helper_width"
- android:layout_height="@dimen/shortcut_helper_height"
- android:orientation="vertical"
- app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
-
- <!-- Drag handle for accessibility -->
- <com.google.android.material.bottomsheet.BottomSheetDragHandleView
- android:id="@+id/drag_handle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
-
- <androidx.compose.ui.platform.ComposeView
- android:id="@+id/shortcut_helper_compose_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
- </FrameLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index 3efe7a5..2a27b47 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -26,10 +26,6 @@
<dimen name="keyguard_clock_top_margin">8dp</dimen>
<dimen name="keyguard_smartspace_top_offset">0dp</dimen>
- <!-- New keyboard shortcut helper -->
- <dimen name="shortcut_helper_width">864dp</dimen>
- <dimen name="shortcut_helper_height">728dp</dimen>
-
<!-- QS-->
<dimen name="qs_panel_padding_top">16dp</dimen>
<dimen name="qs_panel_padding">24dp</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 00846cb..e94248d 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1005,10 +1005,6 @@
<dimen name="ksh_app_item_minimum_height">64dp</dimen>
<dimen name="ksh_category_separator_margin">16dp</dimen>
- <!-- New keyboard shortcut helper -->
- <dimen name="shortcut_helper_width">412dp</dimen>
- <dimen name="shortcut_helper_height">728dp</dimen>
-
<!-- The size of corner radius of the arrow in the onboarding toast. -->
<dimen name="recents_onboarding_toast_arrow_corner_radius">2dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index ba3822b..a3db776 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3711,6 +3711,12 @@
[CHAR LIMIT=NONE]
-->
<string name="shortcut_helper_key_combinations_or_separator">or</string>
+ <!-- Content description of the drag handle that allows to swipe to dismiss the shortcut helper.
+ The helper is a component that shows the user which keyboard shortcuts they can
+ use. The helper shows shortcuts in categories, which can be collapsed or expanded.
+ [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_content_description_drag_handle">Drag handle</string>
+
<!-- Keyboard touchpad tutorial scheduler-->
<!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] -->
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index b10d37e..c95a94e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -34,9 +34,7 @@
import com.android.systemui.CoreStartable
import com.android.systemui.Flags.lightRevealMigration
import com.android.systemui.biometrics.data.repository.FacePropertyRepository
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
-import com.android.systemui.biometrics.shared.model.toSensorType
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.domain.interactor.AuthRippleInteractor
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
@@ -104,7 +102,6 @@
private var udfpsController: UdfpsController? = null
private var udfpsRadius: Float = -1f
- private var udfpsType: FingerprintSensorType = FingerprintSensorType.UNKNOWN
override fun start() {
init()
@@ -373,11 +370,8 @@
private val udfpsControllerCallback =
object : UdfpsController.Callback {
override fun onFingerDown() {
- // only show dwell ripple for device entry non-ultrasonic udfps
- if (
- keyguardUpdateMonitor.isFingerprintDetectionRunning &&
- udfpsType != FingerprintSensorType.UDFPS_ULTRASONIC
- ) {
+ // only show dwell ripple for device entry
+ if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
showDwellRipple()
}
}
@@ -403,7 +397,6 @@
if (it.size > 0) {
udfpsController = udfpsControllerProvider.get()
udfpsRadius = authController.udfpsRadius
- udfpsType = it[0].sensorType.toSensorType()
if (mView.isAttachedToWindow) {
udfpsController?.addCallback(udfpsControllerCallback)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 5ffb9ab2..a3904ca 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -19,6 +19,7 @@
import static android.app.StatusBarManager.SESSION_BIOMETRIC_PROMPT;
import static android.app.StatusBarManager.SESSION_KEYGUARD;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD;
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
import static android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP;
import static android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD;
import static android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING;
@@ -329,6 +330,22 @@
int sensorId,
@BiometricFingerprintConstants.FingerprintAcquired int acquiredInfo
) {
+ if (isUltrasonic()) {
+ if (acquiredInfo == FINGERPRINT_ACQUIRED_START) {
+ mFgExecutor.execute(() -> {
+ for (Callback cb : mCallbacks) {
+ cb.onFingerDown();
+ }
+ });
+ } else {
+ mFgExecutor.execute(() -> {
+ for (Callback cb : mCallbacks) {
+ cb.onFingerUp();
+ }
+ });
+ }
+ }
+
if (BiometricFingerprintConstants.shouldDisableUdfpsDisplayMode(acquiredInfo)) {
boolean acquiredGood = acquiredInfo == FINGERPRINT_ACQUIRED_GOOD;
mFgExecutor.execute(() -> {
@@ -1024,6 +1041,10 @@
return mSensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
}
+ private boolean isUltrasonic() {
+ return mSensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC;
+ }
+
public boolean isFingerDown() {
return mOnFingerDown;
}
@@ -1105,8 +1126,10 @@
}
}
- for (Callback cb : mCallbacks) {
- cb.onFingerDown();
+ if (isOptical()) {
+ for (Callback cb : mCallbacks) {
+ cb.onFingerDown();
+ }
}
}
@@ -1143,8 +1166,10 @@
if (mOnFingerDown) {
mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId, pointerId, x,
y, minor, major, orientation, time, gestureStart, isAod);
- for (Callback cb : mCallbacks) {
- cb.onFingerUp();
+ if (isOptical()) {
+ for (Callback cb : mCallbacks) {
+ cb.onFingerUp();
+ }
}
}
mOnFingerDown = false;
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
index b5e54d5..fdbc18d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
@@ -5,7 +5,6 @@
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import androidx.compose.ui.platform.ComposeView
-import androidx.core.view.isGone
import androidx.lifecycle.Lifecycle
import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.composable.BouncerContainer
@@ -13,7 +12,6 @@
import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
import com.android.systemui.lifecycle.WindowLifecycleState
import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.lifecycle.setSnapshotBinding
import com.android.systemui.lifecycle.viewModel
import kotlinx.coroutines.awaitCancellation
@@ -50,7 +48,6 @@
setContent { BouncerContainer(viewModelFactory, dialogFactory) }
}
)
- view.setSnapshotBinding { view.isGone = !viewModel.isVisible }
awaitCancellation()
} finally {
view.removeAllViews()
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerContainerViewModel.kt
index c60f932..5a4f8eb 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerContainerViewModel.kt
@@ -16,17 +16,15 @@
package com.android.systemui.bouncer.ui.viewmodel
-import androidx.compose.runtime.getValue
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
import com.android.systemui.lifecycle.ExclusiveActivatable
-import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.util.kotlin.Utils.Companion.sample
import com.android.systemui.util.kotlin.sample
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@@ -39,11 +37,6 @@
private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
) : ExclusiveActivatable() {
- private val hydrator = Hydrator("BouncerContainerViewModel")
-
- val isVisible: Boolean by
- hydrator.hydratedStateOf(traceName = "isVisible", source = legacyInteractor.isShowing)
-
override suspend fun onActivated(): Nothing {
coroutineScope {
launch {
@@ -74,8 +67,7 @@
legacyInteractor.hide()
}
}
-
- hydrator.activate()
+ awaitCancellation()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index 4bf552e..93cd1cf4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -36,6 +36,7 @@
import androidx.compose.foundation.layout.FlowRowScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -71,8 +72,6 @@
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
-import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -116,7 +115,6 @@
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.zIndex
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
-import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
@@ -188,10 +186,7 @@
}
}
-@Composable
-private fun shouldUseSinglePane() =
- LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact ||
- LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Compact
+@Composable private fun shouldUseSinglePane() = hasCompactWindowSize()
@Composable
private fun ShortcutHelperSinglePane(
@@ -425,7 +420,7 @@
onKeyboardSettingsClicked: () -> Unit,
) {
val selectedCategory = categories.fastFirstOrNull { it.type == selectedCategoryType }
- Column(modifier = modifier.fillMaxSize().padding(start = 24.dp, end = 24.dp, top = 26.dp)) {
+ Column(modifier = modifier.fillMaxSize().padding(horizontal = 24.dp)) {
TitleBar()
Spacer(modifier = Modifier.height(12.dp))
Row(Modifier.fillMaxWidth()) {
@@ -801,6 +796,8 @@
style = MaterialTheme.typography.headlineSmall,
)
},
+ windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp),
+ expandedHeight = 64.dp,
)
}
@@ -835,6 +832,7 @@
onSearch = {},
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
placeholder = { Text(text = stringResource(R.string.shortcut_helper_search_placeholder)) },
+ windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp),
content = {},
)
}
@@ -847,9 +845,7 @@
shape = RoundedCornerShape(24.dp),
color = Color.Transparent,
modifier =
- Modifier.semantics { role = Role.Button }
- .fillMaxWidth()
- .padding(horizontal = 12.dp),
+ Modifier.semantics { role = Role.Button }.fillMaxWidth().padding(horizontal = 12.dp),
interactionSource = interactionSource,
interactionsConfig =
InteractionsConfig(
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt
new file mode 100644
index 0000000..1f0d696
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.keyboard.shortcut.ui.composable
+
+import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.runtime.Composable
+import com.android.compose.windowsizeclass.LocalWindowSizeClass
+
+/**
+ * returns true if either size of the window is compact. This represents majority of phone windows
+ * portrait
+ */
+@Composable
+fun hasCompactWindowSize() =
+ LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact ||
+ LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Compact
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
index 799999a..b9a16c4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
@@ -18,31 +18,43 @@
import android.content.ActivityNotFoundException
import android.content.Intent
-import android.graphics.Insets
+import android.content.res.Configuration
import android.os.Bundle
import android.provider.Settings
-import android.view.View
-import android.view.WindowInsets
-import androidx.activity.BackEventCompat
import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Surface
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
-import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
-import androidx.core.view.updatePadding
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.android.compose.theme.PlatformTheme
import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper
+import com.android.systemui.keyboard.shortcut.ui.composable.hasCompactWindowSize
import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
-import com.android.systemui.util.dpToPx
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
-import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
import javax.inject.Inject
import kotlinx.coroutines.launch
@@ -55,52 +67,58 @@
constructor(private val userTracker: UserTracker, private val viewModel: ShortcutHelperViewModel) :
ComponentActivity() {
- private val bottomSheetContainer
- get() = requireViewById<View>(R.id.shortcut_helper_sheet_container)
-
- private val bottomSheet
- get() = requireViewById<View>(R.id.shortcut_helper_sheet)
-
- private val bottomSheetBehavior
- get() = BottomSheetBehavior.from(bottomSheet)
-
override fun onCreate(savedInstanceState: Bundle?) {
setupEdgeToEdge()
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_keyboard_shortcut_helper)
- setUpWidth()
- expandBottomSheet()
- setUpInsets()
- setUpPredictiveBack()
- setUpSheetDismissListener()
- setUpDismissOnTouchOutside()
- setUpComposeView()
+ setContent { Content() }
observeFinishRequired()
viewModel.onViewOpened()
}
- private fun setUpWidth() {
- // we override this because when maxWidth isn't specified, material imposes a max width
- // constraint on bottom sheets on larger screens which is smaller than our desired width.
- bottomSheetBehavior.maxWidth =
- resources.getDimension(R.dimen.shortcut_helper_width).dpToPx(resources).toInt()
+ @Composable
+ private fun Content() {
+ CompositionLocalProvider(LocalContext provides userTracker.userContext) {
+ PlatformTheme { BottomSheet { finish() } }
+ }
}
- private fun setUpComposeView() {
- requireViewById<ComposeView>(R.id.shortcut_helper_compose_container).apply {
- setContent {
- CompositionLocalProvider(LocalContext provides userTracker.userContext) {
- PlatformTheme {
- val shortcutsUiState by
- viewModel.shortcutsUiState.collectAsStateWithLifecycle()
- ShortcutHelper(
- shortcutsUiState = shortcutsUiState,
- onKeyboardSettingsClicked = ::onKeyboardSettingsClicked,
- onSearchQueryChanged = { viewModel.onSearchQueryChanged(it) },
- )
- }
- }
- }
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ private fun BottomSheet(onDismiss: () -> Unit) {
+ ModalBottomSheet(
+ onDismissRequest = { onDismiss() },
+ modifier =
+ Modifier.width(getWidth()).padding(top = getTopPadding()).onKeyEvent {
+ if (it.key == Key.Escape) {
+ onDismiss()
+ true
+ } else false
+ },
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+ dragHandle = { DragHandle() },
+ ) {
+ val shortcutsUiState by viewModel.shortcutsUiState.collectAsStateWithLifecycle()
+ ShortcutHelper(
+ shortcutsUiState = shortcutsUiState,
+ onKeyboardSettingsClicked = ::onKeyboardSettingsClicked,
+ onSearchQueryChanged = { viewModel.onSearchQueryChanged(it) },
+ )
+ }
+ }
+
+ @Composable
+ fun DragHandle() {
+ val dragHandleContentDescription =
+ stringResource(id = R.string.shortcut_helper_content_description_drag_handle)
+ Surface(
+ modifier =
+ Modifier.padding(top = 16.dp, bottom = 6.dp).semantics {
+ contentDescription = dragHandleContentDescription
+ },
+ color = MaterialTheme.colorScheme.outlineVariant,
+ shape = MaterialTheme.shapes.extraLarge,
+ ) {
+ Box(Modifier.size(width = 32.dp, height = 4.dp))
}
}
@@ -139,81 +157,27 @@
window.setDecorFitsSystemWindows(false)
}
- private fun setUpInsets() {
- bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets ->
- val safeDrawingInsets = insets.safeDrawing
- // Make sure the bottom sheet is not covered by the status bar.
- bottomSheetBehavior.maxHeight =
- windowManager.maximumWindowMetrics.bounds.height() - safeDrawingInsets.top
- // Make sure the contents inside of the bottom sheet are not hidden by system bars, or
- // cutouts.
- bottomSheet.updatePadding(
- left = safeDrawingInsets.left,
- right = safeDrawingInsets.right,
- bottom = safeDrawingInsets.bottom,
- )
- // The bottom sheet has to be expanded only after setting up insets, otherwise there is
- // a bug and it will not use full height.
- expandBottomSheet()
-
- // Return CONSUMED if you don't want want the window insets to keep passing
- // down to descendant views.
- WindowInsets.CONSUMED
- }
+ @Composable
+ private fun getTopPadding(): Dp {
+ return if (hasCompactWindowSize()) DefaultTopPadding else LargeScreenTopPadding
}
- private fun expandBottomSheet() {
- bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
- bottomSheetBehavior.skipCollapsed = true
- }
-
- private fun setUpPredictiveBack() {
- val onBackPressedCallback =
- object : OnBackPressedCallback(/* enabled= */ true) {
- override fun handleOnBackStarted(backEvent: BackEventCompat) {
- bottomSheetBehavior.startBackProgress(backEvent)
- }
-
- override fun handleOnBackProgressed(backEvent: BackEventCompat) {
- bottomSheetBehavior.updateBackProgress(backEvent)
- }
-
- override fun handleOnBackPressed() {
- bottomSheetBehavior.handleBackInvoked()
- }
-
- override fun handleOnBackCancelled() {
- bottomSheetBehavior.cancelBackProgress()
- }
+ @Composable
+ private fun getWidth(): Dp {
+ return if (hasCompactWindowSize()) {
+ DefaultWidth
+ } else
+ when (LocalConfiguration.current.orientation) {
+ Configuration.ORIENTATION_LANDSCAPE -> LargeScreenWidthLandscape
+ else -> LargeScreenWidthPortrait
}
- onBackPressedDispatcher.addCallback(
- owner = this,
- onBackPressedCallback = onBackPressedCallback,
- )
}
- private fun setUpSheetDismissListener() {
- bottomSheetBehavior.addBottomSheetCallback(
- object : BottomSheetCallback() {
- override fun onStateChanged(bottomSheet: View, newState: Int) {
- if (newState == STATE_HIDDEN) {
- finish()
- }
- }
-
- override fun onSlide(bottomSheet: View, slideOffset: Float) {}
- }
- )
- }
-
- private fun setUpDismissOnTouchOutside() {
- bottomSheetContainer.setOnClickListener { finish() }
+ companion object {
+ private val DefaultTopPadding = 64.dp
+ private val LargeScreenTopPadding = 72.dp
+ private val DefaultWidth = 412.dp
+ private val LargeScreenWidthPortrait = 704.dp
+ private val LargeScreenWidthLandscape = 864.dp
}
}
-
-private val WindowInsets.safeDrawing
- get() =
- getInsets(WindowInsets.Type.systemBars())
- .union(getInsets(WindowInsets.Type.displayCutout()))
-
-private fun Insets.union(insets: Insets): Insets = Insets.max(this, insets)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
index 1f8a24a1..35faa97 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
@@ -17,9 +17,10 @@
package com.android.systemui.qs.panels.ui.compose
import android.content.ClipData
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.draganddrop.dragAndDropSource
import androidx.compose.foundation.draganddrop.dragAndDropTarget
-import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
@@ -104,11 +105,10 @@
@Composable
fun Modifier.dragAndDropTileList(
gridState: LazyGridState,
- contentOffset: Offset,
+ contentOffset: () -> Offset,
dragAndDropState: DragAndDropState,
- onDrop: () -> Unit,
+ onDrop: (TileSpec) -> Unit,
): Modifier {
- val currentContentOffset by rememberUpdatedState(contentOffset)
val target =
remember(dragAndDropState) {
object : DragAndDropTarget {
@@ -118,7 +118,7 @@
override fun onMoved(event: DragAndDropEvent) {
// Drag offset relative to the list's top left corner
- val relativeDragOffset = event.dragOffsetRelativeTo(currentContentOffset)
+ val relativeDragOffset = event.dragOffsetRelativeTo(contentOffset())
val targetItem =
gridState.layoutInfo.visibleItemsInfo.firstOrNull { item ->
// Check if the drag is on this item
@@ -132,7 +132,7 @@
override fun onDrop(event: DragAndDropEvent): Boolean {
return dragAndDropState.draggedCell?.let {
- onDrop()
+ onDrop(it.tile.tileSpec)
dragAndDropState.onDrop()
true
} ?: false
@@ -158,36 +158,39 @@
return item.span != 1 && offset.x > itemCenter.x
}
+@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Modifier.dragAndDropTileSource(
sizedTile: SizedTile<EditTileViewModel>,
dragAndDropState: DragAndDropState,
- onTap: (TileSpec) -> Unit,
- onDoubleTap: (TileSpec) -> Unit = {},
+ onDragStart: () -> Unit,
): Modifier {
- val state by rememberUpdatedState(dragAndDropState)
- return dragAndDropSource {
- detectTapGestures(
- onTap = { onTap(sizedTile.tile.tileSpec) },
- onDoubleTap = { onDoubleTap(sizedTile.tile.tileSpec) },
- onLongPress = {
- state.onStarted(sizedTile)
+ val dragState by rememberUpdatedState(dragAndDropState)
+ @Suppress("DEPRECATION") // b/368361871
+ return dragAndDropSource(
+ block = {
+ detectDragGesturesAfterLongPress(
+ onDrag = { _, _ -> },
+ onDragStart = {
+ dragState.onStarted(sizedTile)
+ onDragStart()
- // The tilespec from the ClipData transferred isn't actually needed as we're moving
- // a tile within the same application. We're using a custom MIME type to limit the
- // drag event to QS.
- startTransfer(
- DragAndDropTransferData(
- ClipData(
- QsDragAndDrop.CLIPDATA_LABEL,
- arrayOf(QsDragAndDrop.TILESPEC_MIME_TYPE),
- ClipData.Item(sizedTile.tile.tileSpec.spec),
+ // The tilespec from the ClipData transferred isn't actually needed as we're
+ // moving a tile within the same application. We're using a custom MIME type to
+ // limit the drag event to QS.
+ startTransfer(
+ DragAndDropTransferData(
+ ClipData(
+ QsDragAndDrop.CLIPDATA_LABEL,
+ arrayOf(QsDragAndDrop.TILESPEC_MIME_TYPE),
+ ClipData.Item(sizedTile.tile.tileSpec.spec),
+ )
)
)
- )
- },
- )
- }
+ },
+ )
+ }
+ )
}
private object QsDragAndDrop {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
index 4830ba7..a4f977b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
@@ -42,10 +42,8 @@
}
/** Holds the temporary state of the tile list during a drag movement where we move tiles around. */
-class EditTileListState(
- tiles: List<SizedTile<EditTileViewModel>>,
- private val columns: Int,
-) : DragAndDropState {
+class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val columns: Int) :
+ DragAndDropState {
private val _draggedCell = mutableStateOf<SizedTile<EditTileViewModel>?>(null)
override val draggedCell
get() = _draggedCell.value
@@ -91,7 +89,8 @@
regenerateGrid(includeSpacers = true)
_tiles.add(insertionIndex.coerceIn(0, _tiles.size), cell)
} else {
- // Add the tile with a temporary row which will get reassigned when regenerating spacers
+ // Add the tile with a temporary row which will get reassigned when
+ // regenerating spacers
_tiles.add(insertionIndex.coerceIn(0, _tiles.size), TileGridCell(draggedTile, 0))
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
index aeb6031..8c2fb25 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -127,13 +127,14 @@
}
@Composable
-private fun LargeTileLabels(
+fun LargeTileLabels(
label: String,
secondaryLabel: String?,
colors: TileColors,
+ modifier: Modifier = Modifier,
accessibilityUiState: AccessibilityUiState? = null,
) {
- Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
+ Column(verticalArrangement = Arrangement.Center, modifier = modifier.fillMaxHeight()) {
Text(label, color = colors.label, modifier = Modifier.tileMarquee())
if (!TextUtils.isEmpty(secondaryLabel)) {
Text(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index a43b880..0e76e18 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -20,6 +20,9 @@
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.animateIntAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -31,6 +34,7 @@
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
@@ -38,6 +42,7 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.grid.GridCells
@@ -56,23 +61,34 @@
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
@@ -82,7 +98,9 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastMap
+import androidx.compose.ui.zIndex
import com.android.compose.modifiers.background
+import com.android.compose.modifiers.height
import com.android.systemui.common.ui.compose.load
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
@@ -92,14 +110,23 @@
import com.android.systemui.qs.panels.ui.compose.dragAndDropTileList
import com.android.systemui.qs.panels.ui.compose.dragAndDropTileSource
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileArrangementPadding
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.ToggleTargetSize
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.CurrentTilesGridPadding
+import com.android.systemui.qs.panels.ui.compose.selection.MutableSelectionState
+import com.android.systemui.qs.panels.ui.compose.selection.ResizingHandle
+import com.android.systemui.qs.panels.ui.compose.selection.TileWidths
+import com.android.systemui.qs.panels.ui.compose.selection.clearSelectionTile
+import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState
+import com.android.systemui.qs.panels.ui.compose.selection.selectableTile
import com.android.systemui.qs.panels.ui.model.GridCell
import com.android.systemui.qs.panels.ui.model.SpacerGridCell
import com.android.systemui.qs.panels.ui.model.TileGridCell
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
-import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.shared.model.groupAndSort
import com.android.systemui.res.R
+import kotlinx.coroutines.delay
object TileType
@@ -109,14 +136,11 @@
otherTiles: List<SizedTile<EditTileViewModel>>,
columns: Int,
modifier: Modifier,
- onAddTile: (TileSpec, Int) -> Unit,
onRemoveTile: (TileSpec) -> Unit,
onSetTiles: (List<TileSpec>) -> Unit,
onResize: (TileSpec) -> Unit,
) {
- val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
- onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
- }
+ val selectionState = rememberSelectionState()
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
Column(
@@ -138,7 +162,7 @@
}
}
- CurrentTilesGrid(currentListState, columns, onRemoveTile, onResize, onSetTiles)
+ CurrentTilesGrid(currentListState, selectionState, columns, onResize, onSetTiles)
// Hide available tiles when dragging
AnimatedVisibility(
@@ -153,7 +177,7 @@
) {
EditGridHeader { Text(text = "Hold and drag to add tiles.") }
- AvailableTileGrid(otherTiles, columns, addTileToEnd, currentListState)
+ AvailableTileGrid(otherTiles, selectionState, columns, currentListState)
}
}
@@ -201,61 +225,67 @@
}
@Composable
-private fun CurrentTilesContainer(content: @Composable () -> Unit) {
- Box(
- Modifier.fillMaxWidth()
- .border(
- width = 1.dp,
- color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f),
- shape = RoundedCornerShape(48.dp),
- )
- .padding(dimensionResource(R.dimen.qs_tile_margin_vertical))
- ) {
- content()
- }
-}
-
-@Composable
private fun CurrentTilesGrid(
listState: EditTileListState,
+ selectionState: MutableSelectionState,
columns: Int,
- onClick: (TileSpec) -> Unit,
onResize: (TileSpec) -> Unit,
onSetTiles: (List<TileSpec>) -> Unit,
) {
val currentListState by rememberUpdatedState(listState)
- val tilePadding = CommonTileDefaults.TileArrangementPadding
+ val tileHeight = CommonTileDefaults.TileHeight
+ val totalRows = listState.tiles.lastOrNull()?.row ?: 0
+ val totalHeight by
+ animateDpAsState(
+ gridHeight(totalRows + 1, tileHeight, TileArrangementPadding, CurrentTilesGridPadding),
+ label = "QSEditCurrentTilesGridHeight",
+ )
+ val gridState = rememberLazyGridState()
+ var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
+ var droppedSpec by remember { mutableStateOf<TileSpec?>(null) }
- CurrentTilesContainer {
- val tileHeight = CommonTileDefaults.TileHeight
- val totalRows = listState.tiles.lastOrNull()?.row ?: 0
- val totalHeight = gridHeight(totalRows + 1, tileHeight, tilePadding)
- val gridState = rememberLazyGridState()
- var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
+ // Select the tile that was dropped. A delay is introduced to avoid clipping issues on the
+ // selected border and resizing handle, as well as letting the selection animation play.
+ LaunchedEffect(droppedSpec) {
+ droppedSpec?.let {
+ delay(200)
+ selectionState.select(it)
- TileLazyGrid(
- state = gridState,
- modifier =
- Modifier.height(totalHeight)
- .dragAndDropTileList(gridState, gridContentOffset, listState) {
- onSetTiles(currentListState.tileSpecs())
- }
- .onGloballyPositioned { coordinates ->
- gridContentOffset = coordinates.positionInRoot()
- }
- .testTag(CURRENT_TILES_GRID_TEST_TAG),
- columns = GridCells.Fixed(columns),
- ) {
- EditTiles(listState.tiles, onClick, listState, onResize = onResize)
+ // Reset droppedSpec in case a tile is dropped twice in a row
+ droppedSpec = null
}
}
+
+ TileLazyGrid(
+ state = gridState,
+ columns = GridCells.Fixed(columns),
+ contentPadding = PaddingValues(CurrentTilesGridPadding),
+ modifier =
+ Modifier.fillMaxWidth()
+ .height { totalHeight.roundToPx() }
+ .border(
+ width = 1.dp,
+ color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f),
+ shape = RoundedCornerShape(48.dp),
+ )
+ .dragAndDropTileList(gridState, { gridContentOffset }, listState) { spec ->
+ onSetTiles(currentListState.tileSpecs())
+ droppedSpec = spec
+ }
+ .onGloballyPositioned { coordinates ->
+ gridContentOffset = coordinates.positionInRoot()
+ }
+ .testTag(CURRENT_TILES_GRID_TEST_TAG),
+ ) {
+ EditTiles(listState.tiles, listState, selectionState, onResize)
+ }
}
@Composable
private fun AvailableTileGrid(
tiles: List<SizedTile<EditTileViewModel>>,
+ selectionState: MutableSelectionState,
columns: Int,
- onClick: (TileSpec) -> Unit,
dragAndDropState: DragAndDropState,
) {
// Available tiles aren't visible during drag and drop, so the row isn't needed
@@ -292,7 +322,7 @@
cell = tileGridCell,
index = index,
dragAndDropState = dragAndDropState,
- onClick = onClick,
+ selectionState = selectionState,
modifier = Modifier.weight(1f).fillMaxHeight(),
)
}
@@ -305,8 +335,8 @@
}
}
-fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp {
- return ((tileHeight + padding) * rows) - padding
+fun gridHeight(rows: Int, tileHeight: Dp, tilePadding: Dp, gridPadding: Dp): Dp {
+ return ((tileHeight + tilePadding) * rows) - tilePadding + gridPadding * 2
}
private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any {
@@ -320,9 +350,9 @@
fun LazyGridScope.EditTiles(
cells: List<GridCell>,
- onClick: (TileSpec) -> Unit,
dragAndDropState: DragAndDropState,
- onResize: (TileSpec) -> Unit = {},
+ selectionState: MutableSelectionState,
+ onResize: (TileSpec) -> Unit,
) {
items(
count = cells.size,
@@ -347,7 +377,7 @@
cell = cell,
index = index,
dragAndDropState = dragAndDropState,
- onClick = onClick,
+ selectionState = selectionState,
onResize = onResize,
)
}
@@ -361,28 +391,171 @@
cell: TileGridCell,
index: Int,
dragAndDropState: DragAndDropState,
- onClick: (TileSpec) -> Unit,
- onResize: (TileSpec) -> Unit = {},
+ selectionState: MutableSelectionState,
+ onResize: (TileSpec) -> Unit,
) {
- val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
+ val selected = selectionState.isSelected(cell.tile.tileSpec)
val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
+ val selectionAlpha by
+ animateFloatAsState(
+ targetValue = if (selected) 1f else 0f,
+ label = "QSEditTileSelectionAlpha",
+ )
- EditTile(
- tileViewModel = cell.tile,
- iconOnly = cell.isIcon,
- modifier =
- Modifier.animateItem()
- .semantics(mergeDescendants = true) {
- onClick(onClickActionName) { false }
- this.stateDescription = stateDescription
+ val modifier =
+ Modifier.animateItem()
+ .semantics(mergeDescendants = true) {
+ this.stateDescription = stateDescription
+ contentDescription = cell.tile.label.text
+ customActions =
+ listOf(
+ // TODO(b/367748260): Add final accessibility actions
+ CustomAccessibilityAction("Toggle size") {
+ onResize(cell.tile.tileSpec)
+ true
+ }
+ )
+ }
+ .height(CommonTileDefaults.TileHeight)
+ .fillMaxWidth()
+
+ val content =
+ @Composable {
+ EditTile(
+ tileViewModel = cell.tile,
+ iconOnly = cell.isIcon,
+ selectionAlpha = { selectionAlpha },
+ modifier =
+ Modifier.fillMaxSize()
+ .selectableTile(cell.tile.tileSpec, selectionState)
+ .dragAndDropTileSource(
+ SizedTileImpl(cell.tile, cell.width),
+ dragAndDropState,
+ selectionState::unSelect,
+ ),
+ )
+ }
+
+ if (selected) {
+ SelectedTile(
+ tileSpec = cell.tile.tileSpec,
+ isIcon = cell.isIcon,
+ selectionAlpha = { selectionAlpha },
+ selectionState = selectionState,
+ onResize = onResize,
+ modifier = modifier.zIndex(2f), // 2f to display this tile over neighbors when dragged
+ content = content,
+ )
+ } else {
+ UnselectedTile(
+ selectionAlpha = { selectionAlpha },
+ selectionState = selectionState,
+ modifier = modifier,
+ content = content,
+ )
+ }
+}
+
+@Composable
+private fun SelectedTile(
+ tileSpec: TileSpec,
+ isIcon: Boolean,
+ selectionAlpha: () -> Float,
+ selectionState: MutableSelectionState,
+ onResize: (TileSpec) -> Unit,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit,
+) {
+ // Current base, min and max width of this tile
+ var tileWidths: TileWidths? by remember { mutableStateOf(null) }
+
+ // Animated diff between the current width and the resized width of the tile. We can't use
+ // animateContentSize here as the tile is sometimes unbounded.
+ val remainingOffset by
+ animateIntAsState(
+ selectionState.resizingState?.let { tileWidths?.base?.minus(it.width) ?: 0 } ?: 0,
+ label = "QSEditTileWidthOffset",
+ )
+
+ val padding = with(LocalDensity.current) { TileArrangementPadding.roundToPx() }
+ Box(
+ modifier.onSizeChanged {
+ val min = if (isIcon) it.width else (it.width - padding) / 2
+ val max = if (isIcon) (it.width * 2) + padding else it.width
+ tileWidths = TileWidths(it.width, min, max)
+ }
+ ) {
+ val handle =
+ @Composable {
+ ResizingHandle(
+ enabled = true,
+ selectionState = selectionState,
+ transition = selectionAlpha,
+ tileWidths = { tileWidths },
+ ) {
+ onResize(tileSpec)
}
- .dragAndDropTileSource(
- SizedTileImpl(cell.tile, cell.width),
- dragAndDropState,
- onClick,
- onResize,
- ),
- )
+ }
+
+ Layout(contents = listOf(content, handle)) {
+ (contentMeasurables, handleMeasurables),
+ constraints ->
+ // Grab the width from the resizing state if a resize is in progress, otherwise fill the
+ // max width
+ val width =
+ selectionState.resizingState?.width ?: (constraints.maxWidth - remainingOffset)
+ val contentPlaceable =
+ contentMeasurables.first().measure(constraints.copy(maxWidth = width))
+ val handlePlaceable = handleMeasurables.first().measure(constraints)
+
+ // Place the dot vertically centered on the right edge
+ val handleX = contentPlaceable.width - (handlePlaceable.width / 2)
+ val handleY = (contentPlaceable.height / 2) - (handlePlaceable.height / 2)
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ contentPlaceable.place(0, 0)
+ handlePlaceable.place(handleX, handleY)
+ }
+ }
+ }
+}
+
+@Composable
+private fun UnselectedTile(
+ selectionAlpha: () -> Float,
+ selectionState: MutableSelectionState,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit,
+) {
+ val handle =
+ @Composable {
+ ResizingHandle(
+ enabled = false,
+ selectionState = selectionState,
+ transition = selectionAlpha,
+ )
+ }
+
+ Box(modifier) {
+ Layout(contents = listOf(content, handle)) {
+ (contentMeasurables, handleMeasurables),
+ constraints ->
+ val contentPlaceable =
+ contentMeasurables
+ .first()
+ .measure(constraints.copy(maxWidth = constraints.maxWidth))
+ val handlePlaceable = handleMeasurables.first().measure(constraints)
+
+ // Place the dot vertically centered on the right edge
+ val handleX = contentPlaceable.width - (handlePlaceable.width / 2)
+ val handleY = (contentPlaceable.height / 2) - (handlePlaceable.height / 2)
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ contentPlaceable.place(0, 0)
+ handlePlaceable.place(handleX, handleY)
+ }
+ }
+ }
}
@Composable
@@ -390,8 +563,8 @@
cell: TileGridCell,
index: Int,
dragAndDropState: DragAndDropState,
+ selectionState: MutableSelectionState,
modifier: Modifier = Modifier,
- onClick: (TileSpec) -> Unit,
) {
val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
@@ -403,21 +576,27 @@
verticalArrangement = spacedBy(CommonTileDefaults.TilePadding, Alignment.Top),
modifier = modifier,
) {
- EditTile(
- tileViewModel = cell.tile,
- iconOnly = true,
+ EditTileContainer(
colors = colors,
modifier =
- Modifier.semantics(mergeDescendants = true) {
+ Modifier.fillMaxWidth()
+ .height(CommonTileDefaults.TileHeight)
+ .clearSelectionTile(selectionState)
+ .semantics(mergeDescendants = true) {
onClick(onClickActionName) { false }
this.stateDescription = stateDescription
}
- .dragAndDropTileSource(
- SizedTileImpl(cell.tile, cell.width),
- dragAndDropState,
- onTap = onClick,
- ),
- )
+ .dragAndDropTileSource(SizedTileImpl(cell.tile, cell.width), dragAndDropState) {
+ selectionState.unSelect()
+ },
+ ) {
+ // Icon
+ SmallTileContent(
+ icon = cell.tile.icon,
+ color = colors.icon,
+ modifier = Modifier.align(Alignment.Center),
+ )
+ }
Box(Modifier.fillMaxSize()) {
Text(
cell.tile.label.text,
@@ -434,7 +613,7 @@
@Composable
private fun SpacerGridCell(modifier: Modifier = Modifier) {
// By default, spacers are invisible and exist purely to catch drag movements
- Box(modifier.height(CommonTileDefaults.TileHeight).fillMaxWidth().tilePadding())
+ Box(modifier.height(CommonTileDefaults.TileHeight).fillMaxWidth())
}
@Composable
@@ -443,20 +622,35 @@
iconOnly: Boolean,
modifier: Modifier = Modifier,
colors: TileColors = EditModeTileDefaults.editTileColors(),
+ selectionAlpha: () -> Float = { 1f },
) {
- EditTileContainer(colors = colors, modifier = modifier) {
- if (iconOnly) {
+ // Animated horizontal alignment from center (0f) to start (-1f)
+ val alignmentValue by
+ animateFloatAsState(
+ targetValue = if (iconOnly) 0f else -1f,
+ label = "QSEditTileContentAlignment",
+ )
+ val alignment by remember {
+ derivedStateOf { BiasAlignment(horizontalBias = alignmentValue, verticalBias = 0f) }
+ }
+
+ EditTileContainer(colors = colors, selectionAlpha = selectionAlpha, modifier = modifier) {
+ // Icon
+ Box(Modifier.size(ToggleTargetSize).align(alignment)) {
SmallTileContent(
icon = tileViewModel.icon,
color = colors.icon,
modifier = Modifier.align(Alignment.Center),
)
- } else {
- LargeTileContent(
+ }
+
+ // Labels, positioned after the icon
+ AnimatedVisibility(visible = !iconOnly, enter = fadeIn(), exit = fadeOut()) {
+ LargeTileLabels(
label = tileViewModel.label.text,
secondaryLabel = tileViewModel.appName?.text,
- icon = tileViewModel.icon,
colors = colors,
+ modifier = Modifier.padding(start = ToggleTargetSize + TileArrangementPadding),
)
}
}
@@ -466,27 +660,41 @@
private fun EditTileContainer(
colors: TileColors,
modifier: Modifier = Modifier,
- content: @Composable BoxScope.() -> Unit,
+ selectionAlpha: () -> Float = { 0f },
+ selectionColor: Color = MaterialTheme.colorScheme.primary,
+ content: @Composable BoxScope.() -> Unit = {},
) {
Box(
- modifier =
- modifier
- .height(CommonTileDefaults.TileHeight)
- .fillMaxWidth()
- .drawBehind {
- drawRoundRect(
- SolidColor(colors.background),
- cornerRadius = CornerRadius(InactiveCornerRadius.toPx()),
- )
- }
- .tilePadding(),
- content = content,
- )
+ Modifier.wrapContentSize().drawWithContent {
+ drawContent()
+ drawRoundRect(
+ SolidColor(selectionColor),
+ cornerRadius = CornerRadius(InactiveCornerRadius.toPx()),
+ style = Stroke(EditModeTileDefaults.SelectedBorderWidth.toPx()),
+ alpha = selectionAlpha(),
+ )
+ }
+ ) {
+ Box(
+ modifier =
+ modifier
+ .drawBehind {
+ drawRoundRect(
+ SolidColor(colors.background),
+ cornerRadius = CornerRadius(InactiveCornerRadius.toPx()),
+ )
+ }
+ .tilePadding(),
+ content = content,
+ )
+ }
}
private object EditModeTileDefaults {
const val PLACEHOLDER_ALPHA = .3f
val EditGridHeaderHeight = 60.dp
+ val SelectedBorderWidth = 2.dp
+ val CurrentTilesGridPadding = 8.dp
@Composable
fun editTileColors(): TileColors =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index f96c27d..4946c01 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -98,7 +98,6 @@
otherTiles = otherTiles,
columns = columns,
modifier = modifier,
- onAddTile = onAddTile,
onRemoveTile = onRemoveTile,
onSetTiles = onSetTiles,
onResize = iconTilesViewModel::resize,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
index 45aad82..afcbed6d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -29,6 +29,7 @@
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -82,6 +83,7 @@
columns: GridCells,
modifier: Modifier = Modifier,
state: LazyGridState = rememberLazyGridState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
content: LazyGridScope.() -> Unit,
) {
LazyVerticalGrid(
@@ -89,6 +91,7 @@
columns = columns,
verticalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
horizontalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
+ contentPadding = contentPadding,
modifier = modifier,
content = content,
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
new file mode 100644
index 0000000..2ea32e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.panels.ui.compose.selection
+
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+/** Creates the state of the current selected tile that is remembered across compositions. */
+@Composable
+fun rememberSelectionState(): MutableSelectionState {
+ return remember { MutableSelectionState() }
+}
+
+/** Holds the state of the current selection. */
+class MutableSelectionState {
+ private var _selectedTile = mutableStateOf<TileSpec?>(null)
+ private var _resizingState = mutableStateOf<ResizingState?>(null)
+
+ /** The [ResizingState] of the selected tile is currently being resized, null if not. */
+ val resizingState by _resizingState
+
+ fun isSelected(tileSpec: TileSpec): Boolean {
+ return _selectedTile.value?.let { it == tileSpec } ?: false
+ }
+
+ fun select(tileSpec: TileSpec) {
+ _selectedTile.value = tileSpec
+ }
+
+ fun unSelect() {
+ _selectedTile.value = null
+ onResizingDragEnd()
+ }
+
+ fun onResizingDrag(offset: Float) {
+ _resizingState.value?.onDrag(offset)
+ }
+
+ fun onResizingDragStart(tileWidths: TileWidths, onResize: () -> Unit) {
+ if (_selectedTile.value == null) return
+
+ _resizingState.value = ResizingState(tileWidths, onResize)
+ }
+
+ fun onResizingDragEnd() {
+ _resizingState.value = null
+ }
+}
+
+/**
+ * Listens for click events to select/unselect the given [TileSpec]. Use this on current tiles as
+ * they can be selected.
+ */
+@Composable
+fun Modifier.selectableTile(tileSpec: TileSpec, selectionState: MutableSelectionState): Modifier {
+ return pointerInput(Unit) {
+ detectTapGestures(
+ onTap = {
+ if (selectionState.isSelected(tileSpec)) {
+ selectionState.unSelect()
+ } else {
+ selectionState.select(tileSpec)
+ }
+ }
+ )
+ }
+}
+
+/**
+ * Listens for click events to unselect any tile. Use this on available tiles as they can't be
+ * selected.
+ */
+@Composable
+fun Modifier.clearSelectionTile(selectionState: MutableSelectionState): Modifier {
+ return pointerInput(Unit) { detectTapGestures(onTap = { selectionState.unSelect() }) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt
new file mode 100644
index 0000000..a084bc2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.panels.ui.compose.selection
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.setValue
+import com.android.systemui.qs.panels.ui.compose.selection.ResizingDefaults.RESIZING_THRESHOLD
+
+class ResizingState(private val widths: TileWidths, private val onResize: () -> Unit) {
+ // Total drag offset of this resize operation
+ private var totalOffset = 0f
+
+ /** Width in pixels of the resizing tile. */
+ var width by mutableIntStateOf(widths.base)
+
+ // Whether the tile is currently over the threshold and should be a large tile
+ private var passedThreshold: Boolean = passedThreshold(calculateProgression(width))
+
+ fun onDrag(offset: Float) {
+ totalOffset += offset
+ width = (widths.base + totalOffset).toInt().coerceIn(widths.min, widths.max)
+
+ passedThreshold(calculateProgression(width)).let {
+ // Resize if we went over the threshold
+ if (passedThreshold != it) {
+ passedThreshold = it
+ onResize()
+ }
+ }
+ }
+
+ private fun passedThreshold(progression: Float): Boolean {
+ return progression >= RESIZING_THRESHOLD
+ }
+
+ /** The progression of the resizing tile between an icon tile (0f) and a large tile (1f) */
+ private fun calculateProgression(width: Int): Float {
+ return ((width - widths.min) / (widths.max - widths.min).toFloat()).coerceIn(0f, 1f)
+ }
+}
+
+/** Holds the width of a tile as well as its min and max widths */
+data class TileWidths(val base: Int, val min: Int, val max: Int) {
+ init {
+ check(max > min) { "The max width needs to be larger than the min width." }
+ }
+}
+
+private object ResizingDefaults {
+ const val RESIZING_THRESHOLD = .25f
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
new file mode 100644
index 0000000..e3acf38
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.panels.ui.compose.selection
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.detectHorizontalDragGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.LocalMinimumInteractiveComponentSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.dp
+import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingDotSize
+
+/**
+ * Dot handling resizing drag events. Use this on the selected tile to resize it
+ *
+ * @param enabled whether resizing drag events should be handled
+ * @param selectionState the [MutableSelectionState] on the grid
+ * @param transition the animated value for the dot, used for its alpha and scale
+ * @param tileWidths the [TileWidths] of the selected tile
+ * @param onResize the callback when the drag passes the resizing threshold
+ */
+@Composable
+fun ResizingHandle(
+ enabled: Boolean,
+ selectionState: MutableSelectionState,
+ transition: () -> Float,
+ tileWidths: () -> TileWidths? = { null },
+ onResize: () -> Unit = {},
+) {
+ if (enabled) {
+ // Manually creating the touch target around the resizing dot to ensure that the next tile
+ // does
+ // not receive the touch input accidentally.
+ val minTouchTargetSize = LocalMinimumInteractiveComponentSize.current
+ Box(
+ Modifier.size(minTouchTargetSize).pointerInput(Unit) {
+ detectHorizontalDragGestures(
+ onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) },
+ onDragStart = {
+ tileWidths()?.let { selectionState.onResizingDragStart(it, onResize) }
+ },
+ onDragEnd = selectionState::onResizingDragEnd,
+ onDragCancel = selectionState::onResizingDragEnd,
+ )
+ }
+ ) {
+ ResizingDot(transition = transition, modifier = Modifier.align(Alignment.Center))
+ }
+ } else {
+ ResizingDot(transition = transition)
+ }
+}
+
+@Composable
+private fun ResizingDot(
+ transition: () -> Float,
+ modifier: Modifier = Modifier,
+ color: Color = MaterialTheme.colorScheme.primary,
+) {
+ Canvas(modifier = modifier.size(ResizingDotSize)) {
+ val v = transition()
+ drawCircle(color = color, radius = (ResizingDotSize / 2).toPx() * v, alpha = v)
+ }
+}
+
+private object SelectionDefaults {
+ val ResizingDotSize = 16.dp
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 1c223db..bf88807 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -30,6 +30,8 @@
import android.view.View;
import android.view.ViewGroup;
+import androidx.core.view.ViewKt;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.AuthKeyguardMessageArea;
import com.android.keyguard.KeyguardUnfoldTransition;
@@ -38,6 +40,7 @@
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags;
import com.android.systemui.bouncer.ui.binder.BouncerViewBinder;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.dagger.SysUISingleton;
@@ -51,10 +54,12 @@
import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.shared.model.Edge;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.res.R;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
+import com.android.systemui.scene.shared.model.Scenes;
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
import com.android.systemui.statusbar.DragDownHelper;
@@ -111,6 +116,7 @@
private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
private final AlternateBouncerInteractor mAlternateBouncerInteractor;
private final QuickSettingsController mQuickSettingsController;
+ private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private final GlanceableHubContainerController
mGlanceableHubContainerController;
private GestureDetector mPulsingWakeupGestureHandler;
@@ -140,6 +146,7 @@
private final PanelExpansionInteractor mPanelExpansionInteractor;
private final ShadeExpansionStateManager mShadeExpansionStateManager;
+ private ViewGroup mBouncerParentView;
/**
* If {@code true}, an external touch sent in {@link #handleExternalTouch(MotionEvent)} has been
* intercepted and all future touch events for the gesture should be processed by this view.
@@ -217,6 +224,7 @@
mPulsingGestureListener = pulsingGestureListener;
mLockscreenHostedDreamGestureListener = lockscreenHostedDreamGestureListener;
mNotificationInsetsController = notificationInsetsController;
+ mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mGlanceableHubContainerController = glanceableHubContainerController;
mFeatureFlagsClassic = featureFlagsClassic;
mSysUIKeyEventHandler = sysUIKeyEventHandler;
@@ -227,7 +235,7 @@
// This view is not part of the newly inflated expanded status bar.
mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
mDisableSubpixelTextTransitionListener = new DisableSubpixelTextTransitionListener(mView);
- bouncerViewBinder.bind(mView.findViewById(R.id.keyguard_bouncer_container));
+ bindBouncer(bouncerViewBinder);
collectFlow(mView, keyguardTransitionInteractor.transition(
Edge.create(LOCKSCREEN, DREAMING)),
@@ -256,6 +264,35 @@
dumpManager.registerDumpable(this);
}
+ private void bindBouncer(BouncerViewBinder bouncerViewBinder) {
+ if (ComposeBouncerFlags.INSTANCE.isOnlyComposeBouncerEnabled()) {
+ collectFlow(mView, mKeyguardTransitionInteractor.isFinishedIn(Scenes.Gone,
+ KeyguardState.GONE), this::removeBouncerParentView);
+ collectFlow(mView, mKeyguardTransitionInteractor.transition(
+ new Edge.StateToState(KeyguardState.GONE, null)),
+ this::handleGoneToAnyOtherStateTransition);
+ collectFlow(mView, mPrimaryBouncerInteractor.isShowing(),
+ (showing) -> ViewKt.setVisible(mBouncerParentView, showing));
+ }
+ mBouncerParentView = mView.findViewById(R.id.keyguard_bouncer_container);
+ bouncerViewBinder.bind(mBouncerParentView);
+ }
+
+ private void handleGoneToAnyOtherStateTransition(TransitionStep transitionStep) {
+ if (transitionStep.getTransitionState() == TransitionState.STARTED) {
+ if (mView.indexOfChild(mBouncerParentView) != -1) {
+ mView.removeView(mBouncerParentView);
+ }
+ mView.addView(mBouncerParentView);
+ }
+ }
+
+ private void removeBouncerParentView(boolean isFinishedInGoneState) {
+ if (isFinishedInGoneState) {
+ mView.removeView(mBouncerParentView);
+ }
+ }
+
/**
* @return Location where to place the KeyguardMessageArea
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/shared/StatusBarRonChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/shared/StatusBarRonChips.kt
deleted file mode 100644
index 4c0c461..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/shared/StatusBarRonChips.kt
+++ /dev/null
@@ -1,61 +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.systemui.statusbar.chips.shared
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the status bar ron chips flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object StatusBarRonChips {
- /** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_STATUS_BAR_RON_CHIPS
-
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
-
- /** Is the refactor enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.statusBarRonChips()
-
- /**
- * Called to ensure code is only run when the flag is enabled. This protects users from the
- * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
- * build to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun isUnexpectedlyInLegacyMode() =
- RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
- /**
- * Called to ensure code is only run when the flag is disabled. This will throw an exception if
- * the flag is not enabled to ensure that the refactor author catches issues in testing.
- * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
- */
- @JvmStatic
- inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
-
- /**
- * Called to ensure code is only run when the flag is disabled. This will throw an exception if
- * the flag is enabled to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
index 02a29e2..9b96931 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
@@ -103,7 +103,8 @@
private boolean mTrackingHeadsUp;
private final HashSet<String> mSwipedOutKeys = new HashSet<>();
private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
- private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
+ @VisibleForTesting
+ public final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
= new ArraySet<>();
private boolean mIsExpanded;
private int mStatusBarState;
@@ -417,7 +418,7 @@
for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
if (isHeadsUpEntry(entry.getKey())) {
// Maybe the heads-up was removed already
- removeEntry(entry.getKey(), "mOnReorderingAllowedListener");
+ removeEntry(entry.getKey(), "allowReorder");
}
}
mEntriesToRemoveWhenReorderingAllowed.clear();
@@ -617,11 +618,8 @@
super.setEntry(entry, removeRunnable);
if (NotificationThrottleHun.isEnabled()) {
- if (!mVisualStabilityProvider.isReorderingAllowed()
- // We don't want to allow reordering while pulsing, but headsup need to
- // time out anyway
- && !entry.showingPulsing()) {
- mEntriesToRemoveWhenReorderingAllowed.add(entry);
+ mEntriesToRemoveWhenReorderingAllowed.add(entry);
+ if (!mVisualStabilityProvider.isReorderingAllowed()) {
entry.setSeenInShade(true);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index 659cee3..6584556 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -55,7 +55,7 @@
import com.android.systemui.statusbar.OperatorNameView;
import com.android.systemui.statusbar.OperatorNameViewController;
import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.chips.shared.StatusBarRonChips;
+import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips;
import com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState;
import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index 5ba5c06..1127f6f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -46,6 +46,8 @@
private val tag = "AvalancheController"
private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG)
+ var baseEntryMapStr : () -> String = { "baseEntryMapStr not initialized" }
+
var enableAtRuntime = true
set(value) {
if (!value) {
@@ -116,32 +118,43 @@
val key = getKey(entry)
if (runnable == null) {
- headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Runnable NULL, stop")
+ headsUpManagerLogger.logAvalancheUpdate(
+ caller, isEnabled, key,
+ "Runnable NULL, stop. ${getStateStr()}"
+ )
return
}
if (!isEnabled) {
- headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key,
- "NOT ENABLED, run runnable")
+ headsUpManagerLogger.logAvalancheUpdate(
+ caller, isEnabled, key,
+ "NOT ENABLED, run runnable. ${getStateStr()}"
+ )
runnable.run()
return
}
if (entry == null) {
- headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Entry NULL, stop")
+ headsUpManagerLogger.logAvalancheUpdate(
+ caller, isEnabled, key,
+ "Entry NULL, stop. ${getStateStr()}"
+ )
return
}
if (debug) {
debugRunnableLabelMap[runnable] = caller
}
- var outcome = ""
+ var stateAfter = ""
if (isShowing(entry)) {
- outcome = "update showing"
runnable.run()
+ stateAfter = "update showing"
+
} else if (entry in nextMap) {
- outcome = "update next"
nextMap[entry]?.add(runnable)
+ stateAfter = "update next"
+
} else if (headsUpEntryShowing == null) {
- outcome = "show now"
showNow(entry, arrayListOf(runnable))
+ stateAfter = "show now"
+
} else {
// Clean up invalid state when entry is in list but not map and vice versa
if (entry in nextMap) nextMap.remove(entry)
@@ -162,8 +175,8 @@
)
}
}
- outcome += getStateStr()
- headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, outcome)
+ stateAfter += getStateStr()
+ headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled = true, key, stateAfter)
}
@VisibleForTesting
@@ -181,32 +194,40 @@
val key = getKey(entry)
if (runnable == null) {
- headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key, "Runnable NULL, stop")
+ headsUpManagerLogger.logAvalancheDelete(
+ caller, isEnabled, key,
+ "Runnable NULL, stop. ${getStateStr()}"
+ )
return
}
if (!isEnabled) {
- headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
- "NOT ENABLED, run runnable")
runnable.run()
+ headsUpManagerLogger.logAvalancheDelete(
+ caller, isEnabled = false, key,
+ "NOT ENABLED, run runnable. ${getStateStr()}"
+ )
return
}
if (entry == null) {
- headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
- "Entry NULL, run runnable")
runnable.run()
+ headsUpManagerLogger.logAvalancheDelete(
+ caller, isEnabled = true, key,
+ "Entry NULL, run runnable. ${getStateStr()}"
+ )
return
}
- var outcome = ""
+ var stateAfter: String
if (entry in nextMap) {
- outcome = "remove from next"
if (entry in nextMap) nextMap.remove(entry)
if (entry in nextList) nextList.remove(entry)
uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_REMOVED)
+ stateAfter = "remove from next. ${getStateStr()}"
+
} else if (entry in debugDropSet) {
- outcome = "remove from dropset"
debugDropSet.remove(entry)
+ stateAfter = "remove from dropset. ${getStateStr()}"
+
} else if (isShowing(entry)) {
- outcome = "remove showing"
previousHunKey = getKey(headsUpEntryShowing)
// Show the next HUN before removing this one, so that we don't tell listeners
// onHeadsUpPinnedModeChanged, which causes
@@ -214,11 +235,13 @@
// HUN is animating out, resulting in a flicker.
showNext()
runnable.run()
+ stateAfter = "remove showing. ${getStateStr()}"
+
} else {
- outcome = "run runnable for untracked shown"
runnable.run()
+ stateAfter = "run runnable for untracked shown HUN. ${getStateStr()}"
}
- headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), outcome)
+ headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), stateAfter)
}
/**
@@ -400,12 +423,14 @@
}
private fun getStateStr(): String {
- return "\navalanche state:" +
+ return "\nAvalancheController:" +
"\n\tshowing: [${getKey(headsUpEntryShowing)}]" +
"\n\tprevious: [$previousHunKey]" +
"\n\tnext list: $nextListStr" +
"\n\tnext map: $nextMapStr" +
- "\n\tdropped: $dropSetStr"
+ "\n\tdropped: $dropSetStr" +
+ "\nBHUM.mHeadsUpEntryMap: " +
+ baseEntryMapStr()
}
private val dropSetStr: String
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index f37393a..30524a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -116,6 +116,7 @@
mAccessibilityMgr = accessibilityManagerWrapper;
mUiEventLogger = uiEventLogger;
mAvalancheController = avalancheController;
+ mAvalancheController.setBaseEntryMapStr(this::getEntryMapStr);
Resources resources = context.getResources();
mMinimumDisplayTime = NotificationThrottleHun.isEnabled()
? 500 : resources.getInteger(R.integer.heads_up_notification_minimum_time);
@@ -589,6 +590,18 @@
dumpInternal(pw, args);
}
+ private String getEntryMapStr() {
+ if (mHeadsUpEntryMap.isEmpty()) {
+ return "EMPTY";
+ }
+ StringBuilder entryMapStr = new StringBuilder();
+ for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
+ entryMapStr.append("\n\t").append(
+ entry.mEntry == null ? "null" : entry.mEntry.getKey());
+ }
+ return entryMapStr.toString();
+ }
+
protected void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
@@ -992,7 +1005,6 @@
* Clear any pending removal runnables.
*/
public void cancelAutoRemovalCallbacks(@Nullable String reason) {
- mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
Runnable runnable = () -> {
final boolean removed = cancelAutoRemovalCallbackInternal();
@@ -1001,6 +1013,7 @@
}
};
if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) {
+ mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks");
} else {
// Just removed
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index 600270c..41112cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -52,7 +52,7 @@
caller: String,
isEnabled: Boolean,
notifEntryKey: String,
- outcome: String
+ stateAfter: String
) {
buffer.log(
TAG,
@@ -60,7 +60,7 @@
{
str1 = caller
str2 = notifEntryKey
- str3 = outcome
+ str3 = stateAfter
bool1 = isEnabled
},
{ "$str1\n\t=> AC[isEnabled:$bool1] update: $str2\n\t=> $str3" }
@@ -71,7 +71,7 @@
caller: String,
isEnabled: Boolean,
notifEntryKey: String,
- outcome: String
+ stateAfter: String
) {
buffer.log(
TAG,
@@ -79,7 +79,7 @@
{
str1 = caller
str2 = notifEntryKey
- str3 = outcome
+ str3 = stateAfter
bool1 = isEnabled
},
{ "$str1\n\t=> AC[isEnabled:$bool1] delete: $str2\n\t=> $str3" }
@@ -136,7 +136,7 @@
str1 = entry.logKey
str2 = reason ?: "unknown"
},
- { "request: cancel auto remove of $str1 reason: $str2" }
+ { "$str2 => request: cancelAutoRemovalCallbacks: $str1" }
)
}
@@ -148,7 +148,7 @@
str1 = entry.logKey
str2 = reason ?: "unknown"
},
- { "cancel auto remove of $str1 reason: $str2" }
+ { "$str2 => cancel auto remove: $str1" }
)
}
@@ -161,7 +161,7 @@
str2 = reason
bool1 = isWaiting
},
- { "request: $str2 => remove entry $str1 isWaiting: $isWaiting" }
+ { "request: $str2 => removeEntry: $str1 isWaiting: $isWaiting" }
)
}
@@ -174,7 +174,7 @@
str2 = reason
bool1 = isWaiting
},
- { "$str2 => remove entry $str1 isWaiting: $isWaiting" }
+ { "$str2 => removeEntry: $str1 isWaiting: $isWaiting" }
)
}
@@ -216,12 +216,12 @@
str1 = logKey(key)
str2 = reason
},
- { "remove notification $str1 when headsUpEntry is null, reason: $str2" }
+ { "remove notif $str1 when headsUpEntry is null, reason: $str2" }
)
}
fun logNotificationActuallyRemoved(entry: NotificationEntry) {
- buffer.log(TAG, INFO, { str1 = entry.logKey }, { "notification removed $str1 " })
+ buffer.log(TAG, INFO, { str1 = entry.logKey }, { "removed: $str1 " })
}
fun logUpdateNotificationRequest(key: String, alert: Boolean, hasEntry: Boolean) {
@@ -233,7 +233,7 @@
bool1 = alert
bool2 = hasEntry
},
- { "request: update notification $str1 alert: $bool1 hasEntry: $bool2" }
+ { "request: update notif $str1 alert: $bool1 hasEntry: $bool2" }
)
}
@@ -246,7 +246,7 @@
bool1 = alert
bool2 = hasEntry
},
- { "update notification $str1 alert: $bool1 hasEntry: $bool2" }
+ { "update notif $str1 alert: $bool1 hasEntry: $bool2" }
)
}
@@ -281,7 +281,7 @@
bool1 = isPinned
str2 = reason
},
- { "$str2 => set entry pinned $str1 pinned: $bool1" }
+ { "$str2 => setEntryPinned[$bool1]: $str1" }
)
}
@@ -290,7 +290,7 @@
TAG,
INFO,
{ bool1 = hasPinnedNotification },
- { "has pinned notification changed to $bool1" }
+ { "hasPinnedNotification[$bool1]" }
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
index bbfa32b..32a4f12 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
@@ -117,7 +117,14 @@
int displayId) {
Runnable showToastRunnable = () -> {
UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
- Context context = mContext.createContextAsUser(userHandle, 0);
+ Context context;
+ try {
+ context = mContext.createContextAsUser(userHandle, 0);
+ } catch (IllegalStateException e) {
+ // b/366533044 : Own package not found for systemui
+ Log.e(TAG, "Cannot create toast because cannot create context", e);
+ return;
+ }
DisplayManager mDisplayManager = mContext.getSystemService(DisplayManager.class);
Display display = mDisplayManager.getDisplay(displayId);
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 7385b82..e764015 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -22,6 +22,7 @@
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
+import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE;
import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
@@ -449,7 +450,8 @@
@Override
public void onEntryRemoved(NotificationEntry entry,
@NotifCollection.CancellationReason int reason) {
- if (reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL) {
+ if (reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL
+ || reason == REASON_PACKAGE_BANNED) {
BubblesManager.this.onEntryRemoved(entry);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
index bbff539..6dc4b10 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
@@ -18,10 +18,6 @@
import android.graphics.Point
import android.hardware.biometrics.BiometricSourceType
-import android.hardware.biometrics.ComponentInfoInternal
-import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.biometrics.SensorProperties
-import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.testing.TestableLooper.RunWithLooper
import android.util.DisplayMetrics
@@ -47,7 +43,6 @@
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.leak.RotationUtils
import com.android.systemui.util.mockito.any
-import javax.inject.Provider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.After
import org.junit.Assert.assertFalse
@@ -67,6 +62,8 @@
import org.mockito.MockitoAnnotations
import org.mockito.MockitoSession
import org.mockito.quality.Strictness
+import javax.inject.Provider
+
@ExperimentalCoroutinesApi
@SmallTest
@@ -82,28 +79,35 @@
@Mock private lateinit var authController: AuthController
@Mock private lateinit var authRippleInteractor: AuthRippleInteractor
@Mock private lateinit var keyguardStateController: KeyguardStateController
- @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
- @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
- @Mock private lateinit var biometricUnlockController: BiometricUnlockController
- @Mock private lateinit var udfpsControllerProvider: Provider<UdfpsController>
- @Mock private lateinit var udfpsController: UdfpsController
- @Mock private lateinit var statusBarStateController: StatusBarStateController
- @Mock private lateinit var lightRevealScrim: LightRevealScrim
- @Mock private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal
+ @Mock
+ private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+ @Mock
+ private lateinit var notificationShadeWindowController: NotificationShadeWindowController
+ @Mock
+ private lateinit var biometricUnlockController: BiometricUnlockController
+ @Mock
+ private lateinit var udfpsControllerProvider: Provider<UdfpsController>
+ @Mock
+ private lateinit var udfpsController: UdfpsController
+ @Mock
+ private lateinit var statusBarStateController: StatusBarStateController
+ @Mock
+ private lateinit var lightRevealScrim: LightRevealScrim
+ @Mock
+ private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal
private val facePropertyRepository = FakeFacePropertyRepository()
private val displayMetrics = DisplayMetrics()
@Captor
private lateinit var biometricUnlockListener:
- ArgumentCaptor<BiometricUnlockController.BiometricUnlockEventsListener>
+ ArgumentCaptor<BiometricUnlockController.BiometricUnlockEventsListener>
@Before
fun setUp() {
mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
MockitoAnnotations.initMocks(this)
- staticMockSession =
- mockitoSession()
+ staticMockSession = mockitoSession()
.mockStatic(RotationUtils::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
@@ -112,26 +116,25 @@
`when`(authController.udfpsProps).thenReturn(listOf(fpSensorProp))
`when`(udfpsControllerProvider.get()).thenReturn(udfpsController)
- controller =
- AuthRippleController(
- context,
- authController,
- configurationController,
- keyguardUpdateMonitor,
- keyguardStateController,
- wakefulnessLifecycle,
- commandRegistry,
- notificationShadeWindowController,
- udfpsControllerProvider,
- statusBarStateController,
- displayMetrics,
- KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)),
- biometricUnlockController,
- lightRevealScrim,
- authRippleInteractor,
- facePropertyRepository,
- rippleView,
- )
+ controller = AuthRippleController(
+ context,
+ authController,
+ configurationController,
+ keyguardUpdateMonitor,
+ keyguardStateController,
+ wakefulnessLifecycle,
+ commandRegistry,
+ notificationShadeWindowController,
+ udfpsControllerProvider,
+ statusBarStateController,
+ displayMetrics,
+ KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)),
+ biometricUnlockController,
+ lightRevealScrim,
+ authRippleInteractor,
+ facePropertyRepository,
+ rippleView,
+ )
controller.init()
}
@@ -147,18 +150,13 @@
`when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation)
controller.onViewAttached()
`when`(keyguardStateController.isShowing).thenReturn(true)
- `when`(
- keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
- eq(BiometricSourceType.FINGERPRINT)
- )
- )
- .thenReturn(true)
+ `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+ eq(BiometricSourceType.FINGERPRINT))).thenReturn(true)
// WHEN fingerprint authenticated
verify(biometricUnlockController).addListener(biometricUnlockListener.capture())
- biometricUnlockListener.value.onBiometricUnlockedWithKeyguardDismissal(
- BiometricSourceType.FINGERPRINT
- )
+ biometricUnlockListener.value
+ .onBiometricUnlockedWithKeyguardDismissal(BiometricSourceType.FINGERPRINT)
// THEN update sensor location and show ripple
verify(rippleView).setFingerprintSensorLocation(fpsLocation, 0f)
@@ -171,12 +169,8 @@
val fpsLocation = Point(5, 5)
`when`(authController.udfpsLocation).thenReturn(fpsLocation)
controller.onViewAttached()
- `when`(
- keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
- eq(BiometricSourceType.FINGERPRINT)
- )
- )
- .thenReturn(true)
+ `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+ eq(BiometricSourceType.FINGERPRINT))).thenReturn(true)
// WHEN keyguard is NOT showing & fingerprint authenticated
`when`(keyguardStateController.isShowing).thenReturn(false)
@@ -185,8 +179,7 @@
captor.value.onBiometricAuthenticated(
0 /* userId */,
BiometricSourceType.FINGERPRINT /* type */,
- false /* isStrongBiometric */
- )
+ false /* isStrongBiometric */)
// THEN no ripple
verify(rippleView, never()).startUnlockedRipple(any())
@@ -201,19 +194,14 @@
`when`(keyguardStateController.isShowing).thenReturn(true)
// WHEN unlocking with fingerprint is NOT allowed & fingerprint authenticated
- `when`(
- keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
- eq(BiometricSourceType.FINGERPRINT)
- )
- )
- .thenReturn(false)
+ `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+ eq(BiometricSourceType.FINGERPRINT))).thenReturn(false)
val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
verify(keyguardUpdateMonitor).registerCallback(captor.capture())
captor.value.onBiometricAuthenticated(
0 /* userId */,
BiometricSourceType.FINGERPRINT /* type */,
- false /* isStrongBiometric */
- )
+ false /* isStrongBiometric */)
// THEN no ripple
verify(rippleView, never()).startUnlockedRipple(any())
@@ -230,8 +218,7 @@
captor.value.onBiometricAuthenticated(
0 /* userId */,
BiometricSourceType.FACE /* type */,
- false /* isStrongBiometric */
- )
+ false /* isStrongBiometric */)
verify(rippleView, never()).startUnlockedRipple(any())
}
@@ -246,17 +233,18 @@
captor.value.onBiometricAuthenticated(
0 /* userId */,
BiometricSourceType.FINGERPRINT /* type */,
- false /* isStrongBiometric */
- )
+ false /* isStrongBiometric */)
verify(rippleView, never()).startUnlockedRipple(any())
}
@Test
fun registersAndDeregisters() {
controller.onViewAttached()
- val captor = ArgumentCaptor.forClass(KeyguardStateController.Callback::class.java)
+ val captor = ArgumentCaptor
+ .forClass(KeyguardStateController.Callback::class.java)
verify(keyguardStateController).addCallback(captor.capture())
- val captor2 = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java)
+ val captor2 = ArgumentCaptor
+ .forClass(WakefulnessLifecycle.Observer::class.java)
verify(wakefulnessLifecycle).addObserver(captor2.capture())
controller.onViewDetached()
verify(keyguardStateController).removeCallback(any())
@@ -271,25 +259,17 @@
`when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation)
controller.onViewAttached()
`when`(keyguardStateController.isShowing).thenReturn(true)
- `when`(
- keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
- BiometricSourceType.FINGERPRINT
- )
- )
- .thenReturn(true)
+ `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+ BiometricSourceType.FINGERPRINT)).thenReturn(true)
`when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
controller.showUnlockRipple(BiometricSourceType.FINGERPRINT)
- assertTrue(
- "reveal didn't start on keyguardFadingAway",
- controller.startLightRevealScrimOnKeyguardFadingAway
- )
+ assertTrue("reveal didn't start on keyguardFadingAway",
+ controller.startLightRevealScrimOnKeyguardFadingAway)
`when`(keyguardStateController.isKeyguardFadingAway).thenReturn(true)
controller.onKeyguardFadingAwayChanged()
- assertFalse(
- "reveal triggers multiple times",
- controller.startLightRevealScrimOnKeyguardFadingAway
- )
+ assertFalse("reveal triggers multiple times",
+ controller.startLightRevealScrimOnKeyguardFadingAway)
}
@Test
@@ -302,27 +282,23 @@
`when`(keyguardStateController.isShowing).thenReturn(true)
`when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
`when`(authController.isUdfpsFingerDown).thenReturn(true)
- `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(eq(BiometricSourceType.FACE)))
- .thenReturn(true)
+ `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+ eq(BiometricSourceType.FACE))).thenReturn(true)
controller.showUnlockRipple(BiometricSourceType.FACE)
- assertTrue(
- "reveal didn't start on keyguardFadingAway",
- controller.startLightRevealScrimOnKeyguardFadingAway
- )
+ assertTrue("reveal didn't start on keyguardFadingAway",
+ controller.startLightRevealScrimOnKeyguardFadingAway)
`when`(keyguardStateController.isKeyguardFadingAway).thenReturn(true)
controller.onKeyguardFadingAwayChanged()
- assertFalse(
- "reveal triggers multiple times",
- controller.startLightRevealScrimOnKeyguardFadingAway
- )
+ assertFalse("reveal triggers multiple times",
+ controller.startLightRevealScrimOnKeyguardFadingAway)
}
@Test
fun testUpdateRippleColor() {
controller.onViewAttached()
- val captor =
- ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
+ val captor = ArgumentCaptor
+ .forClass(ConfigurationController.ConfigurationListener::class.java)
verify(configurationController).addCallback(captor.capture())
reset(rippleView)
@@ -357,40 +333,6 @@
}
@Test
- fun testUltrasonicUdfps_onFingerDown_runningForDeviceEntry_doNotShowDwellRipple() {
- // GIVEN UDFPS is ultrasonic
- `when`(authController.udfpsProps)
- .thenReturn(
- listOf(
- FingerprintSensorPropertiesInternal(
- 0 /* sensorId */,
- SensorProperties.STRENGTH_STRONG,
- 5 /* maxEnrollmentsPerUser */,
- listOf<ComponentInfoInternal>(),
- FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC,
- false /* halControlsIllumination */,
- true /* resetLockoutRequiresHardwareAuthToken */,
- listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT),
- )
- )
- )
-
- // GIVEN fingerprint detection is running on keyguard
- `when`(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(true)
-
- // GIVEN view is already attached
- controller.onViewAttached()
- val captor = ArgumentCaptor.forClass(UdfpsController.Callback::class.java)
- verify(udfpsController).addCallback(captor.capture())
-
- // WHEN finger is down
- captor.value.onFingerDown()
-
- // THEN never show dwell ripple
- verify(rippleView, never()).startDwellRipple(false)
- }
-
- @Test
fun testUdfps_onFingerDown_notDeviceEntry_doesNotShowDwellRipple() {
// GIVEN fingerprint detection is NOT running on keyguard
`when`(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index 755adc6..6423d25 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -64,7 +64,6 @@
otherTiles = listOf(),
columns = 4,
modifier = Modifier.fillMaxSize(),
- onAddTile = { _, _ -> },
onRemoveTile = {},
onSetTiles = onSetTiles,
onResize = {},
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
new file mode 100644
index 0000000..682ed92
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.panels.ui.compose
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.performCustomAccessibilityActionWithLabel
+import androidx.compose.ui.text.AnnotatedString
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ResizingTest : SysuiTestCase() {
+ @get:Rule val composeRule = createComposeRule()
+
+ @Composable
+ private fun EditTileGridUnderTest(listState: EditTileListState, onResize: (TileSpec) -> Unit) {
+ DefaultEditTileGrid(
+ currentListState = listState,
+ otherTiles = listOf(),
+ columns = 4,
+ modifier = Modifier.fillMaxSize(),
+ onRemoveTile = {},
+ onSetTiles = {},
+ onResize = onResize,
+ )
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun resizedIcon_shouldBeLarge() {
+ var tiles by mutableStateOf(TestEditTiles)
+ val listState = EditTileListState(tiles, 4)
+ composeRule.setContent {
+ EditTileGridUnderTest(listState) { spec ->
+ tiles =
+ tiles.map {
+ if (it.tile.tileSpec == spec) {
+ toggleWidth(it)
+ } else {
+ it
+ }
+ }
+ }
+ }
+ composeRule.waitForIdle()
+
+ composeRule
+ .onNodeWithContentDescription("tileA")
+ .performCustomAccessibilityActionWithLabel("Toggle size")
+
+ assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(2)
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun resizedLarge_shouldBeIcon() {
+ var tiles by mutableStateOf(TestEditTiles)
+ val listState = EditTileListState(tiles, 4)
+ composeRule.setContent {
+ EditTileGridUnderTest(listState) { spec ->
+ tiles =
+ tiles.map {
+ if (it.tile.tileSpec == spec) {
+ toggleWidth(it)
+ } else {
+ it
+ }
+ }
+ }
+ }
+ composeRule.waitForIdle()
+
+ composeRule
+ .onNodeWithContentDescription("tileD_large")
+ .performCustomAccessibilityActionWithLabel("Toggle size")
+
+ assertThat(tiles.find { it.tile.tileSpec.spec == "tileD_large" }?.width).isEqualTo(1)
+ }
+
+ companion object {
+ private fun toggleWidth(tile: SizedTile<EditTileViewModel>): SizedTile<EditTileViewModel> {
+ return SizedTileImpl(tile.tile, width = if (tile.isIcon) 2 else 1)
+ }
+
+ private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> {
+ return SizedTileImpl(
+ EditTileViewModel(
+ tileSpec = TileSpec.create(tileSpec),
+ icon =
+ Icon.Resource(
+ android.R.drawable.star_on,
+ ContentDescription.Loaded(tileSpec),
+ ),
+ label = AnnotatedString(tileSpec),
+ appName = null,
+ isCurrent = true,
+ availableEditActions = emptySet(),
+ category = TileCategory.UNKNOWN,
+ ),
+ getWidth(tileSpec),
+ )
+ }
+
+ private fun getWidth(tileSpec: String): Int {
+ return if (tileSpec.endsWith("large")) {
+ 2
+ } else {
+ 1
+ }
+ }
+
+ private val TestEditTiles =
+ listOf(
+ createEditTile("tileA"),
+ createEditTile("tileB"),
+ createEditTile("tileC"),
+ createEditTile("tileD_large"),
+ createEditTile("tileE"),
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 3e7980d..0d39834 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -24,6 +24,7 @@
import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
+import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
import static androidx.test.ext.truth.content.IntentSubject.assertThat;
@@ -1100,6 +1101,18 @@
}
@Test
+ public void testNotifsBanned_entryListenerRemove() {
+ mEntryListener.onEntryAdded(mRow);
+ mBubbleController.updateBubble(mBubbleEntry);
+
+ assertTrue(mBubbleController.hasBubbles());
+
+ // Removes the notification
+ mEntryListener.onEntryRemoved(mRow, REASON_PACKAGE_BANNED);
+ assertFalse(mBubbleController.hasBubbles());
+ }
+
+ @Test
public void removeBubble_intercepted() {
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 765afef..88edb12 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -2169,6 +2169,9 @@
Log.i(TAG, "callingUid=" + callingUid + ", userId=" + accounts.userId
+ " performing rename account");
Account resultingAccount = renameAccountInternal(accounts, accountToRename, newName);
+ if (resultingAccount == null) {
+ resultingAccount = accountToRename;
+ }
Bundle result = new Bundle();
result.putString(AccountManager.KEY_ACCOUNT_NAME, resultingAccount.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE, resultingAccount.type);
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index 34c3d7e..a73a991 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -74,6 +74,7 @@
import android.util.Xml;
import com.android.internal.R;
+import com.android.internal.annotations.Keep;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.util.FrameworkStatsLog;
@@ -214,7 +215,7 @@
class PairingThread extends Thread implements NsdManager.RegistrationListener {
private NsdManager mNsdManager;
- private String mPublicKey;
+ @Keep private String mPublicKey;
private String mPairingCode;
private String mGuid;
private String mServiceName;
diff --git a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
index df45a6e..177eefb 100644
--- a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
+++ b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
@@ -76,11 +76,12 @@
try {
mIsInEmergencyCall = mTelephonyManager.isEmergencyNumber(
intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
- dispatchEmergencyStateChanged();
} catch (IllegalStateException | UnsupportedOperationException e) {
Log.w(TAG, "Failed to call TelephonyManager.isEmergencyNumber().", e);
}
}
+
+ dispatchEmergencyStateChanged();
}
}, new IntentFilter(Intent.ACTION_NEW_OUTGOING_CALL));
@@ -140,9 +141,10 @@
if (mIsInEmergencyCall) {
mEmergencyCallEndRealtimeMs = SystemClock.elapsedRealtime();
mIsInEmergencyCall = false;
- dispatchEmergencyStateChanged();
}
}
+
+ dispatchEmergencyStateChanged();
}
}
}
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index a41675a..6303ecd 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -298,13 +298,12 @@
restoreSettings();
- if (Build.IS_USER) {
- // Wipe all shell overlays on boot, to recover from a potentially broken device
- String shellPkgName = TextUtils.emptyIfNull(
- getContext().getString(android.R.string.config_systemShell));
- mSettings.removeIf(overlayInfo -> overlayInfo.isFabricated
- && shellPkgName.equals(overlayInfo.packageName));
- }
+ // Wipe all shell overlays on boot, to recover from a potentially broken device
+ String shellPkgName = TextUtils.emptyIfNull(
+ getContext().getString(android.R.string.config_systemShell));
+ mSettings.removeIf(overlayInfo -> overlayInfo.isFabricated
+ && shellPkgName.equals(overlayInfo.packageName));
+
initIfNeeded();
onStartUser(UserHandle.USER_SYSTEM);
diff --git a/services/core/java/com/android/server/pm/Android.bp b/services/core/java/com/android/server/pm/Android.bp
new file mode 100644
index 0000000..d625cf2
--- /dev/null
+++ b/services/core/java/com/android/server/pm/Android.bp
@@ -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 {
+ default_team: "trendy_team_framework_android_packages",
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "framework-pm-service-sources",
+ srcs: [
+ "**/*.java",
+ "**/*.aidl",
+ ],
+ exclude_srcs: [
+ "dex/**/*.java",
+ "User*.java",
+ "Shortcut*.java",
+ ],
+ visibility: ["//frameworks/base"],
+}
diff --git a/services/core/java/com/android/server/wm/LaunchParamsPersister.java b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
index 2394da9..b84ef37 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsPersister.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
@@ -50,6 +50,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.CountDownLatch;
import java.util.function.IntFunction;
/**
@@ -92,6 +93,12 @@
new SparseArray<>();
/**
+ * A map from user ID to the active {@link LoadingQueueItem} user when we're loading the launch
+ * params for that user.
+ */
+ private final SparseArray<LoadingQueueItem> mLoadingItemMap = new SparseArray<>();
+
+ /**
* A map from {@link android.content.pm.ActivityInfo.WindowLayout#windowLayoutAffinity} to
* activity's component name for reverse queries from window layout affinities to activities.
* Used to decide if we should use another activity's record with the same affinity.
@@ -117,112 +124,30 @@
}
void onUnlockUser(int userId) {
- loadLaunchParams(userId);
+ if (mLoadingItemMap.contains(userId)) {
+ Slog.e(TAG, "Duplicate onUnlockUser " + userId);
+ return;
+ }
+ final LoadingQueueItem item = new LoadingQueueItem(userId);
+ mLoadingItemMap.put(userId, item);
+ mPersisterQueue.addItem(item, /* flush */ false);
}
void onCleanupUser(int userId) {
+ final LoadingQueueItem item = mLoadingItemMap.removeReturnOld(userId);
+ if (item != null) {
+ item.abort();
+
+ mPersisterQueue.removeItems(
+ queueItem -> queueItem.mUserId == userId, LoadingQueueItem.class);
+ }
mLaunchParamsMap.remove(userId);
}
- private void loadLaunchParams(int userId) {
- final List<File> filesToDelete = new ArrayList<>();
- final File launchParamsFolder = getLaunchParamFolder(userId);
- if (!launchParamsFolder.isDirectory()) {
- Slog.i(TAG, "Didn't find launch param folder for user " + userId);
- return;
- }
-
- final Set<String> packages = new ArraySet<>(mPackageList.getPackageNames());
-
- final File[] paramsFiles = launchParamsFolder.listFiles();
- final ArrayMap<ComponentName, PersistableLaunchParams> map =
- new ArrayMap<>(paramsFiles.length);
- mLaunchParamsMap.put(userId, map);
-
- for (File paramsFile : paramsFiles) {
- if (!paramsFile.isFile()) {
- Slog.w(TAG, paramsFile.getAbsolutePath() + " is not a file.");
- continue;
- }
- if (!paramsFile.getName().endsWith(LAUNCH_PARAMS_FILE_SUFFIX)) {
- Slog.w(TAG, "Unexpected params file name: " + paramsFile.getName());
- filesToDelete.add(paramsFile);
- continue;
- }
- String paramsFileName = paramsFile.getName();
- // Migrate all records from old separator to new separator.
- final int oldSeparatorIndex =
- paramsFileName.indexOf(OLD_ESCAPED_COMPONENT_SEPARATOR);
- if (oldSeparatorIndex != -1) {
- if (paramsFileName.indexOf(
- OLD_ESCAPED_COMPONENT_SEPARATOR, oldSeparatorIndex + 1) != -1) {
- // Rare case. We have more than one old escaped component separator probably
- // because this app uses underscore in their package name. We can't distinguish
- // which one is the real separator so let's skip it.
- filesToDelete.add(paramsFile);
- continue;
- }
- paramsFileName = paramsFileName.replace(
- OLD_ESCAPED_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR);
- final File newFile = new File(launchParamsFolder, paramsFileName);
- if (paramsFile.renameTo(newFile)) {
- paramsFile = newFile;
- } else {
- // Rare case. For some reason we can't rename the file. Let's drop this record
- // instead.
- filesToDelete.add(paramsFile);
- continue;
- }
- }
- final String componentNameString = paramsFileName.substring(
- 0 /* beginIndex */,
- paramsFileName.length() - LAUNCH_PARAMS_FILE_SUFFIX.length())
- .replace(ESCAPED_COMPONENT_SEPARATOR, ORIGINAL_COMPONENT_SEPARATOR);
- final ComponentName name = ComponentName.unflattenFromString(
- componentNameString);
- if (name == null) {
- Slog.w(TAG, "Unexpected file name: " + paramsFileName);
- filesToDelete.add(paramsFile);
- continue;
- }
-
- if (!packages.contains(name.getPackageName())) {
- // Rare case. PersisterQueue doesn't have a chance to remove files for removed
- // packages last time.
- filesToDelete.add(paramsFile);
- continue;
- }
-
- try (InputStream in = new FileInputStream(paramsFile)) {
- final PersistableLaunchParams params = new PersistableLaunchParams();
- final TypedXmlPullParser parser = Xml.resolvePullParser(in);
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT
- && event != XmlPullParser.END_TAG) {
- if (event != XmlPullParser.START_TAG) {
- continue;
- }
-
- final String tagName = parser.getName();
- if (!TAG_LAUNCH_PARAMS.equals(tagName)) {
- Slog.w(TAG, "Unexpected tag name: " + tagName);
- continue;
- }
-
- params.restore(paramsFile, parser);
- }
-
- map.put(name, params);
- addComponentNameToLaunchParamAffinityMapIfNotNull(
- name, params.mWindowLayoutAffinity);
- } catch (Exception e) {
- Slog.w(TAG, "Failed to restore launch params for " + name, e);
- filesToDelete.add(paramsFile);
- }
- }
-
- if (!filesToDelete.isEmpty()) {
- mPersisterQueue.addItem(new CleanUpComponentQueueItem(filesToDelete), true);
+ private void waitForLoading(int userId) {
+ final LoadingQueueItem item = mLoadingItemMap.get(userId);
+ if (item != null) {
+ item.waitUntilFinish();
}
}
@@ -236,6 +161,7 @@
return;
}
final int userId = task.mUserId;
+ waitForLoading(userId);
PersistableLaunchParams params;
ArrayMap<ComponentName, PersistableLaunchParams> map = mLaunchParamsMap.get(userId);
if (map == null) {
@@ -297,6 +223,7 @@
void getLaunchParams(Task task, ActivityRecord activity, LaunchParams outParams) {
final ComponentName name = task != null ? task.realActivity : activity.mActivityComponent;
final int userId = task != null ? task.mUserId : activity.mUserId;
+ waitForLoading(userId);
final String windowLayoutAffinity;
if (task != null) {
windowLayoutAffinity = task.mWindowLayoutAffinity;
@@ -394,6 +321,156 @@
}
}
+ /**
+ * The work item used to load launch parameters with {@link PersisterQueue} in a background
+ * thread, so that we don't block the thread {@link com.android.server.am.UserController} uses
+ * to broadcast user state changes for I/O operations. See b/365983567 for more details.
+ */
+ private class LoadingQueueItem implements PersisterQueue.QueueItem {
+ private final int mUserId;
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private boolean mAborted = false;
+
+ private LoadingQueueItem(int userId) {
+ mUserId = userId;
+ }
+
+ @Override
+ public void process() {
+ try {
+ loadLaunchParams();
+ } finally {
+ synchronized (mSupervisor.mService.getGlobalLock()) {
+ mLoadingItemMap.remove(mUserId);
+ mLatch.countDown();
+ }
+ }
+ }
+
+ private void abort() {
+ mAborted = true;
+ }
+
+ private void waitUntilFinish() {
+ if (mAborted) {
+ return;
+ }
+
+ try {
+ mLatch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void loadLaunchParams() {
+ final List<File> filesToDelete = new ArrayList<>();
+ final File launchParamsFolder = getLaunchParamFolder(mUserId);
+ if (!launchParamsFolder.isDirectory()) {
+ Slog.i(TAG, "Didn't find launch param folder for user " + mUserId);
+ return;
+ }
+
+ final Set<String> packages = new ArraySet<>(mPackageList.getPackageNames());
+
+ final File[] paramsFiles = launchParamsFolder.listFiles();
+ final ArrayMap<ComponentName, PersistableLaunchParams> map =
+ new ArrayMap<>(paramsFiles.length);
+
+ for (File paramsFile : paramsFiles) {
+ if (!paramsFile.isFile()) {
+ Slog.w(TAG, paramsFile.getAbsolutePath() + " is not a file.");
+ continue;
+ }
+ if (!paramsFile.getName().endsWith(LAUNCH_PARAMS_FILE_SUFFIX)) {
+ Slog.w(TAG, "Unexpected params file name: " + paramsFile.getName());
+ filesToDelete.add(paramsFile);
+ continue;
+ }
+ String paramsFileName = paramsFile.getName();
+ // Migrate all records from old separator to new separator.
+ final int oldSeparatorIndex =
+ paramsFileName.indexOf(OLD_ESCAPED_COMPONENT_SEPARATOR);
+ if (oldSeparatorIndex != -1) {
+ if (paramsFileName.indexOf(
+ OLD_ESCAPED_COMPONENT_SEPARATOR, oldSeparatorIndex + 1) != -1) {
+ // Rare case. We have more than one old escaped component separator probably
+ // because this app uses underscore in their package name. We can't
+ // distinguish which one is the real separator so let's skip it.
+ filesToDelete.add(paramsFile);
+ continue;
+ }
+ paramsFileName = paramsFileName.replace(
+ OLD_ESCAPED_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR);
+ final File newFile = new File(launchParamsFolder, paramsFileName);
+ if (paramsFile.renameTo(newFile)) {
+ paramsFile = newFile;
+ } else {
+ // Rare case. For some reason we can't rename the file. Let's drop this
+ // record instead.
+ filesToDelete.add(paramsFile);
+ continue;
+ }
+ }
+ final String componentNameString = paramsFileName.substring(
+ 0 /* beginIndex */,
+ paramsFileName.length() - LAUNCH_PARAMS_FILE_SUFFIX.length())
+ .replace(ESCAPED_COMPONENT_SEPARATOR, ORIGINAL_COMPONENT_SEPARATOR);
+ final ComponentName name = ComponentName.unflattenFromString(
+ componentNameString);
+ if (name == null) {
+ Slog.w(TAG, "Unexpected file name: " + paramsFileName);
+ filesToDelete.add(paramsFile);
+ continue;
+ }
+
+ if (!packages.contains(name.getPackageName())) {
+ // Rare case. PersisterQueue doesn't have a chance to remove files for removed
+ // packages last time.
+ filesToDelete.add(paramsFile);
+ continue;
+ }
+
+ try (InputStream in = new FileInputStream(paramsFile)) {
+ final PersistableLaunchParams params = new PersistableLaunchParams();
+ final TypedXmlPullParser parser = Xml.resolvePullParser(in);
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT
+ && event != XmlPullParser.END_TAG) {
+ if (event != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ final String tagName = parser.getName();
+ if (!TAG_LAUNCH_PARAMS.equals(tagName)) {
+ Slog.w(TAG, "Unexpected tag name: " + tagName);
+ continue;
+ }
+
+ params.restore(paramsFile, parser);
+ }
+
+ map.put(name, params);
+ addComponentNameToLaunchParamAffinityMapIfNotNull(
+ name, params.mWindowLayoutAffinity);
+ } catch (Exception e) {
+ Slog.w(TAG, "Failed to restore launch params for " + name, e);
+ filesToDelete.add(paramsFile);
+ }
+ }
+
+ synchronized (mSupervisor.mService.getGlobalLock()) {
+ if (!mAborted) {
+ mLaunchParamsMap.put(mUserId, map);
+ }
+ }
+
+ if (!filesToDelete.isEmpty()) {
+ mPersisterQueue.addItem(new CleanUpComponentQueueItem(filesToDelete), true);
+ }
+ }
+ }
+
private class LaunchParamsWriteQueueItem
implements PersisterQueue.WriteQueueItem<LaunchParamsWriteQueueItem> {
private final int mUserId;
@@ -466,7 +543,8 @@
}
}
- private class CleanUpComponentQueueItem implements PersisterQueue.WriteQueueItem {
+ private static class CleanUpComponentQueueItem
+ implements PersisterQueue.WriteQueueItem<CleanUpComponentQueueItem> {
private final List<File> mComponentFiles;
private CleanUpComponentQueueItem(List<File> componentFiles) {
@@ -483,7 +561,7 @@
}
}
- private class PersistableLaunchParams {
+ private static class PersistableLaunchParams {
private static final String ATTR_WINDOWING_MODE = "windowing_mode";
private static final String ATTR_DISPLAY_UNIQUE_ID = "display_unique_id";
private static final String ATTR_BOUNDS = "bounds";
diff --git a/services/core/java/com/android/server/wm/PersisterQueue.java b/services/core/java/com/android/server/wm/PersisterQueue.java
index 9dc3d6a..f66069c 100644
--- a/services/core/java/com/android/server/wm/PersisterQueue.java
+++ b/services/core/java/com/android/server/wm/PersisterQueue.java
@@ -49,14 +49,16 @@
/** Special value for mWriteTime to mean don't wait, just write */
private static final long FLUSH_QUEUE = -1;
- /** An {@link WriteQueueItem} that doesn't do anything. Used to trigger {@link
- * Listener#onPreProcessItem}. */
- static final WriteQueueItem EMPTY_ITEM = () -> { };
+ /**
+ * A {@link QueueItem} that doesn't do anything. Used to trigger
+ * {@link Listener#onPreProcessItem}.
+ */
+ static final QueueItem EMPTY_ITEM = () -> { };
private final long mInterWriteDelayMs;
private final long mPreTaskDelayMs;
private final LazyTaskWriterThread mLazyTaskWriterThread;
- private final ArrayList<WriteQueueItem> mWriteQueue = new ArrayList<>();
+ private final ArrayList<QueueItem> mQueue = new ArrayList<>();
private final ArrayList<Listener> mListeners = new ArrayList<>();
@@ -105,10 +107,10 @@
mLazyTaskWriterThread.join();
}
- synchronized void addItem(WriteQueueItem item, boolean flush) {
- mWriteQueue.add(item);
+ synchronized void addItem(QueueItem item, boolean flush) {
+ mQueue.add(item);
- if (flush || mWriteQueue.size() > MAX_WRITE_QUEUE_LENGTH) {
+ if (flush || mQueue.size() > MAX_WRITE_QUEUE_LENGTH) {
mNextWriteTime = FLUSH_QUEUE;
} else if (mNextWriteTime == 0) {
mNextWriteTime = SystemClock.uptimeMillis() + mPreTaskDelayMs;
@@ -116,11 +118,12 @@
notify();
}
- synchronized <T extends WriteQueueItem> T findLastItem(Predicate<T> predicate, Class<T> clazz) {
- for (int i = mWriteQueue.size() - 1; i >= 0; --i) {
- WriteQueueItem writeQueueItem = mWriteQueue.get(i);
- if (clazz.isInstance(writeQueueItem)) {
- T item = clazz.cast(writeQueueItem);
+ synchronized <T extends WriteQueueItem<T>> T findLastItem(Predicate<T> predicate,
+ Class<T> clazz) {
+ for (int i = mQueue.size() - 1; i >= 0; --i) {
+ QueueItem queueItem = mQueue.get(i);
+ if (clazz.isInstance(queueItem)) {
+ T item = clazz.cast(queueItem);
if (predicate.test(item)) {
return item;
}
@@ -134,7 +137,7 @@
* Updates the last item found in the queue that matches the given item, or adds it to the end
* of the queue if no such item is found.
*/
- synchronized <T extends WriteQueueItem> void updateLastOrAddItem(T item, boolean flush) {
+ synchronized <T extends WriteQueueItem<T>> void updateLastOrAddItem(T item, boolean flush) {
final T itemToUpdate = findLastItem(item::matches, (Class<T>) item.getClass());
if (itemToUpdate == null) {
addItem(item, flush);
@@ -148,15 +151,15 @@
/**
* Removes all items with which given predicate returns {@code true}.
*/
- synchronized <T extends WriteQueueItem> void removeItems(Predicate<T> predicate,
+ synchronized <T extends QueueItem> void removeItems(Predicate<T> predicate,
Class<T> clazz) {
- for (int i = mWriteQueue.size() - 1; i >= 0; --i) {
- WriteQueueItem writeQueueItem = mWriteQueue.get(i);
- if (clazz.isInstance(writeQueueItem)) {
- T item = clazz.cast(writeQueueItem);
+ for (int i = mQueue.size() - 1; i >= 0; --i) {
+ QueueItem queueItem = mQueue.get(i);
+ if (clazz.isInstance(queueItem)) {
+ T item = clazz.cast(queueItem);
if (predicate.test(item)) {
if (DEBUG) Slog.d(TAG, "Removing " + item + " from write queue.");
- mWriteQueue.remove(i);
+ mQueue.remove(i);
}
}
}
@@ -201,7 +204,7 @@
// See https://b.corp.google.com/issues/64438652#comment7
// If mNextWriteTime, then don't delay between each call to saveToXml().
- final WriteQueueItem item;
+ final QueueItem item;
synchronized (this) {
if (mNextWriteTime != FLUSH_QUEUE) {
// The next write we don't have to wait so long.
@@ -212,7 +215,7 @@
}
}
- while (mWriteQueue.isEmpty()) {
+ while (mQueue.isEmpty()) {
if (mNextWriteTime != 0) {
mNextWriteTime = 0; // idle.
notify(); // May need to wake up flush().
@@ -224,17 +227,18 @@
}
if (DEBUG) Slog.d(TAG, "LazyTaskWriter: waiting indefinitely.");
wait();
- // Invariant: mNextWriteTime is either FLUSH_QUEUE or PRE_WRITE_DELAY_MS
+ // Invariant: mNextWriteTime is either FLUSH_QUEUE or PRE_TASK_DELAY_MS
// from now.
}
- item = mWriteQueue.remove(0);
+ item = mQueue.remove(0);
+ final boolean isWriteItem = item instanceof WriteQueueItem<?>;
long now = SystemClock.uptimeMillis();
if (DEBUG) {
Slog.d(TAG, "LazyTaskWriter: now=" + now + " mNextWriteTime=" + mNextWriteTime
- + " mWriteQueue.size=" + mWriteQueue.size());
+ + " mWriteQueue.size=" + mQueue.size() + " isWriteItem=" + isWriteItem);
}
- while (now < mNextWriteTime) {
+ while (now < mNextWriteTime && isWriteItem) {
if (DEBUG) {
Slog.d(TAG, "LazyTaskWriter: waiting " + (mNextWriteTime - now));
}
@@ -248,9 +252,18 @@
item.process();
}
- interface WriteQueueItem<T extends WriteQueueItem<T>> {
+ /**
+ * An item the {@link PersisterQueue} processes. Used for loading tasks. Subclasses of this, but
+ * not {@link WriteQueueItem}, aren't subject to waiting.
+ */
+ interface QueueItem {
void process();
+ }
+ /**
+ * A write item the {@link PersisterQueue} processes. Used for persisting tasks.
+ */
+ interface WriteQueueItem<T extends WriteQueueItem<T>> extends QueueItem {
default void updateFrom(T item) {}
default boolean matches(T item) {
@@ -288,7 +301,7 @@
while (true) {
final boolean probablyDone;
synchronized (PersisterQueue.this) {
- probablyDone = mWriteQueue.isEmpty();
+ probablyDone = mQueue.isEmpty();
}
for (int i = mListeners.size() - 1; i >= 0; --i) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 655a6fb..0a47522 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2925,6 +2925,9 @@
final TaskFragment taskFragment = target.asTaskFragment();
final boolean isEmbeddedTaskFragment = taskFragment != null
&& taskFragment.isEmbedded();
+ final IBinder taskFragmentToken =
+ taskFragment != null ? taskFragment.getFragmentToken() : null;
+ change.setTaskFragmentToken(taskFragmentToken);
final ActivityRecord activityRecord = target.asActivityRecord();
if (task != null) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9fdf088..3b334ec 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -436,6 +436,10 @@
private static final String PROFILING_SERVICE_JAR_PATH =
"/apex/com.android.profiling/javalib/service-profiling.jar";
+ private static final String RANGING_APEX_SERVICE_JAR_PATH =
+ "/apex/com.android.uwb/javalib/service-ranging.jar";
+ private static final String RANGING_SERVICE_CLASS = "com.android.server.ranging.RangingService";
+
private static final String TETHERING_CONNECTOR_CLASS = "android.net.ITetheringConnector";
private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst";
@@ -3015,6 +3019,17 @@
t.traceEnd();
}
+ if (com.android.ranging.flags.Flags.rangingStackEnabled()) {
+ if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB)
+ || context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_WIFI_RTT)) {
+ t.traceBegin("RangingService");
+ mSystemServiceManager.startServiceFromJar(RANGING_SERVICE_CLASS,
+ RANGING_APEX_SERVICE_JAR_PATH);
+ t.traceEnd();
+ }
+ }
+
t.traceBegin("StartBootPhaseDeviceSpecificServicesReady");
mSystemServiceManager.startBootPhase(t, SystemService.PHASE_DEVICE_SPECIFIC_SERVICES_READY);
t.traceEnd();
diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
index 1be61c3..62d3949 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
@@ -293,6 +293,7 @@
mUserFolderGetter);
target.onSystemReady();
target.onUnlockUser(TEST_USER_ID);
+ mPersisterQueue.flush();
target.getLaunchParams(mTestTask, null, mResult);
@@ -311,6 +312,7 @@
mUserFolderGetter);
target.onSystemReady();
target.onUnlockUser(TEST_USER_ID);
+ mPersisterQueue.flush();
mTaskWithDifferentComponent.mWindowLayoutAffinity = TEST_WINDOW_LAYOUT_AFFINITY;
target.getLaunchParams(mTaskWithDifferentComponent, null, mResult);
@@ -339,6 +341,7 @@
mUserFolderGetter);
target.onSystemReady();
target.onUnlockUser(TEST_USER_ID);
+ mPersisterQueue.flush();
target.getLaunchParams(mTaskWithDifferentComponent, null, mResult);
@@ -408,6 +411,7 @@
mUserFolderGetter);
target.onSystemReady();
target.onUnlockUser(TEST_USER_ID);
+ mPersisterQueue.flush();
target.getLaunchParams(mTestTask, null, mResult);
@@ -425,6 +429,7 @@
mUserFolderGetter);
target.onSystemReady();
target.onUnlockUser(TEST_USER_ID);
+ mPersisterQueue.flush();
target.getLaunchParams(mTestTask, null, mResult);
@@ -453,6 +458,7 @@
mUserFolderGetter);
target.onSystemReady();
target.onUnlockUser(TEST_USER_ID);
+ mPersisterQueue.flush();
target.getLaunchParams(mTestTask, null, mResult);
@@ -470,6 +476,7 @@
mUserFolderGetter);
target.onSystemReady();
target.onUnlockUser(TEST_USER_ID);
+ mPersisterQueue.flush();
target.getLaunchParams(mTestTask, null, mResult);
@@ -488,12 +495,52 @@
mUserFolderGetter);
target.onSystemReady();
target.onUnlockUser(TEST_USER_ID);
+ mPersisterQueue.flush();
target.getLaunchParams(mTestTask, null, mResult);
assertTrue("Result should be empty.", mResult.isEmpty());
}
+ @Test
+ public void testAbortsLoadingWhenUserCleansUpBeforeLoadingFinishes() {
+ mTarget.saveTask(mTestTask);
+ mPersisterQueue.flush();
+
+ final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor,
+ mUserFolderGetter);
+ target.onSystemReady();
+ target.onUnlockUser(TEST_USER_ID);
+ assertEquals(1, mPersisterQueue.mQueue.size());
+ PersisterQueue.QueueItem item = mPersisterQueue.mQueue.get(0);
+
+ target.onCleanupUser(TEST_USER_ID);
+ mPersisterQueue.flush();
+
+ // Explicitly run the loading item to mimic the situation where the item already started.
+ item.process();
+
+ target.getLaunchParams(mTestTask, null, mResult);
+ assertTrue("Result should be empty.", mResult.isEmpty());
+ }
+
+ @Test
+ public void testGetLaunchParamsNotBlockedByAbortedLoading() {
+ mTarget.saveTask(mTestTask);
+ mPersisterQueue.flush();
+
+ final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor,
+ mUserFolderGetter);
+ target.onSystemReady();
+ target.onUnlockUser(TEST_USER_ID);
+ target.onCleanupUser(TEST_USER_ID);
+
+ // As long as the call in the next line returns, we know it's not waiting for the loading to
+ // finish because we run items synchronously in this test.
+ target.getLaunchParams(mTestTask, null, mResult);
+ assertTrue("Result should be empty.", mResult.isEmpty());
+ }
+
private static boolean deleteRecursively(File file) {
boolean result = true;
if (file.isDirectory()) {
@@ -508,17 +555,17 @@
/**
* Test double to {@link PersisterQueue}. This is not thread-safe and caller should always use
- * {@link #flush()} to execute write items in it.
+ * {@link #flush()} to execute items in it.
*/
static class TestPersisterQueue extends PersisterQueue {
- private List<WriteQueueItem> mWriteQueue = new ArrayList<>();
+ private List<QueueItem> mQueue = new ArrayList<>();
private List<Listener> mListeners = new ArrayList<>();
@Override
void flush() {
- while (!mWriteQueue.isEmpty()) {
- final WriteQueueItem item = mWriteQueue.remove(0);
- final boolean queueEmpty = mWriteQueue.isEmpty();
+ while (!mQueue.isEmpty()) {
+ final QueueItem item = mQueue.remove(0);
+ final boolean queueEmpty = mQueue.isEmpty();
for (Listener listener : mListeners) {
listener.onPreProcessItem(queueEmpty);
}
@@ -537,18 +584,18 @@
}
@Override
- void addItem(WriteQueueItem item, boolean flush) {
- mWriteQueue.add(item);
+ synchronized void addItem(QueueItem item, boolean flush) {
+ mQueue.add(item);
if (flush) {
flush();
}
}
@Override
- synchronized <T extends WriteQueueItem> T findLastItem(Predicate<T> predicate,
+ synchronized <T extends WriteQueueItem<T>> T findLastItem(Predicate<T> predicate,
Class<T> clazz) {
- for (int i = mWriteQueue.size() - 1; i >= 0; --i) {
- WriteQueueItem writeQueueItem = mWriteQueue.get(i);
+ for (int i = mQueue.size() - 1; i >= 0; --i) {
+ QueueItem writeQueueItem = mQueue.get(i);
if (clazz.isInstance(writeQueueItem)) {
T item = clazz.cast(writeQueueItem);
if (predicate.test(item)) {
@@ -561,14 +608,14 @@
}
@Override
- synchronized <T extends WriteQueueItem> void removeItems(Predicate<T> predicate,
+ synchronized <T extends QueueItem> void removeItems(Predicate<T> predicate,
Class<T> clazz) {
- for (int i = mWriteQueue.size() - 1; i >= 0; --i) {
- WriteQueueItem writeQueueItem = mWriteQueue.get(i);
+ for (int i = mQueue.size() - 1; i >= 0; --i) {
+ QueueItem writeQueueItem = mQueue.get(i);
if (clazz.isInstance(writeQueueItem)) {
T item = clazz.cast(writeQueueItem);
if (predicate.test(item)) {
- mWriteQueue.remove(i);
+ mQueue.remove(i);
}
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
index 3e87f1f..ce0e6f8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
@@ -90,9 +90,27 @@
mFactory.setExpectedProcessedItemNumber(1);
mListener.setExpectedOnPreProcessItemCallbackTimes(1);
- final long dispatchTime = SystemClock.uptimeMillis();
mTarget.addItem(mFactory.createItem(), false);
assertTrue("Target didn't process item enough times.",
+ mFactory.waitForAllExpectedItemsProcessed(TIMEOUT_ALLOWANCE));
+ assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
+
+ assertTrue("Target didn't call callback enough times.",
+ mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE));
+ // Once before processing this item, once after that.
+ assertEquals(2, mListener.mProbablyDoneResults.size());
+ // The last one must be called with probably done being true.
+ assertTrue("The last probablyDone must be true.", mListener.mProbablyDoneResults.get(1));
+ }
+
+ @Test
+ public void testProcessOneWriteItem() throws Exception {
+ mFactory.setExpectedProcessedItemNumber(1);
+ mListener.setExpectedOnPreProcessItemCallbackTimes(1);
+
+ final long dispatchTime = SystemClock.uptimeMillis();
+ mTarget.addItem(mFactory.createWriteItem(), false);
+ assertTrue("Target didn't process item enough times.",
mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
final long processDuration = SystemClock.uptimeMillis() - dispatchTime;
@@ -109,12 +127,12 @@
}
@Test
- public void testProcessOneItem_Flush() throws Exception {
+ public void testProcessOneWriteItem_Flush() throws Exception {
mFactory.setExpectedProcessedItemNumber(1);
mListener.setExpectedOnPreProcessItemCallbackTimes(1);
final long dispatchTime = SystemClock.uptimeMillis();
- mTarget.addItem(mFactory.createItem(), true);
+ mTarget.addItem(mFactory.createWriteItem(), true);
assertTrue("Target didn't process item enough times.",
mFactory.waitForAllExpectedItemsProcessed(TIMEOUT_ALLOWANCE));
assertEquals("Target didn't process item.", 1, mFactory.getTotalProcessedItemCount());
@@ -138,8 +156,8 @@
mListener.setExpectedOnPreProcessItemCallbackTimes(2);
final long dispatchTime = SystemClock.uptimeMillis();
- mTarget.addItem(mFactory.createItem(), false);
- mTarget.addItem(mFactory.createItem(), false);
+ mTarget.addItem(mFactory.createWriteItem(), false);
+ mTarget.addItem(mFactory.createWriteItem(), false);
assertTrue("Target didn't call callback enough times.",
mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + INTER_WRITE_DELAY_MS
+ TIMEOUT_ALLOWANCE));
@@ -165,7 +183,7 @@
mFactory.setExpectedProcessedItemNumber(1);
mListener.setExpectedOnPreProcessItemCallbackTimes(1);
long dispatchTime = SystemClock.uptimeMillis();
- mTarget.addItem(mFactory.createItem(), false);
+ mTarget.addItem(mFactory.createWriteItem(), false);
assertTrue("Target didn't process item enough times.",
mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
long processDuration = SystemClock.uptimeMillis() - dispatchTime;
@@ -184,7 +202,7 @@
// Synchronize on the instance to make sure we schedule the item after it starts to wait for
// task indefinitely.
synchronized (mTarget) {
- mTarget.addItem(mFactory.createItem(), false);
+ mTarget.addItem(mFactory.createWriteItem(), false);
}
assertTrue("Target didn't process item enough times.",
mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
@@ -206,9 +224,9 @@
@Test
public void testFindLastItemNotReturnDifferentType() {
synchronized (mTarget) {
- mTarget.addItem(mFactory.createItem(), false);
- assertNull(mTarget.findLastItem(TestItem::shouldKeepOnFilter,
- FilterableTestItem.class));
+ mTarget.addItem(mFactory.createWriteItem(), false);
+ assertNull(mTarget.findLastItem(TestWriteItem::shouldKeepOnFilter,
+ FilterableTestWriteItem.class));
}
}
@@ -216,18 +234,18 @@
public void testFindLastItemNotReturnMismatchItem() {
synchronized (mTarget) {
mTarget.addItem(mFactory.createFilterableItem(false), false);
- assertNull(mTarget.findLastItem(TestItem::shouldKeepOnFilter,
- FilterableTestItem.class));
+ assertNull(mTarget.findLastItem(TestWriteItem::shouldKeepOnFilter,
+ FilterableTestWriteItem.class));
}
}
@Test
public void testFindLastItemReturnMatchedItem() {
synchronized (mTarget) {
- final FilterableTestItem item = mFactory.createFilterableItem(true);
+ final FilterableTestWriteItem item = mFactory.createFilterableItem(true);
mTarget.addItem(item, false);
- assertSame(item, mTarget.findLastItem(TestItem::shouldKeepOnFilter,
- FilterableTestItem.class));
+ assertSame(item, mTarget.findLastItem(TestWriteItem::shouldKeepOnFilter,
+ FilterableTestWriteItem.class));
}
}
@@ -235,8 +253,8 @@
public void testRemoveItemsNotRemoveDifferentType() throws Exception {
mListener.setExpectedOnPreProcessItemCallbackTimes(1);
synchronized (mTarget) {
- mTarget.addItem(mFactory.createItem(), false);
- mTarget.removeItems(TestItem::shouldKeepOnFilter, FilterableTestItem.class);
+ mTarget.addItem(mFactory.createWriteItem(), false);
+ mTarget.removeItems(TestWriteItem::shouldKeepOnFilter, FilterableTestWriteItem.class);
}
assertTrue("Target didn't call callback enough times.",
mListener.waitForAllExpectedCallbackDone(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
@@ -248,7 +266,7 @@
mListener.setExpectedOnPreProcessItemCallbackTimes(1);
synchronized (mTarget) {
mTarget.addItem(mFactory.createFilterableItem(false), false);
- mTarget.removeItems(TestItem::shouldKeepOnFilter, FilterableTestItem.class);
+ mTarget.removeItems(TestWriteItem::shouldKeepOnFilter, FilterableTestWriteItem.class);
}
assertTrue("Target didn't call callback enough times.",
mListener.waitForAllExpectedCallbackDone(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
@@ -258,8 +276,8 @@
@Test
public void testUpdateLastOrAddItemUpdatesMatchedItem() throws Exception {
mListener.setExpectedOnPreProcessItemCallbackTimes(1);
- final FilterableTestItem scheduledItem = mFactory.createFilterableItem(true);
- final FilterableTestItem expected = mFactory.createFilterableItem(true);
+ final FilterableTestWriteItem scheduledItem = mFactory.createFilterableItem(true);
+ final FilterableTestWriteItem expected = mFactory.createFilterableItem(true);
synchronized (mTarget) {
mTarget.addItem(scheduledItem, false);
mTarget.updateLastOrAddItem(expected, false);
@@ -274,8 +292,8 @@
@Test
public void testUpdateLastOrAddItemUpdatesAddItemWhenNoMatch() throws Exception {
mListener.setExpectedOnPreProcessItemCallbackTimes(2);
- final FilterableTestItem scheduledItem = mFactory.createFilterableItem(false);
- final FilterableTestItem expected = mFactory.createFilterableItem(true);
+ final FilterableTestWriteItem scheduledItem = mFactory.createFilterableItem(false);
+ final FilterableTestWriteItem expected = mFactory.createFilterableItem(true);
synchronized (mTarget) {
mTarget.addItem(scheduledItem, false);
mTarget.updateLastOrAddItem(expected, false);
@@ -292,9 +310,9 @@
public void testRemoveItemsRemoveMatchedItem() throws Exception {
mListener.setExpectedOnPreProcessItemCallbackTimes(1);
synchronized (mTarget) {
- mTarget.addItem(mFactory.createItem(), false);
+ mTarget.addItem(mFactory.createWriteItem(), false);
mTarget.addItem(mFactory.createFilterableItem(true), false);
- mTarget.removeItems(TestItem::shouldKeepOnFilter, FilterableTestItem.class);
+ mTarget.removeItems(TestWriteItem::shouldKeepOnFilter, FilterableTestWriteItem.class);
}
assertTrue("Target didn't call callback enough times.",
mListener.waitForAllExpectedCallbackDone(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
@@ -304,8 +322,8 @@
@Test
public void testFlushWaitSynchronously() {
final long dispatchTime = SystemClock.uptimeMillis();
- mTarget.addItem(mFactory.createItem(), false);
- mTarget.addItem(mFactory.createItem(), false);
+ mTarget.addItem(mFactory.createWriteItem(), false);
+ mTarget.addItem(mFactory.createWriteItem(), false);
mTarget.flush();
assertEquals("Flush should wait until all items are processed before return.",
2, mFactory.getTotalProcessedItemCount());
@@ -335,15 +353,18 @@
return new TestItem(mItemCount, mLatch);
}
- FilterableTestItem createFilterableItem(boolean shouldKeepOnFilter) {
- return new FilterableTestItem(shouldKeepOnFilter, mItemCount, mLatch);
+ TestWriteItem createWriteItem() {
+ return new TestWriteItem(mItemCount, mLatch);
+ }
+
+ FilterableTestWriteItem createFilterableItem(boolean shouldKeepOnFilter) {
+ return new FilterableTestWriteItem(shouldKeepOnFilter, mItemCount, mLatch);
}
}
- private static class TestItem<T extends TestItem<T>>
- implements PersisterQueue.WriteQueueItem<T> {
- private AtomicInteger mItemCount;
- private CountDownLatch mLatch;
+ private static class TestItem implements PersisterQueue.QueueItem {
+ private final AtomicInteger mItemCount;
+ private final CountDownLatch mLatch;
TestItem(AtomicInteger itemCount, CountDownLatch latch) {
mItemCount = itemCount;
@@ -359,30 +380,37 @@
mLatch.countDown();
}
}
+ }
+
+ private static class TestWriteItem<T extends TestWriteItem<T>>
+ extends TestItem implements PersisterQueue.WriteQueueItem<T> {
+ TestWriteItem(AtomicInteger itemCount, CountDownLatch latch) {
+ super(itemCount, latch);
+ }
boolean shouldKeepOnFilter() {
return true;
}
}
- private static class FilterableTestItem extends TestItem<FilterableTestItem> {
+ private static class FilterableTestWriteItem extends TestWriteItem<FilterableTestWriteItem> {
private boolean mShouldKeepOnFilter;
- private FilterableTestItem mUpdateFromItem;
+ private FilterableTestWriteItem mUpdateFromItem;
- private FilterableTestItem(boolean shouldKeepOnFilter, AtomicInteger mItemCount,
+ private FilterableTestWriteItem(boolean shouldKeepOnFilter, AtomicInteger mItemCount,
CountDownLatch mLatch) {
super(mItemCount, mLatch);
mShouldKeepOnFilter = shouldKeepOnFilter;
}
@Override
- public boolean matches(FilterableTestItem item) {
+ public boolean matches(FilterableTestWriteItem item) {
return item.mShouldKeepOnFilter;
}
@Override
- public void updateFrom(FilterableTestItem item) {
+ public void updateFrom(FilterableTestWriteItem item) {
mUpdateFromItem = item;
}
diff --git a/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestIgnore.java b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestIgnore.java
new file mode 100644
index 0000000..501fd65
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestIgnore.java
@@ -0,0 +1,34 @@
+/*
+ * 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.hosttest.annotation;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * THIS ANNOTATION IS EXPERIMENTAL. REACH OUT TO g/ravenwood BEFORE USING IT, OR YOU HAVE ANY
+ * QUESTIONS ABOUT IT.
+ *
+ * @hide
+ */
+@Target({METHOD, CONSTRUCTOR})
+@Retention(RetentionPolicy.CLASS)
+public @interface HostSideTestIgnore {
+}
diff --git a/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt b/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
index eba8e62..001943c 100644
--- a/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
+++ b/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
@@ -24,6 +24,9 @@
--remove-annotation
android.hosttest.annotation.HostSideTestRemove
+--ignore-annotation
+ android.hosttest.annotation.HostSideTestIgnore
+
--substitute-annotation
android.hosttest.annotation.HostSideTestSubstitute
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 34aaaa9..165bb57 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -166,6 +166,7 @@
options.keepClassAnnotations,
options.throwAnnotations,
options.removeAnnotations,
+ options.ignoreAnnotations,
options.substituteAnnotations,
options.redirectAnnotations,
options.redirectionClassAnnotations,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index 057a52c..b083d89 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -84,6 +84,7 @@
var keepAnnotations: MutableSet<String> = mutableSetOf(),
var throwAnnotations: MutableSet<String> = mutableSetOf(),
var removeAnnotations: MutableSet<String> = mutableSetOf(),
+ var ignoreAnnotations: MutableSet<String> = mutableSetOf(),
var keepClassAnnotations: MutableSet<String> = mutableSetOf(),
var redirectAnnotations: MutableSet<String> = mutableSetOf(),
@@ -184,6 +185,9 @@
"--remove-annotation" ->
ret.removeAnnotations.addUniqueAnnotationArg()
+ "--ignore-annotation" ->
+ ret.ignoreAnnotations.addUniqueAnnotationArg()
+
"--substitute-annotation" ->
ret.substituteAnnotations.addUniqueAnnotationArg()
@@ -277,6 +281,7 @@
keepAnnotations=$keepAnnotations,
throwAnnotations=$throwAnnotations,
removeAnnotations=$removeAnnotations,
+ ignoreAnnotations=$ignoreAnnotations,
keepClassAnnotations=$keepClassAnnotations,
substituteAnnotations=$substituteAnnotations,
nativeSubstituteAnnotations=$redirectionClassAnnotations,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
index a6b8cdb..36adf06 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
@@ -48,6 +48,7 @@
keepClassAnnotations_: Set<String>,
throwAnnotations_: Set<String>,
removeAnnotations_: Set<String>,
+ ignoreAnnotations_: Set<String>,
substituteAnnotations_: Set<String>,
redirectAnnotations_: Set<String>,
redirectionClassAnnotations_: Set<String>,
@@ -60,6 +61,7 @@
private val keepClassAnnotations = convertToInternalNames(keepClassAnnotations_)
private val throwAnnotations = convertToInternalNames(throwAnnotations_)
private val removeAnnotations = convertToInternalNames(removeAnnotations_)
+ private val ignoreAnnotations = convertToInternalNames(ignoreAnnotations_)
private val redirectAnnotations = convertToInternalNames(redirectAnnotations_)
private val substituteAnnotations = convertToInternalNames(substituteAnnotations_)
private val redirectionClassAnnotations =
@@ -73,6 +75,7 @@
keepClassAnnotations +
throwAnnotations +
removeAnnotations +
+ ignoreAnnotations +
redirectAnnotations +
substituteAnnotations
@@ -107,6 +110,7 @@
in substituteAnnotations -> FilterPolicy.Substitute.withReason(REASON_ANNOTATION)
in throwAnnotations -> FilterPolicy.Throw.withReason(REASON_ANNOTATION)
in removeAnnotations -> FilterPolicy.Remove.withReason(REASON_ANNOTATION)
+ in ignoreAnnotations -> FilterPolicy.Ignore.withReason(REASON_ANNOTATION)
in redirectAnnotations -> FilterPolicy.Redirect.withReason(REASON_ANNOTATION)
else -> null
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/ClassFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/ClassFilter.kt
index 7440b94..d6aa761 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/ClassFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/ClassFilter.kt
@@ -27,16 +27,22 @@
class ClassFilter private constructor(
private val defaultResult: Boolean,
) {
+ private enum class MatchType {
+ Full,
+ Prefix,
+ Suffix,
+ }
+
private class FilterElement(
val allowed: Boolean,
val internalName: String,
- val isPrefix: Boolean,
+ val matchType: MatchType,
) {
fun matches(classInternalName: String): Boolean {
- return if (isPrefix) {
- classInternalName.startsWith(internalName)
- } else {
- classInternalName == internalName
+ return when (matchType) {
+ MatchType.Full -> classInternalName == internalName
+ MatchType.Prefix -> classInternalName.startsWith(internalName)
+ MatchType.Suffix -> classInternalName.endsWith(internalName)
}
}
}
@@ -114,15 +120,29 @@
// Special case -- matches any class names.
if (line == "*") {
- ret.elements.add(FilterElement(allow, "", true))
+ ret.elements.add(FilterElement(allow, "", MatchType.Prefix))
return@forEach
}
- // Handle wildcard -- e.g. "package.name.*"
+ // Handle prefix match -- e.g. "package.name.*"
if (line.endsWith(".*")) {
ret.elements.add(
FilterElement(
- allow, line.substring(0, line.length - 2).toJvmClassName(), true
+ allow,
+ line.substring(0, line.length - 2).toJvmClassName() + "/",
+ MatchType.Prefix
+ )
+ )
+ return@forEach
+ }
+
+ // Handle suffix match -- e.g. "*.Flags"
+ if (line.startsWith("*.")) {
+ ret.elements.add(
+ FilterElement(
+ allow,
+ "/" + line.substring(2, line.length).toJvmClassName(),
+ MatchType.Suffix
)
)
return@forEach
@@ -136,10 +156,10 @@
lineNo
)
}
- ret.elements.add(FilterElement(allow, line.toJvmClassName(), false))
+ ret.elements.add(FilterElement(allow, line.toJvmClassName(), MatchType.Suffix))
}
return ret
}
}
-}
\ No newline at end of file
+}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
index 82586bb..103e152 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
@@ -21,6 +21,26 @@
java.lang.annotation.Retention(
value=Ljava/lang/annotation/RetentionPolicy;.CLASS
)
+## Class: android/hosttest/annotation/HostSideTestIgnore.class
+ Compiled from "HostSideTestIgnore.java"
+public interface android.hosttest.annotation.HostSideTestIgnore extends java.lang.annotation.Annotation
+ minor version: 0
+ major version: 61
+ flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
+ this_class: #x // android/hosttest/annotation/HostSideTestIgnore
+ super_class: #x // java/lang/Object
+ interfaces: 1, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "HostSideTestIgnore.java"
+RuntimeVisibleAnnotations:
+ x: #x(#x=[e#x.#x,e#x.#x])
+ java.lang.annotation.Target(
+ value=[Ljava/lang/annotation/ElementType;.METHOD,Ljava/lang/annotation/ElementType;.CONSTRUCTOR]
+ )
+ x: #x(#x=e#x.#x)
+ java.lang.annotation.Retention(
+ value=Ljava/lang/annotation/RetentionPolicy;.CLASS
+ )
## Class: android/hosttest/annotation/HostSideTestKeep.class
Compiled from "HostSideTestKeep.java"
public interface android.hosttest.annotation.HostSideTestKeep extends java.lang.annotation.Annotation
@@ -382,7 +402,7 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations
super_class: #x // java/lang/Object
- interfaces: 0, fields: 2, methods: 8, attributes: 2
+ interfaces: 0, fields: 2, methods: 9, attributes: 2
public int keep;
descriptor: I
flags: (0x0001) ACC_PUBLIC
@@ -522,6 +542,24 @@
RuntimeInvisibleAnnotations:
x: #x()
android.hosttest.annotation.HostSideTestThrow
+
+ public int toBeIgnored();
+ descriptor: ()I
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=3, locals=1, args_size=1
+ x: new #x // class java/lang/RuntimeException
+ x: dup
+ x: ldc #x // String not supported on host side
+ x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+ x: athrow
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 10 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations;
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestIgnore
}
SourceFile: "TinyFrameworkAnnotations.java"
RuntimeInvisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
index 31bbcc5..eeec554 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
@@ -432,7 +432,7 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations
super_class: #x // java/lang/Object
- interfaces: 0, fields: 1, methods: 6, attributes: 3
+ interfaces: 0, fields: 1, methods: 7, attributes: 3
public int keep;
descriptor: I
flags: (0x0001) ACC_PUBLIC
@@ -554,6 +554,22 @@
RuntimeInvisibleAnnotations:
x: #x()
android.hosttest.annotation.HostSideTestThrow
+
+ public int toBeIgnored();
+ descriptor: ()I
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=1, locals=1, args_size=1
+ x: iconst_0
+ x: ireturn
+ RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsIgnore
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestIgnore
}
SourceFile: "TinyFrameworkAnnotations.java"
RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
index 41f459a..0f8af92 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
@@ -593,7 +593,7 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations
super_class: #x // java/lang/Object
- interfaces: 0, fields: 1, methods: 6, attributes: 3
+ interfaces: 0, fields: 1, methods: 7, attributes: 3
public int keep;
descriptor: I
flags: (0x0001) ACC_PUBLIC
@@ -743,6 +743,27 @@
RuntimeInvisibleAnnotations:
x: #x()
android.hosttest.annotation.HostSideTestThrow
+
+ public int toBeIgnored();
+ descriptor: ()I
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=4, locals=1, args_size=1
+ x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations
+ x: ldc #x // String toBeIgnored
+ x: ldc #x // String ()I
+ x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+ x: iconst_0
+ x: ireturn
+ RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsIgnore
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestIgnore
}
SourceFile: "TinyFrameworkAnnotations.java"
RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations.java
index ed0fa26..3415deb 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkAnnotations.java
@@ -16,6 +16,7 @@
package com.android.hoststubgen.test.tinyframework;
import android.hosttest.annotation.HostSideTestClassLoadHook;
+import android.hosttest.annotation.HostSideTestIgnore;
import android.hosttest.annotation.HostSideTestKeep;
import android.hosttest.annotation.HostSideTestRemove;
import android.hosttest.annotation.HostSideTestSubstitute;
@@ -71,4 +72,9 @@
public String unsupportedMethod() {
return "This value shouldn't be seen on the host side.";
}
+
+ @HostSideTestIgnore
+ public int toBeIgnored() {
+ throw new RuntimeException("not supported on host side");
+ }
}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWideAnnotationsTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWideAnnotationsTest.java
index 34c98e9..1816b38 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWideAnnotationsTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWideAnnotationsTest.java
@@ -102,4 +102,11 @@
assertThat(new TinyFrameworkNestedClasses.StaticNestedClass.Double$NestedClass().value)
.isEqualTo(8);
}
+
+ @Test
+ public void testIgnoreAnnotation() {
+ // The actual method will throw, but because of @Ignore, it'll return 0.
+ assertThat(new TinyFrameworkAnnotations().toBeIgnored())
+ .isEqualTo(0);
+ }
}
diff --git a/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/ClassFilterTest.kt b/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/ClassFilterTest.kt
index 85b6e80..d4e75d4 100644
--- a/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/ClassFilterTest.kt
+++ b/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/ClassFilterTest.kt
@@ -69,6 +69,8 @@
assertThat(f.matches("d/e/f")).isEqualTo(false)
assertThat(f.matches("d/e/f/g")).isEqualTo(true)
assertThat(f.matches("x")).isEqualTo(true)
+
+ assertThat(f.matches("ab/x")).isEqualTo(true)
}
@Test
@@ -96,4 +98,18 @@
assertThat(e.message).contains("line 1")
}
}
+
+ @Test
+ fun testSuffix() {
+ val f = ClassFilter.buildFromString("""
+ *.Abc # allow
+ !* # Disallow by default
+ """.trimIndent(), true, "X")
+ assertThat(f.matches("a/b/c")).isEqualTo(false)
+ assertThat(f.matches("a/Abc")).isEqualTo(true)
+ assertThat(f.matches("a/b/c/Abc")).isEqualTo(true)
+ assertThat(f.matches("a/b/c/Abc\$Nested")).isEqualTo(true)
+
+ assertThat(f.matches("a/XyzAbc")).isEqualTo(false)
+ }
}
\ No newline at end of file