diff options
126 files changed, 2219 insertions, 1344 deletions
diff --git a/TEST_MAPPING b/TEST_MAPPING index e469f167d32f..ce0da7e21071 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -25,6 +25,12 @@ "name": "FrameworksUiServicesTests" }, { + "name": "FrameworksUiServicesNotificationTests" + }, + { + "name": "FrameworksUiServicesZenTests" + }, + { "name": "FrameworksInputMethodSystemServerTests_server_inputmethod" }, { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 32b170a6286b..cd7e434d30d9 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2941,6 +2941,7 @@ package android.app.smartspace.uitemplatedata { package android.app.supervision { @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager { + method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public android.content.Intent createConfirmSupervisionCredentialsIntent(); method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled(); } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 7e5c0fbe1ee1..99a2763650cd 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2473,11 +2473,9 @@ class ContextImpl extends Context { @Override public int getPermissionRequestState(String permission) { Objects.requireNonNull(permission, "Permission name can't be null"); - int deviceId = PermissionManager.resolveDeviceIdForPermissionCheck(this, getDeviceId(), - permission); PermissionManager permissionManager = getSystemService(PermissionManager.class); return permissionManager.getPermissionRequestState(getOpPackageName(), permission, - deviceId); + getDeviceId()); } @Override diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java index 8217ca1953ab..172ed2358a5d 100644 --- a/core/java/android/app/supervision/SupervisionManager.java +++ b/core/java/android/app/supervision/SupervisionManager.java @@ -97,6 +97,8 @@ public class SupervisionManager { * * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_SUPERVISION_MANAGER_APIS) @RequiresPermission(anyOf = {MANAGE_USERS, QUERY_USERS}) @Nullable public Intent createConfirmSupervisionCredentialsIntent() { diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableParcelable.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableParcelable.java index fdde2057a1a0..de3bafb4bb56 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableParcelable.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableParcelable.java @@ -75,7 +75,6 @@ public class MarshalQueryableParcelable<T extends Parcelable> } Parcel parcel = Parcel.obtain(); - byte[] parcelContents; try { value.writeToParcel(parcel, /*flags*/0); @@ -85,17 +84,15 @@ public class MarshalQueryableParcelable<T extends Parcelable> "Parcelable " + value + " must not have file descriptors"); } - parcelContents = parcel.marshall(); + final int position = buffer.position(); + parcel.marshall(buffer); + if (buffer.position() == position) { + throw new AssertionError("No data marshaled for " + value); + } } finally { parcel.recycle(); } - - if (parcelContents.length == 0) { - throw new AssertionError("No data marshaled for " + value); - } - - buffer.put(parcelContents); } @Override diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 92a56fc575e3..8216130e3731 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -433,8 +433,9 @@ public final class DisplayManager { public static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8; /** - * Virtual display flag: Indicates that the display should support system decorations. Virtual - * displays without this flag shouldn't show home, navigation bar or wallpaper. + * Virtual display flag: Indicates that the display supports and should always show system + * decorations. Virtual displays without this flag shouldn't show home, navigation bar or + * wallpaper. * <p>This flag doesn't work without {@link #VIRTUAL_DISPLAY_FLAG_TRUSTED}</p> * * @see #createVirtualDisplay diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 49d3f06eb80f..6cb49b3ea166 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -52,6 +52,8 @@ import com.android.internal.util.ArrayUtils; import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; +import java.nio.BufferOverflowException; +import java.nio.ReadOnlyBufferException; import libcore.util.SneakyThrow; import java.io.ByteArrayInputStream; @@ -62,6 +64,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamClass; import java.io.Serializable; +import java.nio.ByteBuffer; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Array; @@ -457,8 +460,15 @@ public final class Parcel { private static native void nativeDestroy(long nativePtr); private static native byte[] nativeMarshall(long nativePtr); + private static native int nativeMarshallArray( + long nativePtr, byte[] data, int offset, int length); + private static native int nativeMarshallBuffer( + long nativePtr, ByteBuffer buffer, int offset, int length); private static native void nativeUnmarshall( long nativePtr, byte[] data, int offset, int length); + private static native void nativeUnmarshallBuffer( + long nativePtr, ByteBuffer buffer, int offset, int length); + private static native int nativeCompareData(long thisNativePtr, long otherNativePtr); private static native boolean nativeCompareDataInRange( long ptrA, int offsetA, long ptrB, int offsetB, int length); @@ -814,12 +824,80 @@ public final class Parcel { } /** + * Writes the raw bytes of the parcel to a buffer. + * + * <p class="note">The data you retrieve here <strong>must not</strong> + * be placed in any kind of persistent storage (on local disk, across + * a network, etc). For that, you should use standard serialization + * or another kind of general serialization mechanism. The Parcel + * marshalled representation is highly optimized for local IPC, and as + * such does not attempt to maintain compatibility with data created + * in different versions of the platform. + * + * @param buffer The ByteBuffer to write the data to. + * @throws ReadOnlyBufferException if the buffer is read-only. + * @throws BufferOverflowException if the buffer is too small. + * + * @hide + */ + public final void marshall(@NonNull ByteBuffer buffer) { + if (buffer == null) { + throw new NullPointerException(); + } + if (buffer.isReadOnly()) { + throw new ReadOnlyBufferException(); + } + + final int position = buffer.position(); + final int remaining = buffer.remaining(); + + int marshalledSize = 0; + if (buffer.isDirect()) { + marshalledSize = nativeMarshallBuffer(mNativePtr, buffer, position, remaining); + } else if (buffer.hasArray()) { + marshalledSize = nativeMarshallArray( + mNativePtr, buffer.array(), buffer.arrayOffset() + position, remaining); + } else { + throw new IllegalArgumentException(); + } + + buffer.position(position + marshalledSize); + } + + /** * Fills the raw bytes of this Parcel with the supplied data. */ public final void unmarshall(@NonNull byte[] data, int offset, int length) { nativeUnmarshall(mNativePtr, data, offset, length); } + /** + * Fills the raw bytes of this Parcel with data from the supplied buffer. + * + * @param buffer will read buffer.remaining() bytes from the buffer. + * + * @hide + */ + public final void unmarshall(@NonNull ByteBuffer buffer) { + if (buffer == null) { + throw new NullPointerException(); + } + + final int position = buffer.position(); + final int remaining = buffer.remaining(); + + if (buffer.isDirect()) { + nativeUnmarshallBuffer(mNativePtr, buffer, position, remaining); + } else if (buffer.hasArray()) { + nativeUnmarshall( + mNativePtr, buffer.array(), buffer.arrayOffset() + position, remaining); + } else { + throw new IllegalArgumentException(); + } + + buffer.position(position + remaining); + } + public final void appendFrom(Parcel parcel, int offset, int length) { nativeAppendFrom(mNativePtr, parcel.mNativePtr, offset, length); } diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index 561a2c96e6a7..0433c76fbbf4 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -1907,8 +1907,9 @@ public final class PermissionManager { @Context.PermissionRequestState public int getPermissionRequestState(@NonNull String packageName, @NonNull String permission, int deviceId) { + int actualDeviceId = resolveDeviceIdForPermissionCheck(mContext, deviceId, permission); return sPermissionRequestStateCache.query( - new PermissionRequestStateQuery(packageName, permission, deviceId)); + new PermissionRequestStateQuery(packageName, permission, actualDeviceId)); } /** @@ -2035,7 +2036,8 @@ public final class PermissionManager { */ public int checkPackageNamePermission(String permName, String pkgName, int deviceId, @UserIdInt int userId) { - String persistentDeviceId = getPersistentDeviceId(deviceId); + int actualDeviceId = resolveDeviceIdForPermissionCheck(mContext, deviceId, permName); + String persistentDeviceId = getPersistentDeviceId(actualDeviceId); return sPackageNamePermissionCache.query( new PackageNamePermissionQuery(permName, pkgName, persistentDeviceId, userId)); } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index d342494e32e6..85f38c984f5c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -15522,7 +15522,8 @@ public final class Settings { * <ul> * <li><pre>secure</pre>: creates a secure display</li> * <li><pre>own_content_only</pre>: only shows this display's own content</li> - * <li><pre>should_show_system_decorations</pre>: supports system decorations</li> + * <li><pre>should_show_system_decorations</pre>: always shows system decorations</li> + * <li><pre>fixed_content_mode</pre>: does not allow the content mode switch</li> * </ul> * </p><p> * Example: diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java index 7660ed96d30e..815444d195c9 100644 --- a/core/java/android/service/notification/NotificationRankingUpdate.java +++ b/core/java/android/service/notification/NotificationRankingUpdate.java @@ -219,7 +219,7 @@ public class NotificationRankingUpdate implements Parcelable { // Gets a read/write buffer mapping the entire shared memory region. buffer = mRankingMapFd.mapReadWrite(); // Puts the ranking map into the shared memory region buffer. - buffer.put(mapParcel.marshall(), 0, mapSize); + mapParcel.marshall(buffer); // Protects the region from being written to, by setting it to be read-only. mRankingMapFd.setProtect(OsConstants.PROT_READ); // Puts the SharedMemory object in the parcel. diff --git a/core/java/android/service/notification/TEST_MAPPING b/core/java/android/service/notification/TEST_MAPPING index dc7129cde5e5..ea7ee4addbe6 100644 --- a/core/java/android/service/notification/TEST_MAPPING +++ b/core/java/android/service/notification/TEST_MAPPING @@ -4,7 +4,10 @@ "name": "CtsNotificationTestCases_notification" }, { - "name": "FrameworksUiServicesTests_notification" + "name": "FrameworksUiServicesNotificationTests" + }, + { + "name": "FrameworksUiServicesZenTests" } ], "postsubmit": [ diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 3f45e29f2d43..5c816543e191 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -286,7 +286,7 @@ public final class Display { /** * Display flag: Indicates that the display should show system decorations. * <p> - * This flag identifies secondary displays that should show system decorations, such as + * This flag identifies secondary displays that should always show system decorations, such as * navigation bar, home activity or wallpaper. * </p> * <p>Note that this flag doesn't work without {@link #FLAG_TRUSTED}</p> @@ -401,6 +401,18 @@ public final class Display { public static final int FLAG_ROTATES_WITH_CONTENT = 1 << 14; /** + * Display flag: Indicates that the display is allowed to switch the content mode between + * projected/extended and mirroring. This allows the display to dynamically add or remove the + * home and system decorations. + * + * Note that this flag shouldn't be enabled with {@link #FLAG_PRIVATE} or + * {@link #FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS} at the same time; otherwise it will be ignored. + * + * @hide + */ + public static final int FLAG_ALLOWS_CONTENT_MODE_SWITCH = 1 << 15; + + /** * Display flag: Indicates that the contents of the display should not be scaled * to fit the physical screen dimensions. Used for development only to emulate * devices with smaller physicals screens while preserving density. diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index d880072aa404..bf000d5fa39a 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -1125,6 +1125,9 @@ public final class DisplayInfo implements Parcelable { if ((flags & Display.FLAG_REAR) != 0) { result.append(", FLAG_REAR_DISPLAY"); } + if ((flags & Display.FLAG_ALLOWS_CONTENT_MODE_SWITCH) != 0) { + result.append(", FLAG_ALLOWS_CONTENT_MODE_SWITCH"); + } return result.toString(); } } diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java index e3041c00e9fc..50b8bd22350c 100644 --- a/core/java/android/window/DesktopExperienceFlags.java +++ b/core/java/android/window/DesktopExperienceFlags.java @@ -61,6 +61,7 @@ public enum DesktopExperienceFlags { ENABLE_DISPLAY_RECONNECT_INTERACTION(Flags::enableDisplayReconnectInteraction, false), ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, true), ENABLE_DRAG_TO_MAXIMIZE(Flags::enableDragToMaximize, true), + ENABLE_KEYBOARD_SHORTCUTS_TO_SWITCH_DESKS(Flags::keyboardShortcutsToSwitchDesks, false), ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, true), ENABLE_MULTIPLE_DESKTOPS_BACKEND(Flags::enableMultipleDesktopsBackend, false), ENABLE_MULTIPLE_DESKTOPS_FRONTEND(Flags::enableMultipleDesktopsFrontend, false), diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 9781e7046038..53db2b1178cf 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -602,6 +602,13 @@ flag { } flag { + name: "keyboard_shortcuts_to_switch_desks" + namespace: "lse_desktop_experience" + description: "Enable switching the active desk with keyboard shortcuts" + bug: "389957556" +} + +flag { name: "enable_connected_displays_dnd" namespace: "lse_desktop_experience" description: "Enable drag-and-drop between connected displays." diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp index dec724b6a7ff..b4c58b9b246a 100644 --- a/core/jni/android_os_Parcel.cpp +++ b/core/jni/android_os_Parcel.cpp @@ -558,8 +558,7 @@ static void android_os_Parcel_destroy(JNIEnv* env, jclass clazz, jlong nativePtr delete parcel; } -static jbyteArray android_os_Parcel_marshall(JNIEnv* env, jclass clazz, jlong nativePtr) -{ +static Parcel* parcel_for_marshall(JNIEnv* env, jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel == NULL) { return NULL; @@ -577,6 +576,16 @@ static jbyteArray android_os_Parcel_marshall(JNIEnv* env, jclass clazz, jlong na return NULL; } + return parcel; +} + +static jbyteArray android_os_Parcel_marshall(JNIEnv* env, jclass clazz, jlong nativePtr) +{ + Parcel* parcel = parcel_for_marshall(env, nativePtr); + if (parcel == NULL) { + return NULL; + } + jbyteArray ret = env->NewByteArray(parcel->dataSize()); if (ret != NULL) @@ -592,6 +601,58 @@ static jbyteArray android_os_Parcel_marshall(JNIEnv* env, jclass clazz, jlong na return ret; } +static long ensure_capacity(JNIEnv* env, Parcel* parcel, jint remaining) { + long dataSize = parcel->dataSize(); + if (remaining < dataSize) { + jniThrowExceptionFmt(env, "java/nio/BufferOverflowException", + "Destination buffer remaining capacity %d is less than the Parcel data size %d.", + remaining, dataSize); + return -1; + } + return dataSize; +} + +static int android_os_Parcel_marshall_array(JNIEnv* env, jclass clazz, jlong nativePtr, + jbyteArray data, jint offset, jint remaining) +{ + Parcel* parcel = parcel_for_marshall(env, nativePtr); + if (parcel == NULL) { + return 0; + } + + long data_size = ensure_capacity(env, parcel, remaining); + if (data_size < 0) { + return 0; + } + + jbyte* array = (jbyte*)env->GetPrimitiveArrayCritical(data, 0); + if (array != NULL) + { + memcpy(array + offset, parcel->data(), data_size); + env->ReleasePrimitiveArrayCritical(data, array, 0); + } + return data_size; +} + +static int android_os_Parcel_marshall_buffer(JNIEnv* env, jclass clazz, jlong nativePtr, + jobject javaBuffer, jint offset, jint remaining) { + Parcel* parcel = parcel_for_marshall(env, nativePtr); + if (parcel == NULL) { + return 0; + } + + long data_size = ensure_capacity(env, parcel, remaining); + if (data_size < 0) { + return 0; + } + + jbyte* buffer = (jbyte*)env->GetDirectBufferAddress(javaBuffer); + if (buffer != NULL) { + memcpy(buffer + offset, parcel->data(), data_size); + } + return data_size; +} + static void android_os_Parcel_unmarshall(JNIEnv* env, jclass clazz, jlong nativePtr, jbyteArray data, jint offset, jint length) { @@ -613,6 +674,25 @@ static void android_os_Parcel_unmarshall(JNIEnv* env, jclass clazz, jlong native } } +static void android_os_Parcel_unmarshall_buffer(JNIEnv* env, jclass clazz, jlong nativePtr, + jobject javaBuffer, jint offset, jint length) +{ + Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); + if (parcel == NULL || length < 0) { + return; + } + + jbyte* buffer = (jbyte*)env->GetDirectBufferAddress(javaBuffer); + if (buffer) + { + parcel->setDataSize(length); + parcel->setDataPosition(0); + + void* raw = parcel->writeInplace(length); + memcpy(raw, (buffer + offset), length); + } +} + static jint android_os_Parcel_compareData(JNIEnv* env, jclass clazz, jlong thisNativePtr, jlong otherNativePtr) { @@ -911,7 +991,10 @@ static const JNINativeMethod gParcelMethods[] = { {"nativeDestroy", "(J)V", (void*)android_os_Parcel_destroy}, {"nativeMarshall", "(J)[B", (void*)android_os_Parcel_marshall}, + {"nativeMarshallArray", "(J[BII)I", (void*)android_os_Parcel_marshall_array}, + {"nativeMarshallBuffer", "(JLjava/nio/ByteBuffer;II)I", (void*)android_os_Parcel_marshall_buffer}, {"nativeUnmarshall", "(J[BII)V", (void*)android_os_Parcel_unmarshall}, + {"nativeUnmarshallBuffer", "(JLjava/nio/ByteBuffer;II)V", (void*)android_os_Parcel_unmarshall_buffer}, {"nativeCompareData", "(JJ)I", (void*)android_os_Parcel_compareData}, {"nativeCompareDataInRange", "(JIJII)Z", (void*)android_os_Parcel_compareDataInRange}, {"nativeAppendFrom", "(JJII)V", (void*)android_os_Parcel_appendFrom}, diff --git a/core/proto/android/os/incident.proto b/core/proto/android/os/incident.proto index 59e01bfa0c4b..9df351fa39c4 100644 --- a/core/proto/android/os/incident.proto +++ b/core/proto/android/os/incident.proto @@ -35,7 +35,6 @@ import "frameworks/base/core/proto/android/os/system_properties.proto"; import "frameworks/base/core/proto/android/providers/settings.proto"; import "frameworks/base/core/proto/android/server/activitymanagerservice.proto"; import "frameworks/base/core/proto/android/server/alarm/alarmmanagerservice.proto"; -import "frameworks/base/core/proto/android/server/bluetooth_manager_service.proto"; import "frameworks/base/core/proto/android/server/fingerprint.proto"; import "frameworks/base/core/proto/android/server/jobscheduler.proto"; import "frameworks/base/core/proto/android/server/location/context_hub.proto"; @@ -483,10 +482,8 @@ message IncidentProto { (section).args = "connmetrics --proto" ]; - optional com.android.server.BluetoothManagerServiceDumpProto bluetooth_manager = 3050 [ - (section).type = SECTION_DUMPSYS, - (section).args = "bluetooth_manager --proto" - ]; + // Deprecated BluetoothManagerServiceDumpProto + reserved 3050; optional com.android.server.location.ContextHubServiceProto context_hub = 3051 [ (section).type = SECTION_DUMPSYS, diff --git a/core/proto/android/server/Android.bp b/core/proto/android/server/Android.bp deleted file mode 100644 index 362daa73ff14..000000000000 --- a/core/proto/android/server/Android.bp +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright (C) 2021 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -package { - default_applicable_licenses: ["Android-Apache-2.0"], -} - -filegroup { - name: "srcs_bluetooth_manager_service_proto", - srcs: [ - "bluetooth_manager_service.proto", - ], - visibility: ["//packages/modules/Bluetooth:__subpackages__"], -} - diff --git a/core/proto/android/server/bluetooth_manager_service.proto b/core/proto/android/server/bluetooth_manager_service.proto deleted file mode 100644 index c33f66a9aeca..000000000000 --- a/core/proto/android/server/bluetooth_manager_service.proto +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -syntax = "proto2"; -package com.android.server; - -import "frameworks/base/core/proto/android/privacy.proto"; -import "frameworks/proto_logging/stats/enums/bluetooth/enums.proto"; - -option java_multiple_files = true; - -message BluetoothManagerServiceDumpProto { - option (.android.msg_privacy).dest = DEST_AUTOMATIC; - - message ActiveLog { - option (.android.msg_privacy).dest = DEST_AUTOMATIC; - optional int64 timestamp_ms = 1; - optional bool enable = 2; - optional string package_name = 3; - optional .android.bluetooth.EnableDisableReasonEnum reason = 4; - } - - optional bool enabled = 1; - optional int32 state = 2; - optional string state_name = 3; - optional string address = 4 [(.android.privacy).dest = DEST_EXPLICIT]; - optional string name = 5 [(.android.privacy).dest = DEST_EXPLICIT]; - optional int64 last_enabled_time_ms = 6; - optional int64 curr_timestamp_ms = 7; - repeated ActiveLog active_logs = 8; - optional int32 num_crashes = 9; - optional bool crash_log_maxed = 10; - repeated int64 crash_timestamps_ms = 11; - optional int32 num_ble_apps = 12; - repeated string ble_app_package_names = 13; -}
\ No newline at end of file diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java index 3e6520106ab0..bb059108d4b6 100644 --- a/core/tests/coretests/src/android/os/ParcelTest.java +++ b/core/tests/coretests/src/android/os/ParcelTest.java @@ -29,6 +29,8 @@ import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -416,4 +418,63 @@ public class ParcelTest { int binderEndPos = pA.dataPosition(); assertTrue(pA.hasBinders(binderStartPos, binderEndPos - binderStartPos)); } + + private static final byte[] TEST_DATA = new byte[] {4, 8, 15, 16, 23, 42}; + + // Allow for some Parcel overhead + private static final int TEST_DATA_LENGTH = TEST_DATA.length + 100; + + @Test + public void testMarshall_ByteBuffer_wrapped() { + ByteBuffer bb = ByteBuffer.allocate(TEST_DATA_LENGTH); + testMarshall_ByteBuffer(bb); + } + + @Test + public void testMarshall_DirectByteBuffer() { + ByteBuffer bb = ByteBuffer.allocateDirect(TEST_DATA_LENGTH); + testMarshall_ByteBuffer(bb); + } + + private void testMarshall_ByteBuffer(ByteBuffer bb) { + // Ensure that Parcel respects the starting offset by not starting at 0 + bb.position(1); + bb.mark(); + + // Parcel test data, then marshall into the ByteBuffer + Parcel p1 = Parcel.obtain(); + p1.writeByteArray(TEST_DATA); + p1.marshall(bb); + p1.recycle(); + + assertTrue(bb.position() > 1); + bb.reset(); + + // Unmarshall test data into a new Parcel + Parcel p2 = Parcel.obtain(); + bb.reset(); + p2.unmarshall(bb); + assertTrue(bb.position() > 1); + p2.setDataPosition(0); + byte[] marshalled = p2.marshall(); + + bb.reset(); + for (int i = 0; i < TEST_DATA.length; i++) { + assertEquals(bb.get(), marshalled[i]); + } + + byte[] testDataCopy = new byte[TEST_DATA.length]; + p2.setDataPosition(0); + p2.readByteArray(testDataCopy); + for (int i = 0; i < TEST_DATA.length; i++) { + assertEquals(TEST_DATA[i], testDataCopy[i]); + } + + // Test that overflowing the buffer throws an exception + bb.reset(); + // Leave certainly not enough room for the test data + bb.limit(bb.position() + TEST_DATA.length - 1); + assertThrows(BufferOverflowException.class, () -> p2.marshall(bb)); + p2.recycle(); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt index 9c55f0ecda93..7c5f34f979cd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt @@ -152,6 +152,8 @@ class DesktopTilingWindowDecoration( endBounds = destinationBounds, callback, ) + } else { + callback.invoke() } } return isTiled diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 05750a54f566..b139c000a1a9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -889,8 +889,8 @@ public class BackAnimationControllerTest extends ShellTestCase { */ private void doStartEvents(int startX, int moveX) { doMotionEvent(MotionEvent.ACTION_DOWN, startX); - mController.onThresholdCrossed(); doMotionEvent(MotionEvent.ACTION_MOVE, moveX); + mController.onThresholdCrossed(); } private void simulateRemoteAnimationStart() throws RemoteException { diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java index b57476f4341f..6e0821f9f89b 100644 --- a/media/java/android/media/MediaRouter.java +++ b/media/java/android/media/MediaRouter.java @@ -183,7 +183,17 @@ public class MediaRouter { appContext.registerReceiver(new VolumeChangeReceiver(), new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION)); - mDisplayService.registerDisplayListener(this, mHandler); + if (com.android.server.display.feature.flags.Flags + .displayListenerPerformanceImprovements() + && com.android.server.display.feature.flags.Flags + .delayImplicitRrRegistrationUntilRrAccessed()) { + mDisplayService.registerDisplayListener(this, mHandler, + DisplayManager.EVENT_TYPE_DISPLAY_ADDED + | DisplayManager.EVENT_TYPE_DISPLAY_CHANGED + | DisplayManager.EVENT_TYPE_DISPLAY_REMOVED); + } else { + mDisplayService.registerDisplayListener(this, mHandler); + } AudioRoutesInfo newAudioRoutes = null; try { diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index fb0678fedb56..5bba99f84d43 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -255,11 +255,11 @@ public class BugreportProgressService extends Service { /** Always keep remote bugreport files created in the last day. */ private static final long REMOTE_MIN_KEEP_AGE = DateUtils.DAY_IN_MILLIS; - /** Minimum delay for sending last update notification */ - private static final int DELAY_NOTIFICATION_MS = 250; - private final Object mLock = new Object(); +/** Minimum delay between percentage points before sending an update notification */ + private static final int MIN_NOTIFICATION_GAP = 10; + /** Managed bugreport info (keyed by id) */ @GuardedBy("mLock") private final SparseArray<BugreportInfo> mBugreportInfos = new SparseArray<>(); @@ -1460,17 +1460,6 @@ public class BugreportProgressService extends Service { * Sends a notification indicating the bugreport has finished so use can share it. */ private void sendBugreportNotification(BugreportInfo info, boolean takingScreenshot) { - - final long lastUpdate = System.currentTimeMillis() - info.lastUpdate.longValue(); - if (lastUpdate < DELAY_NOTIFICATION_MS) { - Log.d(TAG, "Delaying final notification for " - + (DELAY_NOTIFICATION_MS - lastUpdate) + " ms "); - mMainThreadHandler.postDelayed(() -> { - sendBugreportNotification(info, takingScreenshot); - }, DELAY_NOTIFICATION_MS - lastUpdate); - return; - } - // Since adding the details can take a while, do it before notifying user. addDetailsToZipFile(info); @@ -1523,7 +1512,7 @@ public class BugreportProgressService extends Service { builder.setSubText(info.getName()); } - Log.v(TAG, "Sending 'Share' notification for ID " + info.id + ": " + title); + Log.d(TAG, "Sending 'Share' notification for ID " + info.id + ": " + title); NotificationManager.from(mContext).notify(info.id, builder.build()); } @@ -2753,6 +2742,11 @@ public class BugreportProgressService extends Service { if (progress > CAPPED_PROGRESS) { progress = CAPPED_PROGRESS; } + + if ((progress - info.lastProgress.intValue()) < MIN_NOTIFICATION_GAP) { + return; + } + if (DEBUG) { if (progress != info.progress.intValue()) { Log.v(TAG, "Updating progress for name " + info.getName() + "(id: " + info.id diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt index a352b1eebb81..82e5f5bb6dc8 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt @@ -21,7 +21,6 @@ import android.view.View import android.view.ViewGroup import android.view.ViewGroupOverlay import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -62,6 +61,7 @@ import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.graphics.rememberGraphicsLayer @@ -82,6 +82,7 @@ import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeViewModelStoreOwner import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.android.compose.modifiers.animatedBackground import com.android.compose.modifiers.thenIf import com.android.compose.ui.graphics.FullScreenComposeViewInOverlay import com.android.systemui.animation.ComposableControllerFactory @@ -291,7 +292,7 @@ fun Expandable( .updateExpandableSize() .then(minInteractiveSizeModifier) .then(clickModifier(controller, onClick, interactionSource)) - .background(color, shape) + .animatedBackground(color, shape = shape) .border(controller) .onGloballyPositioned { controller.boundsInComposeViewRoot = it.boundsInRoot() } ) { @@ -307,19 +308,27 @@ private fun WrappedContent( contentColor: Color, content: @Composable (Expandable) -> Unit, ) { - CompositionLocalProvider(LocalContentColor provides contentColor) { - // We make sure that the content itself (wrapped by the background) is at least 40.dp, which - // is the same as the M3 buttons. This applies even if onClick is null, to make it easier to - // write expandables that are sometimes clickable and sometimes not. There shouldn't be any - // Expandable smaller than 40dp because if the expandable is not clickable directly, then - // something in its content should be (and with a size >= 40dp). - val minSize = 40.dp - Box( - Modifier.defaultMinSize(minWidth = minSize, minHeight = minSize), - contentAlignment = Alignment.Center, - ) { - content(expandable) + val minSizeContent = + @Composable { + // We make sure that the content itself (wrapped by the background) is at least 40.dp, + // which is the same as the M3 buttons. This applies even if onClick is null, to make it + // easier to write expandables that are sometimes clickable and sometimes not. There + // shouldn't be any Expandable smaller than 40dp because if the expandable is not + // clickable directly, then something in its content should be (and with a size >= + // 40dp). + val minSize = 40.dp + Box( + Modifier.defaultMinSize(minWidth = minSize, minHeight = minSize), + contentAlignment = Alignment.Center, + ) { + content(expandable) + } } + + if (contentColor.isSpecified) { + CompositionLocalProvider(LocalContentColor provides contentColor, content = minSizeContent) + } else { + minSizeContent() } } @@ -345,7 +354,7 @@ private fun Modifier.expandable( .thenIf(drawContent) { Modifier.border(controller) .then(clickModifier(controller, onClick, interactionSource)) - .background(controller.color, controller.shape) + .animatedBackground(controller.color, shape = controller.shape) } .onPlaced { controller.boundsInComposeViewRoot = it.boundsInRoot() } .drawWithContent { @@ -422,7 +431,7 @@ private class DrawExpandableInOverlayNode( // Background. this@draw.drawBackground( state, - controller.color, + controller.color(), controller.borderStroke, size = Size(state.width.toFloat(), state.height.toFloat()), ) @@ -469,7 +478,7 @@ private fun clickModifier( /** Draw [content] in [overlay] while respecting its screen position given by [animatorState]. */ @Composable private fun AnimatedContentInOverlay( - color: Color, + color: () -> Color, sizeInOriginalLayout: Size, overlay: ViewGroupOverlay, controller: ExpandableControllerImpl, @@ -523,7 +532,7 @@ private fun AnimatedContentInOverlay( return@drawWithContent } - drawBackground(animatorState, color, controller.borderStroke) + drawBackground(animatorState, color(), controller.borderStroke) drawContent() }, // We center the content in the expanding container. diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt index 72da175e26cf..d7d5a48e2b79 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -80,6 +79,24 @@ fun rememberExpandableController( borderStroke: BorderStroke? = null, transitionControllerFactory: ComposableControllerFactory? = null, ): ExpandableController { + return rememberExpandableController( + color = { color }, + shape = shape, + contentColor = contentColor, + borderStroke = borderStroke, + transitionControllerFactory = transitionControllerFactory, + ) +} + +/** Create an [ExpandableController] to control an [Expandable]. */ +@Composable +fun rememberExpandableController( + color: () -> Color, + shape: Shape, + contentColor: Color = Color.Unspecified, + borderStroke: BorderStroke? = null, + transitionControllerFactory: ComposableControllerFactory? = null, +): ExpandableController { val composeViewRoot = LocalView.current val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current @@ -125,7 +142,7 @@ fun rememberExpandableController( } internal class ExpandableControllerImpl( - internal val color: Color, + internal val color: () -> Color, internal val contentColor: Color, internal val shape: Shape, internal val borderStroke: BorderStroke?, diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt index 959f28f2d99e..80cbbea7659f 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt @@ -95,10 +95,20 @@ interface NestedDraggable { * nested scrollable. * * This is called whenever a nested scrollable does not consume some scroll amount. If this - * returns `true`, then [onDragStarted] will be called and this draggable will have priority and + * returns `true`, then [onDragStarted] will be called, this draggable will have priority and * consume all future events during preScroll until the nested scroll is finished. */ - fun shouldConsumeNestedScroll(sign: Float): Boolean + fun shouldConsumeNestedPostScroll(sign: Float): Boolean = true + + /** + * Whether this draggable should consume any scroll amount with the given [sign] *before* it can + * be consumed by a nested scrollable. + * + * This is called before a nested scrollable is able to consume that scroll amount. If this + * returns `true`, then [onDragStarted] will be called, this draggable will have priority and + * consume all future scroll events during preScroll until the nested scroll is finished. + */ + fun shouldConsumeNestedPreScroll(sign: Float): Boolean = false interface Controller { /** @@ -540,6 +550,14 @@ private class NestedDraggableNode( } override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val sign = available.toFloat().sign + maybeCreateNewController( + sign = sign, + condition = { + source == NestedScrollSource.UserInput && + draggable.shouldConsumeNestedPreScroll(sign) + }, + ) val controller = nestedScrollController ?: return Offset.Zero return scrollWithOverscroll(controller, available) } @@ -560,28 +578,34 @@ private class NestedDraggableNode( } val sign = offset.sign + maybeCreateNewController( + sign, + condition = { draggable.shouldConsumeNestedPostScroll(sign) }, + ) + val controller = nestedScrollController ?: return Offset.Zero + return scrollWithOverscroll(controller, available) + } + + private fun maybeCreateNewController(sign: Float, condition: () -> Boolean) { if ( - nestedDragsEnabled && - nestedScrollController == null && - // TODO(b/388231324): Remove this. - !lastEventWasScrollWheel && - draggable.shouldConsumeNestedScroll(sign) && - lastFirstDown != null + !nestedDragsEnabled || + nestedScrollController != null || + lastEventWasScrollWheel || + lastFirstDown == null || + !condition() ) { - val startedPosition = checkNotNull(lastFirstDown) - - // TODO(b/382665591): Ensure that there is at least one pointer down. - val pointersDownCount = pointersDown.size.coerceAtLeast(1) - val pointerType = pointersDown.entries.firstOrNull()?.value - nestedScrollController = - NestedScrollController( - overscrollEffect, - draggable.onDragStarted(startedPosition, sign, pointersDownCount, pointerType), - ) + return } - val controller = nestedScrollController ?: return Offset.Zero - return scrollWithOverscroll(controller, available) + // TODO(b/382665591): Ensure that there is at least one pointer down. + val pointersDownCount = pointersDown.size.coerceAtLeast(1) + val pointerType = pointersDown.entries.firstOrNull()?.value + val startedPosition = checkNotNull(lastFirstDown) + nestedScrollController = + NestedScrollController( + overscrollEffect, + draggable.onDragStarted(startedPosition, sign, pointersDownCount, pointerType), + ) } private fun scrollWithOverscroll(controller: NestedScrollController, offset: Offset): Offset { diff --git a/packages/SystemUI/compose/core/src/com/android/compose/modifiers/AnimatedBackground.kt b/packages/SystemUI/compose/core/src/com/android/compose/modifiers/AnimatedBackground.kt new file mode 100644 index 000000000000..5b1f0a7c6eb6 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/compose/modifiers/AnimatedBackground.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.compose.modifiers + +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.ObserverModifierNode +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.node.observeReads +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.LayoutDirection + +/** + * Draws a background in a given [shape] and with a [color] or [alpha] that can be animated. + * + * @param color color to paint background with + * @param alpha alpha of the background + * @param shape desired shape of the background + */ +fun Modifier.animatedBackground( + color: () -> Color, + alpha: () -> Float = DefaultAlpha, + shape: Shape = RectangleShape, +) = + this.then( + BackgroundElement( + color = color, + alpha = alpha, + shape = shape, + inspectorInfo = + debugInspectorInfo { + name = "background" + value = color + properties["color"] = color + properties["alpha"] = alpha + properties["shape"] = shape + }, + ) + ) + +private val DefaultAlpha = { 1f } + +private class BackgroundElement( + private val color: () -> Color, + private val alpha: () -> Float, + private val shape: Shape, + private val inspectorInfo: InspectorInfo.() -> Unit, +) : ModifierNodeElement<BackgroundNode>() { + override fun create(): BackgroundNode { + return BackgroundNode(color, alpha, shape) + } + + override fun update(node: BackgroundNode) { + node.color = color + node.alpha = alpha + node.shape = shape + } + + override fun InspectorInfo.inspectableProperties() { + inspectorInfo() + } + + override fun hashCode(): Int { + var result = color.hashCode() + result = 31 * result + alpha.hashCode() + result = 31 * result + shape.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + val otherModifier = other as? BackgroundElement ?: return false + return color == otherModifier.color && + alpha == otherModifier.alpha && + shape == otherModifier.shape + } +} + +private class BackgroundNode(var color: () -> Color, var alpha: () -> Float, var shape: Shape) : + DrawModifierNode, Modifier.Node(), ObserverModifierNode { + + // Naively cache outline calculation if input parameters are the same, we manually observe + // reads inside shape#createOutline separately + private var lastSize: Size = Size.Unspecified + private var lastLayoutDirection: LayoutDirection? = null + private var lastOutline: Outline? = null + private var lastShape: Shape? = null + private var tmpOutline: Outline? = null + + override fun ContentDrawScope.draw() { + if (shape === RectangleShape) { + // shortcut to avoid Outline calculation and allocation + drawRect() + } else { + drawOutline() + } + drawContent() + } + + override fun onObservedReadsChanged() { + // Reset cached properties + lastSize = Size.Unspecified + lastLayoutDirection = null + lastOutline = null + lastShape = null + // Invalidate draw so we build the cache again - this is needed because observeReads within + // the draw scope obscures the state reads from the draw scope's observer + invalidateDraw() + } + + private fun ContentDrawScope.drawRect() { + drawRect(color = color(), alpha = alpha()) + } + + private fun ContentDrawScope.drawOutline() { + val outline = getOutline() + drawOutline(outline, color = color(), alpha = alpha()) + } + + private fun ContentDrawScope.getOutline(): Outline { + val outline: Outline? + if (size == lastSize && layoutDirection == lastLayoutDirection && lastShape == shape) { + outline = lastOutline!! + } else { + // Manually observe reads so we can directly invalidate the outline when it changes + // Use tmpOutline to avoid creating an object reference to local var outline + observeReads { tmpOutline = shape.createOutline(size, layoutDirection, this) } + outline = tmpOutline + tmpOutline = null + } + lastOutline = outline + lastSize = size + lastLayoutDirection = layoutDirection + lastShape = shape + return outline!! + } +} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/modifiers/FadingBackground.kt b/packages/SystemUI/compose/core/src/com/android/compose/modifiers/FadingBackground.kt deleted file mode 100644 index 794b7a4a3d30..000000000000 --- a/packages/SystemUI/compose/core/src/com/android/compose/modifiers/FadingBackground.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.compose.modifiers - -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.DrawModifier -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Outline -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.drawOutline -import androidx.compose.ui.graphics.drawscope.ContentDrawScope -import androidx.compose.ui.platform.InspectorInfo -import androidx.compose.ui.platform.InspectorValueInfo -import androidx.compose.ui.platform.debugInspectorInfo -import androidx.compose.ui.unit.LayoutDirection - -/** - * Draws a fading [shape] with a solid [color] and [alpha] behind the content. - * - * @param color color to paint background with - * @param alpha alpha of the background - * @param shape desired shape of the background - */ -fun Modifier.fadingBackground(color: Color, alpha: () -> Float, shape: Shape = RectangleShape) = - this.then( - FadingBackground( - brush = SolidColor(color), - alpha = alpha, - shape = shape, - inspectorInfo = - debugInspectorInfo { - name = "background" - value = color - properties["color"] = color - properties["alpha"] = alpha - properties["shape"] = shape - }, - ) - ) - -private class FadingBackground -constructor( - private val brush: Brush, - private val shape: Shape, - private val alpha: () -> Float, - inspectorInfo: InspectorInfo.() -> Unit, -) : DrawModifier, InspectorValueInfo(inspectorInfo) { - // naive cache outline calculation if size is the same - private var lastSize: Size? = null - private var lastLayoutDirection: LayoutDirection? = null - private var lastOutline: Outline? = null - - override fun ContentDrawScope.draw() { - if (shape === RectangleShape) { - // shortcut to avoid Outline calculation and allocation - drawRect() - } else { - drawOutline() - } - drawContent() - } - - private fun ContentDrawScope.drawRect() { - drawRect(brush, alpha = alpha()) - } - - private fun ContentDrawScope.drawOutline() { - val outline = - if (size == lastSize && layoutDirection == lastLayoutDirection) { - lastOutline!! - } else { - shape.createOutline(size, layoutDirection, this) - } - drawOutline(outline, brush = brush, alpha = alpha()) - lastOutline = outline - lastSize = size - lastLayoutDirection = layoutDirection - } - - override fun hashCode(): Int { - var result = brush.hashCode() - result = 31 * result + alpha.hashCode() - result = 31 * result + shape.hashCode() - return result - } - - override fun equals(other: Any?): Boolean { - val otherModifier = other as? FadingBackground ?: return false - return brush == otherModifier.brush && - alpha == otherModifier.alpha && - shape == otherModifier.shape - } - - override fun toString(): String = "FadingBackground(brush=$brush, alpha = $alpha, shape=$shape)" -} diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt index d8e46ad34c37..a03d769c9c36 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt @@ -971,6 +971,35 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw assertThat(availableToEffectPostFling).isWithin(1f).of(100f) } + @Test + fun consumeNestedPreScroll() { + var consumeNestedPreScroll by mutableStateOf(false) + val draggable = TestDraggable(shouldConsumeNestedPreScroll = { consumeNestedPreScroll }) + + val touchSlop = + rule.setContentWithTouchSlop { + Box( + Modifier.fillMaxSize() + .nestedDraggable(draggable, orientation) + // Always consume everything so that the only way to start the drag is to + // intercept preScroll events. + .scrollable(rememberScrollableState { it }, orientation) + ) + } + + rule.onRoot().performTouchInput { + down(center) + moveBy((touchSlop + 1f).toOffset()) + } + + assertThat(draggable.onDragStartedCalled).isFalse() + + consumeNestedPreScroll = true + rule.onRoot().performTouchInput { moveBy(1f.toOffset()) } + + assertThat(draggable.onDragStartedCalled).isTrue() + } + private fun ComposeContentTestRule.setContentWithTouchSlop( content: @Composable () -> Unit ): Float { @@ -996,7 +1025,8 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw { velocity, _ -> velocity }, - private val shouldConsumeNestedScroll: (Float) -> Boolean = { true }, + private val shouldConsumeNestedPostScroll: (Float) -> Boolean = { true }, + private val shouldConsumeNestedPreScroll: (Float) -> Boolean = { false }, ) : NestedDraggable { var shouldStartDrag = true var onDragStartedCalled = false @@ -1042,8 +1072,12 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw } } - override fun shouldConsumeNestedScroll(sign: Float): Boolean { - return shouldConsumeNestedScroll.invoke(sign) + override fun shouldConsumeNestedPostScroll(sign: Float): Boolean { + return shouldConsumeNestedPostScroll.invoke(sign) + } + + override fun shouldConsumeNestedPreScroll(sign: Float): Boolean { + return shouldConsumeNestedPreScroll.invoke(sign) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt index 216f0a74e1c7..7782705d4c61 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt @@ -73,7 +73,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.compose.animation.Expandable import com.android.compose.animation.scene.ContentScope -import com.android.compose.modifiers.fadingBackground +import com.android.compose.modifiers.animatedBackground import com.android.compose.theme.colorAttr import com.android.systemui.Flags.notificationShadeBlur import com.android.systemui.animation.Expandable @@ -172,8 +172,8 @@ fun FooterActions( val backgroundTopRadius = dimensionResource(R.dimen.qs_corner_radius) val backgroundModifier = remember(backgroundColor, backgroundAlphaValue, backgroundTopRadius) { - Modifier.fadingBackground( - backgroundColor, + Modifier.animatedBackground( + { backgroundColor }, backgroundAlphaValue, RoundedCornerShape(topStart = backgroundTopRadius, topEnd = backgroundTopRadius), ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 1360611ed814..024ca22069ae 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -68,7 +68,7 @@ internal class DraggableHandler( return layoutImpl.swipeDetector.detectSwipe(change) } - override fun shouldConsumeNestedScroll(sign: Float): Boolean { + override fun shouldConsumeNestedPostScroll(sign: Float): Boolean { return this.enabled() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt index 239e02640908..652a2ff21e9b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt @@ -25,6 +25,10 @@ import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase import com.android.systemui.res.R +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -33,7 +37,11 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class ActionIntentCreatorTest : SysuiTestCase() { - val creator = ActionIntentCreator() + private val scheduler = TestCoroutineScheduler() + private val mainDispatcher = UnconfinedTestDispatcher(scheduler) + private val testScope = TestScope(mainDispatcher) + + val creator = ActionIntentCreator(testScope.backgroundScope) @Test fun test_getTextEditorIntent() { @@ -65,7 +73,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { } @Test - fun test_getImageEditIntent() { + fun test_getImageEditIntent() = runTest { context.getOrCreateTestableResources().addOverride(R.string.config_screenshotEditor, "") val fakeUri = Uri.parse("content://foo") var intent = creator.getImageEditIntent(fakeUri, context) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/DefaultIntentCreatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/DefaultIntentCreatorTest.java index 126b3fa9e7ca..10c5c52d21a7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/DefaultIntentCreatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/DefaultIntentCreatorTest.java @@ -34,6 +34,8 @@ import com.android.systemui.res.R; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.atomic.AtomicReference; + @SmallTest @RunWith(AndroidJUnit4.class) public class DefaultIntentCreatorTest extends SysuiTestCase { @@ -73,12 +75,16 @@ public class DefaultIntentCreatorTest extends SysuiTestCase { } @Test - public void test_getImageEditIntent() { + public void test_getImageEditIntentAsync() { getContext().getOrCreateTestableResources().addOverride(R.string.config_screenshotEditor, ""); Uri fakeUri = Uri.parse("content://foo"); - Intent intent = mIntentCreator.getImageEditIntent(fakeUri, getContext()); + final AtomicReference<Intent> intentHolder = new AtomicReference<>(null); + mIntentCreator.getImageEditIntentAsync(fakeUri, getContext(), output -> { + intentHolder.set(output); + }); + Intent intent = intentHolder.get(); assertEquals(Intent.ACTION_EDIT, intent.getAction()); assertEquals("image/*", intent.getType()); assertEquals(null, intent.getComponent()); @@ -90,8 +96,10 @@ public class DefaultIntentCreatorTest extends SysuiTestCase { "com.android.remotecopy.RemoteCopyActivity"); getContext().getOrCreateTestableResources().addOverride(R.string.config_screenshotEditor, fakeComponent.flattenToString()); - intent = mIntentCreator.getImageEditIntent(fakeUri, getContext()); - assertEquals(fakeComponent, intent.getComponent()); + mIntentCreator.getImageEditIntentAsync(fakeUri, getContext(), output -> { + intentHolder.set(output); + }); + assertEquals(fakeComponent, intentHolder.get().getComponent()); } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt index 197b0eea05cb..859137507bbf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt @@ -26,6 +26,7 @@ import android.view.Display.TYPE_INTERNAL import android.view.IWindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.app.displaylib.DisplayRepository.PendingDisplay import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue @@ -71,14 +72,20 @@ class DisplayRepositoryTest : SysuiTestCase() { // that the initial state (soon after construction) contains the expected ones set in every // test. private val displayRepository: DisplayRepositoryImpl by lazy { - DisplayRepositoryImpl( + // TODO b/401305290 - move this to kosmos + val displayRepositoryFromLib = + com.android.app.displaylib.DisplayRepositoryImpl( displayManager, - commandQueue, - windowManager, testHandler, - TestScope(UnconfinedTestDispatcher()), + testScope.backgroundScope, UnconfinedTestDispatcher(), ) + DisplayRepositoryImpl( + commandQueue, + windowManager, + testScope.backgroundScope, + displayRepositoryFromLib, + ) .also { verify(displayManager, never()).registerDisplayListener(any(), any()) // It needs to be called, just once, for the initial value. @@ -403,7 +410,7 @@ class DisplayRepositoryTest : SysuiTestCase() { val pendingDisplay by lastPendingDisplay() sendOnDisplayConnected(1, TYPE_EXTERNAL) - val initialPendingDisplay: DisplayRepository.PendingDisplay? = pendingDisplay + val initialPendingDisplay: PendingDisplay? = pendingDisplay assertThat(pendingDisplay).isNotNull() sendOnDisplayChanged(1) @@ -416,7 +423,7 @@ class DisplayRepositoryTest : SysuiTestCase() { val pendingDisplay by lastPendingDisplay() sendOnDisplayConnected(1, TYPE_EXTERNAL) - val initialPendingDisplay: DisplayRepository.PendingDisplay? = pendingDisplay + val initialPendingDisplay: PendingDisplay? = pendingDisplay assertThat(pendingDisplay).isNotNull() sendOnDisplayConnected(2, TYPE_EXTERNAL) @@ -648,7 +655,7 @@ class DisplayRepositoryTest : SysuiTestCase() { return flowValue } - private fun TestScope.lastPendingDisplay(): FlowValue<DisplayRepository.PendingDisplay?> { + private fun TestScope.lastPendingDisplay(): FlowValue<PendingDisplay?> { val flowValue = collectLastValue(displayRepository.pendingDisplay) captureAddedRemovedListener() verify(displayManager) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayInstanceRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayInstanceRepositoryImplTest.kt index e41d46ce90af..6c7783ae44e1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayInstanceRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayInstanceRepositoryImplTest.kt @@ -31,7 +31,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyString -import org.mockito.kotlin.eq +import org.mockito.kotlin.any import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) @@ -105,7 +105,7 @@ class PerDisplayInstanceRepositoryImplTest : SysuiTestCase() { @Test fun start_registersDumpable() { - verify(kosmos.dumpManager).registerNormalDumpable(anyString(), eq(underTest)) + verify(kosmos.dumpManager).registerNormalDumpable(anyString(), any()) } private fun createDisplay(displayId: Int): Display = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt index 4a954b391f5f..387c62d76083 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt @@ -512,6 +512,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( .setImportance(NotificationManager.IMPORTANCE_HIGH) .build() + whenever(row.canViewBeDismissed()).thenReturn(true) whenever(highPriorityProvider.isHighPriority(entry)).thenReturn(true) val statusBarNotification = entry.sbn gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -534,6 +535,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( any<UiEventLogger>(), /* isDeviceProvisioned = */ eq(false), /* isNonblockable = */ eq(false), + /* isDismissable = */ eq(true), /* wasShownHighPriority = */ eq(true), eq(assistantFeedbackController), eq(metricsLogger), @@ -549,6 +551,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE) .build() + whenever(row.canViewBeDismissed()).thenReturn(true) val statusBarNotification = row.entry.sbn val entry = row.entry @@ -574,6 +577,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( any<UiEventLogger>(), /* isDeviceProvisioned = */ eq(true), /* isNonblockable = */ eq(false), + /* isDismissable = */ eq(true), /* wasShownHighPriority = */ eq(false), eq(assistantFeedbackController), eq(metricsLogger), @@ -589,6 +593,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE) .build() + whenever(row.canViewBeDismissed()).thenReturn(true) val statusBarNotification = row.entry.sbn val entry = row.entry @@ -612,6 +617,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( any<UiEventLogger>(), /* isDeviceProvisioned = */ eq(false), /* isNonblockable = */ eq(false), + /* isDismissable = */ eq(true), /* wasShownHighPriority = */ eq(false), eq(assistantFeedbackController), eq(metricsLogger), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt index 5f817de58b40..b5f3269903b8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt @@ -49,6 +49,7 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger @@ -865,6 +866,31 @@ class NotificationInfoTest : SysuiTestCase() { assertThat(underTest.findViewById<View>(R.id.feedback).visibility).isEqualTo(GONE) } + @Test + @Throws(RemoteException::class) + fun testDismissListenerBound() { + val latch = CountDownLatch(1) + bindNotification(onCloseClick = { _: View? -> latch.countDown() }) + + val dismissView = underTest.findViewById<View>(R.id.inline_dismiss) + assertThat(dismissView.isVisible).isTrue() + dismissView.performClick() + + // Verify that listener was triggered. + assertThat(latch.count).isEqualTo(0) + } + + @Test + @Throws(RemoteException::class) + fun testDismissHiddenWhenUndismissable() { + + entry.sbn.notification.flags = + entry.sbn.notification.flags or android.app.Notification.FLAG_NO_DISMISS + bindNotification(isDismissable = false) + val dismissView = underTest.findViewById<View>(R.id.inline_dismiss) + assertThat(dismissView.isVisible).isFalse() + } + private fun bindNotification( pm: PackageManager = this.mockPackageManager, iNotificationManager: INotificationManager = this.mockINotificationManager, @@ -883,6 +909,7 @@ class NotificationInfoTest : SysuiTestCase() { uiEventLogger: UiEventLogger = this.uiEventLogger, isDeviceProvisioned: Boolean = true, isNonblockable: Boolean = false, + isDismissable: Boolean = true, wasShownHighPriority: Boolean = true, assistantFeedbackController: AssistantFeedbackController = this.assistantFeedbackController, metricsLogger: MetricsLogger = kosmos.metricsLogger, @@ -905,6 +932,7 @@ class NotificationInfoTest : SysuiTestCase() { uiEventLogger, isDeviceProvisioned, isNonblockable, + isDismissable, wasShownHighPriority, assistantFeedbackController, metricsLogger, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java index af52c31b1c53..9fdfca14a5b2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java @@ -28,7 +28,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.platform.test.annotations.DisableFlags; import android.provider.Settings; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; @@ -39,7 +38,6 @@ import android.view.ViewGroup; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.systemui.Flags; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; @@ -415,7 +413,6 @@ public class NotificationMenuRowTest extends LeakCheckedTest { assertTrue("when alpha is .5, menu is visible", row.isMenuVisible()); } - @DisableFlags(Flags.FLAG_MAGNETIC_NOTIFICATION_SWIPES) @Test public void testOnTouchMove() { NotificationMenuRow row = Mockito.spy( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java index 388729743f9e..209dfb2d2ed6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java @@ -152,6 +152,7 @@ public class PromotedNotificationInfoTest extends SysuiTestCase { true, false, true, + true, mAssistantFeedbackController, mMetricsLogger, null); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt index 6c6ba933c03a..ccc8be7de038 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt @@ -130,9 +130,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { kosmos.testScope.runTest { // GIVEN a threshold of 100 px val threshold = 100f - underTest.onDensityChange( - threshold / MagneticNotificationRowManager.MAGNETIC_DETACH_THRESHOLD_DP - ) + underTest.setSwipeThresholdPx(threshold) // GIVEN that targets are set and the rows are being pulled setTargets() @@ -152,9 +150,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { kosmos.testScope.runTest { // GIVEN a threshold of 100 px val threshold = 100f - underTest.onDensityChange( - threshold / MagneticNotificationRowManager.MAGNETIC_DETACH_THRESHOLD_DP - ) + underTest.setSwipeThresholdPx(threshold) // GIVEN that targets are set and the rows are being pulled canRowBeDismissed = false @@ -176,9 +172,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { kosmos.testScope.runTest { // GIVEN a threshold of 100 px val threshold = 100f - underTest.onDensityChange( - threshold / MagneticNotificationRowManager.MAGNETIC_DETACH_THRESHOLD_DP - ) + underTest.setSwipeThresholdPx(threshold) // GIVEN that targets are set and the rows are being pulled setTargets() @@ -198,9 +192,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { kosmos.testScope.runTest { // GIVEN a threshold of 100 px val threshold = 100f - underTest.onDensityChange( - threshold / MagneticNotificationRowManager.MAGNETIC_DETACH_THRESHOLD_DP - ) + underTest.setSwipeThresholdPx(threshold) // GIVEN that targets are set and the rows are being pulled canRowBeDismissed = false @@ -302,29 +294,6 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { assertThat(underTest.isSwipedViewRoundableSet).isFalse() } - @Test - fun isMagneticRowDismissible_isDismissibleWhenDetached() = - kosmos.testScope.runTest { - setDetachedState() - - val isDismissible = underTest.isMagneticRowSwipeDetached(swipedRow) - assertThat(isDismissible).isTrue() - } - - @Test - fun setMagneticRowTranslation_whenDetached_belowAttachThreshold_reattaches() = - kosmos.testScope.runTest { - // GIVEN that the swiped view has been detached - setDetachedState() - - // WHEN setting a new translation above the attach threshold - val translation = 50f - underTest.setMagneticRowTranslation(swipedRow, translation) - - // THEN the swiped view reattaches magnetically and the state becomes PULLING - assertThat(underTest.currentState).isEqualTo(State.PULLING) - } - @After fun tearDown() { // We reset the manager so that all MagneticRowListener can cancel all animations @@ -333,9 +302,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { private fun setDetachedState() { val threshold = 100f - underTest.onDensityChange( - threshold / MagneticNotificationRowManager.MAGNETIC_DETACH_THRESHOLD_DP - ) + underTest.setSwipeThresholdPx(threshold) // Set the pulling state setTargets() @@ -360,8 +327,8 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { private fun MagneticRowListener.asTestableListener(rowIndex: Int): MagneticRowListener { val delegate = this return object : MagneticRowListener { - override fun setMagneticTranslation(translation: Float, trackEagerly: Boolean) { - delegate.setMagneticTranslation(translation, trackEagerly) + override fun setMagneticTranslation(translation: Float) { + delegate.setMagneticTranslation(translation) } override fun triggerMagneticForce( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java index de48f4018989..789701f5e4b0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java @@ -49,7 +49,6 @@ import android.view.ViewConfiguration; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.flags.FakeFeatureFlags; @@ -363,7 +362,6 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { verify(mSwipeHelper, times(1)).isFalseGesture(); } - @DisableFlags(Flags.FLAG_MAGNETIC_NOTIFICATION_SWIPES) @Test public void testIsDismissGesture_farEnough() { doReturn(false).when(mSwipeHelper).isFalseGesture(); @@ -376,20 +374,6 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { verify(mSwipeHelper, times(1)).isFalseGesture(); } - @EnableFlags(Flags.FLAG_MAGNETIC_NOTIFICATION_SWIPES) - @Test - public void testIsDismissGesture_magneticSwipeIsDismissible() { - doReturn(false).when(mSwipeHelper).isFalseGesture(); - doReturn(false).when(mSwipeHelper).swipedFarEnough(); - doReturn(false).when(mSwipeHelper).swipedFastEnough(); - doReturn(true).when(mCallback).isMagneticViewDetached(any()); - when(mCallback.canChildBeDismissedInDirection(any(), anyBoolean())).thenReturn(true); - when(mEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_UP); - - assertTrue("Should be a dismissal", mSwipeHelper.isDismissGesture(mEvent)); - verify(mSwipeHelper, times(1)).isFalseGesture(); - } - @Test public void testIsDismissGesture_notFarOrFastEnough() { doReturn(false).when(mSwipeHelper).isFalseGesture(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt index 91b3896332f5..39cf02dbc772 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt @@ -54,9 +54,7 @@ class FakeHomeStatusBarViewModel( MutableStateFlow(OngoingActivityChipModel.Inactive()) override val ongoingActivityChips = - MutableStateFlow( - ChipsVisibilityModel(MultipleOngoingActivityChipsModel(), areChipsAllowed = false) - ) + ChipsVisibilityModel(MultipleOngoingActivityChipsModel(), areChipsAllowed = false) override val ongoingActivityChipsLegacy = MutableStateFlow(MultipleOngoingActivityChipsModelLegacy()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt index 2da692b4cb45..20cf3ae6b8dd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt @@ -49,6 +49,7 @@ import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.lifecycle.activateIn import com.android.systemui.log.assertLogsWtf import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository @@ -107,7 +108,8 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class HomeStatusBarViewModelImplTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() - private val Kosmos.underTest by Kosmos.Fixture { kosmos.homeStatusBarViewModel } + private val Kosmos.underTest by + Kosmos.Fixture { kosmos.homeStatusBarViewModel.also { it.activateIn(kosmos.testScope) } } @Before fun setUp() { @@ -891,32 +893,26 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @EnableChipsModernization fun ongoingActivityChips_statusBarHidden_noSecureCamera_noHun_notAllowed() = kosmos.runTest { - val latest by collectLastValue(underTest.ongoingActivityChips) - // home status bar not allowed kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen) kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false, taskInfo = null) - assertThat(latest!!.areChipsAllowed).isFalse() + assertThat(underTest.ongoingActivityChips.areChipsAllowed).isFalse() } @Test @EnableChipsModernization fun ongoingActivityChips_statusBarNotHidden_noSecureCamera_noHun_isAllowed() = kosmos.runTest { - val latest by collectLastValue(underTest.ongoingActivityChips) - transitionKeyguardToGone() - assertThat(latest!!.areChipsAllowed).isTrue() + assertThat(underTest.ongoingActivityChips.areChipsAllowed).isTrue() } @Test @EnableChipsModernization fun ongoingActivityChips_statusBarNotHidden_secureCamera_noHun_notAllowed() = kosmos.runTest { - val latest by collectLastValue(underTest.ongoingActivityChips) - fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, to = KeyguardState.OCCLUDED, @@ -924,7 +920,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { ) kosmos.keyguardInteractor.onCameraLaunchDetected(CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) - assertThat(latest!!.areChipsAllowed).isFalse() + assertThat(underTest.ongoingActivityChips.areChipsAllowed).isFalse() } @Test @@ -932,8 +928,6 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @EnableChipsModernization fun ongoingActivityChips_statusBarNotHidden_noSecureCamera_hunBySystem_noHunFlagOff_notAllowed() = kosmos.runTest { - val latest by collectLastValue(underTest.ongoingActivityChips) - transitionKeyguardToGone() headsUpNotificationRepository.setNotifications( @@ -943,7 +937,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { ) ) - assertThat(latest!!.areChipsAllowed).isFalse() + assertThat(underTest.ongoingActivityChips.areChipsAllowed).isFalse() } @Test @@ -951,8 +945,6 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @EnableChipsModernization fun ongoingActivityChips_statusBarNotHidden_noSecureCamera_hunByUser_noHunFlagOff_isAllowed() = kosmos.runTest { - val latest by collectLastValue(underTest.ongoingActivityChips) - transitionKeyguardToGone() headsUpNotificationRepository.setNotifications( @@ -962,16 +954,14 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { ) ) - assertThat(latest!!.areChipsAllowed).isTrue() + assertThat(underTest.ongoingActivityChips.areChipsAllowed).isTrue() } @Test @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) @EnableChipsModernization - fun ongoingActivityChips_tatusBarNotHidden_noSecureCamera_hunBySystem_noHunFlagOn_isAllowed() = + fun ongoingActivityChips_statusBarNotHidden_noSecureCamera_hunBySystem_noHunFlagOn_isAllowed() = kosmos.runTest { - val latest by collectLastValue(underTest.ongoingActivityChips) - transitionKeyguardToGone() headsUpNotificationRepository.setNotifications( @@ -981,7 +971,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { ) ) - assertThat(latest!!.areChipsAllowed).isTrue() + assertThat(underTest.ongoingActivityChips.areChipsAllowed).isTrue() } @Test @@ -989,8 +979,6 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @EnableChipsModernization fun ongoingActivityChips_statusBarNotHidden_noSecureCamera_hunByUser_noHunFlagOn_isAllowed() = kosmos.runTest { - val latest by collectLastValue(underTest.ongoingActivityChips) - transitionKeyguardToGone() headsUpNotificationRepository.setNotifications( @@ -1000,7 +988,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { ) ) - assertThat(latest!!.areChipsAllowed).isTrue() + assertThat(underTest.ongoingActivityChips.areChipsAllowed).isTrue() } @Test @@ -1008,17 +996,16 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @EnableChipsModernization fun ongoingActivityChips_followsChipsViewModel() = kosmos.runTest { - val latest by collectLastValue(underTest.ongoingActivityChips) transitionKeyguardToGone() screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording - assertIsScreenRecordChip(latest!!.chips.active[0]) + assertIsScreenRecordChip(underTest.ongoingActivityChips.chips.active[0]) addOngoingCallState(key = "call") - assertIsScreenRecordChip(latest!!.chips.active[0]) - assertIsCallChip(latest!!.chips.active[1], "call", context) + assertIsScreenRecordChip(underTest.ongoingActivityChips.chips.active[0]) + assertIsCallChip(underTest.ongoingActivityChips.chips.active[1], "call", context) } @Test diff --git a/packages/SystemUI/res/layout/notification_2025_info.xml b/packages/SystemUI/res/layout/notification_2025_info.xml index 7b6916652924..fa852a2b8e85 100644 --- a/packages/SystemUI/res/layout/notification_2025_info.xml +++ b/packages/SystemUI/res/layout/notification_2025_info.xml @@ -18,6 +18,7 @@ <!-- extends LinearLayout --> <com.android.systemui.statusbar.notification.row.NotificationInfo xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/notification_guts" android:layout_width="match_parent" @@ -324,18 +325,34 @@ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout> </LinearLayout> - - <LinearLayout + <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/bottom_buttons" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@*android:dimen/notification_2025_margin" android:minHeight="@dimen/notification_2025_guts_button_size" - android:gravity="center_vertical" - > + android:gravity="center_vertical"> + + <TextView + android:id="@+id/inline_dismiss" + android:text="@string/notification_inline_dismiss" + android:paddingEnd="@dimen/notification_importance_button_padding" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:paddingBottom="@*android:dimen/notification_2025_margin" + app:layout_constraintStart_toStartOf="parent" + android:gravity="center" + android:minWidth="@dimen/notification_2025_min_tap_target_size" + android:minHeight="@dimen/notification_2025_min_tap_target_size" + android:maxWidth="200dp" + style="@style/TextAppearance.NotificationInfo.Button" + android:textSize="@*android:dimen/notification_2025_action_text_size" + /> <TextView android:id="@+id/turn_off_notifications" android:text="@string/inline_turn_off_notifications" + android:paddingStart="@dimen/notification_importance_button_padding" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="32dp" @@ -345,6 +362,8 @@ android:minWidth="@dimen/notification_2025_min_tap_target_size" android:minHeight="@dimen/notification_2025_min_tap_target_size" android:maxWidth="200dp" + app:layout_constraintStart_toEndOf="@id/inline_dismiss" + app:layout_constraintBaseline_toBaselineOf="@id/inline_dismiss" style="@style/TextAppearance.NotificationInfo.Button" android:textSize="@*android:dimen/notification_2025_action_text_size"/> <TextView @@ -354,12 +373,18 @@ android:layout_height="wrap_content" android:paddingTop="8dp" android:paddingBottom="@*android:dimen/notification_2025_margin" - android:gravity="center" + android:gravity="end" + app:layout_constraintEnd_toEndOf="parent" android:minWidth="@dimen/notification_2025_min_tap_target_size" android:minHeight="@dimen/notification_2025_min_tap_target_size" android:maxWidth="125dp" style="@style/TextAppearance.NotificationInfo.Button" android:textSize="@*android:dimen/notification_2025_action_text_size"/> - </LinearLayout> + <androidx.constraintlayout.helper.widget.Flow + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:constraint_referenced_ids="inline_dismiss,turn_off_notifications,done" + app:flow_wrapMode="chain"/> + </androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout> </com.android.systemui.statusbar.notification.row.NotificationInfo> diff --git a/packages/SystemUI/res/layout/notification_info.xml b/packages/SystemUI/res/layout/notification_info.xml index 089ceaee6ce3..d4bd142d9089 100644 --- a/packages/SystemUI/res/layout/notification_info.xml +++ b/packages/SystemUI/res/layout/notification_info.xml @@ -341,15 +341,28 @@ asked for it --> android:paddingEnd="4dp" > <TextView + android:id="@+id/inline_dismiss" + android:text="@string/notification_inline_dismiss" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:gravity="start|center_vertical" + android:minWidth="@dimen/notification_importance_toggle_size" + android:minHeight="@dimen/notification_importance_toggle_size" + android:maxWidth="200dp" + android:paddingEnd="@dimen/notification_importance_button_padding" + style="@style/TextAppearance.NotificationInfo.Button"/> + <TextView android:id="@+id/turn_off_notifications" android:text="@string/inline_turn_off_notifications" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentStart="true" + android:layout_toEndOf="@id/inline_dismiss" android:gravity="start|center_vertical" android:minWidth="@dimen/notification_importance_toggle_size" android:minHeight="@dimen/notification_importance_toggle_size" android:maxWidth="200dp" + android:paddingStart="@dimen/notification_importance_button_padding" style="@style/TextAppearance.NotificationInfo.Button"/> <TextView android:id="@+id/done" diff --git a/packages/SystemUI/res/layout/partial_conversation_info.xml b/packages/SystemUI/res/layout/partial_conversation_info.xml index 4850b35833e5..d1755eff6dab 100644 --- a/packages/SystemUI/res/layout/partial_conversation_info.xml +++ b/packages/SystemUI/res/layout/partial_conversation_info.xml @@ -143,15 +143,28 @@ android:paddingEnd="4dp" > <TextView + android:id="@+id/inline_dismiss" + android:text="@string/notification_inline_dismiss" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:gravity="start|center_vertical" + android:minWidth="@dimen/notification_importance_toggle_size" + android:minHeight="@dimen/notification_importance_toggle_size" + android:maxWidth="200dp" + android:paddingEnd="@dimen/notification_importance_button_padding" + style="@style/TextAppearance.NotificationInfo.Button"/> + <TextView android:id="@+id/turn_off_notifications" android:text="@string/inline_turn_off_notifications" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentStart="true" + android:layout_toEndOf="@id/inline_dismiss" android:gravity="start|center_vertical" android:minWidth="@dimen/notification_importance_toggle_size" android:minHeight="@dimen/notification_importance_toggle_size" android:maxWidth="200dp" + android:paddingStart="@dimen/notification_importance_button_padding" style="@style/TextAppearance.NotificationInfo.Button"/> <TextView android:id="@+id/done" diff --git a/packages/SystemUI/res/layout/promoted_notification_info.xml b/packages/SystemUI/res/layout/promoted_notification_info.xml index 2e0a0ca1185c..3982a6638666 100644 --- a/packages/SystemUI/res/layout/promoted_notification_info.xml +++ b/packages/SystemUI/res/layout/promoted_notification_info.xml @@ -373,15 +373,28 @@ asked for it --> android:paddingEnd="4dp" > <TextView + android:id="@+id/inline_dismiss" + android:text="@string/notification_inline_dismiss" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:gravity="start|center_vertical" + android:minWidth="@dimen/notification_importance_toggle_size" + android:minHeight="@dimen/notification_importance_toggle_size" + android:maxWidth="200dp" + android:paddingEnd="@dimen/notification_importance_button_padding" + style="@style/TextAppearance.NotificationInfo.Button"/> + <TextView android:id="@+id/turn_off_notifications" android:text="@string/inline_turn_off_notifications" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentStart="true" + android:layout_toEndOf="@id/inline_dismiss" android:gravity="start|center_vertical" android:minWidth="@dimen/notification_importance_toggle_size" android:minHeight="@dimen/notification_importance_toggle_size" android:maxWidth="200dp" + android:paddingStart="@dimen/notification_importance_button_padding" style="@style/TextAppearance.NotificationInfo.Button"/> <TextView android:id="@+id/done" diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index cd94a265aa80..fefc222f283f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2056,7 +2056,7 @@ <string name="inline_ok_button">Apply</string> <!-- Notification inline controls: button to show block screen [CHAR_LIMIT=35] --> - <string name="inline_turn_off_notifications">Turn off notifications</string> + <string name="inline_turn_off_notifications">Turn off</string> <!-- [CHAR LIMIT=100] Notification Importance title --> <string name="notification_silence_title">Silent</string> diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index e3afe2e190e9..c78f75a334fd 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -778,26 +778,18 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { protected boolean swipedFarEnough() { float translation = getTranslation(mTouchedView); - return Math.abs(translation) > SWIPED_FAR_ENOUGH_SIZE_FRACTION * getSize(mTouchedView); + return Math.abs(translation) > SWIPED_FAR_ENOUGH_SIZE_FRACTION * getSize( + mTouchedView); } public boolean isDismissGesture(MotionEvent ev) { float translation = getTranslation(mTouchedView); return ev.getActionMasked() == MotionEvent.ACTION_UP && !mFalsingManager.isUnlockingDisabled() - && !isFalseGesture() && isSwipeDismissible() + && !isFalseGesture() && (swipedFastEnough() || swipedFarEnough()) && mCallback.canChildBeDismissedInDirection(mTouchedView, translation > 0); } - /** Can the swipe gesture on the touched view be considered as a dismiss intention */ - public boolean isSwipeDismissible() { - if (magneticNotificationSwipes()) { - return mCallback.isMagneticViewDetached(mTouchedView) || swipedFastEnough(); - } else { - return swipedFastEnough() || swipedFarEnough(); - } - } - /** Returns true if the gesture should be rejected. */ public boolean isFalseGesture() { boolean falsingDetected = mCallback.isAntiFalsingNeeded(); @@ -978,13 +970,6 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { void onMagneticInteractionEnd(View view, float velocity); /** - * Determine if a view managed by magnetic interactions is magnetically detached - * @param view The magnetic view - * @return if the view is detached according to its magnetic state. - */ - boolean isMagneticViewDetached(View view); - - /** * Called when the child is long pressed and available to start drag and drop. * * @param v the view that was long pressed. diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt index df6c1b18e3e9..8cebe04d4e01 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt @@ -24,11 +24,17 @@ import android.content.Intent import android.net.Uri import android.text.TextUtils import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R +import java.util.function.Consumer import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch @SysUISingleton -class ActionIntentCreator @Inject constructor() : IntentCreator { +class ActionIntentCreator +@Inject +constructor(@Application private val applicationScope: CoroutineScope) : IntentCreator { override fun getTextEditorIntent(context: Context?) = Intent(context, EditTextActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) @@ -65,7 +71,7 @@ class ActionIntentCreator @Inject constructor() : IntentCreator { .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - override fun getImageEditIntent(uri: Uri?, context: Context): Intent { + suspend fun getImageEditIntent(uri: Uri?, context: Context): Intent { val editorPackage = context.getString(R.string.config_screenshotEditor) return Intent(Intent.ACTION_EDIT).apply { if (!TextUtils.isEmpty(editorPackage)) { @@ -78,6 +84,14 @@ class ActionIntentCreator @Inject constructor() : IntentCreator { } } + override fun getImageEditIntentAsync( + uri: Uri?, + context: Context, + outputConsumer: Consumer<Intent>, + ) { + applicationScope.launch { outputConsumer.accept(getImageEditIntent(uri, context)) } + } + override fun getRemoteCopyIntent(clipData: ClipData?, context: Context): Intent { val remoteCopyPackage = context.getString(R.string.config_remoteCopyPackage) return Intent(REMOTE_COPY_ACTION).apply { diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index 314b6e7f5a28..984d2478eb72 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -558,8 +558,10 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv private void editImage(Uri uri) { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED); - mContext.startActivity(mIntentCreator.getImageEditIntent(uri, mContext)); - animateOut(); + mIntentCreator.getImageEditIntentAsync(uri, mContext, intent -> { + mContext.startActivity(intent); + animateOut(); + }); } private void editText() { @@ -747,8 +749,10 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv mIntentCreator.getTextEditorIntent(mContext)); break; case IMAGE: - finishWithSharedTransition(CLIPBOARD_OVERLAY_EDIT_TAPPED, - mIntentCreator.getImageEditIntent(mClipboardModel.getUri(), mContext)); + mIntentCreator.getImageEditIntentAsync(mClipboardModel.getUri(), mContext, + intent -> { + finishWithSharedTransition(CLIPBOARD_OVERLAY_EDIT_TAPPED, intent); + }); break; default: Log.w(TAG, "Got preview tapped callback for non-editable type " diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/DefaultIntentCreator.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/DefaultIntentCreator.java index 4b24536ad28f..e9a9cbf106fc 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/DefaultIntentCreator.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/DefaultIntentCreator.java @@ -27,6 +27,8 @@ import android.text.TextUtils; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.res.R; +import java.util.function.Consumer; + import javax.inject.Inject; @SysUISingleton @@ -73,7 +75,7 @@ public class DefaultIntentCreator implements IntentCreator { return chooserIntent; } - public Intent getImageEditIntent(Uri uri, Context context) { + public void getImageEditIntentAsync(Uri uri, Context context, Consumer<Intent> outputConsumer) { String editorPackage = context.getString(R.string.config_screenshotEditor); Intent editIntent = new Intent(Intent.ACTION_EDIT); if (!TextUtils.isEmpty(editorPackage)) { @@ -83,7 +85,7 @@ public class DefaultIntentCreator implements IntentCreator { editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); editIntent.putExtra(EXTRA_EDIT_SOURCE, EDIT_SOURCE_CLIPBOARD); - return editIntent; + outputConsumer.accept(editIntent); } public Intent getRemoteCopyIntent(ClipData clipData, Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java index c8a6b05f090b..283596f9f309 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java @@ -21,9 +21,11 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; +import java.util.function.Consumer; + public interface IntentCreator { Intent getTextEditorIntent(Context context); Intent getShareIntent(ClipData clipData, Context context); - Intent getImageEditIntent(Uri uri, Context context); + void getImageEditIntentAsync(Uri uri, Context context, Consumer<Intent> outputConsumer); Intent getRemoteCopyIntent(ClipData clipData, Context context); } diff --git a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt index 9b181be93b61..f3316958f01d 100644 --- a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt +++ b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt @@ -16,8 +16,13 @@ package com.android.systemui.display +import android.hardware.display.DisplayManager +import android.os.Handler +import com.android.app.displaylib.DisplayLibComponent +import com.android.app.displaylib.createDisplayLibComponent import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DeviceStateRepository import com.android.systemui.display.data.repository.DeviceStateRepositoryImpl import com.android.systemui.display.data.repository.DisplayRepository @@ -28,6 +33,8 @@ import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepos import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepositoryImpl import com.android.systemui.display.data.repository.FocusedDisplayRepository import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl +import com.android.systemui.display.data.repository.PerDisplayRepoDumpHelper +import com.android.systemui.display.data.repository.PerDisplayRepository import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl import com.android.systemui.display.domain.interactor.DisplayWindowPropertiesInteractorModule @@ -40,9 +47,11 @@ import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope /** Module binding display related classes. */ -@Module(includes = [DisplayWindowPropertiesInteractorModule::class]) +@Module(includes = [DisplayWindowPropertiesInteractorModule::class, DisplayLibModule::class]) interface DisplayModule { @Binds fun bindConnectedDisplayInteractor( @@ -73,6 +82,9 @@ interface DisplayModule { impl: DisplayWindowPropertiesRepositoryImpl ): DisplayWindowPropertiesRepository + @Binds + fun dumpRegistrationLambda(helper: PerDisplayRepoDumpHelper): PerDisplayRepository.InitCallback + companion object { @Provides @SysUISingleton @@ -103,3 +115,31 @@ interface DisplayModule { } } } + +/** Module to bind the DisplayRepository from displaylib to the systemui dagger graph. */ +@Module +object DisplayLibModule { + @Provides + @SysUISingleton + fun displayLibComponent( + displayManager: DisplayManager, + @Background backgroundHandler: Handler, + @Background bgApplicationScope: CoroutineScope, + @Background backgroundCoroutineDispatcher: CoroutineDispatcher, + ): DisplayLibComponent { + return createDisplayLibComponent( + displayManager, + backgroundHandler, + bgApplicationScope, + backgroundCoroutineDispatcher, + ) + } + + @Provides + @SysUISingleton + fun providesDisplayRepositoryFromLib( + displayLibComponent: DisplayLibComponent + ): com.android.app.displaylib.DisplayRepository { + return displayLibComponent.displayRepository + } +} diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt index 101e8cc23f17..051fe7e5517c 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt @@ -17,106 +17,30 @@ package com.android.systemui.display.data.repository import android.annotation.SuppressLint -import android.hardware.display.DisplayManager -import android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED -import android.hardware.display.DisplayManager.DisplayListener -import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_ADDED -import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_CHANGED -import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_REMOVED -import android.os.Handler -import android.util.Log -import android.view.Display import android.view.IWindowManager import com.android.app.displaylib.DisplayRepository as DisplayRepositoryFromLib -import com.android.app.tracing.FlowTracing.traceEach -import com.android.app.tracing.traceSection import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.display.data.DisplayEvent import com.android.systemui.statusbar.CommandQueue -import com.android.systemui.util.Compile -import com.android.systemui.util.kotlin.pairwiseBy -import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.stateIn /** Repository for providing access to display related information and events. */ interface DisplayRepository : DisplayRepositoryFromLib { - /** Display change event indicating a change to the given displayId has occurred. */ - val displayChangeEvent: Flow<Int> - - /** Display addition event indicating a new display has been added. */ - val displayAdditionEvent: Flow<Display?> - - /** Display removal event indicating a display has been removed. */ - val displayRemovalEvent: Flow<Int> /** A [StateFlow] that maintains a set of display IDs that should have system decorations. */ val displayIdsWithSystemDecorations: StateFlow<Set<Int>> - - /** - * Provides the current set of display ids. - * - * Note that it is preferred to use this instead of [displays] if only the - * [Display.getDisplayId] is needed. - */ - val displayIds: StateFlow<Set<Int>> - - /** - * Pending display id that can be enabled/disabled. - * - * When `null`, it means there is no pending display waiting to be enabled. - */ - val pendingDisplay: Flow<PendingDisplay?> - - /** Whether the default display is currently off. */ - val defaultDisplayOff: Flow<Boolean> - - /** - * Given a display ID int, return the corresponding Display object, or null if none exist. - * - * This method is guaranteed to not result in any binder call. - */ - fun getDisplay(displayId: Int): Display? = - displays.value.firstOrNull { it.displayId == displayId } - - /** Represents a connected display that has not been enabled yet. */ - interface PendingDisplay { - /** Id of the pending display. */ - val id: Int - - /** Enables the display, making it available to the system. */ - suspend fun enable() - - /** - * Ignores the pending display. When called, this specific display id doesn't appear as - * pending anymore until the display is disconnected and reconnected again. - */ - suspend fun ignore() - - /** Disables the display, making it unavailable to the system. */ - suspend fun disable() - } } @SysUISingleton @@ -124,310 +48,11 @@ interface DisplayRepository : DisplayRepositoryFromLib { class DisplayRepositoryImpl @Inject constructor( - private val displayManager: DisplayManager, private val commandQueue: CommandQueue, private val windowManager: IWindowManager, - @Background backgroundHandler: Handler, @Background bgApplicationScope: CoroutineScope, - @Background backgroundCoroutineDispatcher: CoroutineDispatcher, -) : DisplayRepository { - private val allDisplayEvents: Flow<DisplayEvent> = - conflatedCallbackFlow { - val callback = - object : DisplayListener { - override fun onDisplayAdded(displayId: Int) { - trySend(DisplayEvent.Added(displayId)) - } - - override fun onDisplayRemoved(displayId: Int) { - trySend(DisplayEvent.Removed(displayId)) - } - - override fun onDisplayChanged(displayId: Int) { - trySend(DisplayEvent.Changed(displayId)) - } - } - displayManager.registerDisplayListener( - callback, - backgroundHandler, - EVENT_TYPE_DISPLAY_ADDED or - EVENT_TYPE_DISPLAY_CHANGED or - EVENT_TYPE_DISPLAY_REMOVED, - ) - awaitClose { displayManager.unregisterDisplayListener(callback) } - } - .onStart { emit(DisplayEvent.Changed(Display.DEFAULT_DISPLAY)) } - .debugLog("allDisplayEvents") - .flowOn(backgroundCoroutineDispatcher) - - override val displayChangeEvent: Flow<Int> = - allDisplayEvents.filterIsInstance<DisplayEvent.Changed>().map { event -> event.displayId } - - override val displayRemovalEvent: Flow<Int> = - allDisplayEvents.filterIsInstance<DisplayEvent.Removed>().map { it.displayId } - - // This is necessary because there might be multiple displays, and we could - // have missed events for those added before this process or flow started. - // Note it causes a binder call from the main thread (it's traced). - private val initialDisplays: Set<Display> = - traceSection("$TAG#initialDisplays") { displayManager.displays?.toSet() ?: emptySet() } - private val initialDisplayIds = initialDisplays.map { display -> display.displayId }.toSet() - - /** Propagate to the listeners only enabled displays */ - private val enabledDisplayIds: StateFlow<Set<Int>> = - allDisplayEvents - .scan(initial = initialDisplayIds) { previousIds: Set<Int>, event: DisplayEvent -> - val id = event.displayId - when (event) { - is DisplayEvent.Removed -> previousIds - id - is DisplayEvent.Added, - is DisplayEvent.Changed -> previousIds + id - } - } - .distinctUntilChanged() - .debugLog("enabledDisplayIds") - .stateIn(bgApplicationScope, SharingStarted.WhileSubscribed(), initialDisplayIds) - - private val defaultDisplay by lazy { - getDisplayFromDisplayManager(Display.DEFAULT_DISPLAY) - ?: error("Unable to get default display.") - } - - /** - * Represents displays that went though the [DisplayListener.onDisplayAdded] callback. - * - * Those are commonly the ones provided by [DisplayManager.getDisplays] by default. - */ - private val enabledDisplays: StateFlow<Set<Display>> = - enabledDisplayIds - .mapElementsLazily { displayId -> getDisplayFromDisplayManager(displayId) } - .onEach { - if (it.isEmpty()) Log.wtf(TAG, "No enabled displays. This should never happen.") - } - .flowOn(backgroundCoroutineDispatcher) - .debugLog("enabledDisplays") - .stateIn( - bgApplicationScope, - started = SharingStarted.WhileSubscribed(), - // This triggers a single binder call on the UI thread per process. The - // alternative would be to use sharedFlows, but they are prohibited due to - // performance concerns. - // Ultimately, this is a trade-off between a one-time UI thread binder call and - // the constant overhead of sharedFlows. - initialValue = initialDisplays, - ) - - /** - * Represents displays that went though the [DisplayListener.onDisplayAdded] callback. - * - * Those are commonly the ones provided by [DisplayManager.getDisplays] by default. - */ - override val displays: StateFlow<Set<Display>> = enabledDisplays - - override val displayIds: StateFlow<Set<Int>> = enabledDisplayIds - - /** - * Implementation that maps from [displays], instead of [allDisplayEvents] for 2 reasons: - * 1. Guarantee that it emits __after__ [displays] emitted. This way it is guaranteed that - * calling [getDisplay] for the newly added display will be non-null. - * 2. Reuse the existing instance of [Display] without a new call to [DisplayManager]. - */ - override val displayAdditionEvent: Flow<Display?> = - displays - .pairwiseBy { previousDisplays, currentDisplays -> currentDisplays - previousDisplays } - .flatMapLatest { it.asFlow() } - - val _ignoredDisplayIds = MutableStateFlow<Set<Int>>(emptySet()) - private val ignoredDisplayIds: Flow<Set<Int>> = _ignoredDisplayIds.debugLog("ignoredDisplayIds") - - private fun getInitialConnectedDisplays(): Set<Int> = - traceSection("$TAG#getInitialConnectedDisplays") { - displayManager - .getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) - .map { it.displayId } - .toSet() - .also { - if (DEBUG) { - Log.d(TAG, "getInitialConnectedDisplays: $it") - } - } - } - - /* keeps connected displays until they are disconnected. */ - private val connectedDisplayIds: StateFlow<Set<Int>> = - conflatedCallbackFlow { - val connectedIds = getInitialConnectedDisplays().toMutableSet() - val callback = - object : DisplayConnectionListener { - override fun onDisplayConnected(id: Int) { - if (DEBUG) { - Log.d(TAG, "display with id=$id connected.") - } - connectedIds += id - _ignoredDisplayIds.value -= id - trySend(connectedIds.toSet()) - } - - override fun onDisplayDisconnected(id: Int) { - connectedIds -= id - if (DEBUG) { - Log.d(TAG, "display with id=$id disconnected.") - } - _ignoredDisplayIds.value -= id - trySend(connectedIds.toSet()) - } - } - trySend(connectedIds.toSet()) - displayManager.registerDisplayListener( - callback, - backgroundHandler, - /* eventFlags */ 0, - DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED, - ) - awaitClose { displayManager.unregisterDisplayListener(callback) } - } - .distinctUntilChanged() - .debugLog("connectedDisplayIds") - .stateIn( - bgApplicationScope, - started = SharingStarted.WhileSubscribed(), - // The initial value is set to empty, but connected displays are gathered as soon as - // the flow starts being collected. This is to ensure the call to get displays (an - // IPC) happens in the background instead of when this object - // is instantiated. - initialValue = emptySet(), - ) - - private val connectedExternalDisplayIds: Flow<Set<Int>> = - connectedDisplayIds - .map { connectedDisplayIds -> - traceSection("$TAG#filteringExternalDisplays") { - connectedDisplayIds - .filter { id -> getDisplayType(id) == Display.TYPE_EXTERNAL } - .toSet() - } - } - .flowOn(backgroundCoroutineDispatcher) - .debugLog("connectedExternalDisplayIds") - - private fun getDisplayType(displayId: Int): Int? = - traceSection("$TAG#getDisplayType") { displayManager.getDisplay(displayId)?.type } - - private fun getDisplayFromDisplayManager(displayId: Int): Display? = - traceSection("$TAG#getDisplay") { displayManager.getDisplay(displayId) } - - /** - * Pending displays are the ones connected, but not enabled and not ignored. - * - * A connected display is ignored after the user makes the decision to use it or not. For now, - * the initial decision from the user is final and not reversible. - */ - private val pendingDisplayIds: Flow<Set<Int>> = - combine(enabledDisplayIds, connectedExternalDisplayIds, ignoredDisplayIds) { - enabledDisplaysIds, - connectedExternalDisplayIds, - ignoredDisplayIds -> - if (DEBUG) { - Log.d( - TAG, - "combining enabled=$enabledDisplaysIds, " + - "connectedExternalDisplayIds=$connectedExternalDisplayIds, " + - "ignored=$ignoredDisplayIds", - ) - } - connectedExternalDisplayIds - enabledDisplaysIds - ignoredDisplayIds - } - .debugLog("allPendingDisplayIds") - - /** Which display id should be enabled among the pending ones. */ - private val pendingDisplayId: Flow<Int?> = - pendingDisplayIds.map { it.maxOrNull() }.distinctUntilChanged().debugLog("pendingDisplayId") - - override val pendingDisplay: Flow<DisplayRepository.PendingDisplay?> = - pendingDisplayId - .map { displayId -> - val id = displayId ?: return@map null - object : DisplayRepository.PendingDisplay { - override val id = id - - override suspend fun enable() { - traceSection("DisplayRepository#enable($id)") { - if (DEBUG) { - Log.d(TAG, "Enabling display with id=$id") - } - displayManager.enableConnectedDisplay(id) - } - // After the display has been enabled, it is automatically ignored. - ignore() - } - - override suspend fun ignore() { - traceSection("DisplayRepository#ignore($id)") { - _ignoredDisplayIds.value += id - } - } - - override suspend fun disable() { - ignore() - traceSection("DisplayRepository#disable($id)") { - if (DEBUG) { - Log.d(TAG, "Disabling display with id=$id") - } - displayManager.disableConnectedDisplay(id) - } - } - } - } - .debugLog("pendingDisplay") - - override val defaultDisplayOff: Flow<Boolean> = - displayChangeEvent - .filter { it == Display.DEFAULT_DISPLAY } - .map { defaultDisplay.state == Display.STATE_OFF } - .distinctUntilChanged() - - private fun <T> Flow<T>.debugLog(flowName: String): Flow<T> { - return if (DEBUG) { - traceEach(flowName, logcat = true, traceEmissionCount = true) - } else { - this - } - } - - /** - * Maps a set of T to a set of V, minimizing the number of `createValue` calls taking into - * account the diff between each root flow emission. - * - * This is needed to minimize the number of [getDisplayFromDisplayManager] in this class. Note - * that if the [createValue] returns a null element, it will not be added in the output set. - */ - private fun <T, V> Flow<Set<T>>.mapElementsLazily(createValue: (T) -> V?): Flow<Set<V>> { - data class State<T, V>( - val previousSet: Set<T>, - // Caches T values from the previousSet that were already converted to V - val valueMap: Map<T, V>, - val resultSet: Set<V>, - ) - - val emptyInitialState = State(emptySet<T>(), emptyMap(), emptySet<V>()) - return this.scan(emptyInitialState) { state, currentSet -> - if (currentSet == state.previousSet) { - state - } else { - val removed = state.previousSet - currentSet - val added = currentSet - state.previousSet - val newMap = state.valueMap.toMutableMap() - - added.forEach { key -> createValue(key)?.let { newMap[key] = it } } - removed.forEach { key -> newMap.remove(key) } - - val resultSet = newMap.values.toSet() - State(currentSet, newMap, resultSet) - } - } - .filter { it != emptyInitialState } - .map { it.resultSet } - } + private val displayRepositoryFromLib: com.android.app.displaylib.DisplayRepository, +) : DisplayRepositoryFromLib by displayRepositoryFromLib, DisplayRepository { private val decorationEvents: Flow<Event> = callbackFlow { val callback = @@ -481,20 +106,5 @@ constructor( private companion object { const val TAG = "DisplayRepository" - val DEBUG = Log.isLoggable(TAG, Log.DEBUG) || Compile.IS_DEBUG } } - -/** Used to provide default implementations for all methods. */ -private interface DisplayConnectionListener : DisplayListener { - - override fun onDisplayConnected(id: Int) {} - - override fun onDisplayDisconnected(id: Int) {} - - override fun onDisplayAdded(id: Int) {} - - override fun onDisplayRemoved(id: Int) {} - - override fun onDisplayChanged(id: Int) {} -} diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepoDumpHelper.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepoDumpHelper.kt new file mode 100644 index 000000000000..212d55612935 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepoDumpHelper.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 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.display.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.dump.DumpableFromToString +import javax.inject.Inject + +/** Helper class to register PerDisplayRepository in the dump manager in SystemUI. */ +@SysUISingleton +class PerDisplayRepoDumpHelper @Inject constructor(private val dumpManager: DumpManager) : + PerDisplayRepository.InitCallback { + /** + * Registers PerDisplayRepository in the dump manager. + * + * The repository will be identified by the given debug name. + */ + override fun onInit(debugName: String, instance: Any) { + dumpManager.registerNormalDumpable( + "PerDisplayRepository-$debugName", + DumpableFromToString(instance), + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt index d1d013542fbf..7e00c60dc43a 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt @@ -20,13 +20,10 @@ import android.util.Log import android.view.Display import com.android.app.tracing.coroutines.launchTraced as launch import com.android.app.tracing.traceSection -import com.android.systemui.Dumpable import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dump.DumpManager import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import java.io.PrintWriter import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest @@ -88,6 +85,20 @@ interface PerDisplayRepository<T> { /** Debug name for this repository, mainly for tracing and logging. */ val debugName: String + + /** + * Callback to run when a given repository is initialized. + * + * This allows the caller to perform custom logic when the repository is ready to be used, e.g. + * register to dumpManager. + * + * Note that the instance is *leaked* outside of this class, so it should only be done when + * repository is meant to live as long as the caller. In systemUI this is ok because the + * repository lives as long as the process itself. + */ + interface InitCallback { + fun onInit(debugName: String, instance: Any) + } } /** @@ -110,8 +121,8 @@ constructor( @Assisted private val instanceProvider: PerDisplayInstanceProvider<T>, @Background private val backgroundApplicationScope: CoroutineScope, private val displayRepository: DisplayRepository, - private val dumpManager: DumpManager, -) : PerDisplayRepository<T>, Dumpable { + private val initCallback: PerDisplayRepository.InitCallback, +) : PerDisplayRepository<T> { private val perDisplayInstances = ConcurrentHashMap<Int, T?>() @@ -120,7 +131,7 @@ constructor( } private suspend fun start() { - dumpManager.registerNormalDumpable("PerDisplayRepository-${debugName}", this) + initCallback.onInit(debugName, this) displayRepository.displayIds.collectLatest { displayIds -> val toRemove = perDisplayInstances.keys - displayIds toRemove.forEach { displayId -> @@ -169,8 +180,9 @@ constructor( private const val TAG = "PerDisplayInstanceRepo" } - override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.println(perDisplayInstances) + override fun toString(): String { + return "PerDisplayInstanceRepositoryImpl(" + + "debugName='$debugName', instances=$perDisplayInstances)" } } @@ -193,6 +205,7 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>( private val lazyDefaultDisplayInstance by lazy { instanceProvider.createInstance(Display.DEFAULT_DISPLAY) } + override fun get(displayId: Int): T? = lazyDefaultDisplayInstance } diff --git a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt index 15a3cbdb8072..84f103e8f730 100644 --- a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt @@ -18,6 +18,7 @@ package com.android.systemui.display.domain.interactor import android.companion.virtual.VirtualDeviceManager import android.view.Display +import com.android.app.displaylib.DisplayRepository as DisplayRepositoryFromLib import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DeviceStateRepository @@ -138,7 +139,8 @@ constructor( .distinctUntilChanged() .flowOn(backgroundCoroutineDispatcher) - private fun DisplayRepository.PendingDisplay.toInteractorPendingDisplay(): PendingDisplay = + private fun DisplayRepositoryFromLib.PendingDisplay.toInteractorPendingDisplay(): + PendingDisplay = object : PendingDisplay { override suspend fun enable() = this@toInteractorPendingDisplay.enable() diff --git a/packages/SystemUI/src/com/android/systemui/display/data/DisplayEvent.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpableFromToString.kt index 626a68f6a59d..438931a1962d 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/DisplayEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpableFromToString.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2025 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. @@ -14,11 +14,14 @@ * limitations under the License. */ -package com.android.systemui.display.data +package com.android.systemui.dump -sealed interface DisplayEvent { - val displayId: Int - data class Added(override val displayId: Int) : DisplayEvent - data class Removed(override val displayId: Int) : DisplayEvent - data class Changed(override val displayId: Int) : DisplayEvent +import com.android.systemui.Dumpable +import java.io.PrintWriter + +/** Dumpable implementation that just calls toString() on the instance. */ +class DumpableFromToString<T>(private val instance: T) : Dumpable { + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("$instance") + } } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index cf5c3402792e..f2a10cc43fd9 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -147,14 +147,14 @@ import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.settings.SecureSettings; +import dagger.Lazy; + import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; -import dagger.Lazy; - /** * Helper to show the global actions dialog. Each item is an {@link Action} that may show depending * on whether the keyguard is showing, and whether the device is provisioned. @@ -270,6 +270,16 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private final UserLogoutInteractor mLogoutInteractor; private final GlobalActionsInteractor mInteractor; private final Lazy<DisplayWindowPropertiesRepository> mDisplayWindowPropertiesRepositoryLazy; + private final Handler mHandler; + + private final UserTracker.Callback mOnUserSwitched = new UserTracker.Callback() { + @Override + public void onBeforeUserSwitching(int newUser) { + // Dismiss the dialog as soon as we start switching. This will schedule a message + // in a handler so it will be pretty quick. + dismissDialog(); + } + }; @VisibleForTesting public enum GlobalActionsEvent implements UiEventLogger.UiEventEnum { @@ -425,6 +435,29 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mInteractor = interactor; mDisplayWindowPropertiesRepositoryLazy = displayWindowPropertiesRepository; + mHandler = new Handler(mMainHandler.getLooper()) { + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_DISMISS: + if (mDialog != null) { + if (SYSTEM_DIALOG_REASON_DREAM.equals(msg.obj)) { + // Hide instantly. + mDialog.hide(); + mDialog.dismiss(); + } else { + mDialog.dismiss(); + } + mDialog = null; + } + break; + case MESSAGE_REFRESH: + refreshSilentMode(); + mAdapter.notifyDataSetChanged(); + break; + } + } + }; + // receive broadcasts IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); @@ -537,6 +570,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene expandable != null ? expandable.dialogTransitionController( new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)) : null; + mUserTracker.addCallback(mOnUserSwitched, mBackgroundExecutor); if (controller != null) { mDialogTransitionAnimator.show(mDialog, controller); } else { @@ -1404,6 +1438,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mWindowManagerFuncs.onGlobalActionsHidden(); mLifecycle.setCurrentState(Lifecycle.State.CREATED); mInteractor.onDismissed(); + mUserTracker.removeCallback(mOnUserSwitched); } /** @@ -2228,29 +2263,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mDialogPressDelay = 0; // ms } - private Handler mHandler = new Handler() { - public void handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_DISMISS: - if (mDialog != null) { - if (SYSTEM_DIALOG_REASON_DREAM.equals(msg.obj)) { - // Hide instantly. - mDialog.hide(); - mDialog.dismiss(); - } else { - mDialog.dismiss(); - } - mDialog = null; - } - break; - case MESSAGE_REFRESH: - refreshSilentMode(); - mAdapter.notifyDataSetChanged(); - break; - } - } - }; - private void onAirplaneModeChanged() { // Let the service state callbacks handle the state. if (mHasTelephony || mAirplaneModeOn == null) return; diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt index cedf661d8ee3..5b65531cdd55 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt @@ -127,7 +127,6 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : } holder.seekBar.setMax(data.duration) - val totalTimeDescription = data.durationDescription if (data.scrubbing) { holder.scrubbingTotalTimeView.text = formatTimeLabel(data.duration) } @@ -147,17 +146,9 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : } } - val elapsedTimeDescription = data.elapsedTimeDescription if (data.scrubbing) { holder.scrubbingElapsedTimeView.text = formatTimeLabel(it) } - - holder.seekBar.contentDescription = - holder.seekBar.context.getString( - R.string.controls_media_seekbar_description, - elapsedTimeDescription, - totalTimeDescription, - ) } } @@ -166,6 +157,18 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : return DateUtils.formatElapsedTime(milliseconds / DateUtils.SECOND_IN_MILLIS) } + fun updateContentDescription( + elapsedTimeDescription: CharSequence, + durationDescription: CharSequence, + ) { + holder.seekBar.contentDescription = + holder.seekBar.context.getString( + R.string.controls_media_seekbar_description, + elapsedTimeDescription, + durationDescription, + ) + } + @VisibleForTesting open fun buildResetAnimator(targetTime: Int): Animator { val animator = diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index 006eb203a669..f69985ee5364 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -224,6 +224,8 @@ public class MediaControlPanel { this::setIsScrubbing; private final SeekBarViewModel.EnabledChangeListener mEnabledChangeListener = this::setIsSeekBarEnabled; + private final SeekBarViewModel.ContentDescriptionListener mContentDescriptionListener = + this::setSeekbarContentDescription; private final BroadcastDialogController mBroadcastDialogController; private boolean mIsCurrentBroadcastedApp = false; @@ -327,6 +329,7 @@ public class MediaControlPanel { } mSeekBarViewModel.removeScrubbingChangeListener(mScrubbingChangeListener); mSeekBarViewModel.removeEnabledChangeListener(mEnabledChangeListener); + mSeekBarViewModel.removeContentDescriptionListener(mContentDescriptionListener); mSeekBarViewModel.onDestroy(); mMediaViewController.onDestroy(); } @@ -395,6 +398,10 @@ public class MediaControlPanel { }); } + private void setSeekbarContentDescription(CharSequence elapsedTime, CharSequence duration) { + mSeekBarObserver.updateContentDescription(elapsedTime, duration); + } + /** * Reloads animator duration scale. */ @@ -424,6 +431,7 @@ public class MediaControlPanel { mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar()); mSeekBarViewModel.setScrubbingChangeListener(mScrubbingChangeListener); mSeekBarViewModel.setEnabledChangeListener(mEnabledChangeListener); + mSeekBarViewModel.setContentDescriptionListener(mContentDescriptionListener); mMediaViewController.attach(player); vh.getPlayer().setOnLongClickListener(v -> { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index dba190022c8b..e87d5de56177 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -229,6 +229,20 @@ constructor( } } + private val seekbarDescriptionListener = + object : SeekBarViewModel.ContentDescriptionListener { + override fun onContentDescriptionChanged( + elapsedTimeDescription: CharSequence, + durationDescription: CharSequence, + ) { + if (!SceneContainerFlag.isEnabled) return + seekBarObserver.updateContentDescription( + elapsedTimeDescription, + durationDescription, + ) + } + } + /** * Sets the listening state of the player. * @@ -350,6 +364,7 @@ constructor( } seekBarViewModel.removeScrubbingChangeListener(scrubbingChangeListener) seekBarViewModel.removeEnabledChangeListener(enabledChangeListener) + seekBarViewModel.removeContentDescriptionListener(seekbarDescriptionListener) seekBarViewModel.onDestroy() } mediaHostStatesManager.removeController(this) @@ -653,6 +668,7 @@ constructor( seekBarViewModel.attachTouchHandlers(mediaViewHolder.seekBar) seekBarViewModel.setScrubbingChangeListener(scrubbingChangeListener) seekBarViewModel.setEnabledChangeListener(enabledChangeListener) + seekBarViewModel.setContentDescriptionListener(seekbarDescriptionListener) val mediaCard = mediaViewHolder.player attach(mediaViewHolder.player) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt index 8744c5c9a838..78a8cf8e9432 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt @@ -104,19 +104,20 @@ constructor( ) set(value) { val enabledChanged = value.enabled != field.enabled + field = value if (enabledChanged) { enabledChangeListener?.onEnabledChanged(value.enabled) } + _progress.postValue(value) + bgExecutor.execute { val durationDescription = formatTimeContentDescription(value.duration) val elapsedDescription = value.elapsedTime?.let { formatTimeContentDescription(it) } ?: "" - field = - value.copy( - durationDescription = durationDescription, - elapsedTimeDescription = elapsedDescription, - ) - _progress.postValue(field) + contentDescriptionListener?.onContentDescriptionChanged( + elapsedDescription, + durationDescription, + ) } } @@ -175,6 +176,7 @@ constructor( private var scrubbingChangeListener: ScrubbingChangeListener? = null private var enabledChangeListener: EnabledChangeListener? = null + private var contentDescriptionListener: ContentDescriptionListener? = null /** Set to true when the user is touching the seek bar to change the position. */ private var scrubbing = false @@ -394,6 +396,16 @@ constructor( } } + fun setContentDescriptionListener(listener: ContentDescriptionListener) { + contentDescriptionListener = listener + } + + fun removeContentDescriptionListener(listener: ContentDescriptionListener) { + if (listener == contentDescriptionListener) { + contentDescriptionListener = null + } + } + /** returns a pair of whether seekbar is enabled and the duration of media. */ private fun getEnabledStateAndDuration(metadata: MediaMetadata?): Pair<Boolean, Int> { val duration = metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() ?: 0 @@ -468,6 +480,13 @@ constructor( fun onEnabledChanged(enabled: Boolean) } + interface ContentDescriptionListener { + fun onContentDescriptionChanged( + elapsedTimeDescription: CharSequence, + durationDescription: CharSequence, + ) + } + private class SeekBarChangeListener( val viewModel: SeekBarViewModel, val falsingManager: FalsingManager, @@ -639,7 +658,5 @@ constructor( val duration: Int, /** whether seekBar is listening to progress updates */ val listening: Boolean, - val elapsedTimeDescription: CharSequence = "", - val durationDescription: CharSequence = "", ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt index 22971a9eb703..a7ebb2289814 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt @@ -88,7 +88,11 @@ constructor( LaunchedEffect(listening, pagerState) { snapshotFlow { listening() } .collect { - if (!listening()) { + // Whenever we go from not listening to listening, we should be in the first + // page. If we did this when going from listening to not listening, opening + // edit mode in second page will cause it to go to first page during the + // transition. + if (listening()) { pagerState.scrollToPage(0) } } 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 3c00d5069bc7..6bafd432669a 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 @@ -76,6 +76,7 @@ import androidx.compose.ui.util.trace import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.Expandable import com.android.compose.animation.bounceable +import com.android.compose.animation.rememberExpandableController import com.android.compose.modifiers.thenIf import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.Flags @@ -265,8 +266,7 @@ private fun TileExpandable( content: @Composable (Expandable) -> Unit, ) { Expandable( - color = color(), - shape = shape, + controller = rememberExpandableController(color = color, shape = shape), modifier = modifier.clip(shape).verticalSquish(squishiness), useModifierBasedImplementation = true, ) { diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt index bc15bbb5e57d..263ef09ea767 100644 --- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt @@ -20,6 +20,8 @@ import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.hardware.devicestate.feature.flags.Flags import androidx.annotation.VisibleForTesting +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -28,8 +30,11 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch /** * Provides a {@link com.android.systemui.statusbar.phone.SystemUIDialog} to be shown on the inner @@ -46,6 +51,7 @@ internal constructor( private val rearDisplayStateInteractor: RearDisplayStateInteractor, private val rearDisplayInnerDialogDelegateFactory: RearDisplayInnerDialogDelegate.Factory, @Application private val scope: CoroutineScope, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, ) : CoreStartable, AutoCloseable { companion object { @@ -53,6 +59,16 @@ internal constructor( } @VisibleForTesting var stateChangeListener: Job? = null + private val keyguardVisible = MutableStateFlow(false) + private val keyguardVisibleFlow = keyguardVisible.asStateFlow() + + @VisibleForTesting + val keyguardCallback = + object : KeyguardUpdateMonitorCallback() { + override fun onKeyguardVisibilityChanged(visible: Boolean) { + keyguardVisible.value = visible + } + } override fun close() { stateChangeListener?.cancel() @@ -62,28 +78,39 @@ internal constructor( if (Flags.deviceStateRdmV2()) { var dialog: SystemUIDialog? = null + keyguardUpdateMonitor.registerCallback(keyguardCallback) + stateChangeListener = - rearDisplayStateInteractor.state - .map { - when (it) { - is RearDisplayStateInteractor.State.Enabled -> { - val rearDisplayContext = - context.createDisplayContext(it.innerDisplay) - val delegate = - rearDisplayInnerDialogDelegateFactory.create( - rearDisplayContext, - deviceStateManager::cancelStateRequest, - ) - dialog = delegate.createDialog().apply { show() } - } + scope.launch { + combine(rearDisplayStateInteractor.state, keyguardVisibleFlow) { + rearDisplayState, + keyguardVisible -> + Pair(rearDisplayState, keyguardVisible) + } + .collectLatest { (rearDisplayState, keyguardVisible) -> + when (rearDisplayState) { + is RearDisplayStateInteractor.State.Enabled -> { + if (!keyguardVisible) { + val rearDisplayContext = + context.createDisplayContext( + rearDisplayState.innerDisplay + ) + val delegate = + rearDisplayInnerDialogDelegateFactory.create( + rearDisplayContext, + deviceStateManager::cancelStateRequest, + ) + dialog = delegate.createDialog().apply { show() } + } + } - is RearDisplayStateInteractor.State.Disabled -> { - dialog?.dismiss() - dialog = null + is RearDisplayStateInteractor.State.Disabled -> { + dialog?.dismiss() + dialog = null + } } } - } - .launchIn(scope) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS index 6cebcd98a0ba..6d3c12d139db 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS @@ -18,10 +18,10 @@ per-file *Keyguard* = file:../keyguard/OWNERS per-file *Notification* = file:notification/OWNERS # Files that control blur effects on shade per-file *NotificationShadeDepth* = set noparent -per-file *NotificationShadeDepth* = shanh@google.com, rahulbanerjee@google.com +per-file *NotificationShadeDepth* = shanh@google.com, rahulbanerjee@google.com, tracyzhou@google.com per-file *NotificationShadeDepth* = file:../keyguard/OWNERS per-file *Blur* = set noparent -per-file *Blur* = shanh@google.com, rahulbanerjee@google.com +per-file *Blur* = shanh@google.com, rahulbanerjee@google.com, tracyzhou@google.com # Not setting noparent here, since *Mode* matches many other classes (e.g., *ViewModel*) per-file *Mode* = file:notification/OWNERS per-file *SmartReply* = set noparent diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt index 26c302bf6409..b1a26af336d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt @@ -113,6 +113,10 @@ class BundleEntryAdapter(val entry: BundleEntry) : EntryAdapter { return false } + override fun isPromotedOngoing(): Boolean { + return false + } + override fun isFullScreenCapable(): Boolean { return false } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java index 3118ce56ac69..4299825bd5e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java @@ -132,6 +132,11 @@ public interface EntryAdapter { boolean isAmbient(); + /** + * Returns whether this row represents promoted ongoing notification. + */ + boolean isPromotedOngoing(); + default boolean isFullScreenCapable() { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt index 1168c647c26a..345b6aae9673 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt @@ -137,6 +137,10 @@ class NotificationEntryAdapter( return entry.ranking.isAmbient } + override fun isPromotedOngoing(): Boolean { + return entry.isPromotedOngoing + } + override fun isFullScreenCapable(): Boolean { return entry.sbn.notification.fullScreenIntent != null } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt index 8021d8f58ccc..a552ca554fd5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt @@ -234,21 +234,21 @@ private class ShadeNode(val controller: NodeController) { fun getChildCount(): Int = controller.getChildCount() fun addChildAt(child: ShadeNode, index: Int) { - traceSection("ShadeNode#addChildAt") { + traceSection({ "ShadeNode#${controller::class.simpleName}#addChildAt" }) { controller.addChildAt(child.controller, index) child.controller.onViewAdded() } } fun moveChildTo(child: ShadeNode, index: Int) { - traceSection("ShadeNode#moveChildTo") { + traceSection({ "ShadeNode#${controller::class.simpleName}#moveChildTo" }) { controller.moveChildTo(child.controller, index) child.controller.onViewMoved() } } fun removeChild(child: ShadeNode, isTransfer: Boolean) { - traceSection("ShadeNode#removeChild") { + traceSection({ "ShadeNode#${controller::class.simpleName}#removeChild" }) { controller.removeChild(child.controller, isTransfer) child.controller.onViewRemoved() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt index 2a01a14f56aa..777392df67cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt @@ -19,19 +19,26 @@ package com.android.systemui.statusbar.notification.promoted import android.app.Flags import android.app.Flags.notificationsRedesignTemplates import android.app.Notification +import android.content.Context import android.graphics.PorterDuff import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.View.GONE +import android.view.View.MeasureSpec.AT_MOST +import android.view.View.MeasureSpec.EXACTLY +import android.view.View.MeasureSpec.UNSPECIFIED +import android.view.View.MeasureSpec.makeMeasureSpec import android.view.View.VISIBLE import android.view.ViewGroup.MarginLayoutParams import android.view.ViewStub import android.widget.Chronometer import android.widget.DateTimeView +import android.widget.FrameLayout import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView +import androidx.annotation.DimenRes import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -42,7 +49,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.isVisible @@ -88,22 +97,12 @@ fun AODPromotedNotification( } key(content.identity) { - val sidePaddings = dimensionResource(systemuiR.dimen.notification_side_paddings) - val sidePaddingValues = PaddingValues(horizontal = sidePaddings, vertical = 0.dp) - - val borderStroke = BorderStroke(1.dp, SecondaryText.brush) - - val borderRadius = dimensionResource(systemuiR.dimen.notification_corner_radius) - val borderShape = RoundedCornerShape(borderRadius) - - Box(modifier = modifier.padding(sidePaddingValues)) { - AODPromotedNotificationView( - layoutResource = layoutResource, - content = content, - audiblyAlertedIconVisible = audiblyAlertedIconVisible, - modifier = Modifier.border(borderStroke, borderShape), - ) - } + AODPromotedNotificationView( + layoutResource = layoutResource, + content = content, + audiblyAlertedIconVisible = audiblyAlertedIconVisible, + modifier = modifier, + ) } } @@ -114,27 +113,91 @@ fun AODPromotedNotificationView( audiblyAlertedIconVisible: Boolean, modifier: Modifier = Modifier, ) { - AndroidView( - factory = { context -> - val view = - traceSection("$TAG.inflate") { - LayoutInflater.from(context).inflate(layoutResource, /* root= */ null) - } - - val updater = - traceSection("$TAG.findViews") { AODPromotedNotificationViewUpdater(view) } - - view.setTag(viewUpdaterTagId, updater) - - view - }, - update = { view -> - val updater = view.getTag(viewUpdaterTagId) as AODPromotedNotificationViewUpdater - - traceSection("$TAG.update") { updater.update(content, audiblyAlertedIconVisible) } - }, - modifier = modifier, - ) + val sidePaddings = dimensionResource(systemuiR.dimen.notification_side_paddings) + val sidePaddingValues = PaddingValues(horizontal = sidePaddings, vertical = 0.dp) + + val boxModifier = modifier.padding(sidePaddingValues) + + val borderStroke = BorderStroke(1.dp, SecondaryText.brush) + + val borderRadius = dimensionResource(systemuiR.dimen.notification_corner_radius) + val borderShape = RoundedCornerShape(borderRadius) + + val maxHeight = + with(LocalDensity.current) { + scaledFontHeight(systemuiR.dimen.notification_max_height_for_promoted_ongoing) + .toPx() + } + .toInt() + + val viewModifier = Modifier.border(borderStroke, borderShape) + + Box(modifier = boxModifier) { + AndroidView( + factory = { context -> + val notif = + traceSection("$TAG.inflate") { + LayoutInflater.from(context).inflate(layoutResource, /* root= */ null) + } + val updater = + traceSection("$TAG.findViews") { AODPromotedNotificationViewUpdater(notif) } + + val frame = FrameLayoutWithMaxHeight(maxHeight, context) + frame.addView(notif) + frame.setTag(viewUpdaterTagId, updater) + + frame + }, + update = { frame -> + val updater = frame.getTag(viewUpdaterTagId) as AODPromotedNotificationViewUpdater + + traceSection("$TAG.update") { updater.update(content, audiblyAlertedIconVisible) } + frame.maxHeight = maxHeight + }, + modifier = viewModifier, + ) + } +} + +private class FrameLayoutWithMaxHeight(maxHeight: Int, context: Context) : FrameLayout(context) { + var maxHeight = maxHeight + set(value) { + if (field != value) { + field = value + requestLayout() + } + } + + // This mirrors the logic in NotificationContentView.onMeasure. + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (childCount < 1) { + return + } + + val child = getChildAt(0) + val childLayoutHeight = child.layoutParams.height + val childHeightSpec = + if (childLayoutHeight >= 0) { + makeMeasureSpec(maxHeight.coerceAtMost(childLayoutHeight), EXACTLY) + } else { + makeMeasureSpec(maxHeight, AT_MOST) + } + measureChildWithMargins(child, widthMeasureSpec, 0, childHeightSpec, 0) + val childMeasuredHeight = child.measuredHeight + + val ownHeightMode = MeasureSpec.getMode(heightMeasureSpec) + val ownHeightSize = MeasureSpec.getSize(heightMeasureSpec) + + val ownMeasuredWidth = MeasureSpec.getSize(widthMeasureSpec) + val ownMeasuredHeight = + if (ownHeightMode != UNSPECIFIED) { + childMeasuredHeight.coerceAtMost(ownHeightSize) + } else { + childMeasuredHeight + } + + setMeasuredDimension(ownMeasuredWidth, ownMeasuredHeight) + } } private val PromotedNotificationContentModel.layoutResource: Int? @@ -521,6 +584,11 @@ private enum class AodPromotedNotificationColor(val colorInt: Int) { val brush = SolidColor(androidx.compose.ui.graphics.Color(colorInt)) } +@Composable +private fun scaledFontHeight(@DimenRes dimenId: Int): Dp { + return dimensionResource(dimenId) * LocalDensity.current.fontScale.coerceAtLeast(1f) +} + private val viewUpdaterTagId = systemuiR.id.aod_promoted_notification_view_updater_tag private const val TAG = "AODPromotedNotification" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 689222608abe..e76867373139 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -40,6 +40,7 @@ import android.view.animation.Interpolator; import com.android.app.animation.Interpolators; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.Configuration; +import com.android.systemui.Flags; import com.android.systemui.Gefingerpoken; import com.android.systemui.common.shared.colors.SurfaceEffectColors; import com.android.systemui.res.R; @@ -101,7 +102,8 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private ValueAnimator mBackgroundColorAnimator; private float mAppearAnimationFraction = -1.0f; private float mAppearAnimationTranslation; - private int mNormalColor; + protected int mNormalColor; + protected int mOpaqueColor; private boolean mIsBelowSpeedBump; private long mLastActionUpTime; @@ -130,17 +132,13 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView protected void updateColors() { if (notificationRowTransparency()) { - if (mIsBlurSupported) { - mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext()); - } else { - mNormalColor = mContext.getColor( - com.android.internal.R.color.materialColorSurfaceContainer); - } + mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext()); + mOpaqueColor = mContext.getColor( + com.android.internal.R.color.materialColorSurfaceContainer); } else { mNormalColor = mContext.getColor( com.android.internal.R.color.materialColorSurfaceContainerHigh); } - setBackgroundToNormalColor(); mTintedRippleColor = mContext.getColor( R.color.notification_ripple_tinted_color); mNormalRippleColor = mContext.getColor( @@ -151,12 +149,6 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mOverrideAmount = 0.0f; } - private void setBackgroundToNormalColor() { - if (mBackgroundNormal != null) { - mBackgroundNormal.setNormalColor(mNormalColor); - } - } - /** * Reload background colors from resources and invalidate views. */ @@ -186,7 +178,6 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mBackgroundNormal = findViewById(R.id.backgroundNormal); mFakeShadow = findViewById(R.id.fake_shadow); mShadowHidden = mFakeShadow.getVisibility() != VISIBLE; - setBackgroundToNormalColor(); initBackground(); updateBackgroundTint(); updateOutlineAlpha(); @@ -352,7 +343,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } protected boolean usesTransparentBackground() { - return mIsBlurSupported && notificationRowTransparency(); + return false; } @Override @@ -709,7 +700,11 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView if (withTint && mBgTint != NO_COLOR) { return mBgTint; } else { - return mNormalColor; + if (Flags.notificationRowTransparency()) { + return usesTransparentBackground() ? mNormalColor : mOpaqueColor; + } else { + return mNormalColor; + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 8da2f768bf71..76830646587d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -82,6 +82,7 @@ import androidx.dynamicanimation.animation.SpringAnimation; import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.graphics.ColorUtils; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -979,7 +980,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } else if (isAboveShelf() != wasAboveShelf) { mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); } - updateColors(); + updateBackgroundTint(); } /** @@ -1678,20 +1679,34 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override protected void setBackgroundTintColor(int color) { - super.setBackgroundTintColor(color); - NotificationContentView view = getShowingLayout(); - if (view != null) { - view.setBackgroundTintColor(color); - } - if (notificationRowTransparency() && mBackgroundNormal != null) { + if (notificationRowTransparency()) { + boolean isColorized = false; if (NotificationBundleUi.isEnabled() && mEntryAdapter != null) { - mBackgroundNormal.setBgIsColorized(mEntryAdapter.isColorized()); + isColorized = mEntryAdapter.isColorized(); } else { if (mEntry != null) { - mBackgroundNormal.setBgIsColorized( - mEntry.getSbn().getNotification().isColorized()); + isColorized = mEntry.getSbn().getNotification().isColorized(); } } + boolean isTransparent = usesTransparentBackground(); + if (isColorized) { + // For colorized notifications, use a color that matches the tint color at 90% alpha + // when the row is transparent. + color = ColorUtils.setAlphaComponent( + color, (int) (0xFF * (isTransparent ? 0.9f : 1))); + } else { + // For non-colorized notifications, use the semi-transparent normal color token + // when the row is transparent, and the opaque color token otherwise. + if (!isTransparent && mBgTint == NO_COLOR) { + color = mOpaqueColor; + } + } + } + + super.setBackgroundTintColor(color); + NotificationContentView view = getShowingLayout(); + if (view != null) { + view.setBackgroundTintColor(color); } } @@ -3113,7 +3128,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.setOnKeyguard(onKeyguard); } } - updateColors(); + updateBackgroundTint(); } } @@ -3243,12 +3258,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return false; } - final NotificationEntry entry = mEntry; - if (entry == null) { - return false; + if (NotificationBundleUi.isEnabled()) { + final EntryAdapter entryAdapter = mEntryAdapter; + if (entryAdapter == null) { + return false; + } + return entryAdapter.isPromotedOngoing(); + } else { + final NotificationEntry entry = mEntry; + if (entry == null) { + return false; + } + return entry.isPromotedOngoing(); } - - return entry.isPromotedOngoing(); } private boolean isPromotedNotificationExpanded(boolean allowOnKeyguard) { @@ -4633,6 +4655,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override protected boolean usesTransparentBackground() { - return super.usesTransparentBackground() && !mIsHeadsUp && !mOnKeyguard; + // Row background should be opaque when it's displayed as a heads-up notification or + // displayed on keyguard. + // TODO(b/388891313): Account for isBlurSupported when it is initialized and updated + // correctly. + return notificationRowTransparency() && !mIsHeadsUp && !mOnKeyguard; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index f36a0cf51b97..292f74a65554 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -19,8 +19,6 @@ package com.android.systemui.statusbar.notification.row; import static com.android.systemui.Flags.notificationColorUpdateLogger; import static com.android.systemui.Flags.physicalNotificationMovement; -import static java.lang.Math.abs; - import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.content.res.Configuration; @@ -31,7 +29,6 @@ import android.util.FloatProperty; import android.util.IndentingPrintWriter; import android.util.Log; import android.view.View; -import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.FrameLayout; @@ -113,27 +110,14 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro protected SpringAnimation mMagneticAnimator = new SpringAnimation( this /* object */, DynamicAnimation.TRANSLATION_X); - private int mTouchSlop; - protected MagneticRowListener mMagneticRowListener = new MagneticRowListener() { @Override - public void setMagneticTranslation(float translation, boolean trackEagerly) { - if (!mMagneticAnimator.isRunning()) { - setTranslation(translation); - return; - } - - if (trackEagerly) { - float delta = abs(getTranslation() - translation); - if (delta > mTouchSlop) { - mMagneticAnimator.animateToFinalPosition(translation); - } else { - mMagneticAnimator.cancel(); - setTranslation(translation); - } - } else { + public void setMagneticTranslation(float translation) { + if (mMagneticAnimator.isRunning()) { mMagneticAnimator.animateToFinalPosition(translation); + } else { + setTranslation(translation); } } @@ -199,7 +183,6 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro private void initDimens() { mContentShift = getResources().getDimensionPixelSize( R.dimen.shelf_transform_content_shift); - mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java index 4914e1073059..e4997e4f53ad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java @@ -36,9 +36,9 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.Dumpable; +import com.android.systemui.common.shared.colors.SurfaceEffectColors; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss; import com.android.systemui.util.DrawableDumpKt; @@ -52,7 +52,6 @@ import java.util.Arrays; public class NotificationBackgroundView extends View implements Dumpable, ExpandableNotificationRow.DismissButtonTargetVisibilityListener { - private static final int MAX_ALPHA = 0xFF; private final boolean mDontModifyCorners; private Drawable mBackground; private int mClipTopAmount; @@ -73,8 +72,6 @@ public class NotificationBackgroundView extends View implements Dumpable, private final ColorStateList mLightColoredStatefulColors; private final ColorStateList mDarkColoredStatefulColors; private int mNormalColor; - private boolean mBgIsColorized = false; - private boolean mForceOpaque = false; private final int convexR = 9; private final int concaveR = 22; @@ -88,13 +85,15 @@ public class NotificationBackgroundView extends View implements Dumpable, R.color.notification_state_color_light); mDarkColoredStatefulColors = getResources().getColorStateList( R.color.notification_state_color_dark); + if (notificationRowTransparency()) { + mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext()); + } else { + mNormalColor = mContext.getColor( + com.android.internal.R.color.materialColorSurfaceContainerHigh); + } mFocusOverlayStroke = getResources().getDimension(R.dimen.notification_focus_stroke_width); } - public void setNormalColor(int color) { - mNormalColor = color; - } - @Override public void onTargetVisibilityChanged(boolean targetVisible) { if (NotificationAddXOnHoverToDismiss.isUnexpectedlyInLegacyMode()) { @@ -140,21 +139,6 @@ public class NotificationBackgroundView extends View implements Dumpable, } } - /** - * A way to tell whether the background has been colorized. - */ - public boolean isColorized() { - return mBgIsColorized; - } - - /** - * A way to inform this class whether the background has been colorized. - * We need to know this, in order to *not* override that color. - */ - public void setBgIsColorized(boolean b) { - mBgIsColorized = b; - } - private Path calculateDismissButtonCutoutPath(Rect backgroundBounds) { // TODO(b/365585705): Adapt to RTL after the UX design is finalized. @@ -311,28 +295,21 @@ public class NotificationBackgroundView extends View implements Dumpable, return ((LayerDrawable) mBackground).getDrawable(1); } - private void updateBaseLayerColor() { - // BG base layer being a drawable, there isn't a method like setColor() to color it. - // Instead, we set a color filter that essentially replaces every pixel of the drawable. - // For non-colorized notifications, this function specifies a new color token. - // For colorized notifications, this uses a color that matches the tint color at 90% alpha. - int color = isColorized() - ? ColorUtils.setAlphaComponent(mTintColor, (int) (MAX_ALPHA * 0.9f)) - : mNormalColor; - getBaseBackgroundLayer().setColorFilter( - new PorterDuffColorFilter( - color, - PorterDuff.Mode.SRC)); // SRC operator discards the drawable's color+alpha - } - public void setTint(int tintColor) { Drawable baseLayer = getBaseBackgroundLayer(); - baseLayer.mutate().setTintMode(PorterDuff.Mode.SRC_ATOP); - baseLayer.setTint(tintColor); - mTintColor = tintColor; if (notificationRowTransparency()) { - updateBaseLayerColor(); + // BG base layer being a drawable, there isn't a method like setColor() to color it. + // Instead, we set a color filter that essentially replaces every pixel of the drawable. + baseLayer.setColorFilter( + new PorterDuffColorFilter( + tintColor, + // SRC operator discards the drawable's color+alpha + PorterDuff.Mode.SRC)); + } else { + baseLayer.mutate().setTintMode(PorterDuff.Mode.SRC_ATOP); + baseLayer.setTint(tintColor); } + mTintColor = tintColor; setStatefulColors(); invalidate(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index b3357d01ab7a..8262f4cdc16b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -269,6 +269,7 @@ public class NotificationContentView extends FrameLayout implements Notification mNotificationMaxHeight = maxHeight; } + // This logic is mirrored in FrameLayoutWithMaxHeight.onMeasure in AODPromotedNotification.kt. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int heightMode = MeasureSpec.getMode(heightMeasureSpec); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 869b75ea070e..cdb78d99538b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -430,6 +430,7 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta NotificationBundleUi.isEnabled() ? !row.getEntry().isBlockable() : row.getIsNonblockable(), + row.canViewBeDismissed(), mHighPriorityProvider.isHighPriority(row.getEntry()), mAssistantFeedbackController, mMetricsLogger, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java index 904911fcf545..b6f4ffce8e00 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java @@ -121,6 +121,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G private boolean mIsAutomaticChosen; private boolean mIsSingleDefaultChannel; private boolean mIsNonblockable; + private boolean mIsDismissable; private NotificationEntry mEntry; private StatusBarNotification mSbn; private boolean mIsDeviceProvisioned; @@ -161,6 +162,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G mPressedApply = true; mGutsContainer.closeControls(v, /* save= */ true); }; + private OnClickListener mOnCloseClickListener; public NotificationInfo(Context context, AttributeSet attrs) { super(context, attrs); @@ -204,6 +206,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G UiEventLogger uiEventLogger, boolean isDeviceProvisioned, boolean isNonblockable, + boolean isDismissable, boolean wasShownHighPriority, AssistantFeedbackController assistantFeedbackController, MetricsLogger metricsLogger, @@ -228,11 +231,13 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G mStartingChannelImportance = mSingleNotificationChannel.getImportance(); mWasShownHighPriority = wasShownHighPriority; mIsNonblockable = isNonblockable; + mIsDismissable = isDismissable; mAppUid = mSbn.getUid(); mDelegatePkg = mSbn.getOpPkg(); mIsDeviceProvisioned = isDeviceProvisioned; mShowAutomaticSetting = mAssistantFeedbackController.isFeedbackEnabled(); mUiEventLogger = uiEventLogger; + mOnCloseClickListener = onCloseClick; mIsSystemRegisteredCall = mSbn.getNotification().isStyle(Notification.CallStyle.class) && mINotificationManager.isInCall(mSbn.getPackageName(), mSbn.getUid()); @@ -279,6 +284,11 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G turnOffButton.setVisibility(turnOffButton.hasOnClickListeners() && !mIsNonblockable ? VISIBLE : GONE); + View dismissButton = findViewById(R.id.inline_dismiss); + dismissButton.setOnClickListener(mOnCloseClickListener); + dismissButton.setVisibility(dismissButton.hasOnClickListeners() && mIsDismissable + ? VISIBLE : GONE); + View done = findViewById(R.id.done); done.setOnClickListener(mOnDismissSettings); done.setAccessibilityDelegate(mGutsContainer.getAccessibilityDelegate()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java index 286f07b7413d..e89a76fd5a69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java @@ -32,6 +32,7 @@ import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.provider.Settings; +import android.service.notification.StatusBarNotification; import android.util.ArrayMap; import android.view.LayoutInflater; import android.view.View; @@ -357,9 +358,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl final float dismissThreshold = getDismissThreshold(); final boolean snappingToDismiss = delta < -dismissThreshold || delta > dismissThreshold; if (mSnappingToDismiss != snappingToDismiss) { - if (!Flags.magneticNotificationSwipes()) { - getMenuView().performHapticFeedback(CLOCK_TICK); - } + getMenuView().performHapticFeedback(CLOCK_TICK); } mSnappingToDismiss = snappingToDismiss; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java index 39ffdf29b57c..01ee788f7fd7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java @@ -68,6 +68,7 @@ public class PromotedNotificationInfo extends NotificationInfo { UiEventLogger uiEventLogger, boolean isDeviceProvisioned, boolean isNonblockable, + boolean isDismissable, boolean wasShownHighPriority, AssistantFeedbackController assistantFeedbackController, MetricsLogger metricsLogger, OnClickListener onCloseClick) throws RemoteException { @@ -75,7 +76,7 @@ public class PromotedNotificationInfo extends NotificationInfo { onUserInteractionCallback, channelEditorDialogController, packageDemotionInteractor, pkg, notificationChannel, entry, onSettingsClick, onAppSettingsClick, feedbackClickListener, uiEventLogger, - isDeviceProvisioned, isNonblockable, wasShownHighPriority, + isDeviceProvisioned, isNonblockable, isDismissable, wasShownHighPriority, assistantFeedbackController, metricsLogger, onCloseClick); mNotificationManager = iNotificationManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt index 48cff7497e3c..aa6951715755 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt @@ -33,12 +33,12 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow interface MagneticNotificationRowManager { /** - * Notifies a change in the device density. The density can be used to compute the values of - * thresholds in pixels. + * Set the swipe threshold in pixels. After crossing the threshold, the magnetic target detaches + * and the magnetic neighbors snap back. * - * @param[density] The device density. + * @param[threshold] Swipe threshold in pixels. */ - fun onDensityChange(density: Float) + fun setSwipeThresholdPx(thresholdPx: Float) /** * Set the magnetic and roundable targets of a magnetic swipe interaction. @@ -87,9 +87,6 @@ interface MagneticNotificationRowManager { */ fun onMagneticInteractionEnd(row: ExpandableNotificationRow, velocity: Float? = null) - /** Determine if the given [ExpandableNotificationRow] has been magnetically detached. */ - fun isMagneticRowSwipeDetached(row: ExpandableNotificationRow): Boolean - /* Reset any roundness that magnetic targets may have */ fun resetRoundness() @@ -107,15 +104,12 @@ interface MagneticNotificationRowManager { /** Detaching threshold in dp */ const val MAGNETIC_DETACH_THRESHOLD_DP = 56 - /** Re-attaching threshold in dp */ - const val MAGNETIC_ATTACH_THRESHOLD_DP = 40 - /* An empty implementation of a manager */ @JvmStatic val Empty: MagneticNotificationRowManager get() = object : MagneticNotificationRowManager { - override fun onDensityChange(density: Float) {} + override fun setSwipeThresholdPx(thresholdPx: Float) {} override fun setMagneticAndRoundableTargets( swipingRow: ExpandableNotificationRow, @@ -133,10 +127,6 @@ interface MagneticNotificationRowManager { velocity: Float?, ) {} - override fun isMagneticRowSwipeDetached( - row: ExpandableNotificationRow - ): Boolean = false - override fun resetRoundness() {} override fun reset() {} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt index 3f05cef6a0c6..9bd5a5bd903f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt @@ -47,7 +47,6 @@ constructor( private set private var magneticDetachThreshold = Float.POSITIVE_INFINITY - private var magneticAttachThreshold = 0f // Has the roundable target been set for the magnetic view that is being swiped. val isSwipedViewRoundableSet: Boolean @@ -58,18 +57,13 @@ constructor( SpringForce().setStiffness(DETACH_STIFFNESS).setDampingRatio(DETACH_DAMPING_RATIO) private val snapForce = SpringForce().setStiffness(SNAP_BACK_STIFFNESS).setDampingRatio(SNAP_BACK_DAMPING_RATIO) - private val attachForce = - SpringForce().setStiffness(ATTACH_STIFFNESS).setDampingRatio(ATTACH_DAMPING_RATIO) // Multiplier applied to the translation of a row while swiped val swipedRowMultiplier = MAGNETIC_TRANSLATION_MULTIPLIERS[MAGNETIC_TRANSLATION_MULTIPLIERS.size / 2] - override fun onDensityChange(density: Float) { - magneticDetachThreshold = - density * MagneticNotificationRowManager.MAGNETIC_DETACH_THRESHOLD_DP - magneticAttachThreshold = - density * MagneticNotificationRowManager.MAGNETIC_ATTACH_THRESHOLD_DP + override fun setSwipeThresholdPx(thresholdPx: Float) { + magneticDetachThreshold = thresholdPx } override fun setMagneticAndRoundableTargets( @@ -145,7 +139,8 @@ constructor( } } State.DETACHED -> { - translateDetachedRow(translation) + val swiped = currentMagneticListeners.swipedListener() + swiped?.setMagneticTranslation(translation) } } return true @@ -237,41 +232,6 @@ constructor( ) } - private fun translateDetachedRow(translation: Float) { - val targetTranslation = swipedRowMultiplier * translation - val crossedThreshold = abs(targetTranslation) <= magneticAttachThreshold - if (crossedThreshold) { - attachNeighbors(translation) - updateRoundness(translation) - currentMagneticListeners.swipedListener()?.let { attach(it, translation) } - currentState = State.PULLING - } else { - val swiped = currentMagneticListeners.swipedListener() - swiped?.setMagneticTranslation(translation, trackEagerly = false) - } - } - - private fun attachNeighbors(translation: Float) { - currentMagneticListeners.forEachIndexed { i, target -> - target?.let { - if (i != currentMagneticListeners.size / 2) { - val multiplier = MAGNETIC_TRANSLATION_MULTIPLIERS[i] - target.cancelMagneticAnimations() - target.triggerMagneticForce( - endTranslation = translation * multiplier, - attachForce, - ) - } - } - } - } - - private fun attach(listener: MagneticRowListener, toPosition: Float) { - listener.cancelMagneticAnimations() - listener.triggerMagneticForce(toPosition, attachForce) - msdlPlayer.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR) - } - override fun onMagneticInteractionEnd(row: ExpandableNotificationRow, velocity: Float?) { if (row.isSwipedTarget()) { when (currentState) { @@ -294,9 +254,6 @@ constructor( } } - override fun isMagneticRowSwipeDetached(row: ExpandableNotificationRow): Boolean = - row.isSwipedTarget() && currentState == State.DETACHED - override fun resetRoundness() = notificationRoundnessManager.clear() override fun reset() { @@ -343,8 +300,6 @@ constructor( private const val DETACH_DAMPING_RATIO = 0.95f private const val SNAP_BACK_STIFFNESS = 550f private const val SNAP_BACK_DAMPING_RATIO = 0.6f - private const val ATTACH_STIFFNESS = 800f - private const val ATTACH_DAMPING_RATIO = 0.95f // Maximum value of corner roundness that gets applied during the pre-detach dragging private const val MAX_PRE_DETACH_ROUNDNESS = 0.8f diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt index c3b9024ad9b1..5959ef1e093b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt @@ -21,21 +21,8 @@ import androidx.dynamicanimation.animation.SpringForce /** A listener that responds to magnetic forces applied to an [ExpandableNotificationRow] */ interface MagneticRowListener { - /** - * Set a translation due to a magnetic attachment. - * - * The request to set a View's translation may occur while a magnetic animation is running. In - * such a case, the [trackEagerly] argument will determine if we decide to eagerly track the - * incoming translation or not. If true, the ongoing animation will update its target position - * and continue to animate only if the incoming translation produces a delta higher than a touch - * slop threshold. Otherwise, the animation will be cancelled and the View translation will be - * set directly. If [trackEagerly] is false, we respect the animation and only update the - * animated target. - * - * @param[translation] Incoming gesture translation. - * @param[trackEagerly] Whether we eagerly track the incoming translation directly or not. - */ - fun setMagneticTranslation(translation: Float, trackEagerly: Boolean = true) + /** Set a translation due to a magnetic attachment. */ + fun setMagneticTranslation(translation: Float) /** * Trigger the magnetic behavior when the row detaches or snaps back from its magnetic diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 2d0a1c069c5c..bb3abc1fba38 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -486,22 +486,15 @@ public class NotificationStackScrollLayoutController implements Dumpable { } @Override - public boolean isMagneticViewDetached(View view) { - if (view instanceof ExpandableNotificationRow row) { - return mMagneticNotificationRowManager.isMagneticRowSwipeDetached(row); - } else { - return false; - } - } - - @Override public float getTotalTranslationLength(View animView) { return mView.getTotalTranslationLength(animView); } @Override public void onDensityScaleChange(float density) { - mMagneticNotificationRowManager.onDensityChange(density); + mMagneticNotificationRowManager.setSwipeThresholdPx( + density * MagneticNotificationRowManager.MAGNETIC_DETACH_THRESHOLD_DP + ); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java index 33b94783b24a..c5a846e1da05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java @@ -33,7 +33,6 @@ import android.view.ViewConfiguration; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; -import com.android.systemui.Flags; import com.android.systemui.SwipeHelper; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; @@ -192,11 +191,6 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc @Override public boolean handleUpEvent(MotionEvent ev, View animView, float velocity, float translation) { - if (Flags.magneticNotificationSwipes() - && (mCallback.isMagneticViewDetached(animView) || swipedFastEnough())) { - dismiss(animView, velocity); - return true; - } NotificationMenuRowPlugin menuRow = getCurrentMenuRow(); if (menuRow != null) { menuRow.onTouchEnd(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index ba666512af5a..2f9cff46d687 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -196,9 +196,8 @@ fun StatusBarRoot( setContent { PlatformTheme { - val chipsVisibilityModel by + val chipsVisibilityModel = statusBarViewModel.ongoingActivityChips - .collectAsStateWithLifecycle() if (chipsVisibilityModel.areChipsAllowed) { OngoingActivityChips( chips = chipsVisibilityModel.chips, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index c717b180575c..540babad5dd1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -21,6 +21,8 @@ import android.graphics.Rect import android.view.Display import android.view.View import androidx.compose.runtime.getValue +import com.android.app.tracing.FlowTracing.traceEach +import com.android.app.tracing.TrackGroupUtils.trackGroup import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -130,7 +132,7 @@ interface HomeStatusBarViewModel : Activatable { val primaryOngoingActivityChip: StateFlow<OngoingActivityChipModel> /** All supported activity chips, whether they are currently active or not. */ - val ongoingActivityChips: StateFlow<ChipsVisibilityModel> + val ongoingActivityChips: ChipsVisibilityModel /** * The multiple ongoing activity chips that should be shown on the left-hand side of the status @@ -386,11 +388,9 @@ constructor( } override val isHomeStatusBarAllowed = - isHomeStatusBarAllowedCompat.stateIn( - bgScope, - SharingStarted.WhileSubscribed(), - initialValue = false, - ) + isHomeStatusBarAllowedCompat + .traceEach(trackGroup(TRACK_GROUP, "isHomeStatusBarAllowed"), logcat = true) + .stateIn(bgScope, SharingStarted.WhileSubscribed(), initialValue = false) private val shouldHomeStatusBarBeVisible = combine( @@ -461,24 +461,29 @@ constructor( isHomeStatusBarAllowed && !isSecureCameraActive && !hideStartSideContentForHeadsUp } - override val ongoingActivityChips = + private val chipsVisibilityModel: Flow<ChipsVisibilityModel> = combine(ongoingActivityChipsViewModel.chips, canShowOngoingActivityChips) { chips, canShow -> ChipsVisibilityModel(chips, areChipsAllowed = canShow) } - .stateIn( - bgScope, - SharingStarted.WhileSubscribed(), - initialValue = - ChipsVisibilityModel( - chips = MultipleOngoingActivityChipsModel(), - areChipsAllowed = false, - ), - ) + .traceEach(trackGroup(TRACK_GROUP, "chips"), logcat = true) { + "Chips[allowed=${it.areChipsAllowed} numChips=${it.chips.active.size}]" + } + + override val ongoingActivityChips: ChipsVisibilityModel by + hydrator.hydratedStateOf( + traceName = "ongoingActivityChips", + initialValue = + ChipsVisibilityModel( + chips = MultipleOngoingActivityChipsModel(), + areChipsAllowed = false, + ), + source = chipsVisibilityModel, + ) private val hasOngoingActivityChips = if (StatusBarChipsModernization.isEnabled) { - ongoingActivityChips.map { it.chips.active.any { chip -> !chip.isHidden } } + chipsVisibilityModel.map { it.chips.active.any { chip -> !chip.isHidden } } } else if (StatusBarNotifChips.isEnabled) { ongoingActivityChipsLegacy.map { it.primary is OngoingActivityChipModel.Active } } else { @@ -607,6 +612,8 @@ constructor( private const val COL_PREFIX_NOTIF_CONTAINER = "notifContainer" private const val COL_PREFIX_SYSTEM_INFO = "systemInfo" + private const val TRACK_GROUP = "StatusBar" + fun tableLogBufferName(displayId: Int) = "HomeStatusBarViewModel[$displayId]" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java index 5c893da45b8d..6a4f3da054e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -44,6 +44,7 @@ import android.app.RemoteAction; import android.content.ClipData; import android.content.ClipDescription; import android.content.Context; +import android.content.Intent; import android.graphics.Insets; import android.graphics.Rect; import android.net.Uri; @@ -77,6 +78,7 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.util.Optional; +import java.util.function.Consumer; @SmallTest @RunWith(AndroidJUnit4.class) @@ -158,6 +160,31 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { * is false are removed.[ */ private void initController() { + IntentCreator fakeIntentCreator = new IntentCreator() { + @Override + public Intent getTextEditorIntent(Context context) { + return new Intent(); + } + + @Override + public Intent getShareIntent(ClipData clipData, Context context) { + Intent intent = Intent.createChooser(new Intent(Intent.ACTION_SEND), null); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; + } + + @Override + public void getImageEditIntentAsync(Uri uri, Context context, + Consumer<Intent> outputConsumer) { + outputConsumer.accept(new Intent(Intent.ACTION_EDIT)); + } + + @Override + public Intent getRemoteCopyIntent(ClipData clipData, Context context) { + return new Intent(); + } + }; + mOverlayController = new ClipboardOverlayController( mContext, mClipboardOverlayView, @@ -171,7 +198,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { mClipboardTransitionExecutor, mClipboardIndicationProvider, mUiEventLogger, - new ActionIntentCreator()); + fakeIntentCreator); verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture()); mCallbacks = mOverlayCallbacksCaptor.getValue(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index fb70846049da..061f7984d44b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -134,7 +134,6 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private RingerModeTracker mRingerModeTracker; @Mock private RingerModeLiveData mRingerModeLiveData; @Mock private PackageManager mPackageManager; - @Mock private Handler mHandler; @Mock private UserContextProvider mUserContextProvider; @Mock private VibratorHelper mVibratorHelper; @Mock private ShadeController mShadeController; @@ -148,6 +147,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { private TestableLooper mTestableLooper; private KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); private GlobalActionsInteractor mInteractor; + private Handler mHandler; @Before public void setUp() throws Exception { @@ -166,6 +166,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mGlobalSettings = new FakeGlobalSettings(); mSecureSettings = new FakeSettings(); mInteractor = mKosmos.getGlobalActionsInteractor(); + mHandler = new Handler(mTestableLooper.getLooper()); mGlobalActionsDialogLite = new GlobalActionsDialogLite(mContext, mWindowManagerFuncs, @@ -771,6 +772,31 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY); } + @Test + public void userSwitching_dismissDialog() { + String[] actions = { + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART, + }; + doReturn(actions).when(mResources) + .getStringArray(com.android.internal.R.array.config_globalActionsList); + + mGlobalActionsDialogLite.showOrHideDialog(false, true, null, Display.DEFAULT_DISPLAY); + mTestableLooper.processAllMessages(); + + assertThat(mGlobalActionsDialogLite.mDialog.isShowing()).isTrue(); + + ArgumentCaptor<UserTracker.Callback> captor = + ArgumentCaptor.forClass(UserTracker.Callback.class); + + verify(mUserTracker).addCallback(captor.capture(), any()); + + captor.getValue().onBeforeUserSwitching(100); + mTestableLooper.processAllMessages(); + + assertThat(mGlobalActionsDialogLite.mDialog).isNull(); + } + private UserInfo mockCurrentUser(int flags) { return new UserInfo(10, "A User", flags); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModelTest.kt index f394c805f5b7..8f1d07bac4df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModelTest.kt @@ -855,6 +855,20 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Test fun contentDescriptionUpdated() { + var elapsedTimeDesc: CharSequence? = null + var durationDesc: CharSequence? = null + val listener = + object : SeekBarViewModel.ContentDescriptionListener { + override fun onContentDescriptionChanged( + elapsedTimeDescription: CharSequence, + durationDescription: CharSequence, + ) { + elapsedTimeDesc = elapsedTimeDescription + durationDesc = durationDescription + } + } + viewModel.setContentDescriptionListener(listener) + // When there is a duration and position val duration = (1.5 * 60 * 60 * 1000).toLong() val metadata = @@ -875,9 +889,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { viewModel.updateController(mockController) fakeExecutor.runNextReady() - // Then the content description is set - val result = viewModel.progress.value!! - + // Then the content description listener gets an update val expectedProgress = MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) .formatMeasures(Measure(3, MeasureUnit.SECOND)) @@ -888,7 +900,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { Measure(30, MeasureUnit.MINUTE), Measure(0, MeasureUnit.SECOND), ) - assertThat(result.durationDescription).isEqualTo(expectedDuration) - assertThat(result.elapsedTimeDescription).isEqualTo(expectedProgress) + assertThat(elapsedTimeDesc).isEqualTo(expectedProgress) + assertThat(durationDesc).isEqualTo(expectedDuration) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt index cf54df8565d3..997cf417fe10 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt @@ -22,6 +22,7 @@ import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.Display import androidx.test.filters.SmallTest +import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.SysuiTestCase import com.android.systemui.deviceStateManager import com.android.systemui.display.domain.interactor.RearDisplayStateInteractor @@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import org.junit.Before import org.junit.Test +import org.mockito.Mockito.times import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -59,6 +61,7 @@ class RearDisplayCoreStartableTest : SysuiTestCase() { fakeRearDisplayStateInteractor, kosmos.rearDisplayInnerDialogDelegateFactory, kosmos.testScope, + kosmos.keyguardUpdateMonitor, ) @Before @@ -96,6 +99,26 @@ class RearDisplayCoreStartableTest : SysuiTestCase() { } } + @Test + @EnableFlags(FLAG_DEVICE_STATE_RDM_V2) + fun testDialogResumesAfterKeyguardGone() = + kosmos.runTest { + impl.use { + it.start() + fakeRearDisplayStateInteractor.emitRearDisplay() + + verify(mockDialog).show() + + it.keyguardCallback.onKeyguardVisibilityChanged(true) + // Do not need to check that the dialog is dismissed, since SystemUIDialog + // implementation handles that. We just toggle keyguard here so that the flow + // emits. + + it.keyguardCallback.onKeyguardVisibilityChanged(false) + verify(mockDialog, times(2)).show() + } + } + private class FakeRearDisplayStateInteractor(private val kosmos: Kosmos) : RearDisplayStateInteractor { private val stateFlow = MutableSharedFlow<RearDisplayStateInteractor.State>() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 84f39be2eeed..00ee893e0e4d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -61,6 +61,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.internal.logging.MetricsLogger; import com.android.internal.widget.CachingIconView; import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestableContext; @@ -71,12 +72,18 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.AboveShelfChangedListener; import com.android.systemui.statusbar.notification.FeedbackIcon; +import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.SourceType; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntryAdapter; +import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener; +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization; @@ -942,9 +949,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { public void isExpanded_sensitivePromotedNotification_notExpanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - NotificationEntry entry = mock(NotificationEntry.class); - when(entry.isPromotedOngoing()).thenReturn(true); - row.setEntry(entry); + setRowPromotedOngoing(row); row.setSensitive(/* sensitive= */true, /* hideSensitive= */false); row.setHideSensitiveForIntrinsicHeight(/* hideSensitive= */true); @@ -957,9 +962,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { public void isExpanded_promotedNotificationNotOnKeyguard_expanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - NotificationEntry entry = mock(NotificationEntry.class); - when(entry.isPromotedOngoing()).thenReturn(true); - row.setEntry(entry); + setRowPromotedOngoing(row); row.setOnKeyguard(false); // THEN @@ -971,9 +974,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { public void isExpanded_promotedNotificationAllowOnKeyguard_expanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - NotificationEntry entry = mock(NotificationEntry.class); - when(entry.isPromotedOngoing()).thenReturn(true); - row.setEntry(entry); + setRowPromotedOngoing(row); row.setOnKeyguard(true); // THEN @@ -986,9 +987,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - NotificationEntry entry = mock(NotificationEntry.class); - when(entry.isPromotedOngoing()).thenReturn(true); - row.setEntry(entry); + setRowPromotedOngoing(row); row.setOnKeyguard(true); row.setIgnoreLockscreenConstraints(true); @@ -996,15 +995,31 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { assertThat(row.isExpanded()).isTrue(); } + private static void setRowPromotedOngoing(ExpandableNotificationRow row) { + final NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + if (NotificationBundleUi.isEnabled()) { + final EntryAdapter entryAdapter = new NotificationEntryAdapter( + mock(NotificationActivityStarter.class), + mock(MetricsLogger.class), + mock(PeopleNotificationIdentifier.class), + mock(NotificationIconStyleProvider.class), + mock(VisualStabilityCoordinator.class), + mock(NotificationActionClickManager.class), + entry); + row.setEntryAdapter(entryAdapter); + } else { + row.setEntry(entry); + } + } + @Test @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_promotedNotificationSaveSpaceOnLockScreen_notExpanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - NotificationEntry entry = mock(NotificationEntry.class); - when(entry.isPromotedOngoing()).thenReturn(true); - row.setEntry(entry); + setRowPromotedOngoing(row); row.setOnKeyguard(true); row.setSaveSpaceOnLockscreen(true); @@ -1018,9 +1033,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - NotificationEntry entry = mock(NotificationEntry.class); - when(entry.isPromotedOngoing()).thenReturn(true); - row.setEntry(entry); + setRowPromotedOngoing(row); row.setOnKeyguard(true); row.setSaveSpaceOnLockscreen(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt index dc3652d0c550..10de86644015 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt @@ -391,6 +391,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .setImportance(NotificationManager.IMPORTANCE_HIGH) .build() + whenever(row.canViewBeDismissed()).thenReturn(true) whenever(highPriorityProvider.isHighPriority(entry)).thenReturn(true) val statusBarNotification = entry.sbn gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -412,7 +413,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { any<UiEventLogger>(), eq(true), eq(false), - eq(true), /* wasShownHighPriority */ + eq(true), + eq(true), eq(assistantFeedbackController), any<MetricsLogger>(), any<View.OnClickListener>(), @@ -427,6 +429,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .build() + whenever(row.canViewBeDismissed()).thenReturn(true) val statusBarNotification = row.entry.sbn val entry = row.entry gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -448,7 +451,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { any<UiEventLogger>(), eq(true), eq(false), - eq(false), /* wasShownHighPriority */ + eq(true), /* wasShownHighPriority */ + eq(false), eq(assistantFeedbackController), any<MetricsLogger>(), any<View.OnClickListener>(), @@ -463,6 +467,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .build() + whenever(row.canViewBeDismissed()).thenReturn(true) val statusBarNotification = row.entry.sbn val entry = row.entry gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -484,7 +489,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { any<UiEventLogger>(), eq(true), eq(false), - eq(false), /* wasShownHighPriority */ + eq(true), /* wasShownHighPriority */ + eq(false), eq(assistantFeedbackController), any<MetricsLogger>(), any<View.OnClickListener>(), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt index 663a85330f70..338e4bec7aa2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import com.android.app.displaylib.DisplayRepository.PendingDisplay import org.mockito.Mockito.`when` as whenever /** Creates a mock display. */ @@ -41,8 +42,8 @@ fun display(type: Int, flags: Int = 0, id: Int = 0, state: Int? = null): Display } /** Creates a mock [DisplayRepository.PendingDisplay]. */ -fun createPendingDisplay(id: Int = 0): DisplayRepository.PendingDisplay = - mock<DisplayRepository.PendingDisplay> { whenever(this.id).thenReturn(id) } +fun createPendingDisplay(id: Int = 0): PendingDisplay = + mock<PendingDisplay> { whenever(this.id).thenReturn(id) } @SysUISingleton /** Fake [DisplayRepository] implementation for testing. */ @@ -50,7 +51,7 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { private val flow = MutableStateFlow<Set<Display>>(emptySet()) private val displayIdFlow = MutableStateFlow<Set<Int>>(emptySet()) private val pendingDisplayFlow = - MutableSharedFlow<DisplayRepository.PendingDisplay?>(replay = 1) + MutableSharedFlow<PendingDisplay?>(replay = 1) private val displayAdditionEventFlow = MutableSharedFlow<Display?>(replay = 0) private val displayRemovalEventFlow = MutableSharedFlow<Int>(replay = 0) private val displayIdsWithSystemDecorationsFlow = MutableStateFlow<Set<Int>>(emptySet()) @@ -101,7 +102,7 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { suspend fun emit(value: Set<Display>) = flow.emit(value) /** Emits [value] as [pendingDisplay] flow value. */ - suspend fun emit(value: DisplayRepository.PendingDisplay?) = pendingDisplayFlow.emit(value) + suspend fun emit(value: PendingDisplay?) = pendingDisplayFlow.emit(value) override val displays: StateFlow<Set<Display>> get() = flow @@ -109,7 +110,7 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { override val displayIds: StateFlow<Set<Int>> get() = displayIdFlow - override val pendingDisplay: Flow<DisplayRepository.PendingDisplay?> + override val pendingDisplay: Flow<PendingDisplay?> get() = pendingDisplayFlow private val _defaultDisplayOff: MutableStateFlow<Boolean> = MutableStateFlow(false) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt index aa23aa30b7bc..5ab3b3de49f4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt @@ -66,6 +66,7 @@ class FakePerDisplayInstanceProviderWithTeardown : val Kosmos.fakePerDisplayInstanceProviderWithTeardown by Kosmos.Fixture { FakePerDisplayInstanceProviderWithTeardown() } +val Kosmos.perDisplayDumpHelper by Kosmos.Fixture { PerDisplayRepoDumpHelper(dumpManager) } val Kosmos.fakePerDisplayInstanceRepository by Kosmos.Fixture { PerDisplayInstanceRepositoryImpl( @@ -73,6 +74,6 @@ val Kosmos.fakePerDisplayInstanceRepository by instanceProvider = fakePerDisplayInstanceProviderWithTeardown, testScope.backgroundScope, displayRepository, - dumpManager, + perDisplayDumpHelper, ) } diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING index 1148539187ac..083d2aa15316 100644 --- a/ravenwood/TEST_MAPPING +++ b/ravenwood/TEST_MAPPING @@ -180,9 +180,11 @@ // AUTO-GENERATED-END ], "ravenwood-postsubmit": [ - { - "name": "SystemUiRavenTests", - "host": true - } + // We haven't maintained SystemUiRavenTests, and as a result, it's been demoted already. + // Disable it until we fix the issues: b/319647875 + // { + // "name": "SystemUiRavenTests", + // "host": true + // } ] } diff --git a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java index 805d7f820c8d..94cef418b6c8 100644 --- a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java @@ -165,6 +165,9 @@ public class HearingDevicePhoneCallNotificationController { if (state == TelephonyManager.CALL_STATE_OFFHOOK) { if (com.android.server.accessibility.Flags.hearingInputChangeWhenCommDevice()) { AudioDeviceInfo commDevice = mAudioManager.getCommunicationDevice(); + if (commDevice == null) { + return; + } mHearingDevice = getSupportedInputHearingDeviceInfo(List.of(commDevice)); if (mHearingDevice != null) { showNotificationIfNeeded(); diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index b75b7ddf8181..9ae8ff869800 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -3871,6 +3871,10 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { */ public void notifyCarrierRoamingNtnSignalStrengthChanged(int subId, @NonNull NtnSignalStrength ntnSignalStrength) { + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + log("notifyCarrierRoamingNtnSignalStrengthChanged: invalid subscription id"); + return; + } if (!checkNotifyPermission("notifyCarrierRoamingNtnSignalStrengthChanged")) { log("notifyCarrierRoamingNtnSignalStrengthChanged: caller does not have required " + "permissions."); diff --git a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java index c53e4bdc2205..3a8d5835ccb1 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java @@ -24,6 +24,7 @@ import android.database.DatabaseErrorHandler; import android.database.DefaultDatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteFullException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteRawStatement; import android.os.Environment; @@ -174,11 +175,16 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper { if (DEBUG) { Slog.i(LOG_TAG, "DB execSQL, sql: " + sql); } - SQLiteDatabase db = getWritableDatabase(); - if (bindArgs == null) { - db.execSQL(sql); - } else { - db.execSQL(sql, bindArgs); + try { + SQLiteDatabase db = getWritableDatabase(); + if (bindArgs == null) { + db.execSQL(sql); + } else { + db.execSQL(sql, bindArgs); + } + } catch (SQLiteFullException exception) { + Slog.e(LOG_TAG, "Couldn't exec sql command, disk is full. Discrete ops " + + "db file size (bytes) : " + getDatabaseFile().length(), exception); } } diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java index 3aaf4f6fe85a..7450dffc05ab 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java +++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java @@ -122,8 +122,8 @@ final class DisplayDeviceInfo { public static final int FLAG_MASK_DISPLAY_CUTOUT = 1 << 11; /** - * Flag: This flag identifies secondary displays that should show system decorations, such as - * navigation bar, home activity or wallpaper. + * Flag: This flag identifies secondary displays that should always show system decorations, + * such as navigation bar, home activity or wallpaper. * <p>Note that this flag doesn't work without {@link #FLAG_TRUSTED}</p> * @hide */ @@ -191,6 +191,19 @@ final class DisplayDeviceInfo { public static final int FLAG_STEAL_TOP_FOCUS_DISABLED = 1 << 19; /** + * Flag: Indicates that the display is allowed to switch the content mode between + * projected/extended and mirroring. This allows the display to dynamically add or remove the + * home and system decorations. + * + * Note that this flag should not be enabled with any of {@link #FLAG_PRIVATE}, + * {@link #FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS}, or {@link #FLAG_OWN_CONTENT_ONLY} at the + * same time; otherwise it will be ignored. + * + * @hide + */ + public static final int FLAG_ALLOWS_CONTENT_MODE_SWITCH = 1 << 20; + + /** * Touch attachment: Display does not receive touch. */ public static final int TOUCH_NONE = 0; diff --git a/services/core/java/com/android/server/display/DisplayGroup.java b/services/core/java/com/android/server/display/DisplayGroup.java index f73b66c78fce..ce8d8a6db9d3 100644 --- a/services/core/java/com/android/server/display/DisplayGroup.java +++ b/services/core/java/com/android/server/display/DisplayGroup.java @@ -16,6 +16,8 @@ package com.android.server.display; +import android.util.IndentingPrintWriter; + import java.util.ArrayList; import java.util.List; @@ -97,4 +99,14 @@ public class DisplayGroup { } return displayIds; } + + /** Dumps information about the DisplayGroup. */ + void dumpLocked(IndentingPrintWriter ipw) { + final int numDisplays = mDisplays.size(); + for (int i = 0; i < numDisplays; i++) { + LogicalDisplay logicalDisplay = mDisplays.get(i); + ipw.println("Display " + logicalDisplay.getDisplayIdLocked() + " " + + logicalDisplay.getPrimaryDisplayDeviceLocked()); + } + } } diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 7b714ad2bd9e..2cad7ed2e9e9 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -766,7 +766,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { mInfo.flags |= DisplayDeviceInfo.FLAG_ROUND; } } else { - if (!res.getBoolean(R.bool.config_localDisplaysMirrorContent)) { + if (shouldOwnContentOnly()) { mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; } @@ -780,6 +780,15 @@ final class LocalDisplayAdapter extends DisplayAdapter { } } + if (getFeatureFlags().isDisplayContentModeManagementEnabled()) { + // Public display with FLAG_OWN_CONTENT_ONLY disabled is allowed to switch the + // content mode. + if (mIsFirstDisplay + || (!isDisplayPrivate(physicalAddress) && !shouldOwnContentOnly())) { + mInfo.flags |= DisplayDeviceInfo.FLAG_ALLOWS_CONTENT_MODE_SWITCH; + } + } + if (DisplayCutout.getMaskBuiltInDisplayCutout(res, mInfo.uniqueId)) { mInfo.flags |= DisplayDeviceInfo.FLAG_MASK_DISPLAY_CUTOUT; } @@ -822,6 +831,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { R.string.display_manager_hdmi_display_name); } } + mInfo.frameRateOverrides = mFrameRateOverrides; // The display is trusted since it is created by system. @@ -1467,6 +1477,11 @@ final class LocalDisplayAdapter extends DisplayAdapter { return false; } + private boolean shouldOwnContentOnly() { + final Resources res = getOverlayContext().getResources(); + return !res.getBoolean(R.bool.config_localDisplaysMirrorContent); + } + private boolean isDisplayStealTopFocusDisabled(DisplayAddress.Physical physicalAddress) { if (physicalAddress == null) { return false; diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index b2b9ef17ec8d..0e6870f7ed7d 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -489,6 +489,11 @@ final class LogicalDisplay { if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_STEAL_TOP_FOCUS_DISABLED) != 0) { mBaseDisplayInfo.flags |= Display.FLAG_STEAL_TOP_FOCUS_DISABLED; } + // Rear display should not be allowed to use the content mode switch. + if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_ALLOWS_CONTENT_MODE_SWITCH) != 0 + && mDevicePosition != Layout.Display.POSITION_REAR) { + mBaseDisplayInfo.flags |= Display.FLAG_ALLOWS_CONTENT_MODE_SWITCH; + } Rect maskingInsets = getMaskingInsets(deviceInfo); int maskedWidth = deviceInfo.width - maskingInsets.left - maskingInsets.right; int maskedHeight = deviceInfo.height - maskingInsets.top - maskingInsets.bottom; @@ -1155,6 +1160,7 @@ final class LogicalDisplay { pw.println("mRequestedMinimalPostProcessing=" + mRequestedMinimalPostProcessing); pw.println("mFrameRateOverrides=" + Arrays.toString(mFrameRateOverrides)); pw.println("mPendingFrameRateOverrideUids=" + mPendingFrameRateOverrideUids); + pw.println("mDisplayGroupId=" + mDisplayGroupId); pw.println("mDisplayGroupName=" + mDisplayGroupName); pw.println("mThermalBrightnessThrottlingDataId=" + mThermalBrightnessThrottlingDataId); pw.println("mLeadDisplayId=" + mLeadDisplayId); diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index f4daf8761e9b..4a4c616b34e3 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -478,6 +478,21 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { ipw.decreaseIndent(); ipw.println(); } + + final int displayGroupCount = mDisplayGroups.size(); + ipw.println(); + ipw.println("Display Groups: size=" + displayGroupCount); + for (int i = 0; i < displayGroupCount; i++) { + int groupId = mDisplayGroups.keyAt(i); + DisplayGroup displayGroup = mDisplayGroups.valueAt(i); + ipw.println("Group " + groupId + ":"); + ipw.increaseIndent(); + displayGroup.dumpLocked(ipw); + ipw.decreaseIndent(); + ipw.println(); + } + + mDeviceStateToLayoutMap.dumpLocked(ipw); } diff --git a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java index b5a9b19bc5c5..60b7fca99e7b 100644 --- a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java +++ b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java @@ -76,6 +76,7 @@ import java.util.regex.Pattern; * <li><code>secure</code>: creates a secure display</li> * <li><code>own_content_only</code>: only shows this display's own content</li> * <li><code>should_show_system_decorations</code>: supports system decorations</li> + * <li><code>fixed_content_mode</code>: not allowed to switch content mode</li> * <li><code>gravity_top_left</code>: display the overlay at the top left of the screen</li> * <li><code>gravity_top_right</code>: display the overlay at the top right of the screen</li> * <li><code>gravity_bottom_right</code>: display the overlay at the bottom right of the screen</li> @@ -117,6 +118,18 @@ final class OverlayDisplayAdapter extends DisplayAdapter { private static final String OVERLAY_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS = "should_show_system_decorations"; + /** + * When this flag is set, the overlay display is not allowed to switch content mode. + * Note that it is the opposite of {@link DisplayDeviceInfo#FLAG_ALLOWS_CONTENT_MODE_SWITCH}, + * because we want overlay displays (such as those used for connected display simulation in + * development) to have {@link DisplayDeviceInfo#FLAG_ALLOWS_CONTENT_MODE_SWITCH} enabled by + * default without explicitly specifying it. + * + * @see DisplayDeviceInfo#FLAG_ALLOWS_CONTENT_MODE_SWITCH + */ + private static final String OVERLAY_DISPLAY_FLAG_FIXED_CONTENT_MODE = + "fixed_content_mode"; + // Gravity flags to decide where the overlay should be shown. private static final String GRAVITY_TOP_LEFT = "gravity_top_left"; private static final String GRAVITY_BOTTOM_RIGHT = "gravity_bottom_right"; @@ -384,6 +397,17 @@ final class OverlayDisplayAdapter extends DisplayAdapter { if (mFlags.mShouldShowSystemDecorations) { mInfo.flags |= DisplayDeviceInfo.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; } + if (getFeatureFlags().isDisplayContentModeManagementEnabled()) { + if (!mFlags.mFixedContentMode + && !mFlags.mOwnContentOnly + && !mFlags.mShouldShowSystemDecorations) { + // For overlay displays, if FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS and + // FLAG_OWN_CONTENT_ONLY are both disabled, + // then FLAG_ALLOWS_CONTENT_MODE_SWITCH should be enabled by default, + // unless OVERLAY_DISPLAY_FLAG_FIXED_CONTENT_MODE is set. + mInfo.flags |= DisplayDeviceInfo.FLAG_ALLOWS_CONTENT_MODE_SWITCH; + } + } mInfo.type = Display.TYPE_OVERLAY; mInfo.touch = DisplayDeviceInfo.TOUCH_VIRTUAL; mInfo.state = mState; @@ -628,16 +652,21 @@ final class OverlayDisplayAdapter extends DisplayAdapter { /** See {@link #OVERLAY_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS}. */ final boolean mShouldShowSystemDecorations; + /** See {@link #OVERLAY_DISPLAY_FLAG_FIXED_CONTENT_MODE}. */ + final boolean mFixedContentMode; + final int mGravity; OverlayFlags( boolean secure, boolean ownContentOnly, boolean shouldShowSystemDecorations, + boolean fixedContentMode, int gravity) { mSecure = secure; mOwnContentOnly = ownContentOnly; mShouldShowSystemDecorations = shouldShowSystemDecorations; + mFixedContentMode = fixedContentMode; mGravity = gravity; } @@ -647,12 +676,14 @@ final class OverlayDisplayAdapter extends DisplayAdapter { false /* secure */, false /* ownContentOnly */, false /* shouldShowSystemDecorations */, + false /* fixedContentMode */, Gravity.NO_GRAVITY); } boolean secure = false; boolean ownContentOnly = false; boolean shouldShowSystemDecorations = false; + boolean fixedContentMode = false; int gravity = Gravity.NO_GRAVITY; for (String flag: flagString.split(FLAG_SPLITTER)) { if (OVERLAY_DISPLAY_FLAG_SECURE.equals(flag)) { @@ -661,11 +692,14 @@ final class OverlayDisplayAdapter extends DisplayAdapter { ownContentOnly = true; } else if (OVERLAY_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS.equals(flag)) { shouldShowSystemDecorations = true; + } else if (OVERLAY_DISPLAY_FLAG_FIXED_CONTENT_MODE.equals(flag)) { + fixedContentMode = true; } else { gravity = parseOverlayGravity(flag); } } - return new OverlayFlags(secure, ownContentOnly, shouldShowSystemDecorations, gravity); + return new OverlayFlags(secure, ownContentOnly, shouldShowSystemDecorations, + fixedContentMode, gravity); } @Override @@ -674,6 +708,7 @@ final class OverlayDisplayAdapter extends DisplayAdapter { .append("secure=").append(mSecure) .append(", ownContentOnly=").append(mOwnContentOnly) .append(", shouldShowSystemDecorations=").append(mShouldShowSystemDecorations) + .append(", fixedContentMode=").append(mFixedContentMode) .append(", gravity").append(Gravity.toString(mGravity)) .append("}") .toString(); diff --git a/services/core/java/com/android/server/display/WifiDisplayAdapter.java b/services/core/java/com/android/server/display/WifiDisplayAdapter.java index 902eefa824b5..89679c728127 100644 --- a/services/core/java/com/android/server/display/WifiDisplayAdapter.java +++ b/services/core/java/com/android/server/display/WifiDisplayAdapter.java @@ -666,6 +666,12 @@ final class WifiDisplayAdapter extends DisplayAdapter { mInfo.setAssumedDensityForExternalDisplay(mWidth, mHeight); // The display is trusted since it is created by system. mInfo.flags |= DisplayDeviceInfo.FLAG_TRUSTED; + if (getFeatureFlags().isDisplayContentModeManagementEnabled()) { + // The wifi display is allowed to switch content mode since FLAG_PRIVATE, + // FLAG_OWN_CONTENT_ONLY, and FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS are not + // enabled in WifiDisplayDevice#getDisplayDeviceInfoLocked(). + mInfo.flags |= DisplayDeviceInfo.FLAG_ALLOWS_CONTENT_MODE_SWITCH; + } mInfo.displayShape = DisplayShape.createDefaultDisplayShape(mInfo.width, mInfo.height, false); } diff --git a/services/core/java/com/android/server/notification/TEST_MAPPING b/services/core/java/com/android/server/notification/TEST_MAPPING index dc7129cde5e5..ea7ee4addbe6 100644 --- a/services/core/java/com/android/server/notification/TEST_MAPPING +++ b/services/core/java/com/android/server/notification/TEST_MAPPING @@ -4,7 +4,10 @@ "name": "CtsNotificationTestCases_notification" }, { - "name": "FrameworksUiServicesTests_notification" + "name": "FrameworksUiServicesNotificationTests" + }, + { + "name": "FrameworksUiServicesZenTests" } ], "postsubmit": [ diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index 102dc071c7b6..417b5c7ae62b 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -16,6 +16,7 @@ package com.android.server.power; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UserIdInt; @@ -36,8 +37,8 @@ import android.os.BatteryStats; import android.os.BatteryStatsInternal; import android.os.Bundle; import android.os.Handler; -import android.os.IWakeLockCallback; import android.os.IScreenTimeoutPolicyListener; +import android.os.IWakeLockCallback; import android.os.Looper; import android.os.Message; import android.os.PowerManager; @@ -67,6 +68,7 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.IndentingPrintWriter; import com.android.server.EventLogTags; import com.android.server.LocalServices; import com.android.server.input.InputManagerInternal; @@ -147,7 +149,8 @@ public class Notifier { @Nullable private final StatusBarManagerInternal mStatusBarManagerInternal; private final TrustManager mTrustManager; private final Vibrator mVibrator; - private final WakeLockLog mWakeLockLog; + @NonNull private final WakeLockLog mPartialWakeLockLog; + @NonNull private final WakeLockLog mFullWakeLockLog; private final DisplayManagerInternal mDisplayManagerInternal; private final NotifierHandler mHandler; @@ -250,7 +253,9 @@ public class Notifier { mShowWirelessChargingAnimationConfig = context.getResources().getBoolean( com.android.internal.R.bool.config_showBuiltinWirelessChargingAnim); - mWakeLockLog = mInjector.getWakeLockLog(context); + mFullWakeLockLog = mInjector.getWakeLockLog(context); + mPartialWakeLockLog = mInjector.getWakeLockLog(context); + // Initialize interactive state for battery stats. try { mBatteryStats.noteInteractive(true); @@ -324,7 +329,8 @@ public class Notifier { // Ignore } } - mWakeLockLog.onWakeLockAcquired(tag, ownerUid, flags, /*eventTime=*/ -1); + getWakeLockLog(flags).onWakeLockAcquired(tag, + getUidForWakeLockLog(ownerUid, workSource), flags, /*eventTime=*/ -1); } mWakefulnessSessionObserver.onWakeLockAcquired(flags); } @@ -473,7 +479,8 @@ public class Notifier { // Ignore } } - mWakeLockLog.onWakeLockReleased(tag, ownerUid, /*eventTime=*/ -1); + getWakeLockLog(flags).onWakeLockReleased(tag, + getUidForWakeLockLog(ownerUid, workSource), /*eventTime=*/ -1); } mWakefulnessSessionObserver.onWakeLockReleased(flags, releaseReason); } @@ -960,11 +967,18 @@ public class Notifier { * @param pw The stream to print to. */ public void dump(PrintWriter pw) { - if (mWakeLockLog != null) { - mWakeLockLog.dump(pw); - } + pw.println("Notifier:"); + + IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); + ipw.println("Partial Wakelock Log:"); + mPartialWakeLockLog.dump(ipw); + + ipw.println(""); + ipw.println("Full Wakelock Log:"); + mFullWakeLockLog.dump(ipw); - mWakefulnessSessionObserver.dump(pw); + ipw.println(""); + mWakefulnessSessionObserver.dump(ipw); } private void updatePendingBroadcastLocked() { @@ -1232,7 +1246,9 @@ public class Notifier { // Do Nothing } } - mWakeLockLog.onWakeLockAcquired(tag, ownerUid, flags, currentTime); + + getWakeLockLog(flags).onWakeLockAcquired(tag, getUidForWakeLockLog(ownerUid, workSource), + flags, currentTime); } @SuppressLint("AndroidFrameworkRequiresPermission") @@ -1253,7 +1269,8 @@ public class Notifier { // Ignore } } - mWakeLockLog.onWakeLockReleased(tag, ownerUid, currentTime); + getWakeLockLog(flags).onWakeLockReleased(tag, getUidForWakeLockLog(ownerUid, workSource), + currentTime); } @SuppressLint("AndroidFrameworkRequiresPermission") @@ -1419,6 +1436,15 @@ public class Notifier { } } + private @NonNull WakeLockLog getWakeLockLog(int flags) { + return PowerManagerService.isScreenLock(flags) ? mFullWakeLockLog : mPartialWakeLockLog; + } + + private int getUidForWakeLockLog(int ownerUid, WorkSource workSource) { + int attributionUid = workSource != null ? workSource.getAttributionUid() : -1; + return attributionUid != -1 ? attributionUid : ownerUid; + } + private final class NotifierHandler extends Handler { public NotifierHandler(Looper looper) { @@ -1501,7 +1527,7 @@ public class Notifier { /** * Gets the WakeLockLog object */ - WakeLockLog getWakeLockLog(Context context); + @NonNull WakeLockLog getWakeLockLog(Context context); /** * Gets the AppOpsManager system service @@ -1522,7 +1548,7 @@ public class Notifier { } @Override - public WakeLockLog getWakeLockLog(Context context) { + public @NonNull WakeLockLog getWakeLockLog(Context context) { return new WakeLockLog(context); } diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index dd454cd61b2c..3eac4b54cd2b 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -1723,9 +1723,16 @@ public final class PowerManagerService extends SystemService } } - @SuppressWarnings("deprecation") private static boolean isScreenLock(final WakeLock wakeLock) { - switch (wakeLock.mFlags & PowerManager.WAKE_LOCK_LEVEL_MASK) { + return isScreenLock(wakeLock.mFlags); + } + + /** + * Returns if a wakelock flag corresponds to a screen wake lock. + */ + @SuppressWarnings("deprecation") + public static boolean isScreenLock(int flags) { + switch (flags & PowerManager.WAKE_LOCK_LEVEL_MASK) { case PowerManager.FULL_WAKE_LOCK: case PowerManager.SCREEN_BRIGHT_WAKE_LOCK: case PowerManager.SCREEN_DIM_WAKE_LOCK: diff --git a/services/core/java/com/android/server/power/WakeLockLog.java b/services/core/java/com/android/server/power/WakeLockLog.java index eda222e71c9e..7f152d6fc9fa 100644 --- a/services/core/java/com/android/server/power/WakeLockLog.java +++ b/services/core/java/com/android/server/power/WakeLockLog.java @@ -81,11 +81,12 @@ final class WakeLockLog { private static final int TYPE_ACQUIRE = 0x1; private static final int TYPE_RELEASE = 0x2; private static final int MAX_LOG_ENTRY_BYTE_SIZE = 9; - private static final int LOG_SIZE = 1024 * 10; + private static final int LOG_SIZE = 1024 * 3; private static final int LOG_SIZE_MIN = MAX_LOG_ENTRY_BYTE_SIZE + 1; - private static final int TAG_DATABASE_SIZE = 128; + private static final int TAG_DATABASE_SIZE = 64; private static final int TAG_DATABASE_SIZE_MAX = 128; + private static final int TAG_DATABASE_STARTING_SIZE = 16; private static final int LEVEL_SCREEN_TIMEOUT_OVERRIDE_WAKE_LOCK = 0; private static final int LEVEL_PARTIAL_WAKE_LOCK = 1; @@ -182,7 +183,7 @@ final class WakeLockLog { * @param pw The {@code PrintWriter} to write to. */ public void dump(PrintWriter pw) { - dump(pw, false); + dump(pw, /* includeTagDb= */ true); } @VisibleForTesting @@ -1161,15 +1162,16 @@ final class WakeLockLog { */ static class TagDatabase { private final int mInvalidIndex; - private final TagData[] mArray; + private final int mMaxArraySize; + private TagData[] mArray; private Callback mCallback; TagDatabase(Injector injector) { - int size = Math.min(injector.getTagDatabaseSize(), TAG_DATABASE_SIZE_MAX); - - // Largest possible index used as "INVALID", hence the (size - 1) sizing. - mArray = new TagData[size - 1]; - mInvalidIndex = size - 1; + // Largest possible index used as "INVALID", hence the (size - 1) sizing + mMaxArraySize = Math.min(injector.getTagDatabaseSize(), TAG_DATABASE_SIZE_MAX - 1); + int startingSize = Math.min(mMaxArraySize, injector.getTagDatabaseStartingSize()); + mArray = new TagData[startingSize]; + mInvalidIndex = mMaxArraySize; } @Override @@ -1195,8 +1197,10 @@ final class WakeLockLog { sb.append(", entries: ").append(entries); sb.append(", Bytes used: ").append(byteEstimate); if (DEBUG) { - sb.append(", Avg tag size: ").append(tagSize / tags); - sb.append("\n ").append(Arrays.toString(mArray)); + sb.append(", Avg tag size: ").append(tags == 0 ? 0 : (tagSize / tags)); + for (int i = 0; i < mArray.length; i++) { + sb.append("\n [").append(i).append("] ").append(mArray[i]); + } } return sb.toString(); } @@ -1284,6 +1288,18 @@ final class WakeLockLog { return null; } + // We don't have a spot available, see if we can still increase the array size + if (firstAvailable == -1) { + if (mArray.length < mMaxArraySize) { + int oldSize = mArray.length; + int newSize = Math.min(oldSize * 2, mMaxArraySize); + TagData[] newArray = new TagData[newSize]; + System.arraycopy(mArray, 0, newArray, 0, oldSize); + mArray = newArray; + firstAvailable = oldSize; + } + } + // If we need to remove an index, report to listeners that we are removing an index. boolean useOldest = firstAvailable == -1; if (useOldest && mCallback != null) { @@ -1402,6 +1418,10 @@ final class WakeLockLog { return TAG_DATABASE_SIZE; } + public int getTagDatabaseStartingSize() { + return TAG_DATABASE_STARTING_SIZE; + } + public int getLogSize() { return LOG_SIZE; } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index 6e640d890fb8..424439df3c4b 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -856,10 +856,14 @@ public class WallpaperCropper { BitmapFactory.decodeFile(wallpaperFile.getAbsolutePath(), options); wallpaperImageSize.set(options.outWidth, options.outHeight); } + boolean isRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) + == View.LAYOUT_DIRECTION_RTL; + Rect croppedImageBound = getCrop(displaySize, mDefaultDisplayInfo, wallpaperImageSize, + getRelativeCropHints(wallpaperData), isRtl); - double maxDisplayToImageRatio = Math.max((double) displaySize.x / wallpaperImageSize.x, - (double) displaySize.y / wallpaperImageSize.y); - if (maxDisplayToImageRatio > 1.5) { + double maxDisplayToImageRatio = Math.max((double) displaySize.x / croppedImageBound.width(), + (double) displaySize.y / croppedImageBound.height()); + if (maxDisplayToImageRatio > 1.3) { return false; } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index afacae8d0c13..59a042981375 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -41,6 +41,7 @@ import static android.util.TypedValue.COMPLEX_UNIT_DIP; import static android.util.TypedValue.COMPLEX_UNIT_MASK; import static android.util.TypedValue.COMPLEX_UNIT_SHIFT; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.FLAG_ALLOWS_CONTENT_MODE_SWITCH; import static android.view.Display.FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD; import static android.view.Display.FLAG_PRIVATE; import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; @@ -3266,41 +3267,40 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /* inTopology= */ shouldShowContent); } - /** - * Whether the display is allowed to switch the content mode between extended and mirroring. - * If the content mode is extended, the display will start home activity and show system - * decorations, such as wallpapaer, status bar and navigation bar. - * If the content mode is mirroring, the display will not show home activity or system - * decorations. - * The content mode is switched when {@link Display#canHostTasks()} changes. - * - * Note that we only allow displays that are able to show system decorations to use the content - * mode switch; however, not all displays that are able to show system decorations are allowed - * to use the content mode switch. - */ - boolean allowContentModeSwitch() { - // The default display should always show system decorations. - if (isDefaultDisplay) { + /** + * Whether the display is allowed to switch the content mode between extended and mirroring. + * If the content mode is extended, the display will start home activity and show system + * decorations, such as wallpapaer, status bar and navigation bar. + * If the content mode is mirroring, the display will not show home activity or system + * decorations. + * The content mode is switched when {@link Display#canHostTasks()} changes. + * + * Note that we only allow displays that are able to show system decorations to use the content + * mode switch; however, not all displays that are able to show system decorations are allowed + * to use the content mode switch. + */ + boolean allowContentModeSwitch() { + if ((mDisplay.getFlags() & FLAG_ALLOWS_CONTENT_MODE_SWITCH) == 0) { return false; } - // Private display should never show system decorations. - if (isPrivate()) { + // The default display should always show system decorations. + if (isDefaultDisplay) { return false; } - if (shouldNeverShowSystemDecorations()) { + // Private or untrusted display should never show system decorations. + if (isPrivate() || !isTrusted()) { return false; } - // TODO(b/391965805): Remove this after introducing FLAG_ALLOW_CONTENT_MODE_SWITCH. - if ((mDisplay.getFlags() & Display.FLAG_REAR) != 0) { + if (shouldNeverShowSystemDecorations()) { return false; } - // TODO(b/391965805): Remove this after introducing FLAG_ALLOW_CONTENT_MODE_SWITCH. - // Virtual displays cannot add or remove system decorations during their lifecycle. - if (mDisplay.getType() == Display.TYPE_VIRTUAL) { + // Display with FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS enabled should always show system + // decorations, and should not switch the content mode. + if ((mDisplay.getFlags() & FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS) != 0) { return false; } diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java index de78271acddc..69e856de401a 100644 --- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java @@ -325,7 +325,8 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, } if (newState != INVALID_DEVICE_STATE_IDENTIFIER && newState != mLastReportedState) { - if (Trace.isTagEnabled(TRACE_TAG_SYSTEM_SERVER)) { + if (mLastHingeAngleSensorEvent != null + && Trace.isTagEnabled(TRACE_TAG_SYSTEM_SERVER)) { Trace.instant(TRACE_TAG_SYSTEM_SERVER, "[Device state changed] Last hinge sensor event timestamp: " + mLastHingeAngleSensorEvent.timestamp); diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java index f8b4113a3c04..b8a5f341eb8a 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -234,6 +234,7 @@ public class LocalDisplayAdapterTest { doReturn(true).when(mFlags).isDisplayOffloadEnabled(); doReturn(true).when(mFlags).isEvenDimmerEnabled(); + doReturn(true).when(mFlags).isDisplayContentModeManagementEnabled(); initDisplayOffloadSession(); } @@ -1468,6 +1469,103 @@ public class LocalDisplayAdapterTest { assertFalse(mDisplayOffloadSession.isActive()); } + @Test + public void testAllowsContentSwitch_firstDisplay() throws Exception { + // Set up a first display + setUpDisplay(new FakeDisplay(PORT_A)); + updateAvailableDisplays(); + mAdapter.registerLocked(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + + // The first display should be allowed to use the content mode switch + DisplayDevice firstDisplayDevice = mListener.addedDisplays.get(0); + assertTrue((firstDisplayDevice.getDisplayDeviceInfoLocked().flags + & DisplayDeviceInfo.FLAG_ALLOWS_CONTENT_MODE_SWITCH) != 0); + } + + @Test + public void testAllowsContentSwitch_secondaryDisplayPublicAndNotShouldShowOwnContent() + throws Exception { + // Set up a first display and a secondary display + setUpDisplay(new FakeDisplay(PORT_A)); + setUpDisplay(new FakeDisplay(PORT_B)); + updateAvailableDisplays(); + + // Set the secondary display to be a public display + doReturn(new int[0]).when(mMockedResources) + .getIntArray(com.android.internal.R.array.config_localPrivateDisplayPorts); + // Disable FLAG_OWN_CONTENT_ONLY for the secondary display + doReturn(true).when(mMockedResources) + .getBoolean(com.android.internal.R.bool.config_localDisplaysMirrorContent); + mAdapter.registerLocked(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + + // This secondary display should be allowed to use the content mode switch + DisplayDevice secondaryDisplayDevice = mListener.addedDisplays.get(1); + assertTrue((secondaryDisplayDevice.getDisplayDeviceInfoLocked().flags + & DisplayDeviceInfo.FLAG_ALLOWS_CONTENT_MODE_SWITCH) != 0); + } + + @Test + public void testAllowsContentSwitch_privateDisplay() throws Exception { + // Set up a first display and a secondary display + setUpDisplay(new FakeDisplay(PORT_A)); + setUpDisplay(new FakeDisplay(PORT_B)); + updateAvailableDisplays(); + + // Set the secondary display to be a private display + doReturn(new int[]{ PORT_B }).when(mMockedResources) + .getIntArray(com.android.internal.R.array.config_localPrivateDisplayPorts); + mAdapter.registerLocked(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + + // The private display should not be allowed to use the content mode switch + DisplayDevice secondaryDisplayDevice = mListener.addedDisplays.get(1); + assertTrue((secondaryDisplayDevice.getDisplayDeviceInfoLocked().flags + & DisplayDeviceInfo.FLAG_ALLOWS_CONTENT_MODE_SWITCH) == 0); + } + + @Test + public void testAllowsContentSwitch_ownContentOnlyDisplay() throws Exception { + // Set up a first display and a secondary display + setUpDisplay(new FakeDisplay(PORT_A)); + setUpDisplay(new FakeDisplay(PORT_B)); + updateAvailableDisplays(); + + // Enable FLAG_OWN_CONTENT_ONLY for the secondary display + doReturn(false).when(mMockedResources) + .getBoolean(com.android.internal.R.bool.config_localDisplaysMirrorContent); + mAdapter.registerLocked(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + + // The secondary display with FLAG_OWN_CONTENT_ONLY enabled should not be allowed to use the + // content mode switch + DisplayDevice secondaryDisplayDevice = mListener.addedDisplays.get(1); + assertTrue((secondaryDisplayDevice.getDisplayDeviceInfoLocked().flags + & DisplayDeviceInfo.FLAG_ALLOWS_CONTENT_MODE_SWITCH) == 0); + } + + @Test + public void testAllowsContentSwitch_flagShouldShowSystemDecorations() throws Exception { + // Set up a display + FakeDisplay display = new FakeDisplay(PORT_A); + setUpDisplay(display); + updateAvailableDisplays(); + + mAdapter.registerLocked(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + + // Display with FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS enabled should not be allowed to use the + // content mode switch + DisplayDevice displayDevice = mListener.addedDisplays.get(0); + int flags = displayDevice.getDisplayDeviceInfoLocked().flags; + boolean allowsContentModeSwitch = + ((flags & DisplayDeviceInfo.FLAG_ALLOWS_CONTENT_MODE_SWITCH) != 0); + boolean shouldShowSystemDecorations = + ((flags & DisplayDeviceInfo.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS) != 0); + assertFalse(allowsContentModeSwitch && shouldShowSystemDecorations); + } + private void initDisplayOffloadSession() { when(mDisplayOffloader.startOffload()).thenReturn(true); when(mDisplayOffloader.allowAutoBrightnessInDoze()).thenReturn(true); diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java index 241ffdc19ce4..4f74667f3ff2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java @@ -33,10 +33,8 @@ import static com.android.window.flags.Flags.FLAG_MULTI_CROP; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -48,7 +46,6 @@ import android.platform.test.annotations.RequiresFlagsEnabled; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; -import android.view.Display; import android.view.DisplayInfo; import android.view.WindowInsets; import android.view.WindowManager; @@ -95,7 +92,6 @@ public class WallpaperCropperTest { @Mock private Resources mResources; - private WallpaperCropper mWallpaperCropper; private static final Point PORTRAIT_ONE = new Point(500, 800); private static final Point PORTRAIT_TWO = new Point(400, 1000); @@ -171,7 +167,6 @@ public class WallpaperCropperTest { return getWallpaperTestDir(userId); }).when(() -> WallpaperUtils.getWallpaperDir(anyInt())); - mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper); } private File getWallpaperTestDir(int userId) { @@ -738,13 +733,13 @@ public class WallpaperCropperTest { DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = 2560; displayInfo.logicalHeight = 1044; + setUpWithDisplays(List.of(new Point(displayInfo.logicalWidth, displayInfo.logicalHeight))); doReturn(displayInfo).when(mWallpaperDisplayHelper).getDisplayInfo(eq(DEFAULT_DISPLAY)); WallpaperData wallpaperData = createWallpaperData(/* isStockWallpaper = */ false, new Point(100, 100)); - assertThat( - mWallpaperCropper.isWallpaperCompatibleForDisplay(DEFAULT_DISPLAY, - wallpaperData)).isTrue(); + assertThat(new WallpaperCropper(mWallpaperDisplayHelper).isWallpaperCompatibleForDisplay( + DEFAULT_DISPLAY, wallpaperData)).isTrue(); } // Test isWallpaperCompatibleForDisplay always return true for the stock wallpaper. @@ -755,49 +750,67 @@ public class WallpaperCropperTest { DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = 2560; displayInfo.logicalHeight = 1044; + setUpWithDisplays(List.of(new Point(displayInfo.logicalWidth, displayInfo.logicalHeight))); doReturn(displayInfo).when(mWallpaperDisplayHelper).getDisplayInfo(eq(displayId)); WallpaperData wallpaperData = createWallpaperData(/* isStockWallpaper = */ true, new Point(100, 100)); - assertThat( - mWallpaperCropper.isWallpaperCompatibleForDisplay(displayId, - wallpaperData)).isTrue(); + assertThat(new WallpaperCropper(mWallpaperDisplayHelper).isWallpaperCompatibleForDisplay( + displayId, wallpaperData)).isTrue(); } // Test isWallpaperCompatibleForDisplay wallpaper is suitable for the display and wallpaper // aspect ratio meets the hard-coded aspect ratio. @Test - public void isWallpaperCompatibleForDisplay_wallpaperSizeSuitableForDisplayAndMeetAspectRatio_returnTrue() + public void isWallpaperCompatibleForDisplay_wallpaperSizeLargerThanDisplayAndMeetAspectRatio_returnTrue() throws Exception { final int displayId = 2; DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = 2560; displayInfo.logicalHeight = 1044; + setUpWithDisplays(List.of(new Point(displayInfo.logicalWidth, displayInfo.logicalHeight))); doReturn(displayInfo).when(mWallpaperDisplayHelper).getDisplayInfo(eq(displayId)); WallpaperData wallpaperData = createWallpaperData(/* isStockWallpaper = */ false, new Point(4000, 3000)); - assertThat( - mWallpaperCropper.isWallpaperCompatibleForDisplay(displayId, - wallpaperData)).isTrue(); + assertThat(new WallpaperCropper(mWallpaperDisplayHelper).isWallpaperCompatibleForDisplay( + displayId, wallpaperData)).isTrue(); } - // Test isWallpaperCompatibleForDisplay wallpaper is not suitable for the display and wallpaper - // aspect ratio meets the hard-coded aspect ratio. + // Test isWallpaperCompatibleForDisplay wallpaper is smaller than the display but larger than + // the threshold and wallpaper aspect ratio meets the hard-coded aspect ratio. @Test - public void isWallpaperCompatibleForDisplay_wallpaperSizeNotSuitableForDisplayAndMeetAspectRatio_returnFalse() + public void isWallpaperCompatibleForDisplay_wallpaperSizeSmallerThanDisplayButBeyondThresholdAndMeetAspectRatio_returnTrue() throws Exception { final int displayId = 2; DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = 2560; displayInfo.logicalHeight = 1044; + setUpWithDisplays(List.of(new Point(displayInfo.logicalWidth, displayInfo.logicalHeight))); doReturn(displayInfo).when(mWallpaperDisplayHelper).getDisplayInfo(eq(displayId)); WallpaperData wallpaperData = createWallpaperData(/* isStockWallpaper = */ false, - new Point(1000, 500)); + new Point(2000, 900)); + + assertThat(new WallpaperCropper(mWallpaperDisplayHelper).isWallpaperCompatibleForDisplay( + displayId, wallpaperData)).isTrue(); + } + + // Test isWallpaperCompatibleForDisplay wallpaper is smaller than the display but larger than + // the threshold and wallpaper aspect ratio meets the hard-coded aspect ratio. + @Test + public void isWallpaperCompatibleForDisplay_wallpaperSizeSmallerThanDisplayButAboveThresholdAndMeetAspectRatio_returnFalse() + throws Exception { + final int displayId = 2; + DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = 2560; + displayInfo.logicalHeight = 1044; + setUpWithDisplays(List.of(new Point(displayInfo.logicalWidth, displayInfo.logicalHeight))); + doReturn(displayInfo).when(mWallpaperDisplayHelper).getDisplayInfo(eq(displayId)); + WallpaperData wallpaperData = createWallpaperData(/* isStockWallpaper = */ false, + new Point(2000, 800)); - assertThat( - mWallpaperCropper.isWallpaperCompatibleForDisplay(displayId, - wallpaperData)).isFalse(); + assertThat(new WallpaperCropper(mWallpaperDisplayHelper).isWallpaperCompatibleForDisplay( + displayId, wallpaperData)).isFalse(); } // Test isWallpaperCompatibleForDisplay wallpaper is suitable for the display and wallpaper @@ -809,13 +822,13 @@ public class WallpaperCropperTest { DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = 2560; displayInfo.logicalHeight = 1044; + setUpWithDisplays(List.of(new Point(displayInfo.logicalWidth, displayInfo.logicalHeight))); doReturn(displayInfo).when(mWallpaperDisplayHelper).getDisplayInfo(eq(displayId)); WallpaperData wallpaperData = createWallpaperData(/* isStockWallpaper = */ false, new Point(2000, 4000)); - assertThat( - mWallpaperCropper.isWallpaperCompatibleForDisplay(displayId, - wallpaperData)).isFalse(); + assertThat(new WallpaperCropper(mWallpaperDisplayHelper).isWallpaperCompatibleForDisplay( + displayId, wallpaperData)).isFalse(); } // Test isWallpaperCompatibleForDisplay, portrait display, wallpaper is suitable for the display @@ -827,24 +840,13 @@ public class WallpaperCropperTest { DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = 1044; displayInfo.logicalHeight = 2560; + setUpWithDisplays(List.of(new Point(displayInfo.logicalWidth, displayInfo.logicalHeight))); doReturn(displayInfo).when(mWallpaperDisplayHelper).getDisplayInfo(eq(displayId)); WallpaperData wallpaperData = createWallpaperData(/* isStockWallpaper = */ false, new Point(2000, 4000)); - assertThat( - mWallpaperCropper.isWallpaperCompatibleForDisplay(displayId, - wallpaperData)).isTrue(); - } - - private void mockDisplay(int displayId, Point displayResolution) { - final Display mockDisplay = mock(Display.class); - when(mockDisplay.getDisplayInfo(any(DisplayInfo.class))).thenAnswer(invocation -> { - DisplayInfo displayInfo = invocation.getArgument(0); - displayInfo.displayId = displayId; - displayInfo.logicalWidth = displayResolution.x; - displayInfo.logicalHeight = displayResolution.y; - return true; - }); + assertThat(new WallpaperCropper(mWallpaperDisplayHelper).isWallpaperCompatibleForDisplay( + displayId, wallpaperData)).isTrue(); } private WallpaperData createWallpaperData(boolean isStockWallpaper, Point wallpaperSize) diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java index b6f4e13cad8c..94ce72368a92 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java @@ -41,6 +41,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import android.annotation.NonNull; import android.app.ActivityManagerInternal; import android.app.AppOpsManager; import android.content.Context; @@ -52,8 +53,8 @@ import android.os.BatteryStats; import android.os.BatteryStatsInternal; import android.os.Handler; import android.os.IBinder; -import android.os.IWakeLockCallback; import android.os.IScreenTimeoutPolicyListener; +import android.os.IWakeLockCallback; import android.os.Looper; import android.os.PowerManager; import android.os.RemoteException; @@ -724,6 +725,7 @@ public class NotifierTest { final int uid = 1234; final int pid = 5678; + mNotifier.onWakeLockReleased(PowerManager.PARTIAL_WAKE_LOCK, "wakelockTag", "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null, exceptingCallback); @@ -789,6 +791,55 @@ public class NotifierTest { } @Test + public void test_wakeLockLogUsesWorkSource() { + createNotifier(); + clearInvocations(mWakeLockLog); + IWakeLockCallback exceptingCallback = new IWakeLockCallback.Stub() { + @Override public void onStateChanged(boolean enabled) throws RemoteException { + throw new RemoteException("Just testing"); + } + }; + + final int uid = 1234; + final int pid = 5678; + WorkSource worksource = new WorkSource(1212); + WorkSource worksource2 = new WorkSource(3131); + + mNotifier.onWakeLockAcquired(PowerManager.PARTIAL_WAKE_LOCK, "wakelockTag", + "my.package.name", uid, pid, worksource, /* historyTag= */ null, + exceptingCallback); + verify(mWakeLockLog).onWakeLockAcquired("wakelockTag", 1212, + PowerManager.PARTIAL_WAKE_LOCK, -1); + + // Release the wakelock + mNotifier.onWakeLockReleased(PowerManager.FULL_WAKE_LOCK, "wakelockTag2", + "my.package.name", uid, pid, worksource2, /* historyTag= */ null, + exceptingCallback); + verify(mWakeLockLog).onWakeLockReleased("wakelockTag2", 3131, -1); + + // clear the handler + mTestLooper.dispatchAll(); + + // Now test with improveWakelockLatency flag true + clearInvocations(mWakeLockLog); + when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true); + + mNotifier.onWakeLockAcquired(PowerManager.PARTIAL_WAKE_LOCK, "wakelockTag", + "my.package.name", uid, pid, worksource, /* historyTag= */ null, + exceptingCallback); + mTestLooper.dispatchAll(); + verify(mWakeLockLog).onWakeLockAcquired("wakelockTag", 1212, + PowerManager.PARTIAL_WAKE_LOCK, 1); + + // Release the wakelock + mNotifier.onWakeLockReleased(PowerManager.FULL_WAKE_LOCK, "wakelockTag2", + "my.package.name", uid, pid, worksource2, /* historyTag= */ null, + exceptingCallback); + mTestLooper.dispatchAll(); + verify(mWakeLockLog).onWakeLockReleased("wakelockTag2", 3131, 1); + } + + @Test public void test_notifierProcessesWorkSourceDeepCopy_OnWakelockChanging() throws RemoteException { when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true); @@ -943,15 +994,23 @@ public class NotifierTest { assertEquals(mNotifier.getWakelockMonitorTypeForLogging(PowerManager.PARTIAL_WAKE_LOCK), PowerManager.PARTIAL_WAKE_LOCK); assertEquals(mNotifier.getWakelockMonitorTypeForLogging( + PowerManager.DOZE_WAKE_LOCK), -1); + } + + @Test + public void getWakelockMonitorTypeForLogging_evaluateProximityLevel() { + // How proximity wakelock is evaluated depends on boolean configuration. Test both. + when(mResourcesSpy.getBoolean( + com.android.internal.R.bool.config_suspendWhenScreenOffDueToProximity)) + .thenReturn(false); + createNotifier(); + assertEquals(mNotifier.getWakelockMonitorTypeForLogging( PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK), PowerManager.PARTIAL_WAKE_LOCK); - assertEquals(mNotifier.getWakelockMonitorTypeForLogging( - PowerManager.DOZE_WAKE_LOCK), -1); when(mResourcesSpy.getBoolean( com.android.internal.R.bool.config_suspendWhenScreenOffDueToProximity)) .thenReturn(true); - createNotifier(); assertEquals(mNotifier.getWakelockMonitorTypeForLogging( PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK), -1); @@ -1238,7 +1297,7 @@ public class NotifierTest { } @Override - public WakeLockLog getWakeLockLog(Context context) { + public @NonNull WakeLockLog getWakeLockLog(Context context) { return mWakeLockLog; } diff --git a/services/tests/powerservicetests/src/com/android/server/power/WakeLockLogTest.java b/services/tests/powerservicetests/src/com/android/server/power/WakeLockLogTest.java index c1d7c7b4a4c2..534337ee9dbf 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/WakeLockLogTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/WakeLockLogTest.java @@ -27,6 +27,8 @@ import android.content.pm.PackageManager; import android.os.PowerManager; import android.os.Process; +import com.android.server.power.WakeLockLog.TagData; + import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -62,8 +64,9 @@ public class WakeLockLogTest { @Test public void testAddTwoItems_withNoEventTimeSupplied() { final int tagDatabaseSize = 128; + final int tagStartingSize = 16; final int logSize = 20; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); when(injectorSpy.currentTimeMillis()).thenReturn(1000L); log.onWakeLockAcquired("TagPartial", 101, @@ -93,8 +96,9 @@ public class WakeLockLogTest { @Test public void testAddTwoItems() { final int tagDatabaseSize = 128; + final int tagStartingSize = 16; final int logSize = 20; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); log.onWakeLockAcquired("TagPartial", 101, @@ -117,8 +121,9 @@ public class WakeLockLogTest { @Test public void testAddTwoItemsWithTimeReset() { final int tagDatabaseSize = 128; + final int tagStartingSize = 16; final int logSize = 20; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); log.onWakeLockAcquired("TagPartial", 101, PowerManager.PARTIAL_WAKE_LOCK, 1000L); @@ -136,9 +141,10 @@ public class WakeLockLogTest { @Test public void testAddTwoItemsWithTagOverwrite() { - final int tagDatabaseSize = 2; + final int tagDatabaseSize = 1; + final int tagStartingSize = 1; final int logSize = 20; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); log.onWakeLockAcquired("TagPartial", 101, PowerManager.PARTIAL_WAKE_LOCK, 1000L); @@ -157,8 +163,9 @@ public class WakeLockLogTest { @Test public void testAddFourItemsWithRingBufferOverflow() { final int tagDatabaseSize = 6; + final int tagStartingSize = 2; final int logSize = 10; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); // Wake lock 1 acquired - log size = 3 @@ -206,8 +213,9 @@ public class WakeLockLogTest { @Test public void testAddItemWithBadTag() { final int tagDatabaseSize = 6; + final int tagStartingSize = 2; final int logSize = 10; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); // Bad tag means it wont get written @@ -224,8 +232,9 @@ public class WakeLockLogTest { @Test public void testAddItemWithReducedTagName() { final int tagDatabaseSize = 6; + final int tagStartingSize = 2; final int logSize = 10; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); log.onWakeLockAcquired("*job*/com.one.two.3hree/.one..Last", 101, @@ -242,9 +251,10 @@ public class WakeLockLogTest { @Test public void testAddAcquireAndReleaseWithRepeatTagName() { - final int tagDatabaseSize = 6; + final int tagDatabaseSize = 5; + final int tagStartingSize = 5; final int logSize = 10; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); log.onWakeLockAcquired("HowdyTag", 101, PowerManager.PARTIAL_WAKE_LOCK, 1000L); @@ -263,8 +273,9 @@ public class WakeLockLogTest { @Test public void testAddAcquireAndReleaseWithTimeTravel() { final int tagDatabaseSize = 6; + final int tagStartingSize = 2; final int logSize = 10; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); log.onWakeLockAcquired("HowdyTag", 101, PowerManager.PARTIAL_WAKE_LOCK, 1100L); @@ -283,8 +294,9 @@ public class WakeLockLogTest { @Test public void testAddSystemWakelock() { final int tagDatabaseSize = 6; + final int tagStartingSize = 2; final int logSize = 10; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); log.onWakeLockAcquired("TagPartial", 101, @@ -302,8 +314,9 @@ public class WakeLockLogTest { @Test public void testAddItemWithNoPackageName() { final int tagDatabaseSize = 128; + final int tagStartingSize = 16; final int logSize = 20; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); when(mPackageManager.getPackagesForUid(101)).thenReturn(null); @@ -322,8 +335,9 @@ public class WakeLockLogTest { @Test public void testAddItemWithMultiplePackageNames() { final int tagDatabaseSize = 128; + final int tagStartingSize = 16; final int logSize = 20; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); when(mPackageManager.getPackagesForUid(101)).thenReturn( @@ -344,8 +358,9 @@ public class WakeLockLogTest { @Test public void testAddItemsWithRepeatOwnerUid_UsesCache() { final int tagDatabaseSize = 128; + final int tagStartingSize = 16; final int logSize = 20; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); log.onWakeLockAcquired("TagPartial", 101, @@ -375,8 +390,9 @@ public class WakeLockLogTest { @Test public void testAddItemsWithRepeatOwnerUid_SavedAcquisitions_UsesCache() { final int tagDatabaseSize = 128; + final int tagStartingSize = 16; final int logSize = 10; - TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, logSize)); + TestInjector injectorSpy = spy(new TestInjector(tagDatabaseSize, tagStartingSize, logSize)); WakeLockLog log = new WakeLockLog(injectorSpy, mContext); log.onWakeLockAcquired("TagPartial", 101, @@ -420,6 +436,34 @@ public class WakeLockLogTest { verify(mPackageManager, times(1)).getPackagesForUid(101); } + @Test + public void testTagDatabaseGrowsBeyondStartingSize() { + final int tagDatabaseSize = 3; + final int tagStartingSize = 1; + final int logSize = 10; + // start with size = 1 and max size + TestInjector injector = new TestInjector(tagDatabaseSize, tagStartingSize, logSize); + WakeLockLog.TagDatabase td = new WakeLockLog.TagDatabase(injector); + + // Add one + TagData data1 = td.findOrCreateTag("Tagname1", 1001, /* shouldCreate= */ true); + assertEquals(0, td.getTagIndex(data1)); + + // Check that it grows by adding 1 more + TagData data2 = td.findOrCreateTag("Tagname2", 1001, /* shouldCreate= */ true); + assertEquals(1, td.getTagIndex(data2)); + + // Lets add the last one to fill up the DB to maxSize + TagData data3 = td.findOrCreateTag("Tagname3", 1001, /* shouldCreate= */ true); + assertEquals(2, td.getTagIndex(data3)); + + // Adding a fourth one should replace the oldest one (Tagname1) + TagData data4 = td.findOrCreateTag("Tagname4", 1001, /* shouldCreate= */ true); + assertEquals(0, td.getTagIndex(data4)); + assertEquals(tagDatabaseSize, td.getTagIndex(data1)); + + } + private String dumpLog(WakeLockLog log, boolean includeTagDb) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); @@ -429,10 +473,12 @@ public class WakeLockLogTest { public static class TestInjector extends WakeLockLog.Injector { private final int mTagDatabaseSize; + private final int mTagStartingSize; private final int mLogSize; - public TestInjector(int tagDatabaseSize, int logSize) { + public TestInjector(int tagDatabaseSize, int tagStartingSize, int logSize) { mTagDatabaseSize = tagDatabaseSize; + mTagStartingSize = tagStartingSize; mLogSize = logSize; } @@ -442,6 +488,11 @@ public class WakeLockLogTest { } @Override + public int getTagDatabaseStartingSize() { + return mTagStartingSize; + } + + @Override public int getLogSize() { return mLogSize; } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java index 63c572af37b2..3565244d90b3 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java @@ -36,6 +36,8 @@ import android.content.Context; import android.media.AudioDeviceInfo; import android.media.AudioDevicePort; import android.media.AudioManager; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; @@ -67,6 +69,8 @@ import java.util.concurrent.Executor; public class HearingDevicePhoneCallNotificationControllerTest { @Rule public MockitoRule mockito = MockitoJUnit.rule(); + @Rule + public SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final String TEST_ADDRESS = "55:66:77:88:99:AA"; @@ -118,6 +122,7 @@ public class HearingDevicePhoneCallNotificationControllerTest { AudioManager.DEVICE_OUT_BLE_HEADSET); when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( new AudioDeviceInfo[]{hapDeviceInfo}); + when(mAudioManager.getCommunicationDevice()).thenReturn(hapDeviceInfo); when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo)); mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK); @@ -132,6 +137,7 @@ public class HearingDevicePhoneCallNotificationControllerTest { AudioManager.DEVICE_OUT_BLUETOOTH_A2DP); when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( new AudioDeviceInfo[]{a2dpDeviceInfo}); + when(mAudioManager.getCommunicationDevice()).thenReturn(a2dpDeviceInfo); when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(a2dpDeviceInfo)); mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK); @@ -146,6 +152,7 @@ public class HearingDevicePhoneCallNotificationControllerTest { AudioManager.DEVICE_OUT_BLE_HEADSET); when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( new AudioDeviceInfo[]{hapDeviceInfo}); + when(mAudioManager.getCommunicationDevice()).thenReturn(hapDeviceInfo); when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo)); mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK); @@ -155,6 +162,51 @@ public class HearingDevicePhoneCallNotificationControllerTest { eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH)); } + @Test + @EnableFlags(Flags.FLAG_HEARING_INPUT_CHANGE_WHEN_COMM_DEVICE) + public void onCallStateChanged_nonHearingDevice_offHookThenIdle_callAddAndRemoveListener() { + final ArgumentCaptor<AudioManager.OnCommunicationDeviceChangedListener> listenerCaptor = + ArgumentCaptor.forClass(AudioManager.OnCommunicationDeviceChangedListener.class); + AudioDeviceInfo a2dpDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS, + AudioManager.DEVICE_OUT_BLUETOOTH_A2DP); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[]{a2dpDeviceInfo}); + when(mAudioManager.getCommunicationDevice()).thenReturn(a2dpDeviceInfo); + + mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK); + mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_IDLE); + + verify(mAudioManager).addOnCommunicationDeviceChangedListener(any(Executor.class), + listenerCaptor.capture()); + verify(mAudioManager).removeOnCommunicationDeviceChangedListener( + eq(listenerCaptor.getValue())); + } + + + @Test + @EnableFlags(Flags.FLAG_HEARING_INPUT_CHANGE_WHEN_COMM_DEVICE) + public void onCallStateChanged_hearingDeviceFromCommunicationDeviceChanged_showNotification() { + final ArgumentCaptor<AudioManager.OnCommunicationDeviceChangedListener> listenerCaptor = + ArgumentCaptor.forClass(AudioManager.OnCommunicationDeviceChangedListener.class); + AudioDeviceInfo hapDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS, + AudioManager.DEVICE_OUT_BLE_HEADSET); + AudioDeviceInfo a2dpDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS, + AudioManager.DEVICE_OUT_BLUETOOTH_A2DP); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[]{a2dpDeviceInfo}); + when(mAudioManager.getCommunicationDevice()).thenReturn(a2dpDeviceInfo); + + mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK); + verify(mAudioManager).addOnCommunicationDeviceChangedListener(any(Executor.class), + listenerCaptor.capture()); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[]{hapDeviceInfo}); + listenerCaptor.getValue().onCommunicationDeviceChanged(hapDeviceInfo); + + verify(mNotificationManager).notify( + eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH), any()); + } + private AudioDeviceInfo createAudioDeviceInfo(String address, int type) { AudioDevicePort audioDevicePort = mock(AudioDevicePort.class); doReturn(type).when(audioDevicePort).type(); diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index 66d7611a29c6..d34d74d80882 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -11,13 +11,8 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -android_test { - name: "FrameworksUiServicesTests", - - // Include test java files - srcs: [ - "src/**/*.java", - ], +java_defaults { + name: "FrameworksUiServicesTests-defaults", static_libs: [ "compatibility-device-util-axt-minus-dexmaker", @@ -95,12 +90,72 @@ android_test { javacflags: ["-parameters"], } -test_module_config { - name: "FrameworksUiServicesTests_notification", - base: "FrameworksUiServicesTests", - test_suites: [ - "automotive-tests", - "device-tests", +// Utility files used by multiple tests +filegroup { + name: "shared-srcs", + srcs: [ + "src/android/app/ExampleActivity.java", + "src/android/app/NotificationSystemUtil.java", + "src/com/android/frameworks/tests/uiservices/DummyProvider.java", + "src/com/android/internal/logging/InstanceIdSequenceFake.java", + "src/com/android/server/UiServiceTestCase.java", + "src/com/android/server/notification/ZenChangeOrigin.java", + "src/com/android/server/notification/ZenModeEventLoggerFake.java", + ], + visibility: ["//visibility:private"], +} + +filegroup { + name: "notification-srcs", + srcs: [ + "src/**/Notification*.java", + "src/com/android/server/notification/*.java", + ], + visibility: ["//visibility:private"], +} + +filegroup { + name: "notification-zen-srcs", + srcs: [ + "src/android/app/NotificationManagerZenTest.java", + "src/com/android/server/notification/Zen*Test.java", + ], + visibility: ["//visibility:private"], +} + +android_test { + name: "FrameworksUiServicesTests", + + // Include test java files but not the notification & zen ones which are separated + srcs: [ + "src/**/*.java", + ], + + exclude_srcs: [ + ":notification-srcs", + ":notification-zen-srcs", + ], + + defaults: ["FrameworksUiServicesTests-defaults"], +} + +android_test { + name: "FrameworksUiServicesNotificationTests", + srcs: [ + ":notification-srcs", + ":shared-srcs", + ], + exclude_srcs: [":notification-zen-srcs"], + defaults: ["FrameworksUiServicesTests-defaults"], + test_config: "notification-tests.xml", +} + +android_test { + name: "FrameworksUiServicesZenTests", + srcs: [ + ":notification-zen-srcs", + ":shared-srcs", ], - exclude_annotations: ["androidx.test.filters.LargeTest"], + defaults: ["FrameworksUiServicesTests-defaults"], + test_config: "notification-zen-tests.xml", } diff --git a/services/tests/uiservicestests/AndroidTest.xml b/services/tests/uiservicestests/AndroidTest.xml index 11e8f090a8fa..93c8c72630f1 100644 --- a/services/tests/uiservicestests/AndroidTest.xml +++ b/services/tests/uiservicestests/AndroidTest.xml @@ -15,6 +15,7 @@ --> <configuration description="Runs Frameworks UI Services Tests."> <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="cleanup-apks" value="true" /> <option name="test-file-name" value="FrameworksUiServicesTests.apk" /> </target_preparer> diff --git a/services/tests/uiservicestests/notification-tests.xml b/services/tests/uiservicestests/notification-tests.xml new file mode 100644 index 000000000000..acfd844efe26 --- /dev/null +++ b/services/tests/uiservicestests/notification-tests.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 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. +--> +<configuration description="Runs Frameworks UI Services Tests (notifications subset)."> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="FrameworksUiServicesNotificationTests.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="FrameworksUiServicesNotificationTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.frameworks.tests.uiservices" /> + <option name="runner" value="android.testing.TestableInstrumentation" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/services/tests/uiservicestests/notification-zen-tests.xml b/services/tests/uiservicestests/notification-zen-tests.xml new file mode 100644 index 000000000000..01d8aab83d59 --- /dev/null +++ b/services/tests/uiservicestests/notification-zen-tests.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 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. +--> +<configuration description="Runs Frameworks UI Services Tests (zen mode subset)."> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="FrameworksUiServicesZenTests.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="FrameworksUiServicesZenTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.frameworks.tests.uiservices" /> + <option name="runner" value="android.testing.TestableInstrumentation" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 25fdedf32908..ed00a9e8e74b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -30,7 +30,10 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.os.Build.VERSION_CODES.P; import static android.os.Build.VERSION_CODES.Q; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.FLAG_ALLOWS_CONTENT_MODE_SWITCH; import static android.view.Display.FLAG_PRIVATE; +import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; +import static android.view.Display.FLAG_TRUSTED; import static android.view.DisplayCutout.BOUNDS_POSITION_TOP; import static android.view.DisplayCutout.fromBoundingRect; import static android.view.Surface.ROTATION_0; @@ -2923,6 +2926,63 @@ public class DisplayContentTests extends WindowTestsBase { assertFalse(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); } + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + @Test + public void testSetShouldShowSystemDecorations_shouldShowSystemDecorationsDisplay() { + // Set up a non-default display with FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS enabled + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.flags = FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; + final DisplayContent dc = createNewDisplay(displayInfo); + + dc.onDisplayInfoChangeApplied(); + assertFalse(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); + } + + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + @Test + public void testSetShouldShowSystemDecorations_notAllowContentModeSwitchDisplay() { + // Set up a non-default display without FLAG_ALLOWS_CONTENT_MODE_SWITCH enabled + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.flags = FLAG_TRUSTED; + final DisplayContent dc = createNewDisplay(displayInfo); + + dc.onDisplayInfoChangeApplied(); + assertFalse(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); + } + + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + @Test + public void testSetShouldShowSystemDecorations_untrustedDisplay() { + // Set up a non-default display without FLAG_TRUSTED enabled + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.flags = FLAG_ALLOWS_CONTENT_MODE_SWITCH; + final DisplayContent dc = createNewDisplay(displayInfo); + + dc.onDisplayInfoChangeApplied(); + assertFalse(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); + } + + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + @Test + public void testSetShouldShowSystemDecorations_nonDefaultNonPrivateDisplay() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.flags = (FLAG_ALLOWS_CONTENT_MODE_SWITCH | FLAG_TRUSTED); + final DisplayContent dc = createNewDisplay(displayInfo); + + spyOn(dc.mDisplay); + doReturn(false).when(dc.mDisplay).canHostTasks(); + dc.onDisplayInfoChangeApplied(); + assertFalse(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); + + doReturn(true).when(dc.mDisplay).canHostTasks(); + dc.onDisplayInfoChangeApplied(); + assertTrue(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); + } + @EnableFlags(FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) @Test public void testForcedDensityRatioSetForExternalDisplays_persistDensityScaleFlagEnabled() { @@ -2991,23 +3051,6 @@ public class DisplayContentTests extends WindowTestsBase { assertEquals(320, displayContent.mBaseDisplayDensity); } - @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) - @Test - public void testSetShouldShowSystemDecorations_nonDefaultNonPrivateDisplay() { - final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); - displayInfo.displayId = DEFAULT_DISPLAY + 1; - final DisplayContent dc = createNewDisplay(displayInfo); - - spyOn(dc.mDisplay); - doReturn(false).when(dc.mDisplay).canHostTasks(); - dc.onDisplayInfoChangeApplied(); - assertFalse(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); - - doReturn(true).when(dc.mDisplay).canHostTasks(); - dc.onDisplayInfoChangeApplied(); - assertTrue(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); - } - private void removeRootTaskTests(Runnable runnable) { final TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea(); final Task rootTask1 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN, |