diff options
582 files changed, 14953 insertions, 10506 deletions
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index 5e0428bab467..05d6e886b01a 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -29,8 +29,5 @@ hidden_api_txt_exclude_hook = ${REPO_ROOT}/frameworks/base/tools/hiddenapi/exclu ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES} -# This flag check hook runs only for "packages/SystemUI" subdirectory. If you want to include this check for other subdirectories, please modify flag_check.py. -flag_hook = ${REPO_ROOT}/frameworks/base/packages/SystemUI/flag_check.py --msg=${PREUPLOAD_COMMIT_MESSAGE} --files=${PREUPLOAD_FILES} --project=${REPO_PROJECT} - [Tool Paths] ktfmt = ${REPO_ROOT}/external/ktfmt/ktfmt.sh diff --git a/apct-tests/perftests/core/Android.bp b/apct-tests/perftests/core/Android.bp index 1e299cdf8002..f16f2caccd49 100644 --- a/apct-tests/perftests/core/Android.bp +++ b/apct-tests/perftests/core/Android.bp @@ -50,6 +50,7 @@ android_test { "junit-params", "core-tests-support", "guava", + "perfetto_trace_java_protos", ], libs: ["android.test.base.stubs.system"], diff --git a/apct-tests/perftests/core/src/android/os/TracePerfTest.java b/apct-tests/perftests/core/src/android/os/TracePerfTest.java index 0d64c390f4c2..bf7c96a3cb85 100644 --- a/apct-tests/perftests/core/src/android/os/TracePerfTest.java +++ b/apct-tests/perftests/core/src/android/os/TracePerfTest.java @@ -17,6 +17,8 @@ package android.os; +import static android.os.PerfettoTrace.Category; + import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.perftests.utils.ShellHelper; @@ -31,19 +33,35 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import perfetto.protos.DataSourceConfigOuterClass.DataSourceConfig; +import perfetto.protos.TraceConfigOuterClass.TraceConfig; +import perfetto.protos.TraceConfigOuterClass.TraceConfig.BufferConfig; +import perfetto.protos.TraceConfigOuterClass.TraceConfig.DataSource; +import perfetto.protos.TrackEventConfigOuterClass.TrackEventConfig; + @RunWith(AndroidJUnit4.class) public class TracePerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + private static final String FOO = "foo"; + private static final Category FOO_CATEGORY = new Category(FOO); + private static PerfettoTrace.Session sPerfettoSession; + @BeforeClass public static void startTracing() { ShellHelper.runShellCommandRaw("atrace -c --async_start -a *"); + PerfettoTrace.register(false /* isBackendInProcess */); + FOO_CATEGORY.register(); + sPerfettoSession = new PerfettoTrace.Session(false /* isBackendInProcess */, + getTraceConfig(FOO).toByteArray()); } @AfterClass public static void endTracing() { ShellHelper.runShellCommandRaw("atrace --async_stop"); + FOO_CATEGORY.unregister(); + sPerfettoSession.close(); } @Before @@ -84,4 +102,61 @@ public class TracePerfTest { Trace.setCounter("testCounter", 123); } } + + @Test + public void testInstant() { + Trace.instant(Trace.TRACE_TAG_APP, "testInstantA"); + + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + Trace.instant(Trace.TRACE_TAG_APP, "testInstantA"); + } + } + + @Test + public void testInstantPerfetto() { + PerfettoTrace.instant(FOO_CATEGORY, "testInstantP").emit(); + + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + PerfettoTrace.instant(FOO_CATEGORY, "testInstantP").emit(); + } + } + + @Test + public void testInstantPerfettoWithArgs() { + PerfettoTrace.instant(FOO_CATEGORY, "testInstantP") + .addArg("foo", "bar") + .addFlow(1) + .emit(); + + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + PerfettoTrace.instant(FOO_CATEGORY, "testInstantP") + .addArg("foo", "bar") + .addFlow(1) + .emit(); + } + } + + private static TraceConfig getTraceConfig(String cat) { + BufferConfig bufferConfig = BufferConfig.newBuilder().setSizeKb(1024).build(); + TrackEventConfig trackEventConfig = TrackEventConfig + .newBuilder() + .addEnabledCategories(cat) + .build(); + DataSourceConfig dsConfig = DataSourceConfig + .newBuilder() + .setName("track_event") + .setTargetBuffer(0) + .setTrackEventConfig(trackEventConfig) + .build(); + DataSource ds = DataSource.newBuilder().setConfig(dsConfig).build(); + TraceConfig traceConfig = TraceConfig + .newBuilder() + .addBuffers(bufferConfig) + .addDataSources(ds) + .build(); + return traceConfig; + } } diff --git a/core/api/current.txt b/core/api/current.txt index e9a63f74d59f..21929658cbb9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -8893,8 +8893,8 @@ package android.app.appfunctions { } @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager { - method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>); - method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method @RequiresPermission(value=android.Manifest.permission.EXECUTE_APP_FUNCTIONS, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>); + method @RequiresPermission(value=android.Manifest.permission.EXECUTE_APP_FUNCTIONS, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void isAppFunctionEnabled(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0 @@ -19346,7 +19346,7 @@ package android.hardware.biometrics { public class BiometricManager { method @Deprecated @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public int canAuthenticate(); method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public int canAuthenticate(int); - method @FlaggedApi("android.hardware.biometrics.last_authentication_time") @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public long getLastAuthenticationTime(int); + method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public long getLastAuthenticationTime(int); method @NonNull @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public android.hardware.biometrics.BiometricManager.Strings getStrings(int); field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1 field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE = 20; // 0x14 @@ -19354,7 +19354,7 @@ package android.hardware.biometrics { field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = 21; // 0x15 field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf - field @FlaggedApi("android.hardware.biometrics.last_authentication_time") public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL + field public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL field public static final int BIOMETRIC_SUCCESS = 0; // 0x0 } @@ -19407,7 +19407,7 @@ package android.hardware.biometrics { field public static final int BIOMETRIC_ERROR_UNABLE_TO_PROCESS = 2; // 0x2 field public static final int BIOMETRIC_ERROR_USER_CANCELED = 10; // 0xa field public static final int BIOMETRIC_ERROR_VENDOR = 8; // 0x8 - field @FlaggedApi("android.hardware.biometrics.last_authentication_time") public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL + field public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL } public abstract static class BiometricPrompt.AuthenticationCallback { @@ -20785,6 +20785,7 @@ package android.hardware.display { method public void registerDisplayListener(android.hardware.display.DisplayManager.DisplayListener, android.os.Handler); method @FlaggedApi("com.android.server.display.feature.flags.display_listener_performance_improvements") public void registerDisplayListener(@NonNull java.util.concurrent.Executor, long, @NonNull android.hardware.display.DisplayManager.DisplayListener); method public void unregisterDisplayListener(android.hardware.display.DisplayManager.DisplayListener); + field @FlaggedApi("com.android.server.display.feature.flags.display_category_built_in") public static final String DISPLAY_CATEGORY_BUILT_IN_DISPLAYS = "android.hardware.display.category.BUILT_IN_DISPLAYS"; field public static final String DISPLAY_CATEGORY_PRESENTATION = "android.hardware.display.category.PRESENTATION"; field @FlaggedApi("com.android.server.display.feature.flags.display_listener_performance_improvements") public static final long EVENT_TYPE_DISPLAY_ADDED = 1L; // 0x1L field @FlaggedApi("com.android.server.display.feature.flags.display_listener_performance_improvements") public static final long EVENT_TYPE_DISPLAY_CHANGED = 4L; // 0x4L @@ -21750,6 +21751,7 @@ package android.media { field public static final int CHANNEL_IN_X_AXIS = 2048; // 0x800 field public static final int CHANNEL_IN_Y_AXIS = 4096; // 0x1000 field public static final int CHANNEL_IN_Z_AXIS = 8192; // 0x2000 + field @FlaggedApi("android.media.audio.sony_360ra_mpegh_3d_format") public static final int CHANNEL_OUT_13POINT0 = 30136348; // 0x1cbd81c field public static final int CHANNEL_OUT_5POINT1 = 252; // 0xfc field public static final int CHANNEL_OUT_5POINT1POINT2 = 3145980; // 0x3000fc field public static final int CHANNEL_OUT_5POINT1POINT4 = 737532; // 0xb40fc diff --git a/core/api/system-current.txt b/core/api/system-current.txt index bfbdb7276be0..ab824119d643 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -150,7 +150,6 @@ package android { field @FlaggedApi("com.android.window.flags.untrusted_embedding_any_app_permission") public static final String EMBED_ANY_APP_IN_UNTRUSTED_MODE = "android.permission.EMBED_ANY_APP_IN_UNTRUSTED_MODE"; field @FlaggedApi("android.content.pm.emergency_install_permission") public static final String EMERGENCY_INSTALL_PACKAGES = "android.permission.EMERGENCY_INSTALL_PACKAGES"; field public static final String ENTER_CAR_MODE_PRIORITIZED = "android.permission.ENTER_CAR_MODE_PRIORITIZED"; - field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXECUTE_APP_FUNCTIONS_TRUSTED = "android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED"; field public static final String EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS = "android.permission.EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS"; field public static final String FORCE_BACK = "android.permission.FORCE_BACK"; field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES"; @@ -16042,7 +16041,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isAnyRadioPoweredOn(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isApnMetered(int); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isApplicationOnUicc(int); - method @FlaggedApi("com.android.internal.telephony.flags.enable_identifier_disclosure_transparency") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isCellularIdentifierDisclosureNotificationsEnabled(); + method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isCellularIdentifierDisclosureNotificationsEnabled(); method public boolean isDataConnectivityPossible(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isDataEnabledForApn(int); method @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isDomainSelectionSupported(); @@ -16093,7 +16092,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataActivationState(int); method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataEnabled(int, boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataRoamingEnabled(boolean); - method @FlaggedApi("com.android.internal.telephony.flags.enable_identifier_disclosure_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableCellularIdentifierDisclosureNotifications(boolean); + method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableCellularIdentifierDisclosureNotifications(boolean); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.telephony.PinResult setIccLockEnabled(boolean, @NonNull String); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMobileDataPolicyEnabled(int, boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMultiSimCarrierRestriction(boolean); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 7c1c86823110..0b0738ee14dc 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -909,6 +909,14 @@ package android.companion { method @NonNull public android.companion.AssociationInfo.Builder setTimeApproved(long); } + public final class AssociationRequest implements android.os.Parcelable { + method public boolean isSkipRoleGrant(); + } + + public static final class AssociationRequest.Builder { + method @NonNull @RequiresPermission(android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES) public android.companion.AssociationRequest.Builder setSkipRoleGrant(boolean); + } + public final class CompanionDeviceManager { method @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public void enableSecureTransport(boolean); } @@ -1727,6 +1735,7 @@ package android.hardware.display { method @RequiresPermission(android.Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS) public void setShouldAlwaysRespectAppRequestedMode(boolean); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setUserDisabledHdrTypes(@NonNull int[]); method @RequiresPermission(android.Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS) public boolean shouldAlwaysRespectAppRequestedMode(); + field public static final String DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED = "android.hardware.display.category.ALL_INCLUDING_DISABLED"; field public static final String DISPLAY_CATEGORY_REAR = "android.hardware.display.category.REAR"; field public static final String HDR_OUTPUT_CONTROL_FLAG = "enable_hdr_output_control"; field public static final int SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS = 2; // 0x2 diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 7b9ec4a7821e..2d7ed46fe64a 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -3959,10 +3959,20 @@ public final class ActivityThread extends ClientTransactionHandler /** Converts a process state to a VM process state. */ private static int toVmProcessState(int processState) { - final int state = ActivityManager.isProcStateJankPerceptible(processState) - ? VM_PROCESS_STATE_JANK_PERCEPTIBLE - : VM_PROCESS_STATE_JANK_IMPERCEPTIBLE; - return state; + if (ActivityManager.isProcStateJankPerceptible(processState)) { + return VM_PROCESS_STATE_JANK_PERCEPTIBLE; + } + + if (Flags.jankPerceptibleNarrow()) { + // Unlike other persistent processes, system server is often on + // the critical path for application startup. Mark it explicitly + // as jank perceptible regardless of processState. + if (isSystem()) { + return VM_PROCESS_STATE_JANK_PERCEPTIBLE; + } + } + + return VM_PROCESS_STATE_JANK_IMPERCEPTIBLE; } /** Update VM state based on ActivityManager.PROCESS_STATE_* constants. */ diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index e0a937156906..9d1d9c7b69de 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -162,7 +162,7 @@ public final class AutomaticZenRule implements Parcelable { * both to fields in the rule itself (such as its name) and items with sub-fields. * @hide */ - public static final int MAX_STRING_LENGTH = 1000; + public static final int MAX_STRING_LENGTH = 500; /** * The maximum string length for the trigger description rule, given UI constraints. diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index b5ac4e78c7ad..d91838c4cc2b 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -205,6 +205,15 @@ public class KeyguardManager { public static final String EXTRA_DISALLOW_BIOMETRICS_IF_POLICY_EXISTS = "check_dpm"; /** + * When switching to a secure user, system server will expect a callback when the UI has + * completed the switch. + * + * @hide + */ + public static final String LOCK_ON_USER_SWITCH_CALLBACK = "onSwitchCallback"; + + + /** * * Password lock type, see {@link #setLock} * diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index ba4914954223..c6de9a37f168 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5999,8 +5999,10 @@ public class Notification implements Parcelable setHeaderlessVerticalMargins(contentView, p, hasSecondLine); // Update margins to leave space for the top line (but not for headerless views like - // HUNS, which use a different layout that already accounts for that). - if (Flags.notificationsRedesignTemplates() && !p.mHeaderless) { + // HUNS, which use a different layout that already accounts for that). Templates that + // have content that will be displayed under the small icon also use a different margin. + if (Flags.notificationsRedesignTemplates() + && !p.mHeaderless && !p.mHasContentInLeftMargin) { int margin = getContentMarginTop(mContext, R.dimen.notification_2025_content_margin_top); contentView.setViewLayoutMargin(R.id.notification_main_column, @@ -9502,7 +9504,8 @@ public class Notification implements Parcelable .text(null) .hideLeftIcon(isOneToOne) .hideRightIcon(hideRightIcons || isOneToOne) - .headerTextSecondary(isHeaderless ? null : conversationTitle); + .headerTextSecondary(isHeaderless ? null : conversationTitle) + .hasContentInLeftMargin(true); RemoteViews contentView = mBuilder.applyStandardTemplateWithActions( isConversationLayout ? mBuilder.getConversationLayoutResource() @@ -14673,6 +14676,7 @@ public class Notification implements Parcelable Icon mPromotedPicture; boolean mCallStyleActions; boolean mAllowTextWithProgress; + boolean mHasContentInLeftMargin; int mTitleViewId; int mTextViewId; @Nullable CharSequence mTitle; @@ -14698,6 +14702,7 @@ public class Notification implements Parcelable mPromotedPicture = null; mCallStyleActions = false; mAllowTextWithProgress = false; + mHasContentInLeftMargin = false; mTitleViewId = R.id.title; mTextViewId = R.id.text; mTitle = null; @@ -14764,6 +14769,11 @@ public class Notification implements Parcelable return this; } + public StandardTemplateParams hasContentInLeftMargin(boolean hasContentInLeftMargin) { + mHasContentInLeftMargin = hasContentInLeftMargin; + return this; + } + final StandardTemplateParams hideSnoozeButton(boolean hideSnoozeButton) { this.mHideSnoozeButton = hideSnoozeButton; return this; diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index 660d88007c9a..e9b2ffcdf4cc 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -2321,12 +2321,14 @@ public class PropertyInvalidatedCache<Query, Result> { @GuardedBy("mLock") private int mBlockHash = 0; - // The number of nonces that the native layer can hold. This is maintained for debug and - // logging. - private final int mMaxNonce; + // The number of nonces that the native layer can hold. This is maintained for debug, + // logging, and testing. + @VisibleForTesting + public final int mMaxNonce; // The size of the native byte block. - private final int mMaxByte; + @VisibleForTesting + public final int mMaxByte; /** @hide */ @VisibleForTesting @@ -2483,18 +2485,20 @@ public class PropertyInvalidatedCache<Query, Result> { } } - static final AtomicLong sStoreCount = new AtomicLong(); - - // Add a string to the local copy of the block and write the block to shared memory. // Return the index of the new string. If the string has already been recorded, the - // shared memory is not updated but the index of the existing string is returned. + // shared memory is not updated but the index of the existing string is returned. Only + // mMaxNonce strings can be stored; if mMaxNonce strings have already been allocated, + // the method throws. public int storeName(@NonNull String str) { synchronized (mLock) { Integer handle = mStringHandle.get(str); if (handle == null) { throwIfImmutable(); throwIfBadString(str); + if (mHighestIndex + 1 >= mMaxNonce) { + throw new RuntimeException("nonce limit exceeded"); + } byte[] block = new byte[mMaxByte]; nativeGetByteBlock(mPtr, 0, block); appendStringToMapLocked(str, block); diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 8021ab4865af..ba8fbc121e8d 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -128,7 +128,7 @@ public final class UiAutomation { private static final String LOG_TAG = UiAutomation.class.getSimpleName(); private static final boolean DEBUG = false; - private static final boolean VERBOSE = false; + private static final boolean VERBOSE = Build.IS_DEBUGGABLE; private static final int CONNECTION_ID_UNDEFINED = -1; diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java index 6fd8db995368..0a3891fe47a1 100644 --- a/core/java/android/app/appfunctions/AppFunctionManager.java +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -72,10 +72,10 @@ import java.util.concurrent.Executor; * <p>To execute an app function, the caller app can retrieve the {@code functionIdentifier} from * the {@code AppFunctionStaticMetadata} document and use it to build an {@link * ExecuteAppFunctionRequest}. Then, invoke {@link #executeAppFunction} with the request to execute - * the app function. Callers need the {@code android.permission.EXECUTE_APP_FUNCTIONS} or {@code - * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} permission to execute app functions from other - * apps. An app can always execute its own app functions and doesn't need these permissions. - * AppFunction SDK provides a convenient way to achieve this and is the preferred method. + * the app function. Callers need the {@code android.permission.EXECUTE_APP_FUNCTIONS} permission to + * execute app functions from other apps. An app can always execute its own app functions and + * doesn't need these permissions. AppFunction SDK provides a convenient way to achieve this and + * is the preferred method. * * <h3>Example</h3> * @@ -141,32 +141,24 @@ public final class AppFunctionManager { * Executes the app function. * * <p>Note: Applications can execute functions they define. To execute functions defined in - * another component, apps would need to have {@code - * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code - * android.permission.EXECUTE_APP_FUNCTIONS}. + * another component, apps would need to have the permission + * {@code android.permission.EXECUTE_APP_FUNCTIONS}. * * @param request the request to execute the app function * @param executor the executor to run the callback * @param cancellationSignal the cancellation signal to cancel the execution. * @param callback the callback to receive the function execution result or error. * <p>If the calling app does not own the app function or does not have {@code - * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code * android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code * AppFunctionException.ERROR_DENIED}. - * <p>If the caller only has {@code android.permission.EXECUTE_APP_FUNCTIONS} but the - * function requires {@code android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED}, the execution + * <p>If the caller only has {@code android.permission.EXECUTE_APP_FUNCTIONS}, the execution * result will contain {@code AppFunctionException.ERROR_DENIED} * <p>If the function requested for execution is disabled, then the execution result will * contain {@code AppFunctionException.ERROR_DISABLED} * <p>If the cancellation signal is issued, the operation is cancelled and no response is * returned to the caller. */ - @RequiresPermission( - anyOf = { - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - Manifest.permission.EXECUTE_APP_FUNCTIONS - }, - conditional = true) + @RequiresPermission(value = Manifest.permission.EXECUTE_APP_FUNCTIONS, conditional = true) @UserHandleAware public void executeAppFunction( @NonNull ExecuteAppFunctionRequest request, @@ -222,9 +214,8 @@ public final class AppFunctionManager { * Returns a boolean through a callback, indicating whether the app function is enabled. * * <p>This method can only check app functions owned by the caller, or those where the caller - * has visibility to the owner package and holds either the {@link - * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link - * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission. + * has visibility to the owner package and holds the + * {@link Manifest.permission#EXECUTE_APP_FUNCTIONS} permission. * * <p>If the operation fails, the callback's {@link OutcomeReceiver#onError} is called with * errors: @@ -241,12 +232,7 @@ public final class AppFunctionManager { * @param executor the executor to run the request * @param callback the callback to receive the function enabled check result */ - @RequiresPermission( - anyOf = { - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - Manifest.permission.EXECUTE_APP_FUNCTIONS - }, - conditional = true) + @RequiresPermission(value = Manifest.permission.EXECUTE_APP_FUNCTIONS, conditional = true) public void isAppFunctionEnabled( @NonNull String functionIdentifier, @NonNull String targetPackage, diff --git a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java index 64dece99c5d1..cc3ca03f423d 100644 --- a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java +++ b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java @@ -54,9 +54,8 @@ public class AppFunctionManagerHelper { * Returns (through a callback) a boolean indicating whether the app function is enabled. * * This method can only check app functions owned by the caller, or those where the caller - * has visibility to the owner package and holds either the {@link - * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link - * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission. + * has visibility to the owner package and holds the {@link + * Manifest.permission#EXECUTE_APP_FUNCTIONS} permission. * * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: * diff --git a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java index 3ddda228d145..7743d4862b51 100644 --- a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java +++ b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java @@ -90,8 +90,7 @@ public class AppFunctionRuntimeMetadata extends GenericDocument { * we need to have per-package app function schemas. * * <p>This schema should be set visible to callers from the package owner itself and for callers - * with {@link android.Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link - * android.Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permissions. + * with the permission {@link android.Manifest.permission#EXECUTE_APP_FUNCTIONS}. * * @param packageName The package name to create a schema for. */ @@ -105,9 +104,8 @@ public class AppFunctionRuntimeMetadata extends GenericDocument { /** * Creates a parent schema for all app function runtime schemas. * - * <p>This schema should be set visible to the owner itself and for callers with {@link - * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@link - * android.permission.EXECUTE_APP_FUNCTIONS} permissions. + * <p>This schema should be set visible to the owner itself and for callers with + * the permission {@link android.permission.EXECUTE_APP_FUNCTIONS}. */ public static AppSearchSchema createParentAppFunctionRuntimeSchema() { return getAppFunctionRuntimeSchemaBuilder(RUNTIME_SCHEMA_TYPE).build(); diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl index 72335e40c207..098e1fe8b516 100644 --- a/core/java/android/app/appfunctions/IAppFunctionManager.aidl +++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl @@ -34,7 +34,7 @@ interface IAppFunctionManager { * @param request the request to execute an app function. * @param callback the callback to report the result. */ - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf = {android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional = true)") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = android.Manifest.permission.EXECUTE_APP_FUNCTIONS, conditional = true)") ICancellationSignal executeAppFunction( in ExecuteAppFunctionAidlRequest request, in IExecuteAppFunctionCallback callback diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java index a098a6067491..67dea321a446 100644 --- a/core/java/android/companion/AssociationRequest.java +++ b/core/java/android/companion/AssociationRequest.java @@ -16,6 +16,7 @@ package android.companion; +import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES; import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED; import static com.android.internal.util.CollectionUtils.emptyIfNull; @@ -28,7 +29,10 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.StringDef; +import android.annotation.SuppressLint; +import android.annotation.TestApi; import android.annotation.UserIdInt; +import android.app.KeyguardManager; import android.compat.annotation.UnsupportedAppUsage; import android.graphics.drawable.Icon; import android.os.Build; @@ -214,6 +218,11 @@ public final class AssociationRequest implements Parcelable { private final boolean mForceConfirmation; /** + * Whether to skip the role grant, permission checks and consent dialog. + */ + private final boolean mSkipRoleGrant; + + /** * The app package name of the application the association will belong to. * Populated by the system. * @hide @@ -283,6 +292,7 @@ public final class AssociationRequest implements Parcelable { @Nullable CharSequence displayName, boolean selfManaged, boolean forceConfirmation, + boolean skipRoleGrant, @Nullable Icon deviceIcon) { mSingleDevice = singleDevice; mDeviceFilters = requireNonNull(deviceFilters); @@ -290,6 +300,7 @@ public final class AssociationRequest implements Parcelable { mDisplayName = displayName; mSelfManaged = selfManaged; mForceConfirmation = forceConfirmation; + mSkipRoleGrant = skipRoleGrant; mCreationTime = System.currentTimeMillis(); mDeviceIcon = deviceIcon; } @@ -333,6 +344,18 @@ public final class AssociationRequest implements Parcelable { } /** + * Whether to skip the role grant, permission checks and consent dialog. + * + * @see Builder#setSkipRoleGrant(boolean) + * @hide + */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi + public boolean isSkipRoleGrant() { + return mSkipRoleGrant; + } + + /** * Whether only a single device should match the provided filter. * * When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac @@ -407,6 +430,7 @@ public final class AssociationRequest implements Parcelable { private CharSequence mDisplayName; private boolean mSelfManaged = false; private boolean mForceConfirmation = false; + private boolean mSkipRoleGrant = false; private Icon mDeviceIcon = null; public Builder() {} @@ -494,6 +518,27 @@ public final class AssociationRequest implements Parcelable { } /** + * Do not attempt to grant the role corresponding to the device profile. + * + * <p>This will skip the permission checks and consent dialog but will not fail if the + * role cannot be granted.</p> + * + * <p>Requires that the device not to have secure lock screen and that there no locked SIM + * card. See {@link KeyguardManager#isKeyguardSecure()}</p> + * + * @hide + */ + @RequiresPermission(ASSOCIATE_COMPANION_DEVICES) + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi + @NonNull + public Builder setSkipRoleGrant(boolean skipRoleGrant) { + checkNotUsed(); + mSkipRoleGrant = skipRoleGrant; + return this; + } + + /** * Set the device icon for the self-managed device and to display the icon in the * self-managed association dialog. * <p>The given device icon will be resized to 24dp x 24dp. @@ -521,7 +566,8 @@ public final class AssociationRequest implements Parcelable { + "provide the display name of the device"); } return new AssociationRequest(mSingleDevice, emptyIfNull(mDeviceFilters), - mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation, mDeviceIcon); + mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation, mSkipRoleGrant, + mDeviceIcon); } } @@ -597,6 +643,7 @@ public final class AssociationRequest implements Parcelable { + ", associatedDevice = " + mAssociatedDevice + ", selfManaged = " + mSelfManaged + ", forceConfirmation = " + mForceConfirmation + + ", skipRoleGrant = " + mSkipRoleGrant + ", packageName = " + mPackageName + ", userId = " + mUserId + ", deviceProfilePrivilegesDescription = " + mDeviceProfilePrivilegesDescription @@ -617,6 +664,7 @@ public final class AssociationRequest implements Parcelable { && Objects.equals(mAssociatedDevice, that.mAssociatedDevice) && mSelfManaged == that.mSelfManaged && mForceConfirmation == that.mForceConfirmation + && mSkipRoleGrant == that.mSkipRoleGrant && Objects.equals(mPackageName, that.mPackageName) && mUserId == that.mUserId && Objects.equals(mDeviceProfilePrivilegesDescription, @@ -637,6 +685,7 @@ public final class AssociationRequest implements Parcelable { _hash = 31 * _hash + Objects.hashCode(mAssociatedDevice); _hash = 31 * _hash + Boolean.hashCode(mSelfManaged); _hash = 31 * _hash + Boolean.hashCode(mForceConfirmation); + _hash = 31 * _hash + Boolean.hashCode(mSkipRoleGrant); _hash = 31 * _hash + Objects.hashCode(mPackageName); _hash = 31 * _hash + mUserId; _hash = 31 * _hash + Objects.hashCode(mDeviceProfilePrivilegesDescription); @@ -659,6 +708,7 @@ public final class AssociationRequest implements Parcelable { if (mAssociatedDevice != null) flg |= 0x40; if (mPackageName != null) flg |= 0x80; if (mDeviceProfilePrivilegesDescription != null) flg |= 0x100; + if (mSkipRoleGrant) flg |= 0x200; dest.writeInt(flg); dest.writeParcelableList(mDeviceFilters, flags); @@ -692,6 +742,7 @@ public final class AssociationRequest implements Parcelable { boolean selfManaged = (flg & 0x2) != 0; boolean forceConfirmation = (flg & 0x4) != 0; boolean skipPrompt = (flg & 0x8) != 0; + boolean skipRoleGrant = (flg & 0x200) != 0; List<DeviceFilter<?>> deviceFilters = new ArrayList<>(); in.readParcelableList(deviceFilters, DeviceFilter.class.getClassLoader(), (Class<android.companion.DeviceFilter<?>>) (Class<?>) @@ -714,6 +765,7 @@ public final class AssociationRequest implements Parcelable { this.mAssociatedDevice = associatedDevice; this.mSelfManaged = selfManaged; this.mForceConfirmation = forceConfirmation; + this.mSkipRoleGrant = skipRoleGrant; this.mPackageName = packageName; this.mUserId = userId; com.android.internal.util.AnnotationValidations.validate( diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 469688265ae8..0312ad7a739a 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -12426,8 +12426,8 @@ public class Intent implements Parcelable, Cloneable { } private void collectNestedIntentKeysRecur(Set<Intent> visited, boolean forceUnparcel) { - addExtendedFlags(EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED); if (mExtras != null && (forceUnparcel || !mExtras.isParcelled()) && !mExtras.isEmpty()) { + addExtendedFlags(EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED); for (String key : mExtras.keySet()) { Object value; try { diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 219b20428d7a..b1ea6e9b68eb 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -2628,6 +2628,15 @@ public class PackageParser { return Build.VERSION_CODES.CUR_DEVELOPMENT; } + // STOPSHIP: hack for the pre-release SDK + if (platformSdkCodenames.length == 0 + && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals( + targetCode)) { + Slog.w(TAG, "Package requires development platform " + targetCode + + ", returning current version " + Build.VERSION.SDK_INT); + return Build.VERSION.SDK_INT; + } + // Otherwise, we're looking at an incompatible pre-release SDK. if (platformSdkCodenames.length > 0) { outError[0] = "Requires development platform " + targetCode @@ -2699,6 +2708,15 @@ public class PackageParser { return Build.VERSION_CODES.CUR_DEVELOPMENT; } + // STOPSHIP: hack for the pre-release SDK + if (platformSdkCodenames.length == 0 + && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals( + minCode)) { + Slog.w(TAG, "Package requires min development platform " + minCode + + ", returning current version " + Build.VERSION.SDK_INT); + return Build.VERSION.SDK_INT; + } + // Otherwise, we're looking at an incompatible pre-release SDK. if (platformSdkCodenames.length > 0) { outError[0] = "Requires development platform " + minCode diff --git a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java index e30f871b68eb..d2d3a6840acc 100644 --- a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java +++ b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java @@ -316,6 +316,15 @@ public class FrameworkParsingPackageUtils { return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); } + // STOPSHIP: hack for the pre-release SDK + if (platformSdkCodenames.length == 0 + && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals( + minCode)) { + Slog.w(TAG, "Parsed package requires min development platform " + minCode + + ", returning current version " + Build.VERSION.SDK_INT); + return input.success(Build.VERSION.SDK_INT); + } + // Otherwise, we're looking at an incompatible pre-release SDK. if (platformSdkCodenames.length > 0) { return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, @@ -398,19 +407,27 @@ public class FrameworkParsingPackageUtils { return input.success(targetVers); } + // If it's a pre-release SDK and the codename matches this platform, it + // definitely targets this SDK. + if (matchTargetCode(platformSdkCodenames, targetCode)) { + return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); + } + + // STOPSHIP: hack for the pre-release SDK + if (platformSdkCodenames.length == 0 + && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals( + targetCode)) { + Slog.w(TAG, "Parsed package requires development platform " + targetCode + + ", returning current version " + Build.VERSION.SDK_INT); + return input.success(Build.VERSION.SDK_INT); + } + try { if (allowUnknownCodenames && UnboundedSdkLevel.isAtMost(targetCode)) { return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); } } catch (IllegalArgumentException e) { - // isAtMost() throws it when encountering an older SDK codename - return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, e.getMessage()); - } - - // If it's a pre-release SDK and the codename matches this platform, it - // definitely targets this SDK. - if (matchTargetCode(platformSdkCodenames, targetCode)) { - return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); + return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, "Bad package SDK"); } // Otherwise, we're looking at an incompatible pre-release SDK. diff --git a/core/java/android/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java index 875adbdf3913..7dc6afba3f1c 100644 --- a/core/java/android/hardware/biometrics/BiometricConstants.java +++ b/core/java/android/hardware/biometrics/BiometricConstants.java @@ -334,6 +334,5 @@ public interface BiometricConstants { * Returned from {@link BiometricManager#getLastAuthenticationTime(int)} when there has * been no successful authentication for the given authenticator since boot. */ - @FlaggedApi(Flags.FLAG_LAST_AUTHENTICATION_TIME) long BIOMETRIC_NO_AUTHENTICATION = -1; } diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java index c690c67ed79f..cefe20c15ced 100644 --- a/core/java/android/hardware/biometrics/BiometricManager.java +++ b/core/java/android/hardware/biometrics/BiometricManager.java @@ -116,7 +116,6 @@ public class BiometricManager { * Returned from {@link BiometricManager#getLastAuthenticationTime(int)} when no matching * successful authentication has been performed since boot. */ - @FlaggedApi(Flags.FLAG_LAST_AUTHENTICATION_TIME) public static final long BIOMETRIC_NO_AUTHENTICATION = BiometricConstants.BIOMETRIC_NO_AUTHENTICATION; @@ -777,7 +776,6 @@ public class BiometricManager { */ @RequiresPermission(USE_BIOMETRIC) @ElapsedRealtimeLong - @FlaggedApi(Flags.FLAG_LAST_AUTHENTICATION_TIME) public long getLastAuthenticationTime( @BiometricManager.Authenticators.Types int authenticators) { if (authenticators == 0 diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig index 73b6417a6ba4..4815f3e4f524 100644 --- a/core/java/android/hardware/biometrics/flags.aconfig +++ b/core/java/android/hardware/biometrics/flags.aconfig @@ -2,14 +2,6 @@ package: "android.hardware.biometrics" container: "system" flag { - name: "last_authentication_time" - is_exported: true - namespace: "wallet_integration" - description: "Feature flag for adding getLastAuthenticationTime API to BiometricManager" - bug: "301979982" -} - -flag { name: "add_key_agreement_crypto_object" is_exported: true namespace: "biometrics" diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 0590a06f3f82..a96de4b050a3 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -146,6 +146,22 @@ public final class DisplayManager { "android.hardware.display.category.PRESENTATION"; /** + * Display category: Built in displays. + * + * <p> + * This category can be used to identify displays that are built into the device. The + * displays that are returned may be inactive or disabled at the current moment. The + * returned displays are useful in identifying the various sizes of built-in displays. The + * id from {@link Display#getDisplayId()} is not guaranteed to be stable and may change + * when the display becomes active. + * </p> + * @see #getDisplays(String) + */ + @FlaggedApi(com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_CATEGORY_BUILT_IN) + public static final String DISPLAY_CATEGORY_BUILT_IN_DISPLAYS = + "android.hardware.display.category.BUILT_IN_DISPLAYS"; + + /** * Display category: Rear displays. * <p> * This category can be used to identify complementary internal displays that are facing away @@ -171,6 +187,8 @@ public final class DisplayManager { * @see #getDisplays(String) * @hide */ + @TestApi + @SuppressLint("UnflaggedApi") public static final String DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED = "android.hardware.display.category.ALL_INCLUDING_DISABLED"; @@ -729,10 +747,13 @@ public final class DisplayManager { * @see #DISPLAY_CATEGORY_PRESENTATION */ public Display[] getDisplays(String category) { - boolean includeDisabled = (category != null - && category.equals(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)); + boolean includeDisabled = shouldIncludeDisabledDisplays(category); final int[] displayIds = mGlobal.getDisplayIds(includeDisabled); - if (DISPLAY_CATEGORY_PRESENTATION.equals(category)) { + if (Flags.displayCategoryBuiltIn() + && DISPLAY_CATEGORY_BUILT_IN_DISPLAYS.equals(category)) { + Display[] value = getDisplays(displayIds, DisplayManager::isBuiltInDisplay); + return value; + } else if (DISPLAY_CATEGORY_PRESENTATION.equals(category)) { return getDisplays(displayIds, DisplayManager::isPresentationDisplay); } else if (DISPLAY_CATEGORY_REAR.equals(category)) { return getDisplays(displayIds, DisplayManager::isRearDisplay); @@ -742,6 +763,16 @@ public final class DisplayManager { return new Display[0]; } + private boolean shouldIncludeDisabledDisplays(@Nullable String category) { + if (DISPLAY_CATEGORY_BUILT_IN_DISPLAYS.equals(category)) { + return true; + } + if (DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED.equals(category)) { + return true; + } + return false; + } + private Display[] getDisplays(int[] displayIds, Predicate<Display> predicate) { ArrayList<Display> tmpDisplays = new ArrayList<>(); for (int displayId : displayIds) { @@ -753,6 +784,13 @@ public final class DisplayManager { return tmpDisplays.toArray(new Display[tmpDisplays.size()]); } + private static boolean isBuiltInDisplay(@Nullable Display display) { + if (display == null) { + return false; + } + return display.getType() == Display.TYPE_INTERNAL; + } + private static boolean isPresentationDisplay(@Nullable Display display) { if (display == null || (display.getDisplayId() == DEFAULT_DISPLAY) || (display.getFlags() & Display.FLAG_PRESENTATION) == 0) { diff --git a/core/java/android/hardware/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java index b39bd8c10022..555ff4b271fd 100644 --- a/core/java/android/hardware/display/DisplayTopology.java +++ b/core/java/android/hardware/display/DisplayTopology.java @@ -33,6 +33,7 @@ import android.util.MathUtils; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseIntArray; import android.view.Display; import androidx.annotation.NonNull; @@ -61,6 +62,7 @@ import java.util.Queue; public final class DisplayTopology implements Parcelable { private static final String TAG = "DisplayTopology"; private static final float EPSILON = 0.0001f; + private static final float MAX_GAP = 5; @android.annotation.NonNull public static final Creator<DisplayTopology> CREATOR = @@ -108,7 +110,7 @@ public final class DisplayTopology implements Parcelable { public DisplayTopology() {} - public DisplayTopology(TreeNode root, int primaryDisplayId) { + public DisplayTopology(@Nullable TreeNode root, int primaryDisplayId) { mRoot = root; if (mRoot != null) { // Set mRoot's position and offset to predictable values, just so we don't leak state @@ -181,6 +183,8 @@ public final class DisplayTopology implements Parcelable { if (findDisplay(displayId, mRoot) == null) { return false; } + + // Re-add the other displays to a new tree Queue<TreeNode> queue = new ArrayDeque<>(); queue.add(mRoot); mRoot = null; @@ -191,6 +195,7 @@ public final class DisplayTopology implements Parcelable { } queue.addAll(node.mChildren); } + if (mPrimaryDisplayId == displayId) { if (mRoot != null) { mPrimaryDisplayId = mRoot.mDisplayId; @@ -218,6 +223,9 @@ public final class DisplayTopology implements Parcelable { * IDs in this topology, no more, no less */ public void rearrange(Map<Integer, PointF> newPos) { + if (mRoot == null) { + return; + } var availableParents = new ArrayList<TreeNode>(); availableParents.addLast(mRoot); @@ -447,11 +455,11 @@ public final class DisplayTopology implements Parcelable { // Check that the offset is within bounds areTouching &= switch (targetDisplay.mPosition) { case POSITION_LEFT, POSITION_RIGHT -> - childBounds.bottom + EPSILON >= parentBounds.top - && childBounds.top <= parentBounds.bottom + EPSILON; + childBounds.bottom + EPSILON > parentBounds.top + && childBounds.top < parentBounds.bottom + EPSILON; case POSITION_TOP, POSITION_BOTTOM -> - childBounds.right + EPSILON >= parentBounds.left - && childBounds.left <= parentBounds.right + EPSILON; + childBounds.right + EPSILON > parentBounds.left + && childBounds.left < parentBounds.right + EPSILON; default -> throw new IllegalStateException( "Unexpected value: " + targetDisplay.mPosition); }; @@ -566,7 +574,7 @@ public final class DisplayTopology implements Parcelable { "DisplayTopology: attempting to add a display that already exists"); } if (mRoot == null) { - mRoot = new TreeNode(displayId, width, height, /* position= */ 0, /* offset= */ 0); + mRoot = new TreeNode(displayId, width, height, POSITION_LEFT, /* offset= */ 0); mPrimaryDisplayId = displayId; if (shouldLog) { Slog.i(TAG, "First display added: " + mRoot); @@ -681,13 +689,112 @@ public final class DisplayTopology implements Parcelable { } } - /** Returns the graph representation of the topology */ - public DisplayTopologyGraph getGraph() { - // TODO(b/364907904): implement - return new DisplayTopologyGraph(mPrimaryDisplayId, - new DisplayTopologyGraph.DisplayNode[] { new DisplayTopologyGraph.DisplayNode( - mRoot == null ? Display.DEFAULT_DISPLAY : mRoot.mDisplayId, - new DisplayTopologyGraph.AdjacentDisplay[0])}); + /** + * Check if two displays are touching. + * If the gap between two edges is <= {@link MAX_GAP}, they are still considered adjacent. + * The position indicates where the second display is touching the first one and the offset + * indicates where along the first display the second display is located. + * @param bounds1 The bounds of the first display + * @param bounds2 The bounds of the second display + * @return Empty list if the displays are not adjacent; + * List of one Pair(position, offset) if the displays are adjacent but not by a corner; + * List of two Pair(position, offset) if the displays are adjacent by a corner. + */ + private List<Pair<Integer, Float>> findDisplayPlacements(RectF bounds1, RectF bounds2) { + List<Pair<Integer, Float>> placements = new ArrayList<>(); + if (bounds1.top <= bounds2.bottom + MAX_GAP && bounds2.top <= bounds1.bottom + MAX_GAP) { + if (MathUtils.abs(bounds1.left - bounds2.right) <= MAX_GAP) { + placements.add(new Pair<>(POSITION_LEFT, bounds2.top - bounds1.top)); + } + if (MathUtils.abs(bounds1.right - bounds2.left) <= MAX_GAP) { + placements.add(new Pair<>(POSITION_RIGHT, bounds2.top - bounds1.top)); + } + } + if (bounds1.left <= bounds2.right + MAX_GAP && bounds2.left <= bounds1.right + MAX_GAP) { + if (MathUtils.abs(bounds1.top - bounds2.bottom) < MAX_GAP) { + placements.add(new Pair<>(POSITION_TOP, bounds2.left - bounds1.left)); + } + if (MathUtils.abs(bounds1.bottom - bounds2.top) < MAX_GAP) { + placements.add(new Pair<>(POSITION_BOTTOM, bounds2.left - bounds1.left)); + } + } + return placements; + } + + /** + * @param densityPerDisplay The logical display densities, indexed by logical display ID + * @return The graph representation of the topology. If there is a corner adjacency, the same + * display will appear twice in the list of adjacent displays with both possible placements. + */ + @Nullable + public DisplayTopologyGraph getGraph(SparseIntArray densityPerDisplay) { + // Sort the displays by position + SparseArray<RectF> bounds = getAbsoluteBounds(); + Comparator<Integer> comparator = (displayId1, displayId2) -> { + RectF bounds1 = bounds.get(displayId1); + RectF bounds2 = bounds.get(displayId2); + + int compareX = Float.compare(bounds1.left, bounds2.left); + if (compareX != 0) { + return compareX; + } + return Float.compare(bounds1.top, bounds2.top); + }; + List<Integer> displayIds = new ArrayList<>(bounds.size()); + for (int i = 0; i < bounds.size(); i++) { + displayIds.add(bounds.keyAt(i)); + } + displayIds.sort(comparator); + + SparseArray<List<DisplayTopologyGraph.AdjacentDisplay>> adjacentDisplaysPerId = + new SparseArray<>(); + for (int id : displayIds) { + if (densityPerDisplay.get(id) == 0) { + Slog.w(TAG, "Cannot construct graph, no density for display " + id); + return null; + } + adjacentDisplaysPerId.append(id, new ArrayList<>(Math.min(10, displayIds.size()))); + } + + // Find touching displays + for (int i = 0; i < displayIds.size(); i++) { + int displayId1 = displayIds.get(i); + RectF bounds1 = bounds.get(displayId1); + List<DisplayTopologyGraph.AdjacentDisplay> adjacentDisplays1 = + adjacentDisplaysPerId.get(displayId1); + + for (int j = i + 1; j < displayIds.size(); j++) { + int displayId2 = displayIds.get(j); + RectF bounds2 = bounds.get(displayId2); + List<DisplayTopologyGraph.AdjacentDisplay> adjacentDisplays2 = + adjacentDisplaysPerId.get(displayId2); + + List<Pair<Integer, Float>> placements1 = findDisplayPlacements(bounds1, bounds2); + List<Pair<Integer, Float>> placements2 = findDisplayPlacements(bounds2, bounds1); + for (Pair<Integer, Float> placement : placements1) { + adjacentDisplays1.add(new DisplayTopologyGraph.AdjacentDisplay(displayId2, + /* position= */ placement.first, /* offsetDp= */ placement.second)); + } + for (Pair<Integer, Float> placement : placements2) { + adjacentDisplays2.add(new DisplayTopologyGraph.AdjacentDisplay(displayId1, + /* position= */ placement.first, /* offsetDp= */ placement.second)); + } + if (bounds2.left >= bounds1.right + EPSILON) { + // This and the subsequent displays are already too far away + break; + } + } + } + + DisplayTopologyGraph.DisplayNode[] nodes = + new DisplayTopologyGraph.DisplayNode[adjacentDisplaysPerId.size()]; + for (int i = 0; i < nodes.length; i++) { + int displayId = adjacentDisplaysPerId.keyAt(i); + nodes[i] = new DisplayTopologyGraph.DisplayNode(displayId, + densityPerDisplay.get(displayId), adjacentDisplaysPerId.valueAt(i).toArray( + new DisplayTopologyGraph.AdjacentDisplay[0])); + } + return new DisplayTopologyGraph(mPrimaryDisplayId, nodes); } /** diff --git a/core/java/android/hardware/display/DisplayTopologyGraph.java b/core/java/android/hardware/display/DisplayTopologyGraph.java index 938e6d108f5d..99528cc3a3d2 100644 --- a/core/java/android/hardware/display/DisplayTopologyGraph.java +++ b/core/java/android/hardware/display/DisplayTopologyGraph.java @@ -16,6 +16,8 @@ package android.hardware.display; +import static android.hardware.display.DisplayTopology.TreeNode.positionToString; + /** * Graph of the displays in {@link android.hardware.display.DisplayTopology} tree. * @@ -27,6 +29,7 @@ public record DisplayTopologyGraph(int primaryDisplayId, DisplayNode[] displayNo */ public record DisplayNode( int displayId, + int density, AdjacentDisplay[] adjacentDisplays) {} /** @@ -38,6 +41,18 @@ public record DisplayTopologyGraph(int primaryDisplayId, DisplayNode[] displayNo // Side of the other display which touches this adjacent display. @DisplayTopology.TreeNode.Position int position, - // How many px this display is shifted along the touchingSide, can be negative. - float offsetPx) {} + // The distance from the top edge of the other display to the top edge of this display + // (in case of POSITION_LEFT or POSITION_RIGHT) or from the left edge of the parent + // display to the left edge of this display (in case of POSITION_TOP or + // POSITION_BOTTOM). The unit used is density-independent pixels (dp). + float offsetDp) { + @Override + public String toString() { + return "AdjacentDisplay{" + + "displayId=" + displayId + + ", position=" + positionToString(position) + + ", offsetDp=" + offsetDp + + '}'; + } + } } diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java index cd48047bccb4..af40188c4eba 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -35,7 +35,6 @@ import static com.android.hardware.input.Flags.touchpadVisualizer; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiKeyGestures; import static com.android.input.flags.Flags.FLAG_KEYBOARD_REPEAT_KEYS; -import static com.android.input.flags.Flags.enableInputFilterRustImpl; import static com.android.input.flags.Flags.keyboardRepeatKeys; import android.Manifest; @@ -883,7 +882,7 @@ public class InputSettings { * @hide */ public static boolean isAccessibilityBounceKeysFeatureEnabled() { - return keyboardA11yBounceKeysFlag() && enableInputFilterRustImpl(); + return keyboardA11yBounceKeysFlag(); } /** @@ -967,7 +966,7 @@ public class InputSettings { * @hide */ public static boolean isAccessibilitySlowKeysFeatureFlagEnabled() { - return keyboardA11ySlowKeysFlag() && enableInputFilterRustImpl(); + return keyboardA11ySlowKeysFlag(); } /** @@ -1053,7 +1052,7 @@ public class InputSettings { * @hide */ public static boolean isAccessibilityStickyKeysFeatureEnabled() { - return keyboardA11yStickyKeysFlag() && enableInputFilterRustImpl(); + return keyboardA11yStickyKeysFlag(); } /** diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 79323bf2f2f7..23722ed5bb0d 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -57,16 +57,6 @@ flag { } flag { - namespace: "input_native" - name: "keyboard_layout_manager_multi_user_ime_setup" - description: "Update KeyboardLayoutManager to work correctly with multi-user IME setup" - bug: "354333072" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "modifier_shortcut_dump" namespace: "input" description: "Dump keyboard shortcuts in dumpsys window" @@ -235,3 +225,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "key_event_activity_detection" + namespace: "input" + is_exported: true + description: "Key Event Activity Detection" + bug: "356412905" +} diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index 1cf293d46350..e79b2e7becce 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -142,6 +142,7 @@ public class BaseBundle { /** {@hide} */ @VisibleForTesting public int mFlags; + private boolean mHasIntent = false; /** * Constructs a new, empty Bundle that uses a specific ClassLoader for @@ -258,9 +259,20 @@ public class BaseBundle { // Keep as last statement to ensure visibility of other fields mParcelledData = parcelledData; + mHasIntent = from.mHasIntent; } } + /** @hide */ + public boolean hasIntent() { + return mHasIntent; + } + + /** @hide */ + public void setHasIntent(boolean hasIntent) { + mHasIntent = hasIntent; + } + /** * TODO: optimize this later (getting just the value part of a Bundle * with a single pair) once Bundle.forPair() above is implemented @@ -1837,6 +1849,7 @@ public class BaseBundle { parcel.writeInt(length); parcel.writeInt(mParcelledByNative ? BUNDLE_MAGIC_NATIVE : BUNDLE_MAGIC); parcel.appendFrom(mParcelledData, 0, length); + parcel.writeBoolean(mHasIntent); } return; } @@ -1851,7 +1864,6 @@ public class BaseBundle { int lengthPos = parcel.dataPosition(); parcel.writeInt(-1); // placeholder, will hold length parcel.writeInt(BUNDLE_MAGIC); - int startPos = parcel.dataPosition(); parcel.writeArrayMapInternal(map); int endPos = parcel.dataPosition(); @@ -1861,6 +1873,7 @@ public class BaseBundle { int length = endPos - startPos; parcel.writeInt(length); parcel.setDataPosition(endPos); + parcel.writeBoolean(mHasIntent); } /** @@ -1904,6 +1917,7 @@ public class BaseBundle { mOwnsLazyValues = false; initializeFromParcelLocked(parcel, /*ownsParcel*/ false, isNativeBundle); } + mHasIntent = parcel.readBoolean(); return; } @@ -1922,6 +1936,7 @@ public class BaseBundle { mOwnsLazyValues = true; mParcelledByNative = isNativeBundle; mParcelledData = p; + mHasIntent = parcel.readBoolean(); } /** {@hide} */ diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index 6b1e918a3c47..ee62dea7f9e5 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -149,11 +149,6 @@ public class Binder implements IBinder { private static volatile boolean sStackTrackingEnabled = false; /** - * The extension binder object - */ - private IBinder mExtension = null; - - /** * Enable Binder IPC stack tracking. If enabled, every binder transaction will be logged to * {@link TransactionTracker}. * @@ -1242,9 +1237,7 @@ public class Binder implements IBinder { /** @hide */ @Override - public final @Nullable IBinder getExtension() { - return mExtension; - } + public final native @Nullable IBinder getExtension(); /** * Set the binder extension. @@ -1252,12 +1245,7 @@ public class Binder implements IBinder { * * @hide */ - public final void setExtension(@Nullable IBinder extension) { - mExtension = extension; - setExtensionNative(extension); - } - - private final native void setExtensionNative(@Nullable IBinder extension); + public final native void setExtension(@Nullable IBinder extension); /** * Default implementation rewinds the parcels and calls onTransact. On diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index 819d58d9f059..a24dc5739b7e 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -398,7 +398,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { if ((bundle.mFlags & FLAG_HAS_BINDERS_KNOWN) == 0) { mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } - mFlags |= bundle.mFlags & FLAG_HAS_INTENT; + setHasIntent(hasIntent() || bundle.hasIntent()); } /** @@ -465,7 +465,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * @hide */ public boolean hasIntent() { - return (mFlags & FLAG_HAS_INTENT) != 0; + return super.hasIntent(); } /** {@hide} */ @@ -591,7 +591,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { mFlags &= ~FLAG_HAS_FDS_KNOWN; mFlags &= ~FLAG_HAS_BINDERS_KNOWN; if (intentClass != null && intentClass.isInstance(value)) { - mFlags |= FLAG_HAS_INTENT; + setHasIntent(true); } } diff --git a/core/java/android/os/PerfettoTrace.java b/core/java/android/os/PerfettoTrace.java index e3f251e34b45..68f1570154ff 100644 --- a/core/java/android/os/PerfettoTrace.java +++ b/core/java/android/os/PerfettoTrace.java @@ -154,14 +154,44 @@ public final class PerfettoTrace { } } + /** + * Manages a perfetto tracing session. + * Constructing this object with a config automatically starts a tracing session. Each session + * must be closed after use and then the resulting trace bytes can be read. + * + * The session could be in process or system wide, depending on {@code isBackendInProcess}. + * This functionality is intended for testing. + */ + public static final class Session { + private final long mPtr; + + /** + * Session ctor. + */ + public Session(boolean isBackendInProcess, byte[] config) { + mPtr = native_start_session(isBackendInProcess, config); + } + + /** + * Closes the session and returns the trace. + */ + public byte[] close() { + return native_stop_session(mPtr); + } + } + @CriticalNative private static native long native_get_process_track_uuid(); - @CriticalNative private static native long native_get_thread_track_uuid(long tid); @FastNative private static native void native_activate_trigger(String name, int ttlMs); + @FastNative + private static native void native_register(boolean isBackendInProcess); + + private static native long native_start_session(boolean isBackendInProcess, byte[] config); + private static native byte[] native_stop_session(long ptr); /** * Writes a trace message to indicate a given section of code was invoked. @@ -307,7 +337,7 @@ public final class PerfettoTrace { /** * Registers the process with Perfetto. */ - public static void register() { - Trace.registerWithPerfetto(); + public static void register(boolean isBackendInProcess) { + native_register(isBackendInProcess); } } diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 2a5666cbe83c..e769abec7dd9 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -624,6 +624,7 @@ public final class PowerManager { WAKE_REASON_TAP, WAKE_REASON_LIFT, WAKE_REASON_BIOMETRIC, + WAKE_REASON_DOCK, }) @Retention(RetentionPolicy.SOURCE) public @interface WakeReason{} @@ -765,6 +766,12 @@ public final class PowerManager { public static final int WAKE_REASON_BIOMETRIC = 17; /** + * Wake up reason code: Waking up due to a user docking the device. + * @hide + */ + public static final int WAKE_REASON_DOCK = 18; + + /** * Convert the wake reason to a string for debugging purposes. * @hide */ @@ -788,6 +795,7 @@ public final class PowerManager { case WAKE_REASON_TAP: return "WAKE_REASON_TAP"; case WAKE_REASON_LIFT: return "WAKE_REASON_LIFT"; case WAKE_REASON_BIOMETRIC: return "WAKE_REASON_BIOMETRIC"; + case WAKE_REASON_DOCK: return "WAKE_REASON_DOCK"; default: return Integer.toString(wakeReason); } } diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java index d451109554fa..ddfa3799706e 100644 --- a/core/java/android/os/TestLooperManager.java +++ b/core/java/android/os/TestLooperManager.java @@ -84,17 +84,8 @@ public class TestLooperManager { * interactions with it have completed. */ public Message next() { - // Wait for the looper block to come up, to make sure we don't accidentally get - // the message for the block. - while (!mLooperIsMyLooper && !mLooperBlocked) { - synchronized (this) { - try { - wait(); - } catch (InterruptedException e) { - } - } - } checkReleased(); + waitForLooperHolder(); return mQueue.next(); } @@ -110,6 +101,7 @@ public class TestLooperManager { @Nullable public Message poll() { checkReleased(); + waitForLooperHolder(); return mQueue.pollForTest(); } @@ -124,6 +116,7 @@ public class TestLooperManager { @Nullable public Long peekWhen() { checkReleased(); + waitForLooperHolder(); return mQueue.peekWhenForTest(); } @@ -133,6 +126,7 @@ public class TestLooperManager { @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY) public boolean isBlockedOnSyncBarrier() { checkReleased(); + waitForLooperHolder(); return mQueue.isBlockedOnSyncBarrier(); } @@ -221,6 +215,23 @@ public class TestLooperManager { } } + /** + * Waits until the Looper is blocked by the LooperHolder, if one was posted. + * + * After this method returns, it's guaranteed that the LooperHolder Message + * is not in the underlying queue. + */ + private void waitForLooperHolder() { + while (!mLooperIsMyLooper && !mLooperBlocked) { + synchronized (this) { + try { + wait(); + } catch (InterruptedException e) { + } + } + } + } + private class LooperHolder implements Runnable { @Override public void run() { diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java index 4a37e0a70443..09e6a45dc294 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -164,8 +164,6 @@ public final class Trace { private static native void nativeInstant(long tag, String name); @FastNative private static native void nativeInstantForTrack(long tag, String trackName, String name); - @FastNative - private static native void nativeRegisterWithPerfetto(); private Trace() { } @@ -545,6 +543,6 @@ public final class Trace { * @hide */ public static void registerWithPerfetto() { - nativeRegisterWithPerfetto(); + PerfettoTrace.register(false /* isBackendInProcess */); } } diff --git a/core/java/android/service/contextualsearch/OWNERS b/core/java/android/service/contextualsearch/OWNERS index c435bd87be21..af2ed4daec11 100644 --- a/core/java/android/service/contextualsearch/OWNERS +++ b/core/java/android/service/contextualsearch/OWNERS @@ -1,3 +1,4 @@ srazdan@google.com hyunyoungs@google.com awickham@google.com +rgl@google.com diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index bd6ff4c2af02..ae0e9c623571 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -532,14 +532,30 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int FLAG_NO_FOCUS_CHANGE = MotionEventFlag.NO_FOCUS_CHANGE; /** - * This flag indicates that this event was modified by or generated from an accessibility - * service. Value = 0x800 + * This flag indicates that this event was injected from some + * {@link android.accessibilityservice.AccessibilityService}, which may be either an + * Accessibility Tool OR a service using that API for purposes other than assisting users with + * disabilities. Value = 0x800 + * @see #FLAG_INJECTED_FROM_ACCESSIBILITY_TOOL * @hide */ @TestApi public static final int FLAG_IS_ACCESSIBILITY_EVENT = MotionEventFlag.IS_ACCESSIBILITY_EVENT; /** + * This flag indicates that this event was injected from an + * {@link android.accessibilityservice.AccessibilityService} with the + * {@link android.accessibilityservice.AccessibilityServiceInfo#isAccessibilityTool()} property + * set to true. These services (known as "Accessibility Tools") are used to assist users with + * disabilities, so events from these services should be able to reach all Views including + * Views which set {@link View#isAccessibilityDataSensitive()} to true. + * Value = 0x1000 + * @hide + */ + public static final int FLAG_INJECTED_FROM_ACCESSIBILITY_TOOL = + MotionEventFlag.INJECTED_FROM_ACCESSIBILITY_TOOL; + + /** * Private flag that indicates when the system has detected that this motion event * may be inconsistent with respect to the sequence of previously delivered motion events, * such as when a pointer move event is sent but the pointer is not down. @@ -2534,6 +2550,24 @@ public final class MotionEvent extends InputEvent implements Parcelable { : flags & ~FLAG_TARGET_ACCESSIBILITY_FOCUS); } + /** + * @see #FLAG_IS_ACCESSIBILITY_EVENT + * @hide + */ + public boolean isInjectedFromAccessibilityService() { + final int flags = getFlags(); + return (flags & FLAG_IS_ACCESSIBILITY_EVENT) != 0; + } + + /** + * @see #FLAG_INJECTED_FROM_ACCESSIBILITY_TOOL + * @hide + */ + public boolean isInjectedFromAccessibilityTool() { + final int flags = getFlags(); + return (flags & FLAG_INJECTED_FROM_ACCESSIBILITY_TOOL) != 0; + } + /** @hide */ public final boolean isHoverExitPending() { final int flags = getFlags(); diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java index 3a0e6f187986..73cd5ecd39ef 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/android/view/NotificationHeaderView.java @@ -17,6 +17,7 @@ package android.view; import static android.app.Flags.notificationsRedesignTemplates; +import static android.util.MathUtils.abs; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; @@ -31,6 +32,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; +import android.util.TypedValue; import android.widget.FrameLayout; import android.widget.RelativeLayout; import android.widget.RemoteViews; @@ -60,6 +62,8 @@ public class NotificationHeaderView extends RelativeLayout { private boolean mEntireHeaderClickable; private boolean mExpandOnlyOnButton; private boolean mAcceptAllTouches; + private float mTopLineTranslation; + private float mExpandButtonTranslation; ViewOutlineProvider mProvider = new ViewOutlineProvider() { @Override @@ -205,6 +209,52 @@ public class NotificationHeaderView extends RelativeLayout { mExpandButton.setLayoutParams(lp); } + /** The view containing the app name, timestamp etc at the top of the notification. */ + public NotificationTopLineView getTopLineView() { + return mTopLineView; + } + + /** The view containing the button to expand the notification. */ + public NotificationExpandButton getExpandButton() { + return mExpandButton; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (notificationsRedesignTemplates()) { + mTopLineTranslation = measureCenterTranslation(mTopLineView); + mExpandButtonTranslation = measureCenterTranslation(mExpandButton); + } + } + + private float measureCenterTranslation(View view) { + // When the view is centered (see centerTopLine), its height is MATCH_PARENT + int parentHeight = getMeasuredHeight(); + // When the view is top-aligned, its height is WRAP_CONTENT + float wrapContentHeight = view.getMeasuredHeight(); + // Calculate the translation needed between the two alignments + final MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); + return abs((parentHeight - wrapContentHeight) / 2f - lp.topMargin); + } + + /** + * The vertical translation necessary between the two positions of the top line, to be used in + * the animation. See also {@link NotificationHeaderView#centerTopLine(boolean)}. + */ + public float getTopLineTranslation() { + return mTopLineTranslation; + } + + /** + * The vertical translation necessary between the two positions of the expander, to be used in + * the animation. See also {@link NotificationHeaderView#centerTopLine(boolean)}. + */ + public float getExpandButtonTranslation() { + return mExpandButtonTranslation; + } + /** * This is used to make the low-priority header show the bolded text of a title. * @@ -216,14 +266,20 @@ public class NotificationHeaderView extends RelativeLayout { ? R.style.TextAppearance_DeviceDefault_Notification_Title : R.style.TextAppearance_DeviceDefault_Notification_Info; // Most of the time, we're showing text in the minimized state - View headerText = findViewById(R.id.header_text); - if (headerText instanceof TextView) { - ((TextView) headerText).setTextAppearance(styleResId); + if (findViewById(R.id.header_text) instanceof TextView headerText) { + headerText.setTextAppearance(styleResId); + if (notificationsRedesignTemplates()) { + // TODO: b/378660052 - When inlining the redesign flag, this should be updated + // directly in TextAppearance_DeviceDefault_Notification_Title so we won't need to + // override it here. + float textSize = getContext().getResources().getDimension( + R.dimen.notification_2025_title_text_size); + headerText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + } } // If there's no summary or text, we show the app name instead of nothing - View appNameText = findViewById(R.id.app_name_text); - if (appNameText instanceof TextView) { - ((TextView) appNameText).setTextAppearance(styleResId); + if (findViewById(R.id.app_name_text) instanceof TextView appNameText) { + appNameText.setTextAppearance(styleResId); } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 7206906658ff..0866e0d832b1 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -16654,6 +16654,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Window is obscured, drop this touch. return false; } + if (android.view.accessibility.Flags.preventA11yNontoolFromInjectingIntoSensitiveViews()) { + if (event.isInjectedFromAccessibilityService() + // If the event came from an Accessibility Service that does *not* declare + // itself as AccessibilityServiceInfo#isAccessibilityTool and this View is + // declared sensitive then drop the event. + // Only Accessibility Tools are allowed to interact with sensitive Views. + && !event.isInjectedFromAccessibilityTool() && isAccessibilityDataSensitive()) { + return false; + } + } return true; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index cd8a85a66c1a..bf34069f9445 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -5566,9 +5566,6 @@ public final class ViewRootImpl implements ViewParent, if (mAttachInfo.mContentCaptureManager != null) { ContentCaptureSession session = mAttachInfo.mContentCaptureManager.getMainContentCaptureSession(); - if (android.view.contentcapture.flags.Flags.postCreateAndroidBgThread()) { - session.performStart(); - } session.notifyWindowBoundsChanged(session.getId(), getConfiguration().windowConfiguration.getBounds()); } diff --git a/core/java/android/view/WindowManagerPolicyConstants.java b/core/java/android/view/WindowManagerPolicyConstants.java index 6d2c0d0061dd..bb8958bc9c70 100644 --- a/core/java/android/view/WindowManagerPolicyConstants.java +++ b/core/java/android/view/WindowManagerPolicyConstants.java @@ -17,6 +17,7 @@ package android.view; import static android.os.IInputConstants.POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY; +import static android.os.IInputConstants.POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY_TOOL; import static android.os.IInputConstants.POLICY_FLAG_KEY_GESTURE_TRIGGERED; import android.annotation.IntDef; @@ -37,6 +38,7 @@ public interface WindowManagerPolicyConstants { int FLAG_VIRTUAL = 0x00000002; int FLAG_INJECTED_FROM_ACCESSIBILITY = POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY; + int FLAG_INJECTED_FROM_ACCESSIBILITY_TOOL = POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY_TOOL; int FLAG_KEY_GESTURE_TRIGGERED = POLICY_FLAG_KEY_GESTURE_TRIGGERED; int FLAG_INJECTED = 0x01000000; int FLAG_TRUSTED = 0x02000000; diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index 294e5da1edd1..37f393ec6511 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -4,6 +4,14 @@ container: "system" # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. flag { + name: "a11y_character_in_window_api" + namespace: "accessibility" + description: "Enables new extra data key for an AccessibilityService to request character bounds in unmagnified window coordinates." + bug: "375429616" + is_exported: true +} + +flag { name: "a11y_expansion_state_api" namespace: "accessibility" description: "Enables new APIs for an app to convey if a node is expanded or collapsed." @@ -42,23 +50,15 @@ flag { } flag { - name: "a11y_character_in_window_api" - namespace: "accessibility" - description: "Enables new extra data key for an AccessibilityService to request character bounds in unmagnified window coordinates." - bug: "375429616" - is_exported: true -} - -flag { - namespace: "accessibility" name: "allow_shortcut_chooser_on_lockscreen" + namespace: "accessibility" description: "Allows the a11y shortcut disambig dialog to appear on the lockscreen" bug: "303871725" } flag { - namespace: "accessibility" name: "braille_display_hid" + namespace: "accessibility" is_exported: true description: "Enables new APIs for an AccessibilityService to communicate with a HID Braille display" bug: "303522222" @@ -72,47 +72,62 @@ flag { } flag { - namespace: "accessibility" name: "collection_info_item_counts" + namespace: "accessibility" is_exported: true description: "Fields for total items and the number of important for accessibility items in a collection" bug: "302376158" } flag { - namespace: "accessibility" name: "copy_events_for_gesture_detection" + namespace: "accessibility" description: "Creates copies of MotionEvents and GestureEvents in GestureMatcher" bug: "280130713" } flag { - namespace: "accessibility" name: "deprecate_accessibility_announcement_apis" + namespace: "accessibility" description: "Controls the deprecation of platform APIs related to disruptive accessibility announcements" bug: "376727542" is_exported: true } flag { - namespace: "accessibility" name: "deprecate_ani_label_for_apis" + namespace: "accessibility" description: "Controls the deprecation of AccessibilityNodeInfo labelFor apis" bug: "333783827" is_exported: true } flag { + name: "enable_system_pinch_zoom_gesture" namespace: "accessibility" + description: "Feature flag for system pinch zoom gesture detector and related opt-out apis" + bug: "283323770" +} + +flag { + name: "enable_type_window_control" + namespace: "accessibility" + is_exported: true + description: "adds new TYPE_WINDOW_CONTROL to AccessibilityWindowInfo for detecting Window Decorations" + bug: "320445550" +} + +flag { name: "flash_notification_system_api" + namespace: "accessibility" is_exported: true description: "Makes flash notification APIs as system APIs for calling from mainline module" bug: "303131332" } flag { - namespace: "accessibility" name: "focus_rect_min_size" + namespace: "accessibility" description: "Ensures the a11y focus rect is big enough to be drawn as visible" bug: "368667566" metadata { @@ -121,102 +136,101 @@ flag { } flag { - namespace: "accessibility" name: "force_invert_color" + namespace: "accessibility" description: "Enable force force-dark for smart inversion and dark theme everywhere" bug: "282821643" } flag { - name: "migrate_enable_shortcuts" + name: "global_action_media_play_pause" namespace: "accessibility" - description: "Refactors deprecated code to use AccessibilityManager#enableShortcutsForTargets." - bug: "332006721" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { - name: "motion_event_observing" + description: "Allow AccessibilityService to perform GLOBAL_ACTION_MEDIA_PLAY_PAUSE" + bug: "334954140" is_exported: true - namespace: "accessibility" - description: "Allows accessibility services to intercept but not consume motion events from specified sources." - bug: "297595990" } flag { - namespace: "accessibility" name: "global_action_menu" + namespace: "accessibility" description: "Allow AccessibilityService to perform GLOBAL_ACTION_MENU" bug: "334954140" is_exported: true } flag { + name: "granular_scrolling" namespace: "accessibility" - name: "global_action_media_play_pause" - description: "Allow AccessibilityService to perform GLOBAL_ACTION_MEDIA_PLAY_PAUSE" - bug: "334954140" is_exported: true + description: "Allow the use of granular scrolling. This allows scrollable nodes to scroll by increments other than a full screen" + bug: "302376158" } flag { + name: "indeterminate_range_info" namespace: "accessibility" - name: "granular_scrolling" + description: "Creates a way to create an INDETERMINATE RangeInfo" + bug: "376108874" is_exported: true - description: "Allow the use of granular scrolling. This allows scrollable nodes to scroll by increments other than a full screen" - bug: "302376158" } flag { + name: "migrate_enable_shortcuts" namespace: "accessibility" - name: "reduce_window_content_changed_event_throttle" - description: "Reduces the throttle of AccessibilityEvent of TYPE_WINDOW_CONTENT_CHANGED" - bug: "277305460" + description: "Refactors deprecated code to use AccessibilityManager#enableShortcutsForTargets." + bug: "332006721" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { + name: "motion_event_observing" + is_exported: true namespace: "accessibility" - name: "remove_child_hover_check_for_touch_exploration" - description: "Remove a check for a hovered child that prevents touch events from being delegated to non-direct descendants" - bug: "304770837" + description: "Allows accessibility services to intercept but not consume motion events from specified sources." + bug: "297595990" } flag { - name: "skip_accessibility_warning_dialog_for_trusted_services" + name: "prevent_a11y_nontool_from_injecting_into_sensitive_views" namespace: "accessibility" - description: "Skips showing the accessibility warning dialog for trusted services." - bug: "303511250" + description: "Prevents injected gestures from A11yServices without isAccessibilityTool=true from reaching AccessibilityDataSensitive UI elements" + bug: "284180538" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { + name: "prevent_leaking_viewrootimpl" namespace: "accessibility" - name: "enable_type_window_control" - is_exported: true - description: "adds new TYPE_WINDOW_CONTROL to AccessibilityWindowInfo for detecting Window Decorations" - bug: "320445550" + description: "Clear pending messages and callbacks of the handler in AccessibilityInteractionController when the ViewRootImpl is detached from Window to prevent leaking ViewRootImpl" + bug: "320701910" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { + name: "reduce_window_content_changed_event_throttle" namespace: "accessibility" - name: "update_always_on_a11y_service" - description: "Updates the Always-On A11yService state when the user changes the enablement of the shortcut." - bug: "298869916" + description: "Reduces the throttle of AccessibilityEvent of TYPE_WINDOW_CONTENT_CHANGED" + bug: "277305460" } flag { - name: "enable_system_pinch_zoom_gesture" + name: "remove_child_hover_check_for_touch_exploration" namespace: "accessibility" - description: "Feature flag for system pinch zoom gesture detector and related opt-out apis" - bug: "283323770" + description: "Remove a check for a hovered child that prevents touch events from being delegated to non-direct descendants" + bug: "304770837" } flag { - name: "prevent_leaking_viewrootimpl" + name: "restore_a11y_secure_settings_on_hsum_device" namespace: "accessibility" - description: "Clear pending messages and callbacks of the handler in AccessibilityInteractionController when the ViewRootImpl is detached from Window to prevent leaking ViewRootImpl" - bug: "320701910" + description: "Grab the a11y settings and send the settings restored broadcast for current visible foreground user" + bug: "381294327" metadata { purpose: PURPOSE_BUGFIX } @@ -233,13 +247,10 @@ flag { } flag { - name: "restore_a11y_secure_settings_on_hsum_device" + name: "skip_accessibility_warning_dialog_for_trusted_services" namespace: "accessibility" - description: "Grab the a11y settings and send the settings restored broadcast for current visible foreground user" - bug: "381294327" - metadata { - purpose: PURPOSE_BUGFIX - } + description: "Skips showing the accessibility warning dialog for trusted services." + bug: "303511250" } flag { @@ -274,6 +285,13 @@ flag { } flag { + namespace: "accessibility" + name: "update_always_on_a11y_service" + description: "Updates the Always-On A11yService state when the user changes the enablement of the shortcut." + bug: "298869916" +} + +flag { name: "warning_use_default_dialog_type" namespace: "accessibility" description: "Uses the default type for the A11yService warning dialog, instead of SYSTEM_ALERT_DIALOG" @@ -282,11 +300,3 @@ flag { purpose: PURPOSE_BUGFIX } } - -flag { - name: "indeterminate_range_info" - namespace: "accessibility" - description: "Creates a way to create an INDETERMINATE RangeInfo" - bug: "376108874" - is_exported: true -} diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index 3f3484d5a527..724e8fa830af 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -52,6 +52,7 @@ import android.view.contentcapture.ContentCaptureSession.FlushReason; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.BackgroundThread; import com.android.internal.util.RingBuffer; import com.android.internal.util.SyncResultReceiver; @@ -604,6 +605,7 @@ public final class ContentCaptureManager { mContext, this, prepareUiHandler(), + prepareContentCaptureHandler(), mService ); if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession); @@ -614,6 +616,15 @@ public final class ContentCaptureManager { @NonNull @GuardedBy("mLock") + private Handler prepareContentCaptureHandler() { + if (mContentCaptureHandler == null) { + mContentCaptureHandler = BackgroundThread.getHandler(); + } + return mContentCaptureHandler; + } + + @NonNull + @GuardedBy("mLock") private Handler prepareUiHandler() { if (mUiHandler == null) { mUiHandler = Handler.createAsync(Looper.getMainLooper()); diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java index 6bb2975d9cf1..9aeec20ec9b7 100644 --- a/core/java/android/view/contentcapture/ContentCaptureSession.java +++ b/core/java/android/view/contentcapture/ContentCaptureSession.java @@ -286,9 +286,6 @@ public abstract class ContentCaptureSession implements AutoCloseable { abstract void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags); - /** @hide */ - public void performStart() {} - abstract boolean isDisabled(); /** diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index eddfc42da9bd..2fb78c038ca2 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -57,12 +57,10 @@ import android.view.View; import android.view.ViewStructure; import android.view.autofill.AutofillId; import android.view.contentcapture.ViewNode.ViewStructureImpl; -import android.view.contentcapture.flags.Flags; import android.view.contentprotection.ContentProtectionEventProcessor; import android.view.inputmethod.BaseInputConnection; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.os.BackgroundThread; import com.android.internal.os.IResultReceiver; import com.android.modules.expresslog.Counter; @@ -109,10 +107,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { @NonNull private final Handler mUiHandler; - /** @hide */ - @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) - @Nullable - public Handler mContentCaptureHandler; + @NonNull + private final Handler mContentCaptureHandler; /** * Interface to the system_server binder object - it's only used to start the session (and @@ -191,12 +187,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { @Nullable public ContentProtectionEventProcessor mContentProtectionEventProcessor; - /** - * A runnable object to perform the start of this session. - */ - @Nullable - private Runnable mStartRunnable = null; - private static class SessionStateReceiver extends IResultReceiver.Stub { private final WeakReference<MainContentCaptureSession> mMainSession; @@ -208,7 +198,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession { public void send(int resultCode, Bundle resultData) { final MainContentCaptureSession mainSession = mMainSession.get(); if (mainSession == null) { - Log.w(TAG, "received result after main session released"); + Log.w(TAG, "received result after mina session released"); return; } final IBinder binder; @@ -223,8 +213,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { binder = resultData.getBinder(EXTRA_BINDER); if (binder == null) { Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result"); - // explicitly init the bg thread - mainSession.mContentCaptureHandler = mainSession.prepareContentCaptureHandler(); mainSession.runOnContentCaptureThread(() -> mainSession.resetSession( STATE_DISABLED | STATE_INTERNAL_ERROR)); return; @@ -232,45 +220,23 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } else { binder = null; } - // explicitly init the bg thread - mainSession.mContentCaptureHandler = mainSession.prepareContentCaptureHandler(); mainSession.runOnContentCaptureThread(() -> mainSession.onSessionStarted(resultCode, binder)); } } - /** - * Prepares the content capture handler(i.e. the background thread). - * - * This is expected to be called from the {@link SessionStateReceiver#send} callback, after the - * session {@link performStart}. This is expected to be executed in a binder thread, instead - * of the UI thread. - */ - @NonNull - private Handler prepareContentCaptureHandler() { - if (mContentCaptureHandler == null) { - try { - if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "prepareContentCaptureHandler"); - } - mContentCaptureHandler = BackgroundThread.getHandler(); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - } - return mContentCaptureHandler; - } - /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) public MainContentCaptureSession( @NonNull ContentCaptureManager.StrippedContext context, @NonNull ContentCaptureManager manager, @NonNull Handler uiHandler, + @NonNull Handler contentCaptureHandler, @NonNull IContentCaptureManager systemServerInterface) { mContext = context; mManager = manager; mUiHandler = uiHandler; + mContentCaptureHandler = contentCaptureHandler; mSystemServerInterface = systemServerInterface; final int logHistorySize = mManager.mOptions.logHistorySize; @@ -294,49 +260,18 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } /** - * Performs the start of the session. - * - * This is expected to be called from the UI thread, when the activity finishes its first frame. - * This is a no-op if the session has already been started. - * - * See {@link #start(IBinder, IBinder, ComponentName, int)} for more details. - * - * @hide */ - @Override - public void performStart() { - if (!hasStarted() && mStartRunnable != null) { - mStartRunnable.run(); - } - } - - /** - * Creates a runnable to start this session. - * - * For performance reasons, it is better to only create a task to start the session - * during the creation of the activity and perform the actual start when the activity - * finishes it's first frame. + * Starts this session. */ @Override void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags) { - if (Flags.postCreateAndroidBgThread()) { - mStartRunnable = () -> { - try { - if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "cc session startImpl"); - } - startImpl(token, shareableActivityToken, component, flags); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - }; - } else { - startImpl(token, shareableActivityToken, component, flags); - } + runOnContentCaptureThread( + () -> startImpl(token, shareableActivityToken, component, flags)); } private void startImpl(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags) { + checkOnContentCaptureThread(); if (!isContentCaptureEnabled()) return; if (sVerbose) { @@ -370,7 +305,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e); } } - @Override void onDestroy() { clearAndRunOnContentCaptureThread(() -> { @@ -627,6 +561,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } private boolean hasStarted() { + checkOnContentCaptureThread(); return mState != UNKNOWN_STATE; } @@ -640,11 +575,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { if (sVerbose) Log.v(TAG, "handleScheduleFlush(): session not started yet"); return; } - if (mContentCaptureHandler == null) { - Log.w(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): content capture " - + "thread not ready"); - return; - } if (mDisabled.get()) { // Should not be called on this state, as handleSendEvent checks. @@ -717,11 +647,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { if (!isContentCaptureReceiverEnabled()) { return; } - if (mContentCaptureHandler == null) { - Log.w(TAG, "handleForceFlush(" + getDebugState(reason) + "): content capture thread" - + "not ready"); - return; - } if (mDirectServiceInterface == null) { if (sVerbose) { @@ -838,9 +763,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } mDirectServiceInterface = null; mContentProtectionEventProcessor = null; - if (mContentCaptureHandler != null) { - mContentCaptureHandler.removeMessages(MSG_FLUSH); - } + mContentCaptureHandler.removeMessages(MSG_FLUSH); } @Override @@ -994,10 +917,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * clear the buffer events then starting sending out current event. */ private void enqueueEvent(@NonNull final ContentCaptureEvent event, boolean forceFlush) { - if (mContentCaptureHandler == null) { - mEventProcessQueue.offer(event); - return; - } if (forceFlush || mEventProcessQueue.size() >= mManager.mOptions.maxBufferSize - 1) { // The buffer events are cleared in the same thread first to prevent new events // being added during the time of context switch. This would disrupt the sequence @@ -1200,10 +1119,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * always delegate to the assigned thread from {@code mHandler} for synchronization.</p> */ private void checkOnContentCaptureThread() { - if (mContentCaptureHandler == null) { - Log.e(TAG, "content capture thread is not initiallized!"); - return; - } final boolean onContentCaptureThread = mContentCaptureHandler.getLooper().isCurrentThread(); if (!onContentCaptureThread) { mWrongThreadCount.incrementAndGet(); @@ -1224,12 +1139,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * </p> */ private void runOnContentCaptureThread(@NonNull Runnable r) { - if (mContentCaptureHandler == null) { - Log.e(TAG, "content capture thread is not initiallized!"); - // fall back to UI thread - runOnUiThread(r); - return; - } if (!mContentCaptureHandler.getLooper().isCurrentThread()) { mContentCaptureHandler.post(r); } else { @@ -1238,12 +1147,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } private void clearAndRunOnContentCaptureThread(@NonNull Runnable r, int what) { - if (mContentCaptureHandler == null) { - Log.e(TAG, "content capture thread is not initiallized!"); - // fall back to UI thread - runOnUiThread(r); - return; - } if (!mContentCaptureHandler.getLooper().isCurrentThread()) { mContentCaptureHandler.removeMessages(what); mContentCaptureHandler.post(r); diff --git a/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig index 9df835098268..e7bc004ca2d2 100644 --- a/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig +++ b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig @@ -15,14 +15,3 @@ flag { bug: "380381249" is_exported: true } - -flag { - name: "post_create_android_bg_thread" - namespace: "pixel_state_server" - description: "Feature flag to post create the bg thread when an app is in the allowlist" - bug: "376468525" - is_fixed_read_only: true - metadata { - purpose: PURPOSE_BUGFIX - } -} diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index e904345fb03e..0fb80422833c 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -391,7 +391,8 @@ public final class InputMethodManager { */ @Deprecated @GuardedBy("sLock") - @UnsupportedAppUsage + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM, + publicAlternatives = "Use {@code Context#getSystemService(InputMethodManager.class)}.") static InputMethodManager sInstance; /** diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index 4258ca4fde01..16f41146fd6b 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -19,15 +19,6 @@ flag { } flag { - name: "imm_userhandle_hostsidetests" - is_exported: true - namespace: "input_method" - description: "Feature flag for replacing UserIdInt with UserHandle in some helper IMM functions" - bug: "301713309" - is_fixed_read_only: true -} - -flag { name: "concurrent_input_methods" is_exported: true namespace: "input_method" diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java index 0d1bb77ae8a2..7758dea3ea8a 100644 --- a/core/java/android/window/DesktopExperienceFlags.java +++ b/core/java/android/window/DesktopExperienceFlags.java @@ -16,8 +16,6 @@ package android.window; -import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement; - import android.annotation.Nullable; import android.os.SystemProperties; import android.util.Log; @@ -42,7 +40,32 @@ import java.util.function.BooleanSupplier; * @hide */ public enum DesktopExperienceFlags { - ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT(() -> enableDisplayContentModeManagement(), true); + ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT( + com.android.server.display.feature.flags.Flags::enableDisplayContentModeManagement, + true), + ACTIVITY_EMBEDDING_SUPPORT_FOR_CONNECTED_DISPLAYS( + Flags::activityEmbeddingSupportForConnectedDisplays, false), + BASE_DENSITY_FOR_EXTERNAL_DISPLAYS( + com.android.server.display.feature.flags.Flags::baseDensityForExternalDisplays, true), + CONNECTED_DISPLAYS_CURSOR(com.android.input.flags.Flags::connectedDisplaysCursor, true), + DISPLAY_TOPOLOGY(com.android.server.display.feature.flags.Flags::displayTopology, true), + ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY(Flags::enableBugFixesForSecondaryDisplay, false), + ENABLE_CONNECTED_DISPLAYS_DND(Flags::enableConnectedDisplaysDnd, false), + ENABLE_CONNECTED_DISPLAYS_PIP(Flags::enableConnectedDisplaysPip, false), + ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, false), + ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS(Flags::enableDisplayFocusInShellTransitions, false), + ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, false), + ENABLE_DRAG_TO_MAXIMIZE(Flags::enableDragToMaximize, true), + ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, false), + ENABLE_MULTIPLE_DESKTOPS_BACKEND(Flags::enableMultipleDesktopsBackend, false), + ENABLE_MULTIPLE_DESKTOPS_FRONTEND(Flags::enableMultipleDesktopsFrontend, false), + ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY(Flags::enablePerDisplayDesktopWallpaperActivity, + false), + ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF( + Flags::enablePerDisplayPackageContextCacheInStatusbarNotif, false), + ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS(Flags::enterDesktopByDefaultOnFreeformDisplays, + false), + REPARENT_WINDOW_TOKEN_API(Flags::reparentWindowTokenApi, true); /** * Flag class, to be used in case the enum cannot be used because the flag is not accessible. @@ -55,7 +78,8 @@ public enum DesktopExperienceFlags { // Whether the flag state should be affected by developer option. private final boolean mShouldOverrideByDevOption; - public DesktopExperienceFlag(BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) { + public DesktopExperienceFlag(BooleanSupplier flagFunction, + boolean shouldOverrideByDevOption) { this.mFlagFunction = flagFunction; this.mShouldOverrideByDevOption = shouldOverrideByDevOption; } @@ -77,7 +101,8 @@ public enum DesktopExperienceFlags { // Local cache for toggle override, which is initialized once on its first access. It needs to // be refreshed only on reboots as overridden state is expected to take effect on reboots. - @Nullable private static Boolean sCachedToggleOverride; + @Nullable + private static Boolean sCachedToggleOverride; public static final String SYSTEM_PROPERTY_NAME = "persist.wm.debug.desktop_experience_devopts"; diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index e369d3992b64..9468301ac996 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -79,6 +79,10 @@ public enum DesktopModeFlags { Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true), ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX( Flags::enableDesktopAppLaunchTransitionsBugfix, true), + ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS( + Flags::enableCompatUiVisibilityStatus, true), + ENABLE_DESKTOP_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE_BUGFIX( + Flags::skipCompatUiEducationInDesktopMode, true), INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC( Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true), ENABLE_DESKTOP_WINDOWING_HSUM(Flags::enableDesktopWindowingHsum, true), @@ -91,7 +95,9 @@ public enum DesktopModeFlags { Flags::enableTopVisibleRootTaskPerUserTracking, true), ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX( Flags::enableDesktopRecentsTransitionsCornersBugfix, false), - ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS(Flags::enableDesktopSystemDialogsTransitions, true); + ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS(Flags::enableDesktopSystemDialogsTransitions, true), + ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES( + Flags::enableDesktopWindowingMultiInstanceFeatures, true); /** * Flag class, to be used in case the enum cannot be used because the flag is not accessible. @@ -144,28 +150,22 @@ public enum DesktopModeFlags { return isFlagTrue(mFlagFunction, mShouldOverrideByDevOption); } + public static boolean isDesktopModeForcedEnabled() { + return getToggleOverride() == ToggleOverride.OVERRIDE_ON; + } + private static boolean isFlagTrue(BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) { if (!shouldOverrideByDevOption) return flagFunction.getAsBoolean(); if (Flags.showDesktopExperienceDevOption()) { - return switch (getToggleOverride(null)) { + return switch (getToggleOverride()) { case OVERRIDE_UNSET, OVERRIDE_OFF -> flagFunction.getAsBoolean(); case OVERRIDE_ON -> true; }; } if (Flags.showDesktopWindowingDevOption()) { - Application application = ActivityThread.currentApplication(); - if (application == null) { - Log.w(TAG, "Could not get the current application."); - return flagFunction.getAsBoolean(); - } - ContentResolver contentResolver = application.getContentResolver(); - if (contentResolver == null) { - Log.w(TAG, "Could not get the content resolver for the application."); - return flagFunction.getAsBoolean(); - } boolean shouldToggleBeEnabledByDefault = Flags.enableDesktopWindowingMode(); - return switch (getToggleOverride(contentResolver)) { + return switch (getToggleOverride()) { case OVERRIDE_UNSET -> flagFunction.getAsBoolean(); // When toggle override matches its default state, don't override flags. This // helps users reset their feature overrides. @@ -176,14 +176,13 @@ public enum DesktopModeFlags { return flagFunction.getAsBoolean(); } - private static ToggleOverride getToggleOverride(@Nullable ContentResolver contentResolver) { + private static ToggleOverride getToggleOverride() { // If cached, return it if (sCachedToggleOverride != null) { return sCachedToggleOverride; } - // Otherwise, fetch and cache it - ToggleOverride override = getToggleOverrideFromSystem(contentResolver); + ToggleOverride override = getToggleOverrideFromSystem(); sCachedToggleOverride = override; Log.d(TAG, "Toggle override initialized to: " + override); return override; @@ -192,8 +191,7 @@ public enum DesktopModeFlags { /** * Returns {@link ToggleOverride} from Settings.Global set by toggle. */ - private static ToggleOverride getToggleOverrideFromSystem( - @Nullable ContentResolver contentResolver) { + private static ToggleOverride getToggleOverrideFromSystem() { int settingValue; if (Flags.showDesktopExperienceDevOption()) { settingValue = SystemProperties.getInt( @@ -201,6 +199,16 @@ public enum DesktopModeFlags { ToggleOverride.OVERRIDE_UNSET.getSetting() ); } else { + final Application application = ActivityThread.currentApplication(); + if (application == null) { + Log.w(TAG, "Could not get the current application."); + return ToggleOverride.OVERRIDE_UNSET; + } + final ContentResolver contentResolver = application.getContentResolver(); + if (contentResolver == null) { + Log.w(TAG, "Could not get the content resolver for the application."); + return ToggleOverride.OVERRIDE_UNSET; + } settingValue = Settings.Global.getInt( contentResolver, Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 222088e8a8b9..caccc3c42ce8 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -593,3 +593,27 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_non_default_display_split" + namespace: "lse_desktop_experience" + description: "Enables split screen on non default displays" + bug: "384999213" +} + +flag { + name: "enable_desktop_mode_through_dev_option" + namespace: "lse_desktop_experience" + description: "Enables support for desktop mode through developer options for devices eligible for desktop mode." + bug: "382238347" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_desktop_taskbar_on_freeform_displays" + namespace: "lse_desktop_experience" + description: "Forces pinned taskbar with desktop tasks on freeform displays" + bug: "390665752" +} diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java index 9d7bedc4d0c3..ea54395471cd 100644 --- a/core/java/com/android/internal/app/IntentForwarderActivity.java +++ b/core/java/com/android/internal/app/IntentForwarderActivity.java @@ -599,24 +599,35 @@ public class IntentForwarderActivity extends Activity { Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); sanitizeIntent(forwardIntent); - Intent intentToCheck = forwardIntent; - if (Intent.ACTION_CHOOSER.equals(forwardIntent.getAction())) { + if (!canForwardInner(forwardIntent, sourceUserId, targetUserId, packageManager, + contentResolver)) { return null; } if (forwardIntent.getSelector() != null) { - intentToCheck = forwardIntent.getSelector(); + sanitizeIntent(forwardIntent.getSelector()); + if (!canForwardInner(forwardIntent.getSelector(), sourceUserId, targetUserId, + packageManager, contentResolver)) { + return null; + } + } + return forwardIntent; + } + + private static boolean canForwardInner(Intent intent, int sourceUserId, int targetUserId, + IPackageManager packageManager, ContentResolver contentResolver) { + if (Intent.ACTION_CHOOSER.equals(intent.getAction())) { + return false; } - String resolvedType = intentToCheck.resolveTypeIfNeeded(contentResolver); - sanitizeIntent(intentToCheck); + String resolvedType = intent.resolveTypeIfNeeded(contentResolver); try { if (packageManager.canForwardTo( - intentToCheck, resolvedType, sourceUserId, targetUserId)) { - return forwardIntent; + intent, resolvedType, sourceUserId, targetUserId)) { + return true; } } catch (RemoteException e) { Slog.e(TAG, "PackageManagerService is dead?"); } - return null; + return false; } /** diff --git a/core/java/com/android/internal/notification/NotificationChannelGroupsHelper.java b/core/java/com/android/internal/notification/NotificationChannelGroupsHelper.java new file mode 100644 index 000000000000..2bfb19f10807 --- /dev/null +++ b/core/java/com/android/internal/notification/NotificationChannelGroupsHelper.java @@ -0,0 +1,186 @@ +/* + * 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.internal.notification; + +import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS; +import static android.app.NotificationManager.IMPORTANCE_NONE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.INotificationManager; +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.service.notification.Flags; +import android.util.ArrayMap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * NotificationChannelGroupHelper contains helper methods for associating channels with the groups + * they belong to, matching by ID. + */ +public class NotificationChannelGroupsHelper { + /** + * Set of parameters passed into + * {@link NotificationChannelGroupsHelper#getGroupsWithChannels(Collection, Map, Params)}. + * + * @param includeDeleted Whether to include deleted channels. + * @param includeNonGrouped Whether to include channels that are not associated with a group. + * @param includeEmpty Whether to include groups containing no channels. + * @param includeAllBlockedWithFilter Whether to include channels that are blocked from + * sending notifications along with channels specified by + * the filter. This setting only takes effect when + * channelFilter is not {@code null}, and if true will + * include all blocked channels in the output (regardless + * of whether they are included in the filter). + * @param channelFilter If non-null, a specific set of channels to include. If a channel + * matching this filter is blocked, it will still be included even + * if includeAllBlockedWithFilter=false. + */ + public record Params( + boolean includeDeleted, + boolean includeNonGrouped, + boolean includeEmpty, + boolean includeAllBlockedWithFilter, + Set<String> channelFilter + ) { + /** + * Default set of parameters used to specify the behavior of + * {@link INotificationManager#getNotificationChannelGroups(String)}. This will include + * output for all groups, including those without channels, but not any ungrouped channels. + */ + public static Params forAllGroups() { + return new Params( + /* includeDeleted= */ false, + /* includeNonGrouped= */ false, + /* includeEmpty= */ true, + /* includeAllBlockedWithFilter= */ true, + /* channelFilter= */ null); + } + + /** + * Parameters to get groups for all channels, including those not associated with any groups + * and optionally including deleted channels as well. Channels not associated with a group + * are returned inside a group with id {@code null}. + * + * @param includeDeleted Whether to include deleted channels. + */ + public static Params forAllChannels(boolean includeDeleted) { + return new Params( + includeDeleted, + /* includeNonGrouped= */ true, + /* includeEmpty= */ false, + /* includeAllBlockedWithFilter= */ true, + /* channelFilter= */ null); + } + + /** + * Parameters to collect groups only for channels specified by the channel filter, as well + * as any blocked channels (independent of whether they exist in the filter). + * @param channelFilter Specific set of channels to return. + */ + public static Params onlySpecifiedOrBlockedChannels(Set<String> channelFilter) { + return new Params( + /* includeDeleted= */ false, + /* includeNonGrouped= */ true, + /* includeEmpty= */ false, + /* includeAllBlockedWithFilter= */ true, + channelFilter); + } + } + + /** + * Retrieve the {@link NotificationChannelGroup} object specified by the given groupId, if it + * exists, with the list of channels filled in from the provided available channels. + * + * @param groupId The ID of the group to return. + * @param allChannels A list of all channels associated with the package. + * @param allGroups A map of group ID -> NotificationChannelGroup objects. + */ + public static @Nullable NotificationChannelGroup getGroupWithChannels(@NonNull String groupId, + @NonNull Collection<NotificationChannel> allChannels, + @NonNull Map<String, NotificationChannelGroup> allGroups, + boolean includeDeleted) { + NotificationChannelGroup group = null; + if (allGroups.containsKey(groupId)) { + group = allGroups.get(groupId).clone(); + group.setChannels(new ArrayList<>()); + for (NotificationChannel nc : allChannels) { + if (includeDeleted || !nc.isDeleted()) { + if (groupId.equals(nc.getGroup())) { + group.addChannel(nc); + } + } + } + } + return group; + } + + /** + * Returns a list of groups with their associated channels filled in. + * + * @param allChannels All available channels that may be associated with these groups. + * @param allGroups Map of group ID -> {@link NotificationChannelGroup} objects. + * @param params Params indicating which channels and which groups to include. + */ + public static @NonNull List<NotificationChannelGroup> getGroupsWithChannels( + @NonNull Collection<NotificationChannel> allChannels, + @NonNull Map<String, NotificationChannelGroup> allGroups, + Params params) { + Map<String, NotificationChannelGroup> outputGroups = new ArrayMap<>(); + NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null); + for (NotificationChannel nc : allChannels) { + boolean includeChannel = (params.includeDeleted || !nc.isDeleted()) + && (params.channelFilter == null + || (params.includeAllBlockedWithFilter + && nc.getImportance() == IMPORTANCE_NONE) + || params.channelFilter.contains(nc.getId())) + && (!Flags.notificationClassification() + || !SYSTEM_RESERVED_IDS.contains(nc.getId())); + if (includeChannel) { + if (nc.getGroup() != null) { + if (allGroups.get(nc.getGroup()) != null) { + NotificationChannelGroup ncg = outputGroups.get(nc.getGroup()); + if (ncg == null) { + ncg = allGroups.get(nc.getGroup()).clone(); + ncg.setChannels(new ArrayList<>()); + outputGroups.put(nc.getGroup(), ncg); + } + ncg.addChannel(nc); + } + } else { + nonGrouped.addChannel(nc); + } + } + } + if (params.includeNonGrouped && nonGrouped.getChannels().size() > 0) { + outputGroups.put(null, nonGrouped); + } + if (params.includeEmpty) { + for (NotificationChannelGroup group : allGroups.values()) { + if (!outputGroups.containsKey(group.getId())) { + outputGroups.put(group.getId(), group); + } + } + } + return new ArrayList<>(outputGroups.values()); + } +} diff --git a/core/java/com/android/internal/os/BaseCommand.java b/core/java/com/android/internal/os/BaseCommand.java index c85b5d7aa7a6..af763e4c5fa9 100644 --- a/core/java/com/android/internal/os/BaseCommand.java +++ b/core/java/com/android/internal/os/BaseCommand.java @@ -58,15 +58,23 @@ public abstract class BaseCommand { mRawArgs = args; mArgs.init(null, null, null, null, args, 0); + int status = 1; try { onRun(); + status = 0; } catch (IllegalArgumentException e) { onShowUsage(System.err); System.err.println(); System.err.println("Error: " + e.getMessage()); + status = 0; } catch (Exception e) { e.printStackTrace(System.err); - System.exit(1); + } finally { + System.out.flush(); + System.err.flush(); + } + if (status != 0) { + System.exit(status); } } diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 72cb9d1a20ac..98d1ef6057fd 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -217,9 +217,9 @@ oneway interface IStatusBar void setUdfpsRefreshRateCallback(in IUdfpsRefreshRateRequestCallback callback); /** - * Notifies System UI that the display is ready to show system decorations. + * Notifies System UI that the system decorations should be added on the display. */ - void onDisplayReady(int displayId); + void onDisplayAddSystemDecorations(int displayId); /** * Notifies System UI that the system decorations should be removed from the display. diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 3afe27ea591f..a2f4ca2c1b06 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -280,6 +280,7 @@ cc_library_shared_for_libandroid_runtime { ], static_libs: [ + "android.os.flags-aconfig-cc", "libasync_safe", "libbinderthreadstateutils", "libdmabufinfo", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index aea1734918d6..5c0b72013a06 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -22,6 +22,7 @@ #include <android-base/parsebool.h> #include <android-base/properties.h> #include <android/graphics/jni_runtime.h> +#include <android_os.h> #include <android_runtime/AndroidRuntime.h> #include <assert.h> #include <binder/IBinder.h> @@ -893,9 +894,13 @@ int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool p madviseWillNeedFileSizeOdex, "-XMadviseWillNeedOdexFileSize:"); - parseRuntimeOption("dalvik.vm.madvise.artfile.size", - madviseWillNeedFileSizeArt, - "-XMadviseWillNeedArtFileSize:"); + // Historically, dalvik.vm.madvise.artfile.size was set to UINT_MAX by default. With the + // disable_madvise_art_default flag rollout, we use this default only when the flag is disabled. + // TODO(b/382110550): Remove this property/flag entirely after validating and ramping. + const char* madvise_artfile_size_default = + android::os::disable_madvise_artfile_default() ? "" : "4294967295"; + parseRuntimeOption("dalvik.vm.madvise.artfile.size", madviseWillNeedFileSizeArt, + "-XMadviseWillNeedArtFileSize:", madvise_artfile_size_default); /* * Profile related options. diff --git a/core/jni/android_app_PropertyInvalidatedCache.h b/core/jni/android_app_PropertyInvalidatedCache.h index 00aa281b572f..54a4ac65fce2 100644 --- a/core/jni/android_app_PropertyInvalidatedCache.h +++ b/core/jni/android_app_PropertyInvalidatedCache.h @@ -147,10 +147,12 @@ template<int maxNonce, size_t maxByte> class CacheNonce : public NonceStore { } }; -// The CacheNonce for system server holds 64 nonces with a string block of 8192 bytes. This is -// more than enough for system_server PropertyInvalidatedCache support. The configuration -// values are not defined as visible constants. Clients should use the accessors on the -// SystemCacheNonce instance if they need the sizing parameters. -typedef CacheNonce</* max nonce */ 64, /* byte block size */ 8192> SystemCacheNonce; +// The CacheNonce for system server. The configuration values are not defined as visible +// constants. Clients should use the accessors on the SystemCacheNonce instance if they need +// the sizing parameters. + +// LINT.IfChange(system_nonce_config) +typedef CacheNonce</* max nonce */ 128, /* byte block size */ 8192> SystemCacheNonce; +// LINT.ThenChange(/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java:system_nonce_config) } // namespace android.app.PropertyInvalidatedCache diff --git a/core/jni/android_hardware_display_DisplayTopology.cpp b/core/jni/android_hardware_display_DisplayTopology.cpp index d9e802de81e0..a16f3c3c20b8 100644 --- a/core/jni/android_hardware_display_DisplayTopology.cpp +++ b/core/jni/android_hardware_display_DisplayTopology.cpp @@ -35,6 +35,7 @@ static struct { static struct { jclass clazz; jfieldID displayId; + jfieldID density; jfieldID adjacentDisplays; } gDisplayTopologyGraphNodeClassInfo; @@ -42,7 +43,7 @@ static struct { jclass clazz; jfieldID displayId; jfieldID position; - jfieldID offsetPx; + jfieldID offsetDp; } gDisplayTopologyGraphAdjacentDisplayClassInfo; // ---------------------------------------------------------------------------- @@ -55,19 +56,23 @@ status_t android_hardware_display_DisplayTopologyAdjacentDisplay_toNative( adjacentDisplay->position = static_cast<DisplayTopologyPosition>( env->GetIntField(adjacentDisplayObj, gDisplayTopologyGraphAdjacentDisplayClassInfo.position)); - adjacentDisplay->offsetPx = + adjacentDisplay->offsetDp = env->GetFloatField(adjacentDisplayObj, - gDisplayTopologyGraphAdjacentDisplayClassInfo.offsetPx); + gDisplayTopologyGraphAdjacentDisplayClassInfo.offsetDp); return OK; } status_t android_hardware_display_DisplayTopologyGraphNode_toNative( JNIEnv* env, jobject nodeObj, std::unordered_map<ui::LogicalDisplayId, std::vector<DisplayTopologyAdjacentDisplay>>& - graph) { + graph, + std::unordered_map<ui::LogicalDisplayId, int>& displaysDensity) { ui::LogicalDisplayId displayId = ui::LogicalDisplayId{ env->GetIntField(nodeObj, gDisplayTopologyGraphNodeClassInfo.displayId)}; + displaysDensity[displayId] = + env->GetIntField(nodeObj, gDisplayTopologyGraphNodeClassInfo.density); + jobjectArray adjacentDisplaysArray = static_cast<jobjectArray>( env->GetObjectField(nodeObj, gDisplayTopologyGraphNodeClassInfo.adjacentDisplays)); @@ -109,7 +114,8 @@ DisplayTopologyGraph android_hardware_display_DisplayTopologyGraph_toNative(JNIE } android_hardware_display_DisplayTopologyGraphNode_toNative(env, nodeObj.get(), - topology.graph); + topology.graph, + topology.displaysDensity); } } return topology; @@ -132,6 +138,8 @@ int register_android_hardware_display_DisplayTopology(JNIEnv* env) { gDisplayTopologyGraphNodeClassInfo.clazz = MakeGlobalRefOrDie(env, displayNodeClazz); gDisplayTopologyGraphNodeClassInfo.displayId = GetFieldIDOrDie(env, gDisplayTopologyGraphNodeClassInfo.clazz, "displayId", "I"); + gDisplayTopologyGraphNodeClassInfo.density = + GetFieldIDOrDie(env, gDisplayTopologyGraphNodeClassInfo.clazz, "density", "I"); gDisplayTopologyGraphNodeClassInfo.adjacentDisplays = GetFieldIDOrDie(env, gDisplayTopologyGraphNodeClassInfo.clazz, "adjacentDisplays", "[Landroid/hardware/display/DisplayTopologyGraph$AdjacentDisplay;"); @@ -146,8 +154,8 @@ int register_android_hardware_display_DisplayTopology(JNIEnv* env) { gDisplayTopologyGraphAdjacentDisplayClassInfo.position = GetFieldIDOrDie(env, gDisplayTopologyGraphAdjacentDisplayClassInfo.clazz, "position", "I"); - gDisplayTopologyGraphAdjacentDisplayClassInfo.offsetPx = - GetFieldIDOrDie(env, gDisplayTopologyGraphAdjacentDisplayClassInfo.clazz, "offsetPx", + gDisplayTopologyGraphAdjacentDisplayClassInfo.offsetDp = + GetFieldIDOrDie(env, gDisplayTopologyGraphAdjacentDisplayClassInfo.clazz, "offsetDp", "F"); return 0; } diff --git a/core/jni/android_os_PerfettoTrace.cpp b/core/jni/android_os_PerfettoTrace.cpp index 962aefc482e4..9bedfa27fa1a 100644 --- a/core/jni/android_os_PerfettoTrace.cpp +++ b/core/jni/android_os_PerfettoTrace.cpp @@ -24,9 +24,12 @@ #include <nativehelper/scoped_primitive_array.h> #include <nativehelper/scoped_utf_chars.h> #include <nativehelper/utils.h> +#include <tracing_perfetto.h> #include <tracing_sdk.h> namespace android { +constexpr int kFlushTimeoutMs = 5000; + template <typename T> inline static T* toPointer(jlong ptr) { return reinterpret_cast<T*>(static_cast<uintptr_t>(ptr)); @@ -51,6 +54,10 @@ static void android_os_PerfettoTrace_activate_trigger(JNIEnv* env, jclass, jstri tracing_perfetto::activate_trigger(name_chars.c_str(), static_cast<uint32_t>(ttl_ms)); } +void android_os_PerfettoTrace_register(bool is_backend_in_process) { + tracing_perfetto::registerWithPerfetto(is_backend_in_process); +} + static jlong android_os_PerfettoTraceCategory_init(JNIEnv* env, jclass, jstring name, jstring tag, jstring severity) { ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name); @@ -85,6 +92,36 @@ static jlong android_os_PerfettoTraceCategory_get_extra_ptr(jlong ptr) { return toJLong(category->get()); } +static jlong android_os_PerfettoTrace_start_session(JNIEnv* env, jclass /* obj */, + jboolean is_backend_in_process, + jbyteArray config_bytes) { + jsize length = env->GetArrayLength(config_bytes); + std::vector<uint8_t> data; + data.reserve(length); + env->GetByteArrayRegion(config_bytes, 0, length, reinterpret_cast<jbyte*>(data.data())); + + tracing_perfetto::Session* session = + new tracing_perfetto::Session(is_backend_in_process, data.data(), length); + + return reinterpret_cast<long>(session); +} + +static jbyteArray android_os_PerfettoTrace_stop_session([[maybe_unused]] JNIEnv* env, + jclass /* obj */, jlong ptr) { + tracing_perfetto::Session* session = reinterpret_cast<tracing_perfetto::Session*>(ptr); + + session->FlushBlocking(kFlushTimeoutMs); + session->StopBlocking(); + + std::vector<uint8_t> data = session->ReadBlocking(); + + delete session; + + jbyteArray bytes = env->NewByteArray(data.size()); + env->SetByteArrayRegion(bytes, 0, data.size(), reinterpret_cast<jbyte*>(data.data())); + return bytes; +} + static const JNINativeMethod gCategoryMethods[] = { {"native_init", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)J", (void*)android_os_PerfettoTraceCategory_init}, @@ -101,7 +138,10 @@ static const JNINativeMethod gTraceMethods[] = {"native_get_thread_track_uuid", "(J)J", (void*)android_os_PerfettoTrace_get_thread_track_uuid}, {"native_activate_trigger", "(Ljava/lang/String;I)V", - (void*)android_os_PerfettoTrace_activate_trigger}}; + (void*)android_os_PerfettoTrace_activate_trigger}, + {"native_register", "(Z)V", (void*)android_os_PerfettoTrace_register}, + {"native_start_session", "(Z[B)J", (void*)android_os_PerfettoTrace_start_session}, + {"native_stop_session", "(J)[B", (void*)android_os_PerfettoTrace_stop_session}}; int register_android_os_PerfettoTrace(JNIEnv* env) { int res = jniRegisterNativeMethods(env, "android/os/PerfettoTrace", gTraceMethods, diff --git a/core/jni/android_os_Trace.cpp b/core/jni/android_os_Trace.cpp index 21e056dfa12b..50618c5a07af 100644 --- a/core/jni/android_os_Trace.cpp +++ b/core/jni/android_os_Trace.cpp @@ -131,10 +131,6 @@ static jboolean android_os_Trace_nativeIsTagEnabled(jlong tag) { return tracing_perfetto::isTagEnabled(tag); } -static void android_os_Trace_nativeRegisterWithPerfetto(JNIEnv* env) { - tracing_perfetto::registerWithPerfetto(); -} - static const JNINativeMethod gTraceMethods[] = { /* name, signature, funcPtr */ {"nativeSetAppTracingAllowed", "(Z)V", (void*)android_os_Trace_nativeSetAppTracingAllowed}, @@ -157,7 +153,6 @@ static const JNINativeMethod gTraceMethods[] = { {"nativeInstant", "(JLjava/lang/String;)V", (void*)android_os_Trace_nativeInstant}, {"nativeInstantForTrack", "(JLjava/lang/String;Ljava/lang/String;)V", (void*)android_os_Trace_nativeInstantForTrack}, - {"nativeRegisterWithPerfetto", "()V", (void*)android_os_Trace_nativeRegisterWithPerfetto}, // ----------- @CriticalNative ---------------- {"nativeIsTagEnabled", "(J)Z", (void*)android_os_Trace_nativeIsTagEnabled}, diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp index 91b25c2bda06..639f5bff7614 100644 --- a/core/jni/android_util_Binder.cpp +++ b/core/jni/android_util_Binder.cpp @@ -74,7 +74,6 @@ static struct bindernative_offsets_t jmethodID mExecTransact; jmethodID mGetInterfaceDescriptor; jmethodID mTransactionCallback; - jmethodID mGetExtension; // Object state. jfieldID mObject; @@ -490,12 +489,8 @@ public: if (mVintf) { ::android::internal::Stability::markVintf(b.get()); } - if (mSetExtensionCalled) { - jobject javaIBinderObject = env->CallObjectMethod(obj, gBinderOffsets.mGetExtension); - sp<IBinder> extensionFromJava = ibinderForJavaObject(env, javaIBinderObject); - if (extensionFromJava != nullptr) { - b.get()->setExtension(extensionFromJava); - } + if (mExtension != nullptr) { + b.get()->setExtension(mExtension); } mBinder = b; ALOGV("Creating JavaBinder %p (refs %p) for Object %p, weakCount=%" PRId32 "\n", @@ -521,12 +516,21 @@ public: mVintf = false; } + sp<IBinder> getExtension() { + AutoMutex _l(mLock); + sp<JavaBBinder> b = mBinder.promote(); + if (b != nullptr) { + return b.get()->getExtension(); + } + return mExtension; + } + void setExtension(const sp<IBinder>& extension) { AutoMutex _l(mLock); - mSetExtensionCalled = true; + mExtension = extension; sp<JavaBBinder> b = mBinder.promote(); if (b != nullptr) { - b.get()->setExtension(extension); + b.get()->setExtension(mExtension); } } @@ -538,7 +542,8 @@ private: // is too much binder state here, we can think about making JavaBBinder an // sp here (avoid recreating it) bool mVintf = false; - bool mSetExtensionCalled = false; + + sp<IBinder> mExtension; }; // ---------------------------------------------------------------------------- @@ -1244,6 +1249,10 @@ static void android_os_Binder_blockUntilThreadAvailable(JNIEnv* env, jobject cla return IPCThreadState::self()->blockUntilThreadAvailable(); } +static jobject android_os_Binder_getExtension(JNIEnv* env, jobject obj) { + JavaBBinderHolder* jbh = (JavaBBinderHolder*) env->GetLongField(obj, gBinderOffsets.mObject); + return javaObjectForIBinder(env, jbh->getExtension()); +} static void android_os_Binder_setExtension(JNIEnv* env, jobject obj, jobject extensionObject) { JavaBBinderHolder* jbh = (JavaBBinderHolder*) env->GetLongField(obj, gBinderOffsets.mObject); @@ -1286,7 +1295,8 @@ static const JNINativeMethod gBinderMethods[] = { { "getNativeBBinderHolder", "()J", (void*)android_os_Binder_getNativeBBinderHolder }, { "getNativeFinalizer", "()J", (void*)android_os_Binder_getNativeFinalizer }, { "blockUntilThreadAvailable", "()V", (void*)android_os_Binder_blockUntilThreadAvailable }, - { "setExtensionNative", "(Landroid/os/IBinder;)V", (void*)android_os_Binder_setExtension }, + { "getExtension", "()Landroid/os/IBinder;", (void*)android_os_Binder_getExtension }, + { "setExtension", "(Landroid/os/IBinder;)V", (void*)android_os_Binder_setExtension }, }; // clang-format on @@ -1303,8 +1313,6 @@ static int int_register_android_os_Binder(JNIEnv* env) gBinderOffsets.mTransactionCallback = GetStaticMethodIDOrDie(env, clazz, "transactionCallback", "(IIII)V"); gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J"); - gBinderOffsets.mGetExtension = GetMethodIDOrDie(env, clazz, "getExtension", - "()Landroid/os/IBinder;"); return RegisterMethodsOrDie( env, kBinderPathName, diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index aad8f8a156b5..005c14ddcf69 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8308,26 +8308,9 @@ android:featureFlag="android.app.appfunctions.flags.enable_app_function_manager" android:protectionLevel="signature" /> - <!-- Allows a trusted application to perform actions on behalf of users inside of - applications with privacy guarantees from the system. - <p>This permission is currently only granted to system packages in the - {@link android.app.role.SYSTEM_UI_INTELLIGENCE} role which complies with privacy - requirements outlined in the Android CDD section "9.8.6 Content Capture". - <p>Apps are not able to opt-out from caller having this permission. - <p>Protection level: internal|role - @SystemApi - @hide - @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) --> - <permission android:name="android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED" - android:featureFlag="android.app.appfunctions.flags.enable_app_function_manager" - android:protectionLevel="internal|role" /> - <!-- Allows an application to perform actions on behalf of users inside of applications. <p>This permission is currently only granted to privileged system apps. - <p>Apps contributing app functions can opt to disallow callers with this permission, - limiting to only callers with {@link android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} - instead. <p>Protection level: internal|privileged @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) --> <permission android:name="android.permission.EXECUTE_APP_FUNCTIONS" diff --git a/core/res/res/layout/notification_2025_conversation_header.xml b/core/res/res/layout/notification_2025_conversation_header.xml index 1bde17358825..75bd244cbbf4 100644 --- a/core/res/res/layout/notification_2025_conversation_header.xml +++ b/core/res/res/layout/notification_2025_conversation_header.xml @@ -29,7 +29,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="16sp" + android:textSize="@dimen/notification_2025_title_text_size" android:singleLine="true" android:layout_weight="1" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index d29b7af9e24e..054583297d37 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -102,6 +102,7 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" + android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml index 5beab508aecf..9959b666b3bf 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -104,6 +104,7 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" + android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml index d7c3263904d4..85ca124de8ff 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml @@ -130,6 +130,7 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" + android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_expanded_messaging.xml b/core/res/res/layout/notification_2025_template_expanded_messaging.xml index 7f5a36b5f865..177706c6d58d 100644 --- a/core/res/res/layout/notification_2025_template_expanded_messaging.xml +++ b/core/res/res/layout/notification_2025_template_expanded_messaging.xml @@ -36,14 +36,13 @@ android:clipChildren="false" android:orientation="vertical"> - <!-- Note: the top margin is being set in code based on the estimated space needed for - the header text. --> <com.android.internal.widget.RemeasuringLinearLayout android:id="@+id/notification_main_column" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="top" android:layout_weight="1" + android:layout_marginTop="@dimen/notification_2025_header_height" android:layout_marginEnd="@dimen/notification_content_margin_end" android:orientation="vertical" android:clipChildren="false" diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 586cafdd2b57..a49e03484192 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -7245,6 +7245,9 @@ <!-- Whether desktop mode is supported on the current device --> <bool name="config_isDesktopModeSupported">false</bool> + <!-- Whether the developer option for desktop mode is supported on the current device --> + <bool name="config_isDesktopModeDevOptionSupported">false</bool> + <!-- Maximum number of active tasks on a given Desktop Windowing session. Set to 0 for unlimited. --> <integer name="config_maxDesktopWindowingActiveTasks">0</integer> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index d6b8704a978b..5644cf9dd61d 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -577,6 +577,9 @@ <dimen name="notification_text_size">14sp</dimen> <!-- Size of notification text titles (see TextAppearance.StatusBar.EventContent.Title) --> <dimen name="notification_title_text_size">14sp</dimen> + <!-- Size of notification text titles, 2025 redesign version (see TextAppearance.StatusBar.EventContent.Title) --> + <!-- TODO: b/378660052 - When inlining the redesign flag, this should be updated directly in TextAppearance.DeviceDefault.Notification.Title --> + <dimen name="notification_2025_title_text_size">16sp</dimen> <!-- Size of big notification text titles (see TextAppearance.StatusBar.EventContent.BigTitle) --> <dimen name="notification_big_title_text_size">16sp</dimen> <!-- Size of smaller notification text (see TextAppearance.StatusBar.EventContent.Line2, Info, Time) --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 772a7413a4a7..8bb3c995cf9f 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -574,6 +574,7 @@ <java-symbol type="dimen" name="notification_text_size" /> <java-symbol type="dimen" name="notification_title_text_size" /> <java-symbol type="dimen" name="notification_subtext_size" /> + <java-symbol type="dimen" name="notification_2025_title_text_size" /> <java-symbol type="dimen" name="notification_top_pad" /> <java-symbol type="dimen" name="notification_top_pad_narrow" /> <java-symbol type="dimen" name="notification_top_pad_large_text" /> @@ -5709,6 +5710,9 @@ <!-- Whether desktop mode is supported on the current device --> <java-symbol type="bool" name="config_isDesktopModeSupported" /> + <!-- Whether the developer option for desktop mode is supported on the current device --> + <java-symbol type="bool" name="config_isDesktopModeDevOptionSupported" /> + <!-- Maximum number of active tasks on a given Desktop Windowing session. Set to 0 for unlimited. --> <java-symbol type="integer" name="config_maxDesktopWindowingActiveTasks"/> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index 1b6746ca4b63..c06ad64cc0f5 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -122,7 +122,6 @@ android_test { "android.view.flags-aconfig-java", ], jni_libs: [ - "libperfetto_trace_test_jni", "libpowermanagertest_jni", "libviewRootImplTest_jni", "libworksourceparceltest_jni", diff --git a/core/tests/coretests/jni/Android.bp b/core/tests/coretests/jni/Android.bp index 798ec90eb884..d6379ca8c3e6 100644 --- a/core/tests/coretests/jni/Android.bp +++ b/core/tests/coretests/jni/Android.bp @@ -111,27 +111,3 @@ cc_test_library { ], gtest: false, } - -cc_test_library { - name: "libperfetto_trace_test_jni", - srcs: [ - "PerfettoTraceTest.cpp", - ], - static_libs: [ - "perfetto_trace_protos", - "libtracing_perfetto_test_utils", - ], - shared_libs: [ - "liblog", - "libnativehelper", - "libperfetto_c", - "libprotobuf-cpp-lite", - "libtracing_perfetto", - ], - stl: "libc++_static", - cflags: [ - "-Werror", - "-Wall", - ], - gtest: false, -} diff --git a/core/tests/coretests/jni/PerfettoTraceTest.cpp b/core/tests/coretests/jni/PerfettoTraceTest.cpp deleted file mode 100644 index 41d02ed70c9a..000000000000 --- a/core/tests/coretests/jni/PerfettoTraceTest.cpp +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// #define LOG_NDEBUG 0 -#define LOG_TAG "PerfettoTraceTest" - -#include <nativehelper/JNIHelp.h> -#include <utils/Log.h> - -#include "jni.h" -#include "perfetto/public/abi/data_source_abi.h" -#include "perfetto/public/abi/heap_buffer.h" -#include "perfetto/public/abi/pb_decoder_abi.h" -#include "perfetto/public/abi/tracing_session_abi.h" -#include "perfetto/public/abi/track_event_abi.h" -#include "perfetto/public/compiler.h" -#include "perfetto/public/data_source.h" -#include "perfetto/public/pb_decoder.h" -#include "perfetto/public/producer.h" -#include "perfetto/public/protos/config/trace_config.pzc.h" -#include "perfetto/public/protos/trace/interned_data/interned_data.pzc.h" -#include "perfetto/public/protos/trace/test_event.pzc.h" -#include "perfetto/public/protos/trace/trace.pzc.h" -#include "perfetto/public/protos/trace/trace_packet.pzc.h" -#include "perfetto/public/protos/trace/track_event/debug_annotation.pzc.h" -#include "perfetto/public/protos/trace/track_event/track_descriptor.pzc.h" -#include "perfetto/public/protos/trace/track_event/track_event.pzc.h" -#include "perfetto/public/protos/trace/trigger.pzc.h" -#include "perfetto/public/te_category_macros.h" -#include "perfetto/public/te_macros.h" -#include "perfetto/public/track_event.h" -#include "protos/perfetto/trace/interned_data/interned_data.pb.h" -#include "protos/perfetto/trace/trace.pb.h" -#include "protos/perfetto/trace/trace_packet.pb.h" -#include "tracing_perfetto.h" -#include "utils.h" - -namespace android { -using ::perfetto::protos::EventCategory; -using ::perfetto::protos::EventName; -using ::perfetto::protos::FtraceEvent; -using ::perfetto::protos::FtraceEventBundle; -using ::perfetto::protos::InternedData; -using ::perfetto::protos::Trace; -using ::perfetto::protos::TracePacket; - -using ::perfetto::shlib::test_utils::TracingSession; - -struct TracingSessionHolder { - TracingSession tracing_session; -}; - -static void nativeRegisterPerfetto([[maybe_unused]] JNIEnv* env, jclass /* obj */) { - tracing_perfetto::registerWithPerfetto(false /* test */); -} - -static jlong nativeStartTracing(JNIEnv* env, jclass /* obj */, jbyteArray configBytes) { - jsize length = env->GetArrayLength(configBytes); - std::vector<uint8_t> data; - data.reserve(length); - env->GetByteArrayRegion(configBytes, 0, length, reinterpret_cast<jbyte*>(data.data())); - - TracingSession session = TracingSession::FromBytes(data.data(), length); - TracingSessionHolder* holder = new TracingSessionHolder(std::move(session)); - - return reinterpret_cast<long>(holder); -} - -static jbyteArray nativeStopTracing([[maybe_unused]] JNIEnv* env, jclass /* obj */, jlong ptr) { - TracingSessionHolder* holder = reinterpret_cast<TracingSessionHolder*>(ptr); - - // Stop - holder->tracing_session.FlushBlocking(5000); - holder->tracing_session.StopBlocking(); - - std::vector<uint8_t> data = holder->tracing_session.ReadBlocking(); - - delete holder; - - jbyteArray bytes = env->NewByteArray(data.size()); - env->SetByteArrayRegion(bytes, 0, data.size(), reinterpret_cast<jbyte*>(data.data())); - return bytes; -} - -extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) { - JNIEnv* env; - const JNINativeMethod methodTable[] = {/* name, signature, funcPtr */ - {"nativeStartTracing", "([B)J", - (void*)nativeStartTracing}, - {"nativeStopTracing", "(J)[B", (void*)nativeStopTracing}, - {"nativeRegisterPerfetto", "()V", - (void*)nativeRegisterPerfetto}}; - - if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { - return JNI_ERR; - } - - jniRegisterNativeMethods(env, "android/os/PerfettoTraceTest", methodTable, - sizeof(methodTable) / sizeof(JNINativeMethod)); - - return JNI_VERSION_1_6; -} - -} /* namespace android */ diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java index 5da2564e5b7f..ac78e87aa60d 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -37,8 +37,10 @@ import android.app.PropertyInvalidatedCache.Args; import android.app.PropertyInvalidatedCache.NonceWatcher; import android.app.PropertyInvalidatedCache.NonceStore; import android.os.Binder; +import android.util.Log; import com.android.internal.os.ApplicationSharedMemory; +import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; @@ -410,8 +412,7 @@ public class PropertyInvalidatedCacheTests { // Verify that invalidating the cache from an app process would fail due to lack of permissions. @Test - @android.platform.test.annotations.DisabledOnRavenwood( - reason = "SystemProperties doesn't have permission check") + @DisabledOnRavenwood(reason = "SystemProperties doesn't have permission check") public void testPermissionFailure() { // Create a cache that will write a system nonce. TestCache sysCache = new TestCache(MODULE_SYSTEM, "mode1"); @@ -556,8 +557,7 @@ public class PropertyInvalidatedCacheTests { // storing nonces in shared memory. @RequiresFlagsEnabled(FLAG_APPLICATION_SHARED_MEMORY_ENABLED) @Test - @android.platform.test.annotations.DisabledOnRavenwood( - reason = "PIC doesn't use SharedMemory on Ravenwood") + @DisabledOnRavenwood(reason = "PIC doesn't use SharedMemory on Ravenwood") public void testSharedMemoryStorage() { // Fetch a shared memory instance for testing. ApplicationSharedMemory shmem = ApplicationSharedMemory.create(); @@ -602,6 +602,49 @@ public class PropertyInvalidatedCacheTests { shmem.close(); } + // Verify that the configured number of nonce slots is actually available. This test + // hard-codes the configured number of slots, which means that this test must be changed + // whenever the shared memory configuration changes. + @RequiresFlagsEnabled(FLAG_APPLICATION_SHARED_MEMORY_ENABLED) + @Test + @DisabledOnRavenwood(reason = "PIC doesn't use SharedMemory on Ravenwood") + public void testSharedMemoryNonceConfig() { + // The two configured constants. These are private to this method since they are only + // used here. + // LINT.IfChange(system_nonce_config) + final int maxNonce = 128; + final int maxByte = 8192; + // LINT.ThenChange(/core/jni/android_app_PropertyInvalidatedCache.h:system_nonce_config) + + // Fetch a shared memory instance for testing. + ApplicationSharedMemory shmem = ApplicationSharedMemory.create(); + + // Create a server-side store. + NonceStore server = new NonceStore(shmem.getSystemNonceBlock(), true); + + // Verify that the configured limits are as expected. + assertEquals(server.mMaxNonce, maxNonce); + assertEquals(server.mMaxByte, maxByte); + + // Create mMaxNonce nonces. These all succeed. + for (int i = 0; i < server.mMaxNonce; i++) { + String name = String.format("name_%03d", i); + assertEquals(i, server.storeName(name)); + } + + // Verify that we cannot create a nonce over the limit. + try { + int i = server.mMaxNonce; + String name = String.format("name_%03d", i); + server.storeName(name); + fail("expected a RuntimeException"); + } catch (RuntimeException e) { + // Okay + } + + shmem.close(); + } + // Verify that an invalid module causes an exception. private void testInvalidModule(String module) { try { diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt index f9d449cd3b10..4ad6708cda93 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt @@ -191,7 +191,7 @@ class FontScaleConverterFactoryTest { .fuzzFractions() .mapNotNull{ FontScaleConverterFactory.forScale(it) } .flatMap{ table -> - generateSequenceOfFractions(-2000f..2000f, step = 0.1f) + generateSequenceOfFractions(-20f..100f, step = 0.1f) .fuzzFractions() .map{ Pair(table, it) } } diff --git a/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt index 6d6b56754000..286c1b94fdb8 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt +++ b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt @@ -23,6 +23,7 @@ import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP import android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT import android.util.SparseArray +import android.util.SparseIntArray import android.view.Display import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -811,6 +812,13 @@ class DisplayTopologyTest { @Test fun coordinates() { + // 1122222244 + // 1122222244 + // 11 44 + // 11 44 + // 1133333344 + // 1133333344 + val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, /* height= */ 600f, /* position= */ 0, /* offset= */ 0f) @@ -837,6 +845,243 @@ class DisplayTopologyTest { assertThat(coords.contentEquals(expectedCoords)).isTrue() } + @Test + fun graph() { + // 1122222244 + // 1122222244 + // 11 44 + // 11 44 + // 1133333344 + // 1133333344 + // 555 + // 555 + // 555 + + val densityPerDisplay = SparseIntArray() + + val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, + /* height= */ 600f, /* position= */ 0, /* offset= */ 0f) + val density1 = 100 + densityPerDisplay.append(1, density1) + + val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 600f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ 0f) + display1.addChild(display2) + val density2 = 200 + densityPerDisplay.append(2, density2) + + val primaryDisplayId = 3 + val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ 400f) + display1.addChild(display3) + val density3 = 150 + densityPerDisplay.append(3, density3) + + val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f, + /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) + display2.addChild(display4) + val density4 = 300 + densityPerDisplay.append(4, density4) + + val display5 = DisplayTopology.TreeNode(/* displayId= */ 5, /* width= */ 300f, + /* height= */ 300f, POSITION_BOTTOM, /* offset= */ -100f) + display4.addChild(display5) + val density5 = 300 + densityPerDisplay.append(5, density5) + + topology = DisplayTopology(display1, primaryDisplayId) + val graph = topology.getGraph(densityPerDisplay)!! + val nodes = graph.displayNodes + + assertThat(graph.primaryDisplayId).isEqualTo(primaryDisplayId) + assertThat(nodes.map {it.displayId}).containsExactly(1, 2, 3, 4, 5) + for (node in nodes) { + assertThat(node.density).isEqualTo(densityPerDisplay.get(node.displayId)) + when (node.displayId) { + 1 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 2, POSITION_RIGHT, + /* offsetDp= */ 0f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_RIGHT, + /* offsetDp= */ 400f)) + 2 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 1, POSITION_LEFT, + /* offsetDp= */ 0f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 4, POSITION_RIGHT, + /* offsetDp= */ 0f)) + 3 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 1, POSITION_LEFT, + /* offsetDp= */ -400f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 4, POSITION_RIGHT, + /* offsetDp= */ -400f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 5, POSITION_BOTTOM, + /* offsetDp= */ 500f)) + 4 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 2, POSITION_LEFT, + /* offsetDp= */ 0f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_LEFT, + /* offsetDp= */ 400f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 5, POSITION_BOTTOM, + /* offsetDp= */ -100f)) + 5 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_TOP, + /* offsetDp= */ -500f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 4, POSITION_TOP, + /* offsetDp= */ 100f)) + } + } + } + + @Test + fun graph_corner() { + // 1122244 + // 1122244 + // 1122244 + // 333 + // 55 + + val densityPerDisplay = SparseIntArray() + + val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, + /* height= */ 300f, /* position= */ 0, /* offset= */ 0f) + val density1 = 100 + densityPerDisplay.append(1, density1) + + val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 300f, + /* height= */ 300f, POSITION_RIGHT, /* offset= */ 0f) + display1.addChild(display2) + val density2 = 200 + densityPerDisplay.append(2, density2) + + val primaryDisplayId = 3 + val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 300f, + /* height= */ 100f, POSITION_BOTTOM, /* offset= */ 0f) + display2.addChild(display3) + val density3 = 150 + densityPerDisplay.append(3, density3) + + val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f, + /* height= */ 300f, POSITION_RIGHT, /* offset= */ 0f) + display2.addChild(display4) + val density4 = 300 + densityPerDisplay.append(4, density4) + + val display5 = DisplayTopology.TreeNode(/* displayId= */ 5, /* width= */ 200f, + /* height= */ 100f, POSITION_BOTTOM, /* offset= */ -200f) + display3.addChild(display5) + val density5 = 300 + densityPerDisplay.append(5, density5) + + topology = DisplayTopology(display1, primaryDisplayId) + val graph = topology.getGraph(densityPerDisplay)!! + val nodes = graph.displayNodes + + assertThat(graph.primaryDisplayId).isEqualTo(primaryDisplayId) + assertThat(nodes.map {it.displayId}).containsExactly(1, 2, 3, 4, 5) + for (node in nodes) { + assertThat(node.density).isEqualTo(densityPerDisplay.get(node.displayId)) + when (node.displayId) { + 1 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 2, POSITION_RIGHT, + /* offsetDp= */ 0f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_RIGHT, + /* offsetDp= */ 300f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_BOTTOM, + /* offsetDp= */ 200f)) + 2 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 1, POSITION_LEFT, + /* offsetDp= */ 0f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_BOTTOM, + /* offsetDp= */ 0f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 4, POSITION_RIGHT, + /* offsetDp= */ 0f)) + 3 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 1, POSITION_LEFT, + /* offsetDp= */ -300f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 1, POSITION_TOP, + /* offsetDp= */ -200f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 2, POSITION_TOP, + /* offsetDp= */ 0f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 4, POSITION_RIGHT, + /* offsetDp= */ -300f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 4, POSITION_TOP, + /* offsetDp= */ 300f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 5, POSITION_LEFT, + /* offsetDp= */ 100f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 5, POSITION_BOTTOM, + /* offsetDp= */ -200f)) + 4 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 2, POSITION_LEFT, + /* offsetDp= */ 0f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_LEFT, + /* offsetDp= */ 300f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_BOTTOM, + /* offsetDp= */ -300f)) + 5 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_TOP, + /* offsetDp= */ 200f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_RIGHT, + /* offsetDp= */ -100f)) + } + } + } + + @Test + fun graph_smallGap() { + // 11122 + // 11122 + // 11133 + // 11133 + + // There is a gap between displays 2 and 3, small enough for them to still be adjacent. + + val densityPerDisplay = SparseIntArray() + + val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 300f, + /* height= */ 400f, /* position= */ 0, /* offset= */ 0f) + val density1 = 100 + densityPerDisplay.append(1, density1) + + val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 200f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ -1f) + display1.addChild(display2) + val density2 = 200 + densityPerDisplay.append(2, density2) + + val primaryDisplayId = 3 + val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 200f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ 201f) + display1.addChild(display3) + val density3 = 150 + densityPerDisplay.append(3, density3) + + topology = DisplayTopology(display1, primaryDisplayId) + val graph = topology.getGraph(densityPerDisplay)!! + val nodes = graph.displayNodes + + assertThat(graph.primaryDisplayId).isEqualTo(primaryDisplayId) + assertThat(nodes.map {it.displayId}).containsExactly(1, 2, 3) + for (node in nodes) { + assertThat(node.density).isEqualTo(densityPerDisplay.get(node.displayId)) + when (node.displayId) { + 1 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 2, POSITION_RIGHT, + /* offsetDp= */ -1f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_RIGHT, + /* offsetDp= */ 201f)) + 2 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 1, POSITION_LEFT, + /* offsetDp= */ 1f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 3, POSITION_BOTTOM, + /* offsetDp= */ 0f)) + 3 -> assertThat(node.adjacentDisplays.toSet()).containsExactly( + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 1, POSITION_LEFT, + /* offsetDp= */ -201f), + DisplayTopologyGraph.AdjacentDisplay(/* displayId= */ 2, POSITION_TOP, + /* offsetDp= */ 0f)) + } + } + } + /** * Runs the rearrange algorithm and returns the resulting tree as a list of nodes, with the * root at index 0. The number of nodes is inferred from the number of positions passed. diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java index 31e07524d777..9aac02d0d07e 100644 --- a/core/tests/coretests/src/android/os/BundleTest.java +++ b/core/tests/coretests/src/android/os/BundleTest.java @@ -76,7 +76,7 @@ public class BundleTest { /** * Create a test bundle, parcel it and return the parcel. */ - private Parcel createBundleParcel(boolean withFd) throws Exception { + private Parcel createBundleParcel(boolean withFd, boolean hasIntent) throws Exception { final Bundle source = new Bundle(); source.putString("string", "abc"); source.putInt("int", 1); @@ -85,13 +85,14 @@ public class BundleTest { pipe[1].close(); source.putParcelable("fd", pipe[0]); } + source.setHasIntent(hasIntent); return getParcelledBundle(source); } /** * Verify a bundle generated by {@link #createBundleParcel(boolean)}. */ - private void checkBundle(Bundle b, boolean withFd) { + private void checkBundle(Bundle b, boolean withFd, boolean hasIntent) { // First, do the checks without actually unparceling the bundle. // (Note looking into the contents will unparcel a bundle, so we'll do it later.) assertTrue("mParcelledData shouldn't be null here.", b.isParcelled()); @@ -107,6 +108,8 @@ public class BundleTest { b.mFlags & (Bundle.FLAG_HAS_FDS | Bundle.FLAG_HAS_FDS_KNOWN)); } + assertEquals(b.hasIntent(), hasIntent); + // Then, check the contents. assertEquals("abc", b.getString("string")); assertEquals(1, b.getInt("int")); @@ -139,42 +142,56 @@ public class BundleTest { withFd = false; // new Bundle with p - p = createBundleParcel(withFd); - checkBundle(new Bundle(p), withFd); + p = createBundleParcel(withFd, false); + checkBundle(new Bundle(p), withFd, false); p.recycle(); // new Bundle with p and length - p = createBundleParcel(withFd); + p = createBundleParcel(withFd, false); length = p.readInt(); - checkBundle(new Bundle(p, length), withFd); + checkBundle(new Bundle(p, length), withFd, false); + p.recycle(); + + // readFromParcel() + p = createBundleParcel(withFd, false); + b = new Bundle(); + b.readFromParcel(p); + checkBundle(b, withFd, false); p.recycle(); // readFromParcel() - p = createBundleParcel(withFd); + p = createBundleParcel(withFd, true); b = new Bundle(); b.readFromParcel(p); - checkBundle(b, withFd); + checkBundle(b, withFd, true); p.recycle(); // Same test with FDs. withFd = true; // new Bundle with p - p = createBundleParcel(withFd); - checkBundle(new Bundle(p), withFd); + p = createBundleParcel(withFd, false); + checkBundle(new Bundle(p), withFd, false); p.recycle(); // new Bundle with p and length - p = createBundleParcel(withFd); + p = createBundleParcel(withFd, false); length = p.readInt(); - checkBundle(new Bundle(p, length), withFd); + checkBundle(new Bundle(p, length), withFd, false); + p.recycle(); + + // readFromParcel() + p = createBundleParcel(withFd, false); + b = new Bundle(); + b.readFromParcel(p); + checkBundle(b, withFd, false); p.recycle(); // readFromParcel() - p = createBundleParcel(withFd); + p = createBundleParcel(withFd, true); b = new Bundle(); b.readFromParcel(p); - checkBundle(b, withFd); + checkBundle(b, withFd, true); p.recycle(); } @@ -486,6 +503,7 @@ public class BundleTest { p.writeInt(131313); // Invalid type p.writeInt(0); // Anything, really int end = p.dataPosition(); + p.writeBoolean(false); p.setDataPosition(0); return new Bundle(p, end - start); } diff --git a/core/tests/coretests/src/android/os/PerfettoTraceTest.java b/core/tests/coretests/src/android/os/PerfettoTraceTest.java index ad28383689af..0b5a44665d2b 100644 --- a/core/tests/coretests/src/android/os/PerfettoTraceTest.java +++ b/core/tests/coretests/src/android/os/PerfettoTraceTest.java @@ -28,7 +28,6 @@ import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.ArraySet; -import android.util.Log; import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -84,19 +83,9 @@ public class PerfettoTraceTest { private final Set<String> mDebugAnnotationNames = new ArraySet<>(); private final Set<String> mTrackNames = new ArraySet<>(); - static { - try { - System.loadLibrary("perfetto_trace_test_jni"); - Log.i(TAG, "Successfully loaded trace_test native library"); - } catch (UnsatisfiedLinkError ule) { - Log.w(TAG, "Could not load trace_test native library"); - } - } - @Before public void setUp() { - PerfettoTrace.register(); - nativeRegisterPerfetto(); + PerfettoTrace.register(true); FOO_CATEGORY.register(); mCategoryNames.clear(); @@ -110,7 +99,7 @@ public class PerfettoTraceTest { public void testDebugAnnotations() throws Exception { TraceConfig traceConfig = getTraceConfig(FOO); - long ptr = nativeStartTracing(traceConfig.toByteArray()); + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.instant(FOO_CATEGORY, "event") .addFlow(2) @@ -121,7 +110,7 @@ public class PerfettoTraceTest { .addArg("string_val", FOO) .emit(); - byte[] traceBytes = nativeStopTracing(ptr); + byte[] traceBytes = session.close(); Trace trace = Trace.parseFrom(traceBytes); @@ -165,11 +154,11 @@ public class PerfettoTraceTest { public void testDebugAnnotationsWithLambda() throws Exception { TraceConfig traceConfig = getTraceConfig(FOO); - long ptr = nativeStartTracing(traceConfig.toByteArray()); + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.instant(FOO_CATEGORY, "event").addArg("long_val", 123L).emit(); - byte[] traceBytes = nativeStopTracing(ptr); + byte[] traceBytes = session.close(); Trace trace = Trace.parseFrom(traceBytes); @@ -200,7 +189,7 @@ public class PerfettoTraceTest { public void testNamedTrack() throws Exception { TraceConfig traceConfig = getTraceConfig(FOO); - long ptr = nativeStartTracing(traceConfig.toByteArray()); + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.begin(FOO_CATEGORY, "event") .usingNamedTrack(PerfettoTrace.getProcessTrackUuid(), FOO) @@ -211,7 +200,7 @@ public class PerfettoTraceTest { .usingNamedTrack(PerfettoTrace.getThreadTrackUuid(Process.myTid()), "bar") .emit(); - Trace trace = Trace.parseFrom(nativeStopTracing(ptr)); + Trace trace = Trace.parseFrom(session.close()); boolean hasTrackEvent = false; boolean hasTrackUuid = false; @@ -248,7 +237,7 @@ public class PerfettoTraceTest { public void testProcessThreadNamedTrack() throws Exception { TraceConfig traceConfig = getTraceConfig(FOO); - long ptr = nativeStartTracing(traceConfig.toByteArray()); + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.begin(FOO_CATEGORY, "event") .usingProcessNamedTrack(FOO) @@ -259,7 +248,7 @@ public class PerfettoTraceTest { .usingThreadNamedTrack(Process.myTid(), "%s-%s", "bar", "stool") .emit(); - Trace trace = Trace.parseFrom(nativeStopTracing(ptr)); + Trace trace = Trace.parseFrom(session.close()); boolean hasTrackEvent = false; boolean hasTrackUuid = false; @@ -296,13 +285,13 @@ public class PerfettoTraceTest { public void testCounterSimple() throws Exception { TraceConfig traceConfig = getTraceConfig(FOO); - long ptr = nativeStartTracing(traceConfig.toByteArray()); + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.counter(FOO_CATEGORY, 16, FOO).emit(); PerfettoTrace.counter(FOO_CATEGORY, 3.14, "bar").emit(); - Trace trace = Trace.parseFrom(nativeStopTracing(ptr)); + Trace trace = Trace.parseFrom(session.close()); boolean hasTrackEvent = false; boolean hasCounterValue = false; @@ -339,7 +328,7 @@ public class PerfettoTraceTest { public void testCounter() throws Exception { TraceConfig traceConfig = getTraceConfig(FOO); - long ptr = nativeStartTracing(traceConfig.toByteArray()); + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.counter(FOO_CATEGORY, 16) .usingCounterTrack(PerfettoTrace.getProcessTrackUuid(), FOO).emit(); @@ -348,7 +337,7 @@ public class PerfettoTraceTest { .usingCounterTrack(PerfettoTrace.getThreadTrackUuid(Process.myTid()), "%s-%s", "bar", "stool").emit(); - Trace trace = Trace.parseFrom(nativeStopTracing(ptr)); + Trace trace = Trace.parseFrom(session.close()); boolean hasTrackEvent = false; boolean hasCounterValue = false; @@ -385,14 +374,14 @@ public class PerfettoTraceTest { public void testProcessThreadCounter() throws Exception { TraceConfig traceConfig = getTraceConfig(FOO); - long ptr = nativeStartTracing(traceConfig.toByteArray()); + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.counter(FOO_CATEGORY, 16).usingProcessCounterTrack(FOO).emit(); PerfettoTrace.counter(FOO_CATEGORY, 3.14) .usingThreadCounterTrack(Process.myTid(), "%s-%s", "bar", "stool").emit(); - Trace trace = Trace.parseFrom(nativeStopTracing(ptr)); + Trace trace = Trace.parseFrom(session.close()); boolean hasTrackEvent = false; boolean hasCounterValue = false; @@ -429,7 +418,7 @@ public class PerfettoTraceTest { public void testProto() throws Exception { TraceConfig traceConfig = getTraceConfig(FOO); - long ptr = nativeStartTracing(traceConfig.toByteArray()); + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.instant(FOO_CATEGORY, "event_proto") .beginProto() @@ -441,7 +430,7 @@ public class PerfettoTraceTest { .endProto() .emit(); - byte[] traceBytes = nativeStopTracing(ptr); + byte[] traceBytes = session.close(); Trace trace = Trace.parseFrom(traceBytes); @@ -477,7 +466,7 @@ public class PerfettoTraceTest { public void testProtoNested() throws Exception { TraceConfig traceConfig = getTraceConfig(FOO); - long ptr = nativeStartTracing(traceConfig.toByteArray()); + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.instant(FOO_CATEGORY, "event_proto_nested") .beginProto() @@ -494,7 +483,7 @@ public class PerfettoTraceTest { .endProto() .emit(); - byte[] traceBytes = nativeStopTracing(ptr); + byte[] traceBytes = session.close(); Trace trace = Trace.parseFrom(traceBytes); @@ -538,13 +527,13 @@ public class PerfettoTraceTest { public void testActivateTrigger() throws Exception { TraceConfig traceConfig = getTriggerTraceConfig(FOO, FOO); - long ptr = nativeStartTracing(traceConfig.toByteArray()); + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.instant(FOO_CATEGORY, "event_trigger").emit(); PerfettoTrace.activateTrigger(FOO, 1000); - byte[] traceBytes = nativeStopTracing(ptr); + byte[] traceBytes = session.close(); Trace trace = Trace.parseFrom(traceBytes); @@ -569,7 +558,7 @@ public class PerfettoTraceTest { TraceConfig traceConfig = getTraceConfig(BAR); Category barCategory = new Category(BAR); - long ptr = nativeStartTracing(traceConfig.toByteArray()); + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.instant(barCategory, "event") .addArg("before", 1) @@ -581,7 +570,7 @@ public class PerfettoTraceTest { .addArg("after", 1) .emit(); - byte[] traceBytes = nativeStopTracing(ptr); + byte[] traceBytes = session.close(); Trace trace = Trace.parseFrom(traceBytes); @@ -603,10 +592,6 @@ public class PerfettoTraceTest { assertThat(mDebugAnnotationNames).doesNotContain("before"); } - private static native long nativeStartTracing(byte[] config); - private static native void nativeRegisterPerfetto(); - private static native byte[] nativeStopTracing(long ptr); - private TrackEvent getTrackEvent(Trace trace, int idx) { int curIdx = 0; for (TracePacket packet: trace.getPacketList()) { diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java index 4a5123ec0663..b42bcee77c67 100644 --- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java +++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java @@ -499,57 +499,6 @@ public class MainContentCaptureSessionTest { assertThat(session.mEventProcessQueue).hasSize(1); } - @Test - public void notifyContentCaptureEvents_beforeSessionPerformStart() throws RemoteException { - ContentCaptureOptions options = - createOptions( - /* enableContentCaptureReceiver= */ true, - /* enableContentProtectionReceiver= */ true); - MainContentCaptureSession session = createSession(options); - session.mContentCaptureHandler = null; - session.mDirectServiceInterface = null; - - notifyContentCaptureEvents(session); - mTestableLooper.processAllMessages(); - - assertThat(session.mEvents).isNull(); - assertThat(session.mEventProcessQueue).hasSize(7); // 5 view events + 2 view tree events - } - - @Test - public void notifyViewAppeared_beforeSessionPerformStart() throws RemoteException { - ContentCaptureOptions options = - createOptions( - /* enableContentCaptureReceiver= */ true, - /* enableContentProtectionReceiver= */ true); - MainContentCaptureSession session = createSession(options); - session.mContentCaptureHandler = null; - session.mDirectServiceInterface = null; - - View view = prepareView(session); - session.notifyViewAppeared(session.newViewStructure(view)); - - assertThat(session.mEvents).isNull(); - assertThat(session.mEventProcessQueue).hasSize(1); - } - - @Test - public void flush_beforeSessionPerformStart() throws Exception { - ContentCaptureOptions options = - createOptions( - /* enableContentCaptureReceiver= */ true, - /* enableContentProtectionReceiver= */ true); - MainContentCaptureSession session = createSession(options); - session.mEvents = new ArrayList<>(Arrays.asList(EVENT)); - session.mContentCaptureHandler = null; - session.mDirectServiceInterface = null; - - session.flush(REASON); - - assertThat(session.mEvents).hasSize(1); - assertThat(session.mEventProcessQueue).isEmpty(); - } - /** Simulates the regular content capture events sequence. */ private void notifyContentCaptureEvents(final MainContentCaptureSession session) { final ArrayList<Object> events = new ArrayList<>( @@ -612,8 +561,8 @@ public class MainContentCaptureSessionTest { sStrippedContext, manager, testHandler, + testHandler, mMockSystemServerInterface); - session.mContentCaptureHandler = testHandler; session.mComponentName = COMPONENT_NAME; return session; } diff --git a/core/tests/coretests/src/com/android/internal/notification/NotificationChannelGroupsHelperTest.java b/core/tests/coretests/src/com/android/internal/notification/NotificationChannelGroupsHelperTest.java new file mode 100644 index 000000000000..26e96ea79e4b --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/notification/NotificationChannelGroupsHelperTest.java @@ -0,0 +1,268 @@ +/* + * 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.internal.notification; + +import static android.app.NotificationManager.IMPORTANCE_DEFAULT; +import static android.app.NotificationManager.IMPORTANCE_NONE; + +import static com.android.internal.notification.NotificationChannelGroupsHelper.getGroupWithChannels; +import static com.android.internal.notification.NotificationChannelGroupsHelper.getGroupsWithChannels; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.notification.NotificationChannelGroupsHelper.Params; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +public class NotificationChannelGroupsHelperTest { + private Collection<NotificationChannel> mChannels; + private Map<String, NotificationChannelGroup> mGroups; + + @Before + public void setUp() { + // Test data setup. + // Channels and their corresponding groups: + // * "regular": a channel that is not deleted or blocked. In group A. + // * "blocked": blocked channel. In group A. + // * "deleted": deleted channel. In group A. + // * "adrift": regular channel. No group. + // * "gone": deleted channel. No group. + // * "alternate": regular channel. In group B. + // * "another blocked": blocked channel. In group B. + // * "another deleted": deleted channel. In group C. + // * Additionally, there is an empty group D. + mChannels = List.of(makeChannel("regular", "a", false, false), + makeChannel("blocked", "a", true, false), + makeChannel("deleted", "a", false, true), + makeChannel("adrift", null, false, false), + makeChannel("gone", null, false, true), + makeChannel("alternate", "b", false, false), + makeChannel("anotherBlocked", "b", true, false), + makeChannel("anotherDeleted", "c", false, true)); + + mGroups = Map.of("a", new NotificationChannelGroup("a", "a"), + "b", new NotificationChannelGroup("b", "b"), + "c", new NotificationChannelGroup("c", "c"), + "d", new NotificationChannelGroup("d", "d")); + } + + @Test + public void testGetGroup_noDeleted() { + NotificationChannelGroup res = getGroupWithChannels("a", mChannels, mGroups, false); + assertThat(res).isNotNull(); + assertThat(res.getChannels()).hasSize(2); // "regular" & "blocked" + assertThat(res.getChannels()).containsExactlyElementsIn(List.of( + makeChannel("regular", "a", false, false), + makeChannel("blocked", "a", true, false))); + } + + @Test + public void testGetGroup_includeDeleted() { + NotificationChannelGroup res = getGroupWithChannels("c", mChannels, mGroups, true); + assertThat(res).isNotNull(); + assertThat(res.getChannels()).hasSize(1); + assertThat(res.getChannels().getFirst()).isEqualTo( + makeChannel("anotherDeleted", "c", false, true)); + } + + @Test + public void testGetGroup_empty() { + NotificationChannelGroup res = getGroupWithChannels("d", mChannels, mGroups, true); + assertThat(res).isNotNull(); + assertThat(res.getChannels()).isEmpty(); + } + + @Test + public void testGetGroup_emptyBecauseNoChannelMatch() { + NotificationChannelGroup res = getGroupWithChannels("c", mChannels, mGroups, false); + assertThat(res).isNotNull(); + assertThat(res.getChannels()).isEmpty(); + } + + @Test + public void testGetGroup_nonexistent() { + NotificationChannelGroup res = getGroupWithChannels("e", mChannels, mGroups, true); + assertThat(res).isNull(); + } + + @Test + public void testGetGroups_paramsForAllGroups() { + // deleted=false, nongrouped=false, empty=true, blocked=true, no channel filter + List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups, + Params.forAllGroups()); + + NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a"); + expectedA.setChannels(List.of( + makeChannel("regular", "a", false, false), + makeChannel("blocked", "a", true, false))); + + NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b"); + expectedB.setChannels(List.of( + makeChannel("alternate", "b", false, false), + makeChannel("anotherBlocked", "b", true, false))); + + NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c"); + expectedC.setChannels(new ArrayList<>()); // empty, no deleted + + NotificationChannelGroup expectedD = new NotificationChannelGroup("d", "d"); + expectedD.setChannels(new ArrayList<>()); // empty + + assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedD); + } + + @Test + public void testGetGroups_paramsForAllChannels_noDeleted() { + // Excluding deleted channels to means group C is not included because it's "empty" + List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups, + Params.forAllChannels(false)); + + NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a"); + expectedA.setChannels(List.of( + makeChannel("regular", "a", false, false), + makeChannel("blocked", "a", true, false))); + + NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b"); + expectedB.setChannels(List.of( + makeChannel("alternate", "b", false, false), + makeChannel("anotherBlocked", "b", true, false))); + + NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null); + expectedUngrouped.setChannels(List.of( + makeChannel("adrift", null, false, false), + makeChannel("gone", null, false, true))); + + assertThat(res).containsExactly(expectedA, expectedB, expectedUngrouped); + } + + @Test + public void testGetGroups_paramsForAllChannels_withDeleted() { + // This will get everything! + List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups, + Params.forAllChannels(true)); + + NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a"); + expectedA.setChannels(List.of( + makeChannel("regular", "a", false, false), + makeChannel("blocked", "a", true, false), + makeChannel("deleted", "a", false, true))); + + NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b"); + expectedB.setChannels(List.of( + makeChannel("alternate", "b", false, false), + makeChannel("anotherBlocked", "b", true, false))); + + NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c"); + expectedC.setChannels(List.of(makeChannel("anotherDeleted", "c", false, true))); + + // no D, because D is empty + + NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null); + expectedUngrouped.setChannels(List.of(makeChannel("adrift", null, false, false))); + + assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedUngrouped); + } + + @Test + public void testGetGroups_onlySpecifiedOrBlocked() { + Set<String> filter = Set.of("regular", "blocked", "adrift", "anotherDeleted"); + + // also not including deleted channels to check intersection of those params + List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups, + Params.onlySpecifiedOrBlockedChannels(filter)); + + NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a"); + expectedA.setChannels(List.of( + makeChannel("regular", "a", false, false), + makeChannel("blocked", "a", true, false))); + + // While nothing matches the filter from group B, includeBlocked=true means all blocked + // channels are included even if they are not in the filter. + NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b"); + expectedB.setChannels(List.of(makeChannel("anotherBlocked", "b", true, false))); + + NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c"); + expectedC.setChannels(new ArrayList<>()); // deleted channel not included + + NotificationChannelGroup expectedD = new NotificationChannelGroup("d", "d"); + expectedD.setChannels(new ArrayList<>()); // empty + + NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null); + expectedUngrouped.setChannels(List.of(makeChannel("adrift", null, false, false))); + + assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedD, + expectedUngrouped); + } + + + @Test + public void testGetGroups_noBlockedWithFilter() { + Set<String> filter = Set.of("regular", "blocked", "adrift"); + + // The includeBlocked setting only takes effect if there is a channel filter. + List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups, + new Params(true, true, true, false, filter)); + + // Even though includeBlocked=false, "blocked" is included because it's explicitly specified + // by the channel filter. + NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a"); + expectedA.setChannels(List.of( + makeChannel("regular", "a", false, false), + makeChannel("blocked", "a", true, false))); + + NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b"); + expectedB.setChannels(new ArrayList<>()); // no matches; blocked channel not in filter + + NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c"); + expectedC.setChannels(new ArrayList<>()); // no matches + + NotificationChannelGroup expectedD = new NotificationChannelGroup("d", "d"); + expectedD.setChannels(new ArrayList<>()); // empty + + NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null); + expectedUngrouped.setChannels(List.of(makeChannel("adrift", null, false, false))); + + assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedD, + expectedUngrouped); + } + + private NotificationChannel makeChannel(String id, String groupId, boolean blocked, + boolean deleted) { + NotificationChannel c = new NotificationChannel(id, id, + blocked ? IMPORTANCE_NONE : IMPORTANCE_DEFAULT); + if (deleted) { + c.setDeleted(true); + } + if (groupId != null) { + c.setGroup(groupId); + } + return c; + } +} diff --git a/core/tests/coretests/src/com/android/internal/notification/OWNERS b/core/tests/coretests/src/com/android/internal/notification/OWNERS new file mode 100644 index 000000000000..396fd1213aca --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/notification/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/notification/OWNERS diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index b8059d08756a..1edbffa9d572 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -594,7 +594,6 @@ applications that come with the platform <!-- Permission required for CTS test - FileIntegrityManagerTest --> <permission name="android.permission.SETUP_FSVERITY" /> <!-- Permissions required for CTS test - AppFunctionManagerTest --> - <permission name="android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED" /> <permission name="android.permission.EXECUTE_APP_FUNCTIONS" /> <!-- Permission required for CTS test - CtsNfcTestCases --> <permission name="android.permission.NFC_SET_CONTROLLER_ALWAYS_ON" /> diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 065644627393..13d0169c47c5 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -177,10 +177,3 @@ flag { description: "Factor task-view state tracking out of taskviewtransitions" bug: "384976265" } - -flag { - name: "enable_non_default_display_split" - namespace: "multitasking" - description: "Enables split screen on non default displays" - bug: "384999213" -} diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml index fd578a959e3b..95cd1c72a2af 100644 --- a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml +++ b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml @@ -1,10 +1,19 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.wm.shell.multivalenttests"> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/> + <application android:debuggable="true" android:supportsRtl="true" > <uses-library android:name="android.test.runner" /> <activity android:name="com.android.wm.shell.bubbles.bar.BubbleBarAnimationHelperTest$TestActivity" android:exported="true"/> + + <activity android:name=".bubbles.TestActivity" + android:allowEmbedded="true" + android:documentLaunchMode="always" + android:excludeFromRecents="true" + android:exported="false" + android:resizeableActivity="true" /> </application> <instrumentation diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt index 09a93d501f8e..77acbde7a465 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt @@ -35,7 +35,6 @@ import com.android.internal.statusbar.IStatusBarService import com.android.wm.shell.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.bubbles.Bubbles.SysuiProxy -import com.android.wm.shell.bubbles.properties.ProdBubbleProperties import com.android.wm.shell.bubbles.storage.BubblePersistentRepository import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayImeController @@ -288,7 +287,7 @@ class BubbleControllerBubbleBarTest { mock<Transitions>(), SyncTransactionQueue(TransactionPool(), mainExecutor), mock<IWindowManager>(), - ProdBubbleProperties, + BubbleResizabilityChecker() ) } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerTest.kt new file mode 100644 index 000000000000..cec67f26af92 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerTest.kt @@ -0,0 +1,231 @@ +/* + * 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.wm.shell.bubbles + +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.content.pm.PackageManager +import android.graphics.drawable.Icon +import android.os.Handler +import android.os.UserHandle +import android.os.UserManager +import android.view.IWindowManager +import android.view.WindowManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.internal.protolog.ProtoLog +import com.android.internal.statusbar.IStatusBarService +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.bubbles.Bubbles.SysuiProxy +import com.android.wm.shell.bubbles.storage.BubblePersistentRepository +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayImeController +import com.android.wm.shell.common.DisplayInsetsController +import com.android.wm.shell.common.FloatingContentCoordinator +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.common.TaskStackListenerImpl +import com.android.wm.shell.common.TestShellExecutor +import com.android.wm.shell.common.TestSyncExecutor +import com.android.wm.shell.draganddrop.DragAndDropController +import com.android.wm.shell.recents.RecentTasksController +import com.android.wm.shell.shared.TransactionPool +import com.android.wm.shell.sysui.ShellCommandHandler +import com.android.wm.shell.sysui.ShellController +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.taskview.TaskViewRepository +import com.android.wm.shell.taskview.TaskViewTransitions +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.unfold.UnfoldAnimationController +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.util.Optional + +/** Tests for [BubbleControllerTest] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleControllerTest { + + private val context = ApplicationProvider.getApplicationContext<Context>() + + private lateinit var bubbleController: BubbleController + private lateinit var bubblePositioner: BubblePositioner + private lateinit var uiEventLoggerFake: UiEventLoggerFake + private lateinit var bubbleLogger: BubbleLogger + private lateinit var mainExecutor: TestShellExecutor + private lateinit var bgExecutor: TestShellExecutor + private lateinit var bubbleData: BubbleData + private lateinit var eduController: BubbleEducationController + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + ProtoLog.init() + + uiEventLoggerFake = UiEventLoggerFake() + bubbleLogger = BubbleLogger(uiEventLoggerFake) + eduController = BubbleEducationController(context) + + mainExecutor = TestShellExecutor() + bgExecutor = TestShellExecutor() + + // Tests don't have permission to add our window to windowManager, so we mock it :( + val windowManager = mock<WindowManager>() + val realWindowManager = context.getSystemService(WindowManager::class.java) + // But we do want the metrics from the real one + whenever(windowManager.currentWindowMetrics) + .thenReturn(realWindowManager.currentWindowMetrics) + + bubblePositioner = BubblePositioner(context, windowManager) + bubblePositioner.setShowingInBubbleBar(true) + + bubbleData = BubbleData( + context, bubbleLogger, bubblePositioner, eduController, + mainExecutor, bgExecutor + ) + + bubbleController = + createBubbleController( + bubbleData, + windowManager, + bubbleLogger, + bubblePositioner, + mainExecutor, + bgExecutor, + ) + bubbleController.asBubbles().setSysuiProxy(Mockito.mock(SysuiProxy::class.java)) + // Flush so that proxy gets set + mainExecutor.flushAll() + } + + @After + fun tearDown() { + getInstrumentation().waitForIdleSync() + } + + @Test + fun showOrHideNotesBubble_createsNoteBubble() { + val intent = Intent(context, TestActivity::class.java) + intent.setPackage(context.packageName) + val user = UserHandle.of(0) + val expectedKey = Bubble.getNoteBubbleKeyForApp(intent.getPackage(), user) + + getInstrumentation().runOnMainSync { + bubbleController.showOrHideNotesBubble(intent, user, mock<Icon>()) + } + getInstrumentation().waitForIdleSync() + + assertThat(bubbleController.hasBubbles()).isTrue() + assertThat(bubbleData.getAnyBubbleWithKey(expectedKey)).isNotNull() + assertThat(bubbleData.getAnyBubbleWithKey(expectedKey)!!.isNoteBubble).isTrue() + } + + + fun createBubbleController( + bubbleData: BubbleData, + windowManager: WindowManager?, + bubbleLogger: BubbleLogger, + bubblePositioner: BubblePositioner, + mainExecutor: TestShellExecutor, + bgExecutor: TestShellExecutor, + ): BubbleController { + val shellInit = ShellInit(mainExecutor) + val shellCommandHandler = ShellCommandHandler() + val shellController = + ShellController( + context, + shellInit, + shellCommandHandler, + mock<DisplayInsetsController>(), + mainExecutor, + ) + val surfaceSynchronizer = { obj: Runnable -> obj.run() } + + val bubbleDataRepository = + BubbleDataRepository( + mock<LauncherApps>(), + mainExecutor, + bgExecutor, + BubblePersistentRepository(context), + ) + + val shellTaskOrganizer = ShellTaskOrganizer( + Mockito.mock<ShellInit>(ShellInit::class.java), + ShellCommandHandler(), + null, + Optional.empty<UnfoldAnimationController>(), + Optional.empty<RecentTasksController>(), + TestSyncExecutor() + ) + + val resizeChecker: ResizabilityChecker = + object : ResizabilityChecker { + override fun isResizableActivity( + intent: Intent?, + packageManager: PackageManager, key: String + ): Boolean { + return true + } + } + + val bubbleController = BubbleController( + context, + shellInit, + shellCommandHandler, + shellController, + bubbleData, + surfaceSynchronizer, + FloatingContentCoordinator(), + bubbleDataRepository, + mock<IStatusBarService>(), + windowManager, + mock<DisplayInsetsController>(), + mock<DisplayImeController>(), + mock<UserManager>(), + mock<LauncherApps>(), + bubbleLogger, + mock<TaskStackListenerImpl>(), + shellTaskOrganizer, + bubblePositioner, + mock<DisplayController>(), + Optional.empty(), + mock<DragAndDropController>(), + mainExecutor, + mock<Handler>(), + bgExecutor, + mock<TaskViewRepository>(), + mock<TaskViewTransitions>(), + mock<Transitions>(), + SyncTransactionQueue(TransactionPool(), mainExecutor), + mock<IWindowManager>(), + resizeChecker, + ) + bubbleController.setInflateSynchronously(true) + bubbleController.onInit() + + return bubbleController + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt index 1d0c5057c77f..9711889ad028 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -154,19 +154,19 @@ class BubblePositionerTest { /** Test that the default resting position on tablet is middle right. */ @Test - fun testGetDefaultPosition_appBubble_onTablet() { + fun testGetDefaultPosition_noteBubble_onTablet() { positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) - val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */) + val startPosition = positioner.getDefaultStartPosition(true /* isNoteBubble */) assertThat(startPosition.x).isEqualTo(allowableStackRegion.right) assertThat(startPosition.y).isEqualTo(defaultYPosition) } @Test - fun testGetRestingPosition_appBubble_onTablet_RTL() { + fun testGetRestingPosition_noteBubble_onTablet_RTL() { positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) - val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */) + val startPosition = positioner.getDefaultStartPosition(true /* isNoteBubble */) assertThat(startPosition.x).isEqualTo(allowableStackRegion.left) assertThat(startPosition.y).isEqualTo(defaultYPosition) } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt index f1ba0423b422..77aee98e6f6e 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt @@ -35,7 +35,6 @@ import com.android.internal.protolog.ProtoLog import com.android.internal.statusbar.IStatusBarService import com.android.launcher3.icons.BubbleIconFactory import com.android.wm.shell.ShellTaskOrganizer -import com.android.wm.shell.bubbles.properties.BubbleProperties import com.android.wm.shell.bubbles.storage.BubblePersistentRepository import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayImeController @@ -161,7 +160,7 @@ class BubbleViewInfoTaskTest { mock<Transitions>(), SyncTransactionQueue(TransactionPool(), mainExecutor), mock<IWindowManager>(), - mock<BubbleProperties>() + BubbleResizabilityChecker() ) // TODO: (b/371829099) - when optional overflow is no longer flagged we can enable this diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt index 3c013d3636e8..adcd835d72be 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt @@ -38,7 +38,7 @@ class FakeBubbleExpandedViewManager(var bubbleBar: Boolean = false, var expanded override fun dismissBubble(bubble: Bubble, reason: Int) {} - override fun setAppBubbleTaskId(key: String, taskId: Int) {} + override fun setNoteBubbleTaskId(key: String, taskId: Int) {} override fun isStackExpanded(): Boolean { return expanded diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/TestActivity.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/TestActivity.kt new file mode 100644 index 000000000000..40e80d02e7b3 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/TestActivity.kt @@ -0,0 +1,27 @@ +/* + * 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.wm.shell.bubbles + +import android.app.Activity +import android.os.Bundle +import android.widget.FrameLayout + +class TestActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(FrameLayout(getApplicationContext())) + } +} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt index a6492476176b..c022a298e972 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt @@ -43,13 +43,13 @@ import com.android.wm.shell.bubbles.BubbleDataRepository import com.android.wm.shell.bubbles.BubbleExpandedViewManager import com.android.wm.shell.bubbles.BubbleLogger import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.BubbleResizabilityChecker import com.android.wm.shell.bubbles.Bubbles.SysuiProxy import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager import com.android.wm.shell.bubbles.FakeBubbleFactory import com.android.wm.shell.bubbles.FakeBubbleTaskViewFactory import com.android.wm.shell.bubbles.UiEventSubject.Companion.assertThat import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix -import com.android.wm.shell.bubbles.properties.BubbleProperties import com.android.wm.shell.bubbles.storage.BubblePersistentRepository import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayImeController @@ -200,7 +200,7 @@ class BubbleBarLayerViewTest { mock<Transitions>(), SyncTransactionQueue(TransactionPool(), mainExecutor), mock<IWindowManager>(), - mock<BubbleProperties>(), + BubbleResizabilityChecker() ) } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestSyncExecutor.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestSyncExecutor.kt new file mode 100644 index 000000000000..50d9f77389c8 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestSyncExecutor.kt @@ -0,0 +1,34 @@ +/* + * 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.wm.shell.common + +class TestSyncExecutor : ShellExecutor { + override fun execute(runnable: Runnable) { + runnable.run() + } + + override fun executeDelayed(runnable: Runnable, delayMillis: Long) { + runnable.run() + } + + override fun removeCallbacks(runnable: Runnable) { + } + + override fun hasCallback(runnable: Runnable): Boolean { + return false + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/ids.xml b/libs/WindowManager/Shell/res/values/ids.xml index debcba071d9c..c6082b3bd60f 100644 --- a/libs/WindowManager/Shell/res/values/ids.xml +++ b/libs/WindowManager/Shell/res/values/ids.xml @@ -25,6 +25,7 @@ <item type="id" name="action_move_tl_50" /> <item type="id" name="action_move_tl_30" /> <item type="id" name="action_move_rb_full" /> + <item type="id" name="action_swap_apps" /> <!-- For saving PhysicsAnimationLayout animations/animators as view tags. --> <item type="id" name="translation_x_dynamicanimation_tag"/> @@ -46,4 +47,9 @@ <item type="id" name="action_move_bubble_bar_right"/> <item type="id" name="dismiss_view"/> + + <!-- Accessibility actions for desktop windowing. --> + <item type="id" name="action_snap_left"/> + <item type="id" name="action_snap_right"/> + <item type="id" name="action_maximize_restore"/> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 468c345259d0..a2231dd64112 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -90,6 +90,8 @@ <string name="accessibility_action_divider_left_30">Left 30%</string> <!-- Accessibility action for moving docked stack divider to make the right screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_right_full">Right full screen</string> + <!-- Accessibility action for swapping the apps around the divider (double tap action) [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_swap">Swap Apps</string> <!-- Accessibility action for moving docked stack divider to make the top screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_top_full">Top full screen</string> @@ -333,6 +335,28 @@ <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string> + <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_a11y_action_snap_left">Resize app window left</string> + <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_a11y_action_snap_right">Resize app window right</string> + <!-- Accessibility text for the Maximize Menu's snap maximize/restore [CHAR LIMIT=NONE] --> + <string name="desktop_mode_a11y_action_maximize_restore">Maximize or restore window size</string> + + <!-- Accessibility action replacement for caption handle menu split screen button [CHAR LIMIT=NONE] --> + <string name="app_handle_menu_talkback_split_screen_mode_button_text">Enter split screen mode</string> + <!-- Accessibility action replacement for caption handle menu enter desktop mode button [CHAR LIMIT=NONE] --> + <string name="app_handle_menu_talkback_desktop_mode_button_text">Enter desktop windowing mode</string> + <!-- Accessibility action replacement for maximize menu enter snap left button [CHAR LIMIT=NONE] --> + <string name="maximize_menu_talkback_action_snap_left_text">Resize window to left</string> + <!-- Accessibility action replacement for maximize menu enter snap right button [CHAR LIMIT=NONE] --> + <string name="maximize_menu_talkback_action_snap_right_text">Resize window to right</string> + <!-- Accessibility action replacement for maximize menu enter maximize/restore button [CHAR LIMIT=NONE] --> + <string name="maximize_menu_talkback_action_maximize_restore_text">Maximize or restore window size</string> + <!-- Accessibility action replacement for app header maximize/restore button [CHAR LIMIT=NONE] --> + <string name="maximize_button_talkback_action_maximize_restore_text">Maximize or restore window size</string> + <!-- Accessibility action replacement for app header minimize button [CHAR LIMIT=NONE] --> + <string name="minimize_button_talkback_action_maximize_restore_text">Minimize app window</string> + <!-- Accessibility text for open by default settings button [CHAR LIMIT=NONE] --> <string name="open_by_default_settings_text">Open by default settings</string> <!-- Subheader for open by default menu string. --> diff --git a/libs/WindowManager/Shell/shared/Android.bp b/libs/WindowManager/Shell/shared/Android.bp index 261c63948a94..af46ca298efe 100644 --- a/libs/WindowManager/Shell/shared/Android.bp +++ b/libs/WindowManager/Shell/shared/Android.bp @@ -74,6 +74,7 @@ java_library { "**/desktopmode/*.kt", ], static_libs: [ + "WindowManager-Shell-shared-AOSP", "com.android.window.flags.window-aconfig-java", "wm_shell-shared-utils", ], diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt index 0ea3c2a80fb4..835456b2868e 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt @@ -20,20 +20,23 @@ import android.app.TaskInfo import android.content.Context import android.window.DesktopModeFlags import com.android.internal.R +import java.util.ArrayList /** * Class to decide whether to apply app compat policies in desktop mode. */ // TODO(b/347289970): Consider replacing with API -class DesktopModeCompatPolicy(context: Context) { +class DesktopModeCompatPolicy(private val context: Context) { private val systemUiPackage: String = context.resources.getString(R.string.config_systemUi) + private val defaultHomePackage: String? + get() = context.getPackageManager().getHomeActivities(ArrayList())?.packageName /** * If the top activity should be exempt from desktop windowing and forced back to fullscreen. - * Currently includes all system ui activities and modal dialogs. However if the top activity is - * not being displayed, regardless of its configuration, we will not exempt it as to remain in - * the desktop windowing environment. + * Currently includes all system ui, default home and transparent stack activities. However if + * the top activity is not being displayed, regardless of its configuration, we will not exempt + * it as to remain in the desktop windowing environment. */ fun isTopActivityExemptFromDesktopWindowing(task: TaskInfo) = isTopActivityExemptFromDesktopWindowing(task.baseActivity?.packageName, @@ -43,6 +46,7 @@ class DesktopModeCompatPolicy(context: Context) { numActivities: Int, isTopActivityNoDisplay: Boolean, isActivityStackTransparent: Boolean) = DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue && ((isSystemUiTask(packageName) + || isPartOfDefaultHomePackage(packageName) || isTransparentTask(isActivityStackTransparent, numActivities)) && !isTopActivityNoDisplay) @@ -57,4 +61,10 @@ class DesktopModeCompatPolicy(context: Context) { isActivityStackTransparent && numActivities > 0 private fun isSystemUiTask(packageName: String?) = packageName == systemUiPackage + + /** + * Returns true if the tasks base activity is part of the default home package. + */ + private fun isPartOfDefaultHomePackage(packageName: String?) = + packageName != null && packageName == defaultHomePackage } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 1ee71ca78815..ea0894bf1eea 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -18,6 +18,8 @@ package com.android.wm.shell.shared.desktopmode; import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED; +import static com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper.enableBubbleToFullscreen; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -212,10 +214,18 @@ public class DesktopModeStatus { } /** + * Return {@code true} if the current device supports the developer option for desktop mode. + */ + private static boolean isDesktopModeDevOptionSupported(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_isDesktopModeDevOptionSupported); + } + + /** * Return {@code true} if desktop mode dev option should be shown on current device */ public static boolean canShowDesktopModeDevOption(@NonNull Context context) { - return isDeviceEligibleForDesktopMode(context) && Flags.showDesktopWindowingDevOption(); + return isDeviceEligibleForDesktopModeDevOption(context) + && Flags.showDesktopWindowingDevOption(); } /** @@ -226,17 +236,25 @@ public class DesktopModeStatus { } /** Returns if desktop mode dev option should be enabled if there is no user override. */ - public static boolean shouldDevOptionBeEnabledByDefault() { - return Flags.enableDesktopWindowingMode(); + public static boolean shouldDevOptionBeEnabledByDefault(Context context) { + return isDeviceEligibleForDesktopMode(context) && Flags.enableDesktopWindowingMode(); } /** * Return {@code true} if desktop mode is enabled and can be entered on the current device. */ public static boolean canEnterDesktopMode(@NonNull Context context) { - if (!isDeviceEligibleForDesktopMode(context)) return false; + return (isDeviceEligibleForDesktopMode(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()) + || isDesktopModeEnabledByDevOption(context); + } - return DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue(); + /** + * Check if Desktop mode should be enabled because the dev option is shown and enabled. + */ + private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) { + return DesktopModeFlags.isDesktopModeForcedEnabled() + && canShowDesktopModeDevOption(context); } /** @@ -254,7 +272,8 @@ public class DesktopModeStatus { * necessarily enabling desktop mode */ public static boolean overridesShowAppHandle(@NonNull Context context) { - return Flags.showAppHandleLargeScreens() && deviceHasLargeScreen(context); + return (Flags.showAppHandleLargeScreens() || enableBubbleToFullscreen()) + && deviceHasLargeScreen(context); } /** @@ -298,7 +317,21 @@ public class DesktopModeStatus { * Return {@code true} if desktop mode is unrestricted and is supported in the device. */ public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { - return !enforceDeviceRestrictions() || isDesktopModeSupported(context); + return !enforceDeviceRestrictions() || isDesktopModeSupported(context) || ( + Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionSupported( + context)); + } + + /** + * Return {@code true} if the developer option for desktop mode is unrestricted and is supported + * in the device. + * + * Note that, if {@link #isDeviceEligibleForDesktopMode(Context)} is true, then + * {@link #isDeviceEligibleForDesktopModeDevOption(Context)} is also true. + */ + private static boolean isDeviceEligibleForDesktopModeDevOption(@NonNull Context context) { + return !enforceDeviceRestrictions() || isDesktopModeSupported(context) + || isDesktopModeDevOptionSupported(context); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java index b4ef9f0fc2ac..55ed5fa4b56f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java @@ -168,7 +168,8 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { mAnimationRunner.cancelAnimationFromMerge(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 8dabd54a01ff..d1c7f7d7dcad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -1463,7 +1463,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (mClosePrepareTransition == transition) { mClosePrepareTransition = null; @@ -1476,7 +1478,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (info.getType() == TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION && !mCloseTransitionRequested && info.getChanges().isEmpty() && mApps == null) { finishCallback.onTransitionFinished(null); - t.apply(); + startT.apply(); applyFinishOpenTransition(); return; } @@ -1489,7 +1491,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } // Handle the commit transition if this handler is running the open transition. finishCallback.onTransitionFinished(null); - t.apply(); + startT.apply(); if (mCloseTransitionRequested) { if (mApps == null || mApps.length == 0) { // animation was done diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index c40a276cb7bd..947dbd276d3a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -72,12 +72,18 @@ import java.util.concurrent.Executor; public class Bubble implements BubbleViewProvider { private static final String TAG = "Bubble"; - /** A string suffix used in app bubbles' {@link #mKey}. */ + /** A string prefix used in app bubbles' {@link #mKey}. */ public static final String KEY_APP_BUBBLE = "key_app_bubble"; + /** A string prefix used in note bubbles' {@link #mKey}. */ + public static final String KEY_NOTE_BUBBLE = "key_note_bubble"; + /** Whether the bubble is an app bubble. */ private final boolean mIsAppBubble; + /** Whether the bubble is a notetaking bubble. */ + private final boolean mIsNoteBubble; + private final String mKey; @Nullable private final String mGroupKey; @@ -245,6 +251,7 @@ public class Bubble implements BubbleViewProvider { mTaskId = taskId; mBubbleMetadataFlagListener = listener; mIsAppBubble = false; + mIsNoteBubble = false; } private Bubble( @@ -252,6 +259,7 @@ public class Bubble implements BubbleViewProvider { UserHandle user, @Nullable Icon icon, boolean isAppBubble, + boolean isNoteBubble, String key, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { @@ -261,6 +269,7 @@ public class Bubble implements BubbleViewProvider { mUser = user; mIcon = icon; mIsAppBubble = isAppBubble; + mIsNoteBubble = isNoteBubble; mKey = key; mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; @@ -279,6 +288,7 @@ public class Bubble implements BubbleViewProvider { mUser = info.getUserHandle(); mIcon = info.getIcon(); mIsAppBubble = false; + mIsNoteBubble = false; mKey = getBubbleKeyForShortcut(info); mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; @@ -303,6 +313,7 @@ public class Bubble implements BubbleViewProvider { mUser = user; mIcon = icon; mIsAppBubble = true; + mIsNoteBubble = false; mKey = key; mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; @@ -313,6 +324,17 @@ public class Bubble implements BubbleViewProvider { mPackageName = task.baseActivity.getPackageName(); } + /** Creates a notetaking bubble. */ + public static Bubble createNotesBubble(Intent intent, UserHandle user, @Nullable Icon icon, + @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { + return new Bubble(intent, + user, + icon, + /* isAppBubble= */ true, + /* isNoteBubble= */ true, + /* key= */ getNoteBubbleKeyForApp(intent.getPackage(), user), + mainExecutor, bgExecutor); + } /** Creates an app bubble. */ public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon, @@ -321,6 +343,7 @@ public class Bubble implements BubbleViewProvider { user, icon, /* isAppBubble= */ true, + /* isNoteBubble= */ false, /* key= */ getAppBubbleKeyForApp(intent.getPackage(), user), mainExecutor, bgExecutor); } @@ -353,6 +376,16 @@ public class Bubble implements BubbleViewProvider { } /** + * Returns the key for a note bubble from an app with package name, {@code packageName} on an + * Android user, {@code user}. + */ + public static String getNoteBubbleKeyForApp(String packageName, UserHandle user) { + Objects.requireNonNull(packageName); + Objects.requireNonNull(user); + return KEY_NOTE_BUBBLE + ":" + user.getIdentifier() + ":" + packageName; + } + + /** * Returns the key for a shortcut bubble using {@code packageName}, {@code user}, and the * {@code shortcutInfo} id. */ @@ -375,6 +408,7 @@ public class Bubble implements BubbleViewProvider { final Bubbles.PendingIntentCanceledListener intentCancelListener, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { mIsAppBubble = false; + mIsNoteBubble = false; mKey = entry.getKey(); mGroupKey = entry.getGroupKey(); mLocusId = entry.getLocusId(); @@ -1122,12 +1156,19 @@ public class Bubble implements BubbleViewProvider { } /** - * Returns whether this bubble is from an app versus a notification. + * Returns whether this bubble is from an app (as well as notetaking) versus a notification. */ public boolean isAppBubble() { return mIsAppBubble; } + /** + * Returns whether this bubble is specific from the notetaking API. + */ + public boolean isNoteBubble() { + return mIsNoteBubble; + } + /** Creates open app settings intent */ public Intent getSettingsIntent(final Context context) { final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 5cd04b11bbfd..1a03eb5e4a42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -49,7 +49,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.ActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; @@ -95,7 +94,6 @@ import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; -import com.android.wm.shell.bubbles.properties.BubbleProperties; import com.android.wm.shell.bubbles.shortcut.BubbleShortcutHelper; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; @@ -205,9 +203,9 @@ public class BubbleController implements ConfigurationChangeListener, private final ShellController mShellController; private final ShellCommandHandler mShellCommandHandler; private final IWindowManager mWmService; - private final BubbleProperties mBubbleProperties; private final BubbleTaskViewFactory mBubbleTaskViewFactory; private final BubbleExpandedViewManager mExpandedViewManager; + private final ResizabilityChecker mResizabilityChecker; // Used to post to main UI thread private final ShellExecutor mMainExecutor; @@ -323,7 +321,7 @@ public class BubbleController implements ConfigurationChangeListener, Transitions transitions, SyncTransactionQueue syncQueue, IWindowManager wmService, - BubbleProperties bubbleProperties) { + ResizabilityChecker resizabilityChecker) { mContext = context; mShellCommandHandler = shellCommandHandler; mShellController = shellController; @@ -372,7 +370,6 @@ public class BubbleController implements ConfigurationChangeListener, mDragAndDropController = dragAndDropController; mSyncQueue = syncQueue; mWmService = wmService; - mBubbleProperties = bubbleProperties; shellInit.addInitCallback(this::onInit, this); mBubbleTaskViewFactory = new BubbleTaskViewFactory() { @Override @@ -385,6 +382,7 @@ public class BubbleController implements ConfigurationChangeListener, } }; mExpandedViewManager = BubbleExpandedViewManager.fromBubbleController(this); + mResizabilityChecker = resizabilityChecker; } private void registerOneHandedState(OneHandedController oneHanded) { @@ -590,8 +588,7 @@ public class BubbleController implements ConfigurationChangeListener, * <p>If bubble bar is supported, bubble views will be updated to switch to bar mode. */ public void registerBubbleStateListener(Bubbles.BubbleStateListener listener) { - mBubbleProperties.refresh(); - if (canShowAsBubbleBar() && listener != null) { + if (Flags.enableBubbleBar() && mBubblePositioner.isLargeScreen() && listener != null) { // Only set the listener if we can show the bubble bar. mBubbleStateListener = listener; setUpBubbleViewsForMode(); @@ -608,7 +605,6 @@ public class BubbleController implements ConfigurationChangeListener, * will be updated accordingly. */ public void unregisterBubbleStateListener() { - mBubbleProperties.refresh(); if (mBubbleStateListener != null) { mBubbleStateListener = null; setUpBubbleViewsForMode(); @@ -640,6 +636,14 @@ public class BubbleController implements ConfigurationChangeListener, mOnImeHidden = onImeHidden; mBubblePositioner.setImeVisible(false /* visible */, 0 /* height */); int displayId = mWindowManager.getDefaultDisplay().getDisplayId(); + // if the device is locked we can't use the status bar service to hide the IME because + // the IME state is frozen and it will lead to internal IME state going out of sync. This + // will make the IME visible when the device is unlocked. Instead we use + // DisplayImeController directly to make sure the state is correct when the device unlocks. + if (isDeviceLocked()) { + mDisplayImeController.hideImeForBubblesWhenLocked(displayId); + return; + } try { mBarService.hideCurrentInputMethodForBubbles(displayId); } catch (RemoteException e) { @@ -679,8 +683,20 @@ public class BubbleController implements ConfigurationChangeListener, ? mNotifEntryToExpandOnShadeUnlock.getKey() : "null")); mIsStatusBarShade = isShade; if (!mIsStatusBarShade && didChange) { - // Only collapse stack on change - collapseStack(); + if (mBubbleData.isExpanded()) { + // If the IME is visible, hide it first and then collapse. + if (mBubblePositioner.isImeVisible()) { + hideCurrentInputMethod(this::collapseStack); + } else { + collapseStack(); + } + } else if (mOnImeHidden != null) { + // a request to collapse started before we're notified that the device is locking. + // we're currently waiting for the IME to collapse, before mOnImeHidden can be + // executed, which may not happen since the screen may already be off. hide the IME + // immediately now that we're locked and pass the same runnable so it can complete. + hideCurrentInputMethod(mOnImeHidden); + } } if (mNotifEntryToExpandOnShadeUnlock != null) { @@ -746,14 +762,11 @@ public class BubbleController implements ConfigurationChangeListener, } } - /** Whether bubbles are showing in the bubble bar. */ + /** Whether bubbles would be shown with the bubble bar UI. */ public boolean isShowingAsBubbleBar() { - return canShowAsBubbleBar() && mBubbleStateListener != null; - } - - /** Whether the current configuration supports showing as bubble bar. */ - private boolean canShowAsBubbleBar() { - return mBubbleProperties.isBubbleBarEnabled() && mBubblePositioner.isLargeScreen(); + return Flags.enableBubbleBar() + && mBubblePositioner.isLargeScreen() + && mBubbleStateListener != null; } /** @@ -762,7 +775,7 @@ public class BubbleController implements ConfigurationChangeListener, */ @Nullable public BubbleBarLocation getBubbleBarLocation() { - if (canShowAsBubbleBar()) { + if (isShowingAsBubbleBar()) { return mBubblePositioner.getBubbleBarLocation(); } return null; @@ -773,7 +786,7 @@ public class BubbleController implements ConfigurationChangeListener, */ public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation, @BubbleBarLocation.UpdateSource int source) { - if (canShowAsBubbleBar()) { + if (isShowingAsBubbleBar()) { BubbleBarLocation previousLocation = mBubblePositioner.getBubbleBarLocation(); mBubblePositioner.setBubbleBarLocation(bubbleBarLocation); if (mLayerView != null && !mLayerView.isExpandedViewDragged()) { @@ -825,7 +838,7 @@ public class BubbleController implements ConfigurationChangeListener, * {@link #setBubbleBarLocation(BubbleBarLocation, int)}. */ public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { - if (canShowAsBubbleBar()) { + if (isShowingAsBubbleBar()) { mBubbleStateListener.animateBubbleBarLocation(bubbleBarLocation); } } @@ -1540,78 +1553,80 @@ public class BubbleController implements ConfigurationChangeListener, /** * This method has different behavior depending on: - * - if an app bubble exists - * - if an app bubble is expanded + * - if a notes bubble exists + * - if a notes bubble is expanded * - * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * If no notes bubble exists, this will add and expand a bubble with the provided intent. The * intent must be explicit (i.e. include a package name or fully qualified component class name) * and the activity for it should be resizable. * - * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is - * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * If a notes bubble exists, this will toggle the visibility of it, i.e. if the notes bubble is + * expanded, calling this method will collapse it. If the notes bubble is not expanded, calling * this method will expand it. * * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses * the bubble or bubble stack. * - * Some notes: - * - Only one app bubble is supported at a time, regardless of users. Multi-users support is - * tracked in b/273533235. - * - Calling this method with a different intent than the existing app bubble will do nothing + * Some details: + * - Calling this method with a different intent than the existing bubble will do nothing * * @param intent the intent to display in the bubble expanded view. * @param user the {@link UserHandle} of the user to start this activity for. * @param icon the {@link Icon} to use for the bubble view. */ - public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) { + public void showOrHideNotesBubble(Intent intent, UserHandle user, @Nullable Icon icon) { if (intent == null || intent.getPackage() == null) { - Log.w(TAG, "App bubble failed to show, invalid intent: " + intent + Log.w(TAG, "Notes bubble failed to show, invalid intent: " + intent + ((intent != null) ? " with package: " + intent.getPackage() : " ")); return; } - String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user); + String noteBubbleKey = Bubble.getNoteBubbleKeyForApp(intent.getPackage(), user); PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier()); - if (!isResizableActivity(intent, packageManager, appBubbleKey)) return; // logs errors + if (!mResizabilityChecker.isResizableActivity(intent, packageManager, noteBubbleKey)) { + // resize check logs any errors + return; + } - Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(appBubbleKey); + Bubble existingNotebubble = mBubbleData.getBubbleInStackWithKey(noteBubbleKey); ProtoLog.d(WM_SHELL_BUBBLES, - "showOrHideAppBubble, key=%s existingAppBubble=%s stackVisibility=%s " + "showOrHideNotesBubble, key=%s existingAppBubble=%s stackVisibility=%s " + "statusBarShade=%s", - appBubbleKey, existingAppBubble, + noteBubbleKey, existingNotebubble, (mStackView != null ? mStackView.getVisibility() : "null"), mIsStatusBarShade); - if (existingAppBubble != null) { + if (existingNotebubble != null) { BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); if (isStackExpanded()) { - if (selectedBubble != null && appBubbleKey.equals(selectedBubble.getKey())) { - ProtoLog.d(WM_SHELL_BUBBLES, "collapseStack for %s", appBubbleKey); - // App bubble is expanded, lets collapse + if (selectedBubble != null && noteBubbleKey.equals(selectedBubble.getKey())) { + ProtoLog.d(WM_SHELL_BUBBLES, "collapseStack for %s", noteBubbleKey); + // Notes bubble is expanded, lets collapse collapseStack(); } else { - ProtoLog.d(WM_SHELL_BUBBLES, "setSelected for %s", appBubbleKey); - // App bubble is not selected, select it - mBubbleData.setSelectedBubble(existingAppBubble); + ProtoLog.d(WM_SHELL_BUBBLES, "setSelected for %s", noteBubbleKey); + // Notes bubble is not selected, select it + mBubbleData.setSelectedBubble(existingNotebubble); } } else { - ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleAndExpandStack %s", appBubbleKey); - // App bubble is not selected, select it & expand - mBubbleData.setSelectedBubbleAndExpandStack(existingAppBubble); + ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleAndExpandStack %s", noteBubbleKey); + // Notes bubble is not selected, select it & expand + mBubbleData.setSelectedBubbleAndExpandStack(existingNotebubble); } } else { // Check if it exists in the overflow - Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey); + Bubble b = mBubbleData.getOverflowBubbleWithKey(noteBubbleKey); if (b != null) { // It's in the overflow, so remove it & reinflate - mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL); + mBubbleData.dismissBubbleWithKey(noteBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL); // Update the bubble entry in the overflow with the latest intent. b.setAppBubbleIntent(intent); } else { - // App bubble does not exist, lets add and expand it - b = Bubble.createAppBubble(intent, user, icon, mMainExecutor, mBackgroundExecutor); + // Notes bubble does not exist, lets add and expand it + b = Bubble.createNotesBubble(intent, user, icon, mMainExecutor, + mBackgroundExecutor); } - ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", appBubbleKey); + ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", noteBubbleKey); b.setShouldAutoExpand(true); inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); } @@ -1659,9 +1674,9 @@ public class BubbleController implements ConfigurationChangeListener, } } - /** Sets the app bubble's taskId which is cached for SysUI. */ - public void setAppBubbleTaskId(String key, int taskId) { - mImpl.mCachedState.setAppBubbleTaskId(key, taskId); + /** Sets the note bubble's taskId which is cached for SysUI. */ + public void setNoteBubbleTaskId(String key, int taskId) { + mImpl.mCachedState.setNoteBubbleTaskId(key, taskId); } /** @@ -2483,6 +2498,10 @@ public class BubbleController implements ConfigurationChangeListener, mBubbleData.setSelectedBubbleAndExpandStack(bubbleToSelect); } + private boolean isDeviceLocked() { + return !mIsStatusBarShade; + } + /** * Description of current bubble state. */ @@ -2515,7 +2534,7 @@ public class BubbleController implements ConfigurationChangeListener, * @param context the context to use. * @param entry the entry to bubble. */ - static boolean canLaunchInTaskView(Context context, BubbleEntry entry) { + boolean canLaunchInTaskView(Context context, BubbleEntry entry) { if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) return true; PendingIntent intent = entry.getBubbleMetadata() != null ? entry.getBubbleMetadata().getIntent() @@ -2530,26 +2549,8 @@ public class BubbleController implements ConfigurationChangeListener, } PackageManager packageManager = getPackageManagerForUser( context, entry.getStatusBarNotification().getUser().getIdentifier()); - return isResizableActivity(intent.getIntent(), packageManager, entry.getKey()); - } - - static boolean isResizableActivity(Intent intent, PackageManager packageManager, String key) { - if (intent == null) { - Log.w(TAG, "Unable to send as bubble: " + key + " null intent"); - return false; - } - ActivityInfo info = intent.resolveActivityInfo(packageManager, 0); - if (info == null) { - Log.w(TAG, "Unable to send as bubble: " + key - + " couldn't find activity info for intent: " + intent); - return false; - } - if (!ActivityInfo.isResizeableMode(info.resizeMode)) { - Log.w(TAG, "Unable to send as bubble: " + key - + " activity is not resizable for intent: " + intent); - return false; - } - return true; + return mResizabilityChecker.isResizableActivity(intent.getIntent(), packageManager, + entry.getKey()); } static PackageManager getPackageManagerForUser(Context context, int userId) { @@ -2781,7 +2782,7 @@ public class BubbleController implements ConfigurationChangeListener, private HashMap<String, String> mSuppressedGroupToNotifKeys = new HashMap<>(); private HashMap<String, Bubble> mShortcutIdToBubble = new HashMap<>(); - private HashMap<String, Integer> mAppBubbleTaskIds = new HashMap(); + private HashMap<String, Integer> mNoteBubbleTaskIds = new HashMap(); private ArrayList<Bubble> mTmpBubbles = new ArrayList<>(); @@ -2813,20 +2814,20 @@ public class BubbleController implements ConfigurationChangeListener, mSuppressedBubbleKeys.clear(); mShortcutIdToBubble.clear(); - mAppBubbleTaskIds.clear(); + mNoteBubbleTaskIds.clear(); for (Bubble b : mTmpBubbles) { mShortcutIdToBubble.put(b.getShortcutId(), b); updateBubbleSuppressedState(b); - if (b.isAppBubble()) { - mAppBubbleTaskIds.put(b.getKey(), b.getTaskId()); + if (b.isNoteBubble()) { + mNoteBubbleTaskIds.put(b.getKey(), b.getTaskId()); } } } - /** Sets the app bubble's taskId which is cached for SysUI. */ - synchronized void setAppBubbleTaskId(String key, int taskId) { - mAppBubbleTaskIds.put(key, taskId); + /** Sets the note bubble's taskId which is cached for SysUI. */ + synchronized void setNoteBubbleTaskId(String key, int taskId) { + mNoteBubbleTaskIds.put(key, taskId); } /** @@ -2878,7 +2879,7 @@ public class BubbleController implements ConfigurationChangeListener, pw.println(" suppressing: " + key); } - pw.println("mAppBubbleTaskIds: " + mAppBubbleTaskIds.values()); + pw.println("mNoteBubbleTaskIds: " + mNoteBubbleTaskIds.values()); } } @@ -2929,14 +2930,14 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) { + public void showOrHideNoteBubble(Intent intent, UserHandle user, @Nullable Icon icon) { mMainExecutor.execute( - () -> BubbleController.this.showOrHideAppBubble(intent, user, icon)); + () -> BubbleController.this.showOrHideNotesBubble(intent, user, icon)); } @Override - public boolean isAppBubbleTaskId(int taskId) { - return mCachedState.mAppBubbleTaskIds.values().contains(taskId); + public boolean isNoteBubbleTaskId(int taskId) { + return mCachedState.mNoteBubbleTaskIds.values().contains(taskId); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 97b03a9f58e4..ac74a42d1359 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -285,9 +285,9 @@ public class BubbleExpandedView extends LinearLayout { // The taskId is saved to use for removeTask, preventing appearance in recent tasks. mTaskId = taskId; - if (mBubble != null && mBubble.isAppBubble()) { + if (mBubble != null && mBubble.isNoteBubble()) { // Let the controller know sooner what the taskId is. - mManager.setAppBubbleTaskId(mBubble.getKey(), mTaskId); + mManager.setNoteBubbleTaskId(mBubble.getKey(), mTaskId); } // With the task org, the taskAppeared callback will only happen once the task has diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt index a02623138f1e..6be49ddc549a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt @@ -28,7 +28,7 @@ interface BubbleExpandedViewManager { fun promoteBubbleFromOverflow(bubble: Bubble) fun removeBubble(key: String, reason: Int) fun dismissBubble(bubble: Bubble, reason: Int) - fun setAppBubbleTaskId(key: String, taskId: Int) + fun setNoteBubbleTaskId(key: String, taskId: Int) fun isStackExpanded(): Boolean fun isShowingAsBubbleBar(): Boolean fun hideCurrentInputMethod() @@ -73,8 +73,8 @@ interface BubbleExpandedViewManager { controller.dismissBubble(bubble, reason) } - override fun setAppBubbleTaskId(key: String, taskId: Int) { - controller.setAppBubbleTaskId(key, taskId) + override fun setNoteBubbleTaskId(key: String, taskId: Int) { + controller.setNoteBubbleTaskId(key, taskId) } override fun isStackExpanded(): Boolean = controller.isStackExpanded diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index a725e04d3f8a..0e2fc3a77c6d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -758,20 +758,20 @@ public class BubblePositioner { * is being shown, for a normal bubble. */ public PointF getDefaultStartPosition() { - return getDefaultStartPosition(false /* isAppBubble */); + return getDefaultStartPosition(false /* isNoteBubble */); } /** * The stack position to use if we don't have a saved location or if user education * is being shown. * - * @param isAppBubble whether this start position is for an app bubble or not. + * @param isNoteBubble whether this start position is for a note bubble or not. */ - public PointF getDefaultStartPosition(boolean isAppBubble) { + public PointF getDefaultStartPosition(boolean isNoteBubble) { // Normal bubbles start on the left if we're in LTR, right otherwise. // TODO (b/294284894): update language around "app bubble" here // App bubbles start on the right in RTL, left otherwise. - final boolean startOnLeft = isAppBubble ? mDeviceConfig.isRtl() : !mDeviceConfig.isRtl(); + final boolean startOnLeft = isNoteBubble ? mDeviceConfig.isRtl() : !mDeviceConfig.isRtl(); return getStartPosition(startOnLeft ? StackPinnedEdge.LEFT : StackPinnedEdge.RIGHT); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleResizabilityChecker.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleResizabilityChecker.kt new file mode 100644 index 000000000000..6ca08215152f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleResizabilityChecker.kt @@ -0,0 +1,58 @@ +/* + * 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.wm.shell.bubbles + +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.util.Log + +/** + * Checks if an intent is resizable to display in a bubble. + */ +class BubbleResizabilityChecker : ResizabilityChecker { + + override fun isResizableActivity( + intent: Intent?, + packageManager: PackageManager, key: String + ): Boolean { + if (intent == null) { + Log.w(TAG, "Unable to send as bubble: $key null intent") + return false + } + val info = intent.resolveActivityInfo(packageManager, 0) + if (info == null) { + Log.w( + TAG, ("Unable to send as bubble: " + key + + " couldn't find activity info for intent: " + intent) + ) + return false + } + if (!ActivityInfo.isResizeableMode(info.resizeMode)) { + Log.w( + TAG, ("Unable to send as bubble: " + key + + " activity is not resizable for intent: " + intent) + ) + return false + } + return true + } + + companion object { + private const val TAG = "BubbleResizeChecker" + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index f1f49eda75b6..2b2112fd461f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -1975,12 +1975,11 @@ public class BubbleStackView extends FrameLayout return; } - if (firstBubble && bubble.isAppBubble() && !mPositioner.hasUserModifiedDefaultPosition()) { - // TODO (b/294284894): update language around "app bubble" here - // If it's an app bubble and we don't have a previous resting position, update the - // controllers to use the default position for the app bubble (it'd be different from + if (firstBubble && bubble.isNoteBubble() && !mPositioner.hasUserModifiedDefaultPosition()) { + // If it's an note bubble and we don't have a previous resting position, update the + // controllers to use the default position for the note bubble (it'd be different from // the position initialized with the controllers originally). - PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */); + PointF startPosition = mPositioner.getDefaultStartPosition(true /* isNoteBubble */); mStackOnLeftOrWillBe = mPositioner.isStackOnLeft(startPosition); mStackAnimationController.setStackPosition(startPosition); mExpandedAnimationController.setCollapsePoint(startPosition); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index a6b858500dcb..83d311ed6cd9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -167,9 +167,9 @@ public class BubbleTaskViewHelper { // The taskId is saved to use for removeTask, preventing appearance in recent tasks. mTaskId = taskId; - if (mBubble != null && mBubble.isAppBubble()) { + if (mBubble != null && mBubble.isNoteBubble()) { // Let the controller know sooner what the taskId is. - mExpandedViewManager.setAppBubbleTaskId(mBubble.getKey(), mTaskId); + mExpandedViewManager.setNoteBubbleTaskId(mBubble.getKey(), mTaskId); } // With the task org, the taskAppeared callback will only happen once the task has diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java index 29fb1a23017c..48b83ce49e61 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -248,7 +248,9 @@ public class BubbleTransitions { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { } @@ -423,7 +425,9 @@ public class BubbleTransitions { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 4297fac0f6a8..44ae74479949 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -135,33 +135,31 @@ public interface Bubbles { /** * This method has different behavior depending on: - * - if an app bubble exists - * - if an app bubble is expanded + * - if a notes bubble exists + * - if a notes bubble is expanded * - * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * If no notes bubble exists, this will add and expand a bubble with the provided intent. The * intent must be explicit (i.e. include a package name or fully qualified component class name) * and the activity for it should be resizable. * - * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is - * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * If a notes bubble exists, this will toggle the visibility of it, i.e. if the notes bubble is + * expanded, calling this method will collapse it. If the notes bubble is not expanded, calling * this method will expand it. * * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses * the bubble or bubble stack. * - * Some notes: - * - Only one app bubble is supported at a time, regardless of users. Multi-users support is - * tracked in b/273533235. - * - Calling this method with a different intent than the existing app bubble will do nothing + * Some details: + * - Calling this method with a different intent than the existing bubble will do nothing * * @param intent the intent to display in the bubble expanded view. - * @param user the {@link UserHandle} of the user to start this activity for. - * @param icon the {@link Icon} to use for the bubble view. + * @param user the {@link UserHandle} of the user to start this activity for. + * @param icon the {@link Icon} to use for the bubble view. */ - void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon); + void showOrHideNoteBubble(Intent intent, UserHandle user, @Nullable Icon icon); /** @return true if the specified {@code taskId} corresponds to app bubble's taskId. */ - boolean isAppBubbleTaskId(int taskId); + boolean isNoteBubbleTaskId(int taskId); /** ` * @return a {@link SynchronousScreenCaptureListener} after performing a screenshot that may diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ResizabilityChecker.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ResizabilityChecker.kt new file mode 100644 index 000000000000..1ccc05d38d80 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ResizabilityChecker.kt @@ -0,0 +1,35 @@ +/* + * 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.wm.shell.bubbles + +import android.content.Intent +import android.content.pm.PackageManager + +/** + * Interface to check whether the activity backed by a specific intent is resizable. + */ +interface ResizabilityChecker { + + /** + * Returns whether the provided intent represents a resizable activity. + * + * @param intent the intent to check + * @param packageManager the package manager to use to do the look up + * @param key a key representing thing being checked (used for error logging) + */ + fun isResizableActivity(intent: Intent?, packageManager: PackageManager, key: String): Boolean +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt deleted file mode 100644 index 4206d9320b7d..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.bubbles.properties - -/** - * An interface for exposing bubble properties via flags which can be controlled easily in tests. - */ -interface BubbleProperties { - /** - * Whether bubble bar is enabled. - * - * When this is `true`, depending on additional factors, such as screen size and taskbar state, - * bubbles will be displayed in the bubble bar instead of floating. - * - * When this is `false`, bubbles will be floating. - */ - val isBubbleBarEnabled: Boolean - - /** Refreshes the current value of [isBubbleBarEnabled]. */ - fun refresh() -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt deleted file mode 100644 index 33b61b164988..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.bubbles.properties - -import android.os.SystemProperties -import com.android.wm.shell.Flags - -/** Provides bubble properties in production. */ -object ProdBubbleProperties : BubbleProperties { - - private var _isBubbleBarEnabled = Flags.enableBubbleBar() || - SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false) - - override val isBubbleBarEnabled - get() = _isBubbleBarEnabled - - override fun refresh() { - _isBubbleBarEnabled = Flags.enableBubbleBar() || - SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false) - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index e69d60ddd6c6..4c3bde9b2b3a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -39,6 +39,7 @@ import androidx.annotation.BinderThread; import com.android.window.flags.Flags; import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; @@ -91,7 +92,8 @@ public class DisplayController { onDisplayAdded(displayIds[i]); } - if (Flags.enableConnectedDisplaysWindowDrag()) { + if (Flags.enableConnectedDisplaysWindowDrag() + && DesktopModeStatus.canEnterDesktopMode(mContext)) { mDisplayManager.registerTopologyListener(mMainExecutor, this::onDisplayTopologyChanged); onDisplayTopologyChanged(mDisplayManager.getDisplayTopology()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index 94e629a6887f..8377a35a9e7d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -224,6 +224,12 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } + /** Hides the IME for Bubbles when the device is locked. */ + public void hideImeForBubblesWhenLocked(int displayId) { + PerDisplay pd = mImePerDisplay.get(displayId); + pd.setImeInputTargetRequestedVisibility(false, pd.getImeSourceControl().getImeStatsToken()); + } + /** An implementation of {@link IDisplayWindowInsetsController} for a given display id. */ public class PerDisplay implements DisplayInsetsController.OnInsetsChangedListener { final int mDisplayId; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt index 0577f9e625ca..16938647001b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt @@ -25,6 +25,7 @@ import android.util.SparseArray import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.sysui.UserChangeListener +import androidx.core.util.size /** Creates and manages contexts for all the profiles of the current user. */ class UserProfileContexts( @@ -35,6 +36,8 @@ class UserProfileContexts( // Contexts for all the profiles of the current user. private val currentProfilesContext = SparseArray<Context>() + private val shellUserId = baseContext.userId + lateinit var userContext: Context private set @@ -49,6 +52,9 @@ class UserProfileContexts( currentProfilesContext.clear() this@UserProfileContexts.userContext = userContext currentProfilesContext.put(newUserId, userContext) + if (newUserId != shellUserId) { + currentProfilesContext.put(shellUserId, baseContext) + } } override fun onUserProfilesChanged(profiles: List<UserInfo>) { @@ -69,9 +75,9 @@ class UserProfileContexts( currentProfilesContext.put(profile.id, profileContext) } val profilesToRemove = buildList<Int> { - for (i in 0..<currentProfilesContext.size()) { + for (i in 0..<currentProfilesContext.size) { val userId = currentProfilesContext.keyAt(i) - if (profiles.none { it.id == userId }) { + if (userId != shellUserId && profiles.none { it.id == userId }) { add(userId) } } @@ -80,4 +86,12 @@ class UserProfileContexts( } operator fun get(userId: Int): Context? = currentProfilesContext.get(userId) + + fun getOrCreate(userId: Int): Context { + val context = currentProfilesContext[userId] + if (context != null) return context + return baseContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 0).also { + currentProfilesContext[userId] = it + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index 2c418d34f09a..06044ccc1c61 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -125,11 +125,13 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { } }; - private final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() { + final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() { @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); final DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; + info.addAction(new AccessibilityAction(R.id.action_swap_apps, + mContext.getString(R.string.accessibility_action_divider_swap))); if (mSplitLayout.isLeftRightSplit()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_full, mContext.getString(R.string.accessibility_action_divider_left_full))); @@ -172,6 +174,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { @Override public boolean performAccessibilityAction(@NonNull View host, int action, @Nullable Bundle args) { + if (action == R.id.action_swap_apps) { + mSplitLayout.onDoubleTappedDivider(); + return true; + } + DividerSnapAlgorithm.SnapTarget nextTarget = null; DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; if (action == R.id.action_move_tl_full) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index cd5c135691d7..bd89f5cf45f6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -394,11 +394,19 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * Returns the divider position as a fraction from 0 to 1. */ public float getDividerPositionAsFraction() { - return Math.min(1f, Math.max(0f, mIsLeftRightSplit - ? (float) ((getTopLeftBounds().right + getBottomRightBounds().left) / 2f) - / getBottomRightBounds().right - : (float) ((getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f) - / getBottomRightBounds().bottom)); + if (Flags.enableFlexibleTwoAppSplit()) { + return Math.min(1f, Math.max(0f, mIsLeftRightSplit + ? (getTopLeftBounds().right + getBottomRightBounds().left) / 2f + / getDisplayWidth() + : (getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f + / getDisplayHeight())); + } else { + return Math.min(1f, Math.max(0f, mIsLeftRightSplit + ? (float) ((getTopLeftBounds().right + getBottomRightBounds().left) / 2f) + / getBottomRightBounds().right + : (float) ((getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f) + / getBottomRightBounds().bottom)); + } } private void updateInvisibleRect() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 1323fe0fa9ca..201870fe0181 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -37,9 +37,9 @@ import android.view.Display; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.accessibility.AccessibilityManager; +import android.window.DesktopModeFlags; import com.android.internal.annotations.VisibleForTesting; -import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener; @@ -71,7 +71,6 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.IntPredicate; import java.util.function.Predicate; /** @@ -874,6 +873,7 @@ public class CompatUIController implements OnDisplaysChangedListener, } boolean isDesktopModeShowing = mDesktopUserRepositories.get().getCurrent() .getVisibleTaskCount(taskInfo.displayId) > 0; - return Flags.skipCompatUiEducationInDesktopMode() && isDesktopModeShowing; + return DesktopModeFlags.ENABLE_DESKTOP_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE_BUGFIX + .isTrue() && isDesktopModeShowing; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index e0a829df79ad..43f1a1037cab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -31,6 +31,7 @@ import android.os.SystemProperties; import android.provider.Settings; import android.view.IWindowManager; import android.view.accessibility.AccessibilityManager; +import android.window.DesktopModeFlags; import android.window.SystemPerformanceHinter; import com.android.internal.logging.UiEventLogger; @@ -315,7 +316,7 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides static CompatUIStatusManager provideCompatUIStatusManager(@NonNull Context context) { - if (Flags.enableCompatUiVisibilityStatus()) { + if (DesktopModeFlags.ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS.isTrue()) { return new CompatUIStatusManager( newState -> Settings.Secure.putInt(context.getContentResolver(), COMPAT_UI_EDUCATION_SHOWING, newState), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index c81838f56a74..6f16e047a968 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -59,7 +59,7 @@ import com.android.wm.shell.bubbles.BubbleDataRepository; import com.android.wm.shell.bubbles.BubbleEducationController; import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubblePositioner; -import com.android.wm.shell.bubbles.properties.ProdBubbleProperties; +import com.android.wm.shell.bubbles.BubbleResizabilityChecker; import com.android.wm.shell.bubbles.storage.BubblePersistentRepository; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; @@ -111,6 +111,7 @@ import com.android.wm.shell.desktopmode.education.AppToWebEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository; import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer; +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver; import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer; import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository; import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer; @@ -293,7 +294,7 @@ public abstract class WMShellModule { transitions, syncQueue, wmService, - ProdBubbleProperties.INSTANCE); + new BubbleResizabilityChecker()); } // @@ -760,6 +761,7 @@ public abstract class WMShellModule { Optional<BubbleController> bubbleController, OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, DesksOrganizer desksOrganizer, + DesksTransitionObserver desksTransitionObserver, UserProfileContexts userProfileContexts, DesktopModeCompatPolicy desktopModeCompatPolicy) { return new DesktopTasksController( @@ -797,6 +799,7 @@ public abstract class WMShellModule { bubbleController, overviewToDesktopTransitionObserver, desksOrganizer, + desksTransitionObserver, userProfileContexts, desktopModeCompatPolicy); } @@ -1134,6 +1137,7 @@ public abstract class WMShellModule { Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler, Optional<BackAnimationController> backAnimationController, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, + @NonNull DesksTransitionObserver desksTransitionObserver, ShellInit shellInit) { return desktopUserRepositories.flatMap( repository -> @@ -1146,11 +1150,20 @@ public abstract class WMShellModule { desktopMixedTransitionHandler.get(), backAnimationController.get(), desktopWallpaperActivityTokenProvider, + desksTransitionObserver, shellInit))); } @WMSingleton @Provides + static DesksTransitionObserver provideDesksTransitionObserver( + @NonNull @DynamicOverride DesktopUserRepositories desktopUserRepositories + ) { + return new DesksTransitionObserver(desktopUserRepositories); + } + + @WMSingleton + @Provides static Optional<DesktopMixedTransitionHandler> provideDesktopMixedTransitionHandler( Context context, Transitions transitions, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index 164d04bbde65..b93d2e396402 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -152,8 +152,8 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: desk id should be an integer") return false } - pw.println("Not implemented.") - return false + controller.removeDesk(deskId) + return true } private fun runRemoveAllDesks(args: Array<String>, pw: PrintWriter): Boolean { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 32ee319a053b..621ccba40db2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -59,6 +59,8 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; /** * Animated visual indicator for Desktop Mode windowing transitions. @@ -149,12 +151,19 @@ public class DesktopModeVisualIndicator { // left, and split right for the right edge. This is universal across all drag event types. if (inputCoordinates.x < 0) return TO_SPLIT_LEFT_INDICATOR; if (inputCoordinates.x > layout.width()) return TO_SPLIT_RIGHT_INDICATOR; - // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone. - // In drags not originating on a freeform caption, we should default to a TO_DESKTOP - // indicator. - IndicatorType result = mDragStartState == DragStartState.FROM_FREEFORM - ? NO_INDICATOR - : TO_DESKTOP_INDICATOR; + IndicatorType result; + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen() + && !DesktopModeStatus.canEnterDesktopMode(mContext)) { + // If desktop is not available, default to "no indicator" + result = NO_INDICATOR; + } else { + // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone. + // In drags not originating on a freeform caption, we should default to a TO_DESKTOP + // indicator. + result = mDragStartState == DragStartState.FROM_FREEFORM + ? NO_INDICATOR + : TO_DESKTOP_INDICATOR; + } final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness); // Because drags in freeform use task position for indicator calculation, we need to diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index 4ff1a5f1be31..043b353ba380 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -174,6 +174,9 @@ class DesktopRepository( /** Returns the number of desks in the given display. */ fun getNumberOfDesks(displayId: Int) = desktopData.getNumberOfDesks(displayId) + /** Returns the display the given desk is in. */ + fun getDisplayForDesk(deskId: Int) = desktopData.getDisplayForDesk(deskId) + /** Adds [regionListener] to inform about changes to exclusion regions for all Desktop tasks. */ fun setExclusionRegionListener(regionListener: Consumer<Region>, executor: Executor) { desktopGestureExclusionListener = regionListener @@ -207,6 +210,14 @@ class DesktopRepository( desktopData.createDesk(displayId, deskId) } + /** Returns the ids of the existing desks in the given display. */ + @VisibleForTesting + fun getDeskIds(displayId: Int): Set<Int> = + desktopData.desksSequence(displayId).map { desk -> desk.deskId }.toSet() + + /** Returns the id of the default desk in the given display. */ + fun getDefaultDeskId(displayId: Int): Int? = getDefaultDesk(displayId)?.deskId + /** Returns the default desk in the given display. */ private fun getDefaultDesk(displayId: Int): Desk? = desktopData.getDefaultDesk(displayId) @@ -716,17 +727,13 @@ class DesktopRepository( } } - /** - * Removes the active desk for the given [displayId] and returns the active tasks on that desk. - * - * TODO: b/389960283 - add explicit [deskId] argument. - */ - fun removeDesk(displayId: Int): ArraySet<Int> { - val desk = desktopData.getActiveDesk(displayId) - if (desk == null) { - logW("Could not find desk to remove: displayId=%d", displayId) - return ArraySet() - } + /** Removes the given desk and returns the active tasks in that desk. */ + fun removeDesk(deskId: Int): Set<Int> { + val desk = + desktopData.getDesk(deskId) + ?: return emptySet<Int>().also { + logW("Could not find desk to remove: deskId=%d", deskId) + } val activeTasks = ArraySet(desk.activeTasks) desktopData.remove(desk.deskId) return activeTasks @@ -1066,7 +1073,7 @@ class DesktopRepository( } override fun getDisplayForDesk(deskId: Int): Int = - getAllActiveDesks().find { it.deskId == deskId }?.displayId + desksSequence().find { it.deskId == deskId }?.displayId ?: error("Display for desk=$deskId not found") } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 29f61ef8b13f..3f88e7bddd34 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -103,7 +103,9 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCR import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler +import com.android.wm.shell.desktopmode.multidesks.DeskTransition import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter @@ -185,6 +187,7 @@ class DesktopTasksController( private val bubbleController: Optional<BubbleController>, private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver, private val desksOrganizer: DesksOrganizer, + private val desksTransitionObserver: DesksTransitionObserver, private val userProfileContexts: UserProfileContexts, private val desktopModeCompatPolicy: DesktopModeCompatPolicy, ) : @@ -775,6 +778,12 @@ class DesktopTasksController( val wct = WindowContainerTransaction() addMoveToFullscreenChanges(wct, task) + // We are moving a freeform task to fullscreen, put the home task under the fullscreen task. + if (!forceEnterDesktop(task.displayId)) { + moveHomeTask(wct, toTop = true, task.displayId) + wct.reorder(task.token, /* onTop= */ true) + } + exitDesktopTaskTransitionHandler.startTransition( transitionSource, wct, @@ -970,11 +979,13 @@ class DesktopTasksController( cascadeWindow(bounds, displayLayout, displayId) } val pendingIntent = - PendingIntent.getActivity( + PendingIntent.getActivityAsUser( context, /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE, + /* options= */ null, + UserHandle.of(userId), ) val ops = ActivityOptions.fromBundle(options).apply { @@ -1517,11 +1528,16 @@ class DesktopTasksController( private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) { logV("addWallpaperActivity") if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) { + + // If the wallpaper activity for this display already exists, let's reorder it to top. + val wallpaperActivityToken = desktopWallpaperActivityTokenProvider.getToken(displayId) + if (wallpaperActivityToken != null) { + wct.reorder(wallpaperActivityToken, /* onTop= */ true) + return + } + val intent = Intent(context, DesktopWallpaperActivity::class.java) - if ( - desktopWallpaperActivityTokenProvider.getToken(displayId) == null && - Flags.enablePerDisplayDesktopWallpaperActivity() - ) { + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) } @@ -1862,9 +1878,10 @@ class DesktopTasksController( // need updates in some cases. val baseActivity = callingTaskInfo.baseActivity ?: return val fillIn: Intent = - userProfileContexts[callingTaskInfo.userId] - ?.packageManager - ?.getLaunchIntentForPackage(baseActivity.packageName) ?: return + userProfileContexts + .getOrCreate(callingTaskInfo.userId) + .packageManager + .getLaunchIntentForPackage(baseActivity.packageName) ?: return fillIn.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) val launchIntent = PendingIntent.getActivity( @@ -2360,20 +2377,62 @@ class DesktopTasksController( ) } - fun removeDesktop(displayId: Int) { + /** Removes the default desk in the given display. */ + @Deprecated("Deprecated with multi-desks.", ReplaceWith("removeDesk()")) + fun removeDefaultDeskInDisplay(displayId: Int) { + val deskId = + checkNotNull(taskRepository.getDefaultDeskId(displayId)) { + "Expected a default desk to exist" + } + removeDesk(displayId = displayId, deskId = deskId) + } + + /** Removes the given desk. */ + fun removeDesk(deskId: Int) { + val displayId = taskRepository.getDisplayForDesk(deskId) + removeDesk(displayId = displayId, deskId = deskId) + } + + private fun removeDesk(displayId: Int, deskId: Int) { if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) return + logV("removeDesk deskId=%d from displayId=%d", deskId, displayId) - val tasksToRemove = taskRepository.removeDesk(displayId) - val wct = WindowContainerTransaction() - tasksToRemove.forEach { - val task = shellTaskOrganizer.getRunningTaskInfo(it) - if (task != null) { - wct.removeTask(task.token) + val tasksToRemove = + if (Flags.enableMultipleDesktopsBackend()) { + taskRepository.getActiveTaskIdsInDesk(deskId) } else { - recentTasksController?.removeBackgroundTask(it) + // TODO: 362720497 - make sure minimized windows are also removed in WM + // and the repository. + taskRepository.removeDesk(deskId) + } + + val wct = WindowContainerTransaction() + if (!Flags.enableMultipleDesktopsBackend()) { + tasksToRemove.forEach { + val task = shellTaskOrganizer.getRunningTaskInfo(it) + if (task != null) { + wct.removeTask(task.token) + } else { + recentTasksController?.removeBackgroundTask(it) + } } + } else { + // TODO: 362720497 - double check background tasks are also removed. + desksOrganizer.removeDesk(wct, deskId) + } + if (!Flags.enableMultipleDesktopsBackend() && wct.isEmpty) return + val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null) + if (Flags.enableMultipleDesktopsBackend()) { + desksTransitionObserver.addPendingTransition( + DeskTransition.RemoveDesk( + token = transition, + displayId = displayId, + deskId = deskId, + tasks = tasksToRemove, + onDeskRemovedListener = onDeskRemovedListener, + ) + ) } - if (!wct.isEmpty) transitions.startTransition(TRANSIT_CLOSE, wct, null) } /** Enter split by using the focused desktop task in given `displayId`. */ @@ -3077,7 +3136,7 @@ class DesktopTasksController( override fun removeDesktop(displayId: Int) { executeRemoteCallWithTaskPermission(controller, "removeDesktop") { c -> - c.removeDesktop(displayId) + c.removeDefaultDeskInDisplay(displayId) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index b3648699ed0b..3ada988ba2a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -37,6 +37,7 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.back.BackAnimationController import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isExitDesktopModeTransition import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -58,6 +59,7 @@ class DesktopTasksTransitionObserver( private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, private val backAnimationController: BackAnimationController, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, + private val desksTransitionObserver: DesksTransitionObserver, shellInit: ShellInit, ) : Transitions.TransitionObserver { @@ -87,6 +89,7 @@ class DesktopTasksTransitionObserver( finishTransaction: SurfaceControl.Transaction, ) { // TODO: b/332682201 Update repository state + desksTransitionObserver.onTransitionReady(transition, info) if ( DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 91f10dc4faf5..cc3d86c0c056 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -458,7 +458,8 @@ sealed class DragToDesktopTransitionHandler( override fun mergeAnimation( transition: IBinder, info: TransitionInfo, - t: SurfaceControl.Transaction, + startT: SurfaceControl.Transaction, + finishT: SurfaceControl.Transaction, mergeTarget: IBinder, finishCallback: Transitions.TransitionFinishCallback, ) { @@ -488,18 +489,18 @@ sealed class DragToDesktopTransitionHandler( if (isEndTransition) { setupEndDragToDesktop( info, - startTransaction = t, + startTransaction = startT, finishTransaction = startTransactionFinishT, ) // Call finishCallback to merge animation before startTransitionFinishCb is called finishCallback.onTransitionFinished(/* wct= */ null) - animateEndDragToDesktop(startTransaction = t, startTransitionFinishCb) + animateEndDragToDesktop(startTransaction = startT, startTransitionFinishCb) } else if (isCancelTransition) { info.changes.forEach { change -> - t.show(change.leash) + startT.show(change.leash) startTransactionFinishT.show(change.leash) } - t.apply() + startT.apply() finishCallback.onTransitionFinished(/* wct= */ null) startTransitionFinishCb.onTransitionFinished(/* wct= */ null) clearState() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt index 39dc48d6d206..5d8355625b94 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt @@ -140,6 +140,27 @@ class AppHandleEducationController( windowingEducationViewController.hideEducationTooltip() } } + + // Listens to a [NoCaption] state change to dismiss any tooltip if the app handle or app + // header is gone or de-focused (e.g. when a user swipes up to home, overview, or enters + // split screen) + applicationCoroutineScope.launch { + if ( + isAppHandleHintViewed() && + isEnterDesktopModeHintViewed() && + isExitDesktopModeHintViewed() + ) + return@launch + windowDecorCaptionHandleRepository.captionStateFlow + .filter { captionState -> + captionState is CaptionState.NoCaption && + !isAppHandleHintViewed() && + !isEnterDesktopModeHintViewed() && + !isExitDesktopModeHintViewed() + } + .flowOn(backgroundDispatcher) + .collectLatest { windowingEducationViewController.hideEducationTooltip() } + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt new file mode 100644 index 000000000000..47088c0b545a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt @@ -0,0 +1,33 @@ +/* + * 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.wm.shell.desktopmode.multidesks + +import android.os.IBinder + +/** Represents shell-started transitions involving desks. */ +sealed class DeskTransition { + /** The transition token. */ + abstract val token: IBinder + + /** A transition to remove a desk and its tasks from a display. */ + data class RemoveDesk( + override val token: IBinder, + val displayId: Int, + val deskId: Int, + val tasks: Set<Int>, + val onDeskRemovedListener: OnDeskRemovedListener?, + ) : DeskTransition() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt new file mode 100644 index 000000000000..3e49b8a4538b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt @@ -0,0 +1,58 @@ +/* + * 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.wm.shell.desktopmode.multidesks + +import android.os.IBinder +import android.view.WindowManager.TRANSIT_CLOSE +import android.window.TransitionInfo +import com.android.window.flags.Flags +import com.android.wm.shell.desktopmode.DesktopUserRepositories + +/** + * Observer of desk-related transitions, such as adding, removing or activating a whole desk. It + * tracks pending transitions and updates repository state once they finish. + */ +class DesksTransitionObserver(private val desktopUserRepositories: DesktopUserRepositories) { + private val deskTransitions = mutableMapOf<IBinder, DeskTransition>() + + /** Adds a pending desk transition to be tracked. */ + fun addPendingTransition(transition: DeskTransition) { + if (!Flags.enableMultipleDesktopsBackend()) return + deskTransitions[transition.token] = transition + } + + /** + * Called when any transition is ready, which may include transitions not tracked by this + * observer. + */ + fun onTransitionReady(transition: IBinder, info: TransitionInfo) { + if (!Flags.enableMultipleDesktopsBackend()) return + val deskTransition = deskTransitions.remove(transition) ?: return + val desktopRepository = desktopUserRepositories.current + when (deskTransition) { + is DeskTransition.RemoveDesk -> { + check(info.type == TRANSIT_CLOSE) { "Expected close transition for desk removal" } + // TODO: b/362720497 - consider verifying the desk was actually removed through the + // DesksOrganizer. The transition info won't have changes if the desk was not + // visible, such as when dismissing from Overview. + val deskId = deskTransition.deskId + val displayId = deskTransition.displayId + desktopRepository.removeDesk(deskTransition.deskId) + deskTransition.onDeskRemovedListener?.onDeskRemoved(displayId, deskId) + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md index faa97ac4512f..f50d253ddf42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md @@ -1,4 +1,5 @@ # Making changes in the Shell +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md index 7070dead9957..9b09904527bf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md @@ -1,4 +1,5 @@ # Usage of Dagger in the Shell library +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md index 09e627c0e02c..dd5827af97d9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md @@ -1,4 +1,5 @@ # Debugging in the Shell +[Back to home](README.md) --- @@ -50,6 +51,11 @@ adb shell wm shell protolog enable-text TAG adb shell wm shell protolog disable-text TAG ``` +### R8 optimizations & ProtoLog + +If the APK that the Shell library is included into has R8 optimizations enabled, then you may need +to update the proguard flags to keep the generated protolog classes (ie. AOSP SystemUI's [proguard.flags](base/packages/SystemUI/proguard_common.flags)). + ## Winscope Tracing The Winscope tool is extremely useful in determining what is happening on-screen in both @@ -57,25 +63,42 @@ WindowManager and SurfaceFlinger. Follow [go/winscope](http://go/winscope-help) use the tool. This trace will contain all the information about the windows/activities/surfaces on screen. -## WindowManager/SurfaceFlinger hierarchy dump +## WindowManager/SurfaceFlinger/InputDispatcher information A quick way to view the WindowManager hierarchy without a winscope trace is via the wm dumps: ```shell adb shell dumpsys activity containers +# The output lists the containers in the hierarchy from top -> bottom in z-order +``` + +To get more information about windows on the screen: +```shell +# All windows in WM +adb shell dumpsys window -a +# The windows are listed from top -> bottom in z-order + +# Visible windows only +adb shell dumpsys window -a visible ``` Likewise, the SurfaceFlinger hierarchy can be dumped for inspection by running: ```shell adb shell dumpsys SurfaceFlinger -# Search output for "Layer Hierarchy" +# Search output for "Layer Hierarchy", the surfaces in the table are listed bottom -> top in z-order +``` + +And the visible input windows can be dumped via: +```shell +adb shell dumpsys input +# Search output for "Windows:", they are ordered top -> bottom in z-order ``` ## Tracing global SurfaceControl transaction updates While Winscope traces are very useful, it sometimes doesn't give you enough information about which part of the code is initiating the transaction updates. In such cases, it can be helpful to get -stack traces when specific surface transaction calls are made, which is possible by enabling the -following system properties for example: +stack traces when specific surface transaction calls are made (regardless of process), which is +possible by enabling the following system properties for example: ```shell # Enabling adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,setPosition # matches the name of the SurfaceControlTransaction methods @@ -94,9 +117,16 @@ properties. It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite noisy if unfiltered. -It can sometimes be useful to trace specific logs and when they are applied (sometimes we build -transactions that can be applied later). You can do this by adding the "merge" and "apply" calls to -the set of requested calls: +### Tracing transaction merge & apply + +Tracing the method calls on SurfaceControl.Transaction tells you where a change is requested, but +the changes are not actually committed until the transaction itself is applied. And because +transactions can be passed across processes, or prepared in advance for later application (ie. +when restoring state after a Transition), the ordering of the change logs is not always clear +by itself. + +In such cases, you can also enable the "merge" and "apply" calls to get additional information +about how/when transactions are respectively merged/applied: ```shell # Enabling adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,merge,apply # apply will dump logs of each setAlpha or merge call on that tx @@ -104,6 +134,11 @@ adb reboot adb logcat -s "SurfaceControlRegistry" ``` +Using those logs, you can first look at where the desired change is called, note the transaction +id, and then search the logs for where that transaction id is used. If it is merged into another +transaction, you can continue the search using the merged transaction until you find the final +transaction which is applied. + ## Tracing activity starts & finishes in the app process It's sometimes useful to know when to see a stack trace of when an activity starts in the app code diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md index 061ae00e2b25..f7707da33189 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md @@ -1,4 +1,5 @@ # Extending the Shell for Products/OEMs +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md index b489fe8ea1a9..bed0fba453d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md @@ -1,4 +1,5 @@ # What is the WindowManager Shell +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md index 5e92010d4b68..47383b0a81a0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md @@ -1,4 +1,5 @@ # Shell & SystemUI +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md index 98af930c4486..b4553131284b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md @@ -1,4 +1,5 @@ # Testing +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md index 837a6dd32ff2..bde722357308 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md @@ -1,4 +1,5 @@ # Threading +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java index 52b6c62b0721..31715f0444a9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java @@ -175,7 +175,9 @@ public class FreeformTaskTransitionHandler @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { ArrayList<Animator> animations = mAnimations.get(mergeTarget); if (animations == null) return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java index f8e6285b0493..d666126b91ba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java @@ -277,7 +277,8 @@ public class KeyguardTransitionHandler @Override public void mergeAnimation(@NonNull IBinder nextTransition, @NonNull TransitionInfo nextInfo, - @NonNull SurfaceControl.Transaction nextT, @NonNull IBinder currentTransition, + @NonNull SurfaceControl.Transaction nextT, @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder currentTransition, @NonNull TransitionFinishCallback nextFinishCallback) { final StartedTransition playing = mStartedTransitions.get(currentTransition); if (playing == null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 2f3c15208621..f0e6ae45c389 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -372,7 +372,9 @@ public class PipTransition extends PipTransitionController { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { end(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java index d3ae411469cc..0fa6a116350e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java @@ -653,7 +653,9 @@ public class TvPipTransition extends PipTransitionController { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: merge animation", TAG); if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index d3e630ddc703..bb9b479524e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -236,7 +236,9 @@ public class PipTransition extends PipTransitionController implements @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { // Just jump-cut the current animation if any, but do not merge. if (info.getType() == TRANSIT_EXIT_PIP) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 55133780f517..8ad2e1d3c7c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -307,7 +307,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, @Override public void mergeAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { final RecentsController controller = findController(mergeTarget); if (controller == null) { @@ -315,7 +317,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "RecentsTransitionHandler.mergeAnimation: no controller found"); return; } - controller.merge(info, t, mergeTarget, finishCallback); + controller.merge(info, startT, mergeTarget, finishCallback); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index 3091be574a53..fed336b17f19 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -461,12 +461,14 @@ class SplitScreenTransitions { return transition; } - void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, + void mergeAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { if (mergeTarget != mAnimatingTransition) return; if (mActiveRemoteHandler != null) { - mActiveRemoteHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mActiveRemoteHandler.mergeAnimation(transition, info, startT, + finishT, mergeTarget, finishCallback); } else { for (int i = mAnimations.size() - 1; i >= 0; --i) { final Animator anim = mAnimations.get(i); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 2174017996a8..6783df8f8324 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2977,10 +2977,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void mergeAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "mergeAnimation: transition=%d", info.getDebugId()); - mSplitTransitions.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mSplitTransitions.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); } /** Jump the current transition animation to the end. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 1eaae7ec83d9..9af23080351f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -652,9 +652,16 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV } continue; } - startTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()); - finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()) - .setPosition(chg.getLeash(), 0, 0); + final Rect boundsOnScreen = tv.prepareOpen(chg.getTaskInfo(), chg.getLeash()); + if (boundsOnScreen != null) { + if (wct == null) wct = new WindowContainerTransaction(); + updateBounds(tv, boundsOnScreen, startTransaction, finishTransaction, + chg.getTaskInfo(), chg.getLeash(), wct); + } else { + startTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()); + finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()) + .setPosition(chg.getLeash(), 0, 0); + } changesHandled++; } } @@ -683,30 +690,8 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV WindowContainerTransaction wct) { final Rect boundsOnScreen = taskView.prepareOpen(taskInfo, leash); if (boundsOnScreen != null) { - final SurfaceControl tvSurface = taskView.getSurfaceControl(); - // Surface is ready, so just reparent the task to this surface control - startTransaction.reparent(leash, tvSurface) - .show(leash); - // Also reparent on finishTransaction since the finishTransaction will reparent back - // to its "original" parent by default. - if (finishTransaction != null) { - finishTransaction.reparent(leash, tvSurface) - .setPosition(leash, 0, 0) - // TODO: maybe once b/280900002 is fixed this will be unnecessary - .setWindowCrop(leash, boundsOnScreen.width(), boundsOnScreen.height()); - } - if (useRepo()) { - final TaskViewRepository.TaskViewState state = mTaskViewRepo.byTaskView(taskView); - if (state != null) { - state.mBounds.set(boundsOnScreen); - state.mVisible = true; - } - } else { - updateBoundsState(taskView, boundsOnScreen); - updateVisibilityState(taskView, true /* visible */); - } - wct.setBounds(taskInfo.token, boundsOnScreen); - taskView.applyCaptionInsetsIfNeeded(); + updateBounds(taskView, boundsOnScreen, startTransaction, finishTransaction, taskInfo, + leash, wct); } else { // The surface has already been destroyed before the task has appeared, // so go ahead and hide the task entirely @@ -730,6 +715,36 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV taskView.notifyAppeared(newTask); } + private void updateBounds(TaskViewTaskController taskView, Rect boundsOnScreen, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction, + ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, + WindowContainerTransaction wct) { + final SurfaceControl tvSurface = taskView.getSurfaceControl(); + // Surface is ready, so just reparent the task to this surface control + startTransaction.reparent(leash, tvSurface) + .show(leash); + // Also reparent on finishTransaction since the finishTransaction will reparent back + // to its "original" parent by default. + if (finishTransaction != null) { + finishTransaction.reparent(leash, tvSurface) + .setPosition(leash, 0, 0) + .setWindowCrop(leash, boundsOnScreen.width(), boundsOnScreen.height()); + } + if (useRepo()) { + final TaskViewRepository.TaskViewState state = mTaskViewRepo.byTaskView(taskView); + if (state != null) { + state.mBounds.set(boundsOnScreen); + state.mVisible = true; + } + } else { + updateBoundsState(taskView, boundsOnScreen); + updateVisibilityState(taskView, true /* visible */); + } + wct.setBounds(taskInfo.token, boundsOnScreen); + taskView.applyCaptionInsetsIfNeeded(); + } + /** Interface for running an external transition in this object's pending queue. */ public interface ExternalTransition { /** Starts a transition and returns an identifying key for lookup. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index d8e7c2ccb15f..743bd052995e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -176,7 +176,9 @@ public class DefaultMixedHandler implements MixedTransitionHandler, abstract void mergeAnimation( @NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback); abstract void onTransitionConsumed( @@ -691,7 +693,9 @@ public class DefaultMixedHandler implements MixedTransitionHandler, @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { for (int i = 0; i < mActiveTransitions.size(); ++i) { if (mActiveTransitions.get(i).mTransition != mergeTarget) continue; @@ -701,7 +705,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler, // Already done, so no need to end it. return; } - mixed.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mixed.mergeAnimation(transition, info, startT, finishT, mergeTarget, finishCallback); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java index 29a58d7f75dc..1853ffa96dfc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java @@ -384,7 +384,8 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { @Override void mergeAnimation( @NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { switch (mType) { case TYPE_DISPLAY_AND_SPLIT_CHANGE: @@ -394,7 +395,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING: mPipHandler.end(); mActivityEmbeddingController.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); + transition, info, startT, finishT, mergeTarget, finishCallback); return; case TYPE_ENTER_PIP_FROM_SPLIT: if (mAnimType == ANIM_TYPE_GOING_HOME) { @@ -405,26 +406,28 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { mPipHandler.end(); if (mLeftoversHandler != null) { mLeftoversHandler.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); + transition, info, startT, finishT, mergeTarget, finishCallback); } } return; case TYPE_KEYGUARD: - mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mKeyguardHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); return; case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE: mPipHandler.end(); if (mLeftoversHandler != null) { mLeftoversHandler.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); + transition, info, startT, finishT, mergeTarget, finishCallback); } return; case TYPE_UNFOLD: - mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mUnfoldHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); return; case TYPE_OPEN_IN_DESKTOP: mDesktopTasksController.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); + transition, info, startT, finishT, mergeTarget, finishCallback); return; default: throw new IllegalStateException("Playing a default mixed transition with unknown or" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index ac6e4c5cd69e..28bba2e5e731 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -708,7 +708,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { ArrayList<Animator> anims = mAnimations.get(mergeTarget); if (anims == null) return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java index 209fc39b096a..ec737389c351 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java @@ -96,7 +96,9 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Merging registered One-shot remote" + " transition %s for (#%d).", mRemote, info.getDebugId()); @@ -111,7 +113,7 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { // process won't be cleared if the remote applied it. We don't actually know if the // remote applied the transaction, but applying twice will break surfaceflinger // so just assume the worst-case and clear the local transaction. - t.clear(); + startT.clear(); mMainExecutor.execute(() -> { finishCallback.onTransitionFinished(wct); }); @@ -121,8 +123,8 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { // If the remote is actually in the same process, then make a copy of parameters since // remote impls assume that they have to clean-up native references. final SurfaceControl.Transaction remoteT = - RemoteTransitionHandler.copyIfLocal(t, mRemote.getRemoteTransition()); - final TransitionInfo remoteInfo = remoteT == t ? info : info.localRemoteCopy(); + RemoteTransitionHandler.copyIfLocal(startT, mRemote.getRemoteTransition()); + final TransitionInfo remoteInfo = remoteT == startT ? info : info.localRemoteCopy(); mRemote.getRemoteTransition().mergeAnimation( transition, remoteInfo, remoteT, mergeTarget, cb); } catch (RemoteException e) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index 1847af07f275..f40dc8ad93b5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -193,21 +193,24 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { @Override void mergeAnimation( @NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { switch (mType) { case TYPE_RECENTS_DURING_DESKTOP: - mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mLeftoversHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); return; case TYPE_RECENTS_DURING_KEYGUARD: if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) { - handoverTransitionLeashes(mInfo, info, t, mFinishT); + handoverTransitionLeashes(mInfo, info, startT, finishT); if (animateKeyguard( - this, info, t, mFinishT, mFinishCB, mKeyguardHandler, mPipHandler)) { + this, info, startT, finishT, mFinishCB, mKeyguardHandler, + mPipHandler)) { finishCallback.onTransitionFinished(null); } } - mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, + mLeftoversHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, finishCallback); return; case TYPE_RECENTS_DURING_SPLIT: @@ -216,7 +219,8 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { // another pair. mAnimType = DefaultMixedHandler.MixedTransition.ANIM_TYPE_PAIR_TO_PAIR; } - mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mLeftoversHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); return; default: throw new IllegalStateException("Playing a Recents mixed transition with unknown or" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index dec28fefd789..c4a410b0e28a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -211,7 +211,9 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { final RemoteTransition remoteTransition = mRequestedRemotes.get(mergeTarget); if (remoteTransition == null) return; @@ -230,7 +232,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { // process won't be cleared if the remote applied it. We don't actually know if the // remote applied the transaction, but applying twice will break surfaceflinger // so just assume the worst-case and clear the local transaction. - t.clear(); + startT.clear(); mMainExecutor.execute(() -> { if (!mRequestedRemotes.containsKey(mergeTarget)) { Log.e(TAG, "Merged transition finished after it's mergeTarget (the " @@ -245,8 +247,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { try { // If the remote is actually in the same process, then make a copy of parameters since // remote impls assume that they have to clean-up native references. - final SurfaceControl.Transaction remoteT = copyIfLocal(t, remote); - final TransitionInfo remoteInfo = remoteT == t ? info : info.localRemoteCopy(); + final SurfaceControl.Transaction remoteT = copyIfLocal(startT, remote); + final TransitionInfo remoteInfo = remoteT == startT ? info : info.localRemoteCopy(); remote.mergeAnimation(transition, remoteInfo, remoteT, mergeTarget, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error attempting to merge remote transition.", e); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index b83b7e2f07a3..72cbc4702ac8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -922,7 +922,7 @@ public class Transitions implements RemoteCallable<Transitions>, + " %s is still animating. Notify the animating transition" + " in case they can be merged", ready, playing); mTransitionTracer.logMergeRequested(ready.mInfo.getDebugId(), playing.mInfo.getDebugId()); - playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, + playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, ready.mFinishT, playing.mToken, (wct) -> onMerged(playingToken, readyToken)); } @@ -1356,7 +1356,7 @@ public class Transitions implements RemoteCallable<Transitions>, // fast-forward. ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt to merge sync %s" + " into %s via a SLEEP proxy", nextSync, playing); - playing.mHandler.mergeAnimation(nextSync.mToken, dummyInfo, dummyT, + playing.mHandler.mergeAnimation(nextSync.mToken, dummyInfo, dummyT, dummyT, playing.mToken, (wct) -> {}); // it's possible to complete immediately. If that happens, just repeat the signal // loop until we either finish everything or start playing an animation that isn't @@ -1404,7 +1404,9 @@ public class Transitions implements RemoteCallable<Transitions>, * @param finishTransaction the transaction given to the handler to be applied after the * transition animation. Unlike startTransaction, the handler is NOT * expected to apply this transaction. The Transition system will - * apply it when finishCallback is called. + * apply it when finishCallback is called. If additional transitions + * are merged, then the finish transactions for those transitions + * will be applied after this transaction. * @param finishCallback Call this when finished. This MUST be called on main thread. * @return true if transition was handled, false if not (falls-back to default). */ @@ -1414,6 +1416,17 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull TransitionFinishCallback finishCallback); /** + * See {@link #mergeAnimation(IBinder, TransitionInfo, SurfaceControl.Transaction, SurfaceControl.Transaction, IBinder, TransitionFinishCallback)} + * + * This deprecated method header is provided until downstream implementation can migrate to + * the call that takes both start & finish transactions. + */ + @Deprecated + default void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull IBinder mergeTarget, @NonNull TransitionFinishCallback finishCallback) { } + + /** * Attempts to merge a different transition's animation into an animation that this handler * is currently playing. If a merge is not possible/supported, this should be a no-op. * @@ -1430,14 +1443,25 @@ public class Transitions implements RemoteCallable<Transitions>, * * @param transition This is the transition that wants to be merged. * @param info Information about what is changing in the transition. - * @param t Contains surface changes that resulted from the transition. + * @param startTransaction The start transaction containing surface changes that resulted + * from the incoming transition. This should be applied by this + * active handler only if it chooses to merge the transition. + * @param finishTransaction The finish transaction for the incoming transition. Unlike + * startTransaction, the handler is NOT expected to apply this + * transaction. If the transition is merged, the Transition system + * will apply after finishCallback is called following the finish + * transaction provided in `#startAnimation()`. * @param mergeTarget This is the transition that we are attempting to merge with (ie. the * one this handler is currently already animating). * @param finishCallback Call this if merged. This MUST be called on main thread. */ default void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, - @NonNull TransitionFinishCallback finishCallback) { } + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull IBinder mergeTarget, @NonNull TransitionFinishCallback finishCallback) { + // Call the legacy implementation by default + mergeAnimation(transition, info, startTransaction, mergeTarget, finishCallback); + } /** * Checks whether this handler is capable of taking over a transition matching `info`. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java index 3e0e15afc53a..7fd19a7d2a88 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java @@ -225,7 +225,9 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull TransitionFinishCallback finishCallback) { if (info.getType() != TRANSIT_CHANGE) { return; @@ -246,7 +248,7 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene } } // Apply changes happening during the unfold animation immediately - t.apply(); + startT.apply(); finishCallback.onTransitionFinished(null); if (getDefaultDisplayChange(info) == DefaultDisplayChange.DEFAULT_DISPLAY_FOLD) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 195e8195089f..fb4ce13c441f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -50,7 +50,6 @@ import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; import android.app.IActivityManager; import android.app.IActivityTaskManager; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Point; @@ -132,6 +131,7 @@ import com.android.wm.shell.recents.RecentsTransitionStateListener; import com.android.wm.shell.shared.FocusTransitionListener; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; @@ -1440,13 +1440,17 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDragToDesktopAnimationStartBounds.set( relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds()); boolean dragFromStatusBarAllowed = false; + final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode(); if (DesktopModeStatus.canEnterDesktopMode(mContext)) { // In proto2 any full screen or multi-window task can be dragged to // freeform. - final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode(); dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_MULTI_WINDOW; } + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + // TODO(b/388851898): add support for split screen (multi-window wm mode) + dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN; + } final boolean shouldStartTransitionDrag = relevantDecor.checkTouchEventInFocusedCaptionHandle(ev) || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue(); @@ -1654,9 +1658,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, if (mDesktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) { return false; } - if (isPartOfDefaultHomePackage(taskInfo)) { - return false; - } final boolean isOnLargeScreen = taskInfo.getConfiguration().smallestScreenWidthDp >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; if (!DesktopModeStatus.canEnterDesktopMode(mContext) @@ -1672,14 +1673,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop(); } - private boolean isPartOfDefaultHomePackage(RunningTaskInfo taskInfo) { - final ComponentName currentDefaultHome = - mContext.getPackageManager().getHomeActivities(new ArrayList<>()); - return currentDefaultHome != null && taskInfo.baseActivity != null - && currentDefaultHome.getPackageName() - .equals(taskInfo.baseActivity.getPackageName()); - } - private void createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 387dbfa807fc..c1a6240516c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -27,18 +27,17 @@ import static android.view.MotionEvent.ACTION_UP; import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION; import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS; - import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode; import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopModeOrShowAppHandle; import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge.NONE; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeHandleEdgeInset; -import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener; import android.annotation.NonNull; import android.annotation.Nullable; @@ -112,14 +111,14 @@ import kotlin.Unit; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; -import kotlinx.coroutines.CoroutineScope; -import kotlinx.coroutines.MainCoroutineDispatcher; - import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.MainCoroutineDispatcher; + /** * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with * {@link DesktopModeWindowDecorViewModel}. @@ -579,6 +578,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin closeHandleMenu(); closeManageWindowsMenu(); closeMaximizeMenu(); + notifyNoCaptionHandle(); } updateDragResizeListener(oldDecorationSurface, inFullImmersive); updateMaximizeMenu(startT, inFullImmersive); @@ -717,7 +717,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } private void notifyCaptionStateChanged() { - // TODO: b/366159408 - Ensure bounds sent with notification account for RTL mode. if (!canEnterDesktopMode(mContext) || !isEducationEnabled()) { return; } @@ -847,6 +846,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mOnCaptionButtonClickListener, mOnCaptionLongClickListener, mOnCaptionGenericMotionListener, + mOnLeftSnapClickListener, + mOnRightSnapClickListener, + mOnMaximizeOrRestoreClickListener, mOnMaximizeHoverListener); } throw new IllegalArgumentException("Unexpected layout resource id"); @@ -1362,7 +1364,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin updateGenericLink(); final boolean supportsMultiInstance = mMultiInstanceHelper .supportsMultiInstanceSplit(mTaskInfo.baseActivity, mTaskInfo.userId) - && Flags.enableDesktopWindowingMultiInstanceFeatures(); + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES.isTrue(); final boolean shouldShowManageWindowsButton = supportsMultiInstance && mMinimumInstancesFound; final boolean shouldShowChangeAspectRatioButton = HandleMenu.Companion diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index 053850480ecc..1d9564948772 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -44,6 +44,8 @@ import android.window.SurfaceSyncGroup import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.compose.ui.graphics.toArgb +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK import androidx.core.view.isGone import com.android.window.flags.Flags import com.android.wm.shell.R @@ -55,8 +57,8 @@ import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer import com.android.wm.shell.windowdecor.common.DecorThemeUtil -import com.android.wm.shell.windowdecor.common.calculateMenuPosition import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader +import com.android.wm.shell.windowdecor.common.calculateMenuPosition import com.android.wm.shell.windowdecor.extension.isFullscreen import com.android.wm.shell.windowdecor.extension.isMultiWindow import com.android.wm.shell.windowdecor.extension.isPinned @@ -250,6 +252,7 @@ class HandleMenu( view = handleMenuView.rootView, forciblyShownTypes = if (forceShowSystemBars) { systemBars() } else { 0 }, ignoreCutouts = Flags.showAppHandleLargeScreens() + || BubbleAnythingFlagHelper.enableBubbleToFullscreen() ) } else { parentDecor.addWindow( @@ -536,6 +539,20 @@ class HandleMenu( } return@setOnTouchListener true } + + with(context.resources) { + // Update a11y read out to say "double tap to enter desktop windowing mode" + ViewCompat.replaceAccessibilityAction( + desktopBtn, ACTION_CLICK, + getString(R.string.app_handle_menu_talkback_desktop_mode_button_text), null + ) + + // Update a11y read out to say "double tap to enter split screen mode" + ViewCompat.replaceAccessibilityAction( + splitscreenBtn, ACTION_CLICK, + getString(R.string.app_handle_menu_talkback_split_screen_mode_button_text), null + ) + } } /** Binds the menu views to the new data. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 1ce0366728b9..be3ea4e7b71e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -34,6 +34,7 @@ import android.graphics.drawable.LayerDrawable import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.StateListDrawable import android.graphics.drawable.shapes.RoundRectShape +import android.os.Bundle import android.util.StateSet import android.view.LayoutInflater import android.view.MotionEvent.ACTION_HOVER_ENTER @@ -51,12 +52,16 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.WindowlessWindowManager import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.Button import android.widget.TextView import android.window.TaskConstants import androidx.compose.material3.ColorScheme import androidx.compose.ui.graphics.toArgb import androidx.core.animation.addListener +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import androidx.core.view.isGone import androidx.core.view.isVisible import com.android.wm.shell.R @@ -403,6 +408,96 @@ class MaximizeMenu( true } + sizeToggleButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + onMaximizeClickListener?.invoke() + } + return super.performAccessibilityAction(host, action, args) + } + } + + snapLeftButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + onLeftSnapClickListener?.invoke() + } + return super.performAccessibilityAction(host, action, args) + } + } + + snapRightButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + onRightSnapClickListener?.invoke() + } + return super.performAccessibilityAction(host, action, args) + } + } + + with(context.resources) { + ViewCompat.replaceAccessibilityAction( + snapLeftButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.maximize_menu_talkback_action_snap_left_text), + null + ) + + ViewCompat.replaceAccessibilityAction( + snapRightButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.maximize_menu_talkback_action_snap_right_text), + null + ) + + ViewCompat.replaceAccessibilityAction( + sizeToggleButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.maximize_menu_talkback_action_maximize_restore_text), + null + ) + } + // Maximize/restore button. val sizeToggleBtnTextId = if (sizeToggleDirection == SizeToggleDirection.RESTORE) R.string.desktop_mode_maximize_menu_restore_button_text diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt index 1bc48f89ea6d..801048adda4d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt @@ -153,9 +153,7 @@ class WindowDecorTaskResourceLoader( private fun loadAppResources(taskInfo: RunningTaskInfo): AppResources { Trace.beginSection("$TAG#loadAppResources") try { - val pm = checkNotNull(userProfilesContexts[taskInfo.userId]?.packageManager) { - "Could not get context for user ${taskInfo.userId}" - } + val pm = userProfilesContexts.getOrCreate(taskInfo.userId).packageManager val activityInfo = getActivityInfo(taskInfo, pm) val appName = pm.getApplicationLabel(activityInfo.applicationInfo) val appIconDrawable = iconProvider.getIcon(activityInfo) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index 1264c013faf5..2948fdaf16af 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -41,6 +41,7 @@ import com.android.internal.policy.SystemBarUtils import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.shared.animation.Interpolators +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper import com.android.wm.shell.windowdecor.WindowManagerWrapper import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer @@ -146,6 +147,7 @@ internal class AppHandleViewHolder( taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, ignoreCutouts = Flags.showAppHandleLargeScreens() + || BubbleAnythingFlagHelper.enableBubbleToFullscreen() ) val view = statusBarInputLayer?.view ?: error("Unable to find statusBarInputLayer View") val lp = statusBarInputLayer?.lp ?: error("Unable to find statusBarInputLayer " + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index 9f8ca7740182..db12f899f42f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -27,10 +27,13 @@ import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.RoundRectShape +import android.os.Bundle import android.view.View import android.view.View.OnLongClickListener import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView @@ -49,6 +52,8 @@ import com.android.internal.R.color.materialColorSurfaceDim import com.android.window.flags.Flags import com.android.wm.shell.R import android.window.DesktopModeFlags +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import com.android.wm.shell.windowdecor.MaximizeButtonView import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.OPACITY_100 @@ -71,7 +76,10 @@ class AppHeaderViewHolder( onCaptionButtonClickListener: View.OnClickListener, private val onLongClickListener: OnLongClickListener, onCaptionGenericMotionListener: View.OnGenericMotionListener, - onMaximizeHoverAnimationFinishedListener: () -> Unit + mOnLeftSnapClickListener: () -> Unit, + mOnRightSnapClickListener: () -> Unit, + mOnMaximizeOrRestoreClickListener: () -> Unit, + onMaximizeHoverAnimationFinishedListener: () -> Unit, ) : WindowDecorationViewHolder<AppHeaderViewHolder.HeaderData>(rootView) { data class HeaderData( @@ -153,6 +161,91 @@ class AppHeaderViewHolder( minimizeWindowButton.setOnTouchListener(onCaptionTouchListener) maximizeButtonView.onHoverAnimationFinishedListener = onMaximizeHoverAnimationFinishedListener + + val a11yActionSnapLeft = AccessibilityAction( + R.id.action_snap_left, + context.resources.getString(R.string.desktop_mode_a11y_action_snap_left) + ) + val a11yActionSnapRight = AccessibilityAction( + R.id.action_snap_right, + context.resources.getString(R.string.desktop_mode_a11y_action_snap_right) + ) + val a11yActionMaximizeRestore = AccessibilityAction( + R.id.action_maximize_restore, + context.resources.getString(R.string.desktop_mode_a11y_action_maximize_restore) + ) + + captionHandle.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(a11yActionSnapLeft) + info.addAction(a11yActionSnapRight) + info.addAction(a11yActionMaximizeRestore) + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + when (action) { + R.id.action_snap_left -> mOnLeftSnapClickListener.invoke() + R.id.action_snap_right -> mOnRightSnapClickListener.invoke() + R.id.action_maximize_restore -> mOnMaximizeOrRestoreClickListener.invoke() + } + + return super.performAccessibilityAction(host, action, args) + } + } + maximizeWindowButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + info.addAction(a11yActionSnapLeft) + info.addAction(a11yActionSnapRight) + info.addAction(a11yActionMaximizeRestore) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + when (action) { + AccessibilityAction.ACTION_CLICK.id -> host.performClick() + R.id.action_snap_left -> mOnLeftSnapClickListener.invoke() + R.id.action_snap_right -> mOnRightSnapClickListener.invoke() + R.id.action_maximize_restore -> mOnMaximizeOrRestoreClickListener.invoke() + } + + return super.performAccessibilityAction(host, action, args) + } + } + + with(context.resources) { + // Update a11y read out to say "double tap to maximize or restore window size" + ViewCompat.replaceAccessibilityAction( + maximizeWindowButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.maximize_button_talkback_action_maximize_restore_text), + null + ) + + // Update a11y read out to say "double tap to minimize app window" + ViewCompat.replaceAccessibilityAction( + minimizeWindowButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.minimize_button_talkback_action_maximize_restore_text), + null + ) + } } override fun bindData(data: HeaderData) { @@ -628,6 +721,9 @@ class AppHeaderViewHolder( onCaptionButtonClickListener: View.OnClickListener, onLongClickListener: OnLongClickListener, onCaptionGenericMotionListener: View.OnGenericMotionListener, + mOnLeftSnapClickListener: () -> Unit, + mOnRightSnapClickListener: () -> Unit, + mOnMaximizeOrRestoreClickListener: () -> Unit, onMaximizeHoverAnimationFinishedListener: () -> Unit, ): AppHeaderViewHolder = AppHeaderViewHolder( rootView, @@ -635,6 +731,9 @@ class AppHeaderViewHolder( onCaptionButtonClickListener, onLongClickListener, onCaptionGenericMotionListener, + mOnLeftSnapClickListener, + mOnRightSnapClickListener, + mOnMaximizeOrRestoreClickListener, onMaximizeHoverAnimationFinishedListener, ) } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt index b5b7847e205d..80e4c47a5f68 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.RequiresFlagsDisabled import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder @@ -51,6 +52,7 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ +@FlakyTest(bugId = 391734110) @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java index 40b685c243b4..4972fa907ce7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java @@ -23,6 +23,9 @@ import static org.junit.Assume.assumeTrue; import android.content.Context; import android.content.pm.PackageManager; import android.hardware.display.DisplayManager; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.TestableContext; import androidx.test.platform.app.InstrumentationRegistry; @@ -31,6 +34,8 @@ import com.android.internal.protolog.ProtoLog; import org.junit.After; import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; import org.mockito.MockitoAnnotations; /** @@ -38,6 +43,16 @@ import org.mockito.MockitoAnnotations; */ public abstract class ShellTestCase { + @ClassRule + public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Rule + public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + protected TestableContext mContext; private PackageManager mPm; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java index bba9418db66a..94dc774a6737 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java @@ -41,7 +41,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.window.TransitionInfo; @@ -55,7 +54,6 @@ import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -73,9 +71,6 @@ import java.util.Arrays; @RunWith(TestParameterInjector.class) public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase { - @Rule - public SetFlagsRule mRule = new SetFlagsRule(); - @Before public void setup() { super.setUp(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java index 39d55079ca3a..9f29ef71930a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java @@ -34,7 +34,6 @@ import android.animation.ValueAnimator; import android.graphics.Rect; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -46,7 +45,6 @@ import com.android.window.flags.Flags; import com.android.wm.shell.transition.TransitionInfoBuilder; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -64,9 +62,6 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation private static final Rect EMBEDDED_LEFT_BOUNDS = new Rect(0, 0, 500, 500); private static final Rect EMBEDDED_RIGHT_BOUNDS = new Rect(500, 0, 1000, 500); - @Rule - public SetFlagsRule mRule = new SetFlagsRule(); - @Before public void setup() { super.setUp(); @@ -276,7 +271,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation mController.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction, mFinishCallback); verify(mFinishCallback, never()).onTransitionFinished(any()); - mController.mergeAnimation(mTransition, info, new SurfaceControl.Transaction(), + mController.mergeAnimation(mTransition, info, + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), mTransition, (wct) -> {}); verify(mFinishCallback).onTransitionFinished(any()); } 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 bbdb90f0a37c..05750a54f566 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 @@ -60,7 +60,6 @@ import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.IRemoteAnimationRunner; @@ -91,7 +90,6 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -152,9 +150,6 @@ public class BackAnimationControllerTest extends ShellTestCase { private BackAnimationController.BackTransitionHandler mBackTransitionHandler; - @Rule - public SetFlagsRule mSetflagsRule = new SetFlagsRule(); - @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -671,7 +666,7 @@ public class BackAnimationControllerTest extends ShellTestCase { Transitions.TransitionFinishCallback mergeCallback = mock(Transitions.TransitionFinishCallback.class); mBackTransitionHandler.mergeAnimation( - mock(IBinder.class), tInfo2, st, mock(IBinder.class), mergeCallback); + mock(IBinder.class), tInfo2, st, ft, mock(IBinder.class), mergeCallback); mBackTransitionHandler.onAnimationFinished(); verify(callback).onTransitionFinished(any()); verify(mergeCallback).onTransitionFinished(any()); @@ -706,7 +701,7 @@ public class BackAnimationControllerTest extends ShellTestCase { mBackTransitionHandler.mClosePrepareTransition = mock(IBinder.class); mergeCallback = mock(Transitions.TransitionFinishCallback.class); mBackTransitionHandler.mergeAnimation(mBackTransitionHandler.mClosePrepareTransition, - tInfo2, st, mock(IBinder.class), mergeCallback); + tInfo2, st, ft, mock(IBinder.class), mergeCallback); assertTrue("Change should be consumed", tInfo2.getChanges().isEmpty()); verify(callback).onTransitionFinished(any()); } @@ -752,7 +747,7 @@ public class BackAnimationControllerTest extends ShellTestCase { final TransitionInfo closeInfo = createTransitionInfo(TRANSIT_CLOSE, close); Transitions.TransitionFinishCallback mergeCallback = mock(Transitions.TransitionFinishCallback.class); - mBackTransitionHandler.mergeAnimation(mock(IBinder.class), closeInfo, ft, + mBackTransitionHandler.mergeAnimation(mock(IBinder.class), closeInfo, st, ft, mock(IBinder.class), mergeCallback); verify(callback2).onTransitionFinished(any()); verify(mergeCallback, never()).onTransitionFinished(any()); @@ -771,7 +766,7 @@ public class BackAnimationControllerTest extends ShellTestCase { openTaskId2, TRANSIT_OPEN, FLAG_MOVED_TO_TOP); final TransitionInfo openInfo = createTransitionInfo(TRANSIT_OPEN, open2, close); mergeCallback = mock(Transitions.TransitionFinishCallback.class); - mBackTransitionHandler.mergeAnimation(mock(IBinder.class), openInfo, ft, + mBackTransitionHandler.mergeAnimation(mock(IBinder.class), openInfo, st, ft, mock(IBinder.class), mergeCallback); verify(callback3).onTransitionFinished(any()); verify(mergeCallback, never()).onTransitionFinished(any()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java index 6d7a18d7fca4..2ef6c558b0b5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java @@ -32,6 +32,8 @@ import android.window.BackProgressAnimator; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.wm.shell.ShellTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,7 +44,7 @@ import java.util.concurrent.TimeUnit; @SmallTest @TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner.class) -public class BackProgressAnimatorTest { +public class BackProgressAnimatorTest extends ShellTestCase { private BackProgressAnimator mProgressAnimator; private BackEvent mReceivedBackEvent; private float mTargetProgress = 0.5f; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt index 417b43a9c6c0..22cc65d8ffaf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt @@ -34,7 +34,6 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.bubbles.bar.BubbleBarLayerView -import com.android.wm.shell.bubbles.properties.BubbleProperties import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayImeController import com.android.wm.shell.common.DisplayInsetsController @@ -143,7 +142,7 @@ class BubbleViewInfoTest : ShellTestCase() { mock<Transitions>(), mock<SyncTransactionQueue>(), mock<IWindowManager>(), - mock<BubbleProperties>() + BubbleResizabilityChecker() ) val bubbleStackViewManager = BubbleStackViewManager.fromBubbleController(bubbleController) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java index f8eb50b978a5..622e4cbf5ece 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java @@ -38,6 +38,7 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.transition.TransitionInfoBuilder; import org.junit.Before; @@ -49,7 +50,7 @@ import org.mockito.MockitoAnnotations; * Tests of {@link BubblesTransitionObserver}. */ @SmallTest -public class BubblesTransitionObserverTest { +public class BubblesTransitionObserverTest extends ShellTestCase { @Mock private BubbleController mBubbleController; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java index f8ee300e411c..3323740697f3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java @@ -29,6 +29,7 @@ import android.content.Context; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; @@ -41,7 +42,7 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) -public class DevicePostureControllerTest { +public class DevicePostureControllerTest extends ShellTestCase { @Mock private Context mContext; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java index 6f3a3ec4fd20..ee9d17706372 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java @@ -39,8 +39,6 @@ import android.graphics.Point; import android.os.Looper; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.IWindowManager; import android.view.InsetsSource; import android.view.InsetsSourceControl; @@ -55,7 +53,6 @@ import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -70,9 +67,6 @@ import java.util.concurrent.Executor; */ @SmallTest public class DisplayImeControllerTest extends ShellTestCase { - @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - @Mock private SurfaceControl.Transaction mT; @Mock diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt index ef0b8ab14c81..56d401779654 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt @@ -69,6 +69,7 @@ class UserProfileContextsTest : ShellTestCase() { } .whenever(baseContext) .createContextAsUser(any<UserHandle>(), anyInt()) + doReturn(DEFAULT_USER).whenever(baseContext).userId // Define users and profiles val currentUser = ActivityManager.getCurrentUser() whenever(userManager.getProfiles(eq(currentUser))) @@ -136,6 +137,25 @@ class UserProfileContextsTest : ShellTestCase() { assertThat(userProfilesContexts[SECOND_PROFILE]?.userId).isEqualTo(SECOND_PROFILE) } + @Test + fun onUserProfilesChanged_keepDefaultUser() { + val userChangeListener = retrieveUserChangeListener() + val newUserContext = createContextForUser(SECOND_USER) + + userChangeListener.onUserChanged(SECOND_USER, newUserContext) + userChangeListener.onUserProfilesChanged(SECOND_PROFILES) + + assertThat(userProfilesContexts[DEFAULT_USER]).isEqualTo(baseContext) + } + + @Test + fun getOrCreate_newUser_shouldCreateTheUser() { + val newContext = userProfilesContexts.getOrCreate(SECOND_USER) + + assertThat(newContext).isNotNull() + assertThat(userProfilesContexts[SECOND_USER]).isEqualTo(newContext) + } + private fun retrieveUserChangeListener(): UserChangeListener { val captor = argumentCaptor<UserChangeListener>() @@ -155,6 +175,7 @@ class UserProfileContextsTest : ShellTestCase() { const val MAIN_PROFILE = 11 const val SECOND_PROFILE = 15 const val SECOND_PROFILE_2 = 17 + const val DEFAULT_USER = 25 val SECOND_PROFILES = listOf( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java index fd3d3b5b6e2f..8c34c1946702 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java @@ -36,6 +36,7 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; @@ -66,9 +67,9 @@ public class DividerViewTest extends ShellTestCase { public void setup() { MockitoAnnotations.initMocks(this); Configuration configuration = getConfiguration(); - mSplitLayout = new SplitLayout("TestSplitLayout", mContext, configuration, + mSplitLayout = spy(new SplitLayout("TestSplitLayout", mContext, configuration, mSplitLayoutHandler, mCallbacks, mDisplayController, mDisplayImeController, - mTaskOrganizer, SplitLayout.PARALLAX_NONE, mSplitState, mHandler); + mTaskOrganizer, SplitLayout.PARALLAX_NONE, mSplitState, mHandler)); SplitWindowManager splitWindowManager = new SplitWindowManager("TestSplitWindowManager", mContext, configuration, mCallbacks); @@ -98,6 +99,14 @@ public class DividerViewTest extends ShellTestCase { "false", false); } + @Test + public void swapDividerActionForA11y() { + mDividerView.setAccessibilityDelegate(mDividerView.mHandleDelegate); + mDividerView.getAccessibilityDelegate().performAccessibilityAction(mDividerView, + R.id.action_swap_apps, null); + verify(mSplitLayout, times(1)).onDoubleTappedDivider(); + } + private static MotionEvent getMotionEvent(long eventTime, int action, float x, float y) { MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties(); properties.id = 0; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java index 5a49d01f2991..979cee9d63c2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java @@ -16,24 +16,10 @@ package com.android.wm.shell.compatui; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; -import android.platform.test.flag.junit.SetFlagsRule; - import com.android.wm.shell.ShellTestCase; -import org.junit.Rule; - /** * Base class for CompatUI tests. */ public class CompatUIShellTestCase extends ShellTestCase { - - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index 61b6d803c8be..010474e42195 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -38,6 +38,7 @@ import android.app.ActivityManager; import android.app.TaskInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsDisabled; import android.testing.AndroidTestingRunner; import android.util.Pair; @@ -394,8 +395,8 @@ public class CompatUIWindowManagerTest extends CompatUIShellTestCase { @Test @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) + @EnableFlags(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON) public void testShouldShowSizeCompatRestartButton() { - mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON); doReturn(85).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, mCallback, mTaskListener, mDisplayLayout, new CompatUIHintsState(), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt index 319122d1e051..d3a2c9a411ef 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.compatui.impl import android.graphics.Point -import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.testing.AndroidTestingRunner import android.view.View import androidx.test.filters.SmallTest @@ -29,7 +28,6 @@ import com.android.wm.shell.compatui.api.CompatUISpec import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -45,9 +43,6 @@ class DefaultCompatUIRepositoryTest { lateinit var repository: CompatUIRepository - @get:Rule - val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() - @Before fun setUp() { repository = DefaultCompatUIRepository() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt index 78bb721d1028..008c499cb88e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt @@ -20,7 +20,6 @@ import android.graphics.Point import android.graphics.Rect import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CLOSE @@ -36,14 +35,12 @@ import com.android.wm.shell.transition.Transitions import com.android.wm.shell.util.TransitionObserverInputBuilder import com.android.wm.shell.util.executeTransitionObserverTest import java.util.function.Consumer -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock -import org.mockito.kotlin.times import org.mockito.kotlin.verify /** @@ -56,9 +53,6 @@ import org.mockito.kotlin.verify @SmallTest class LetterboxTransitionObserverTest : ShellTestCase() { - @get:Rule - val setFlagsRule: SetFlagsRule = SetFlagsRule() - @Test @DisableFlags(Flags.FLAG_APP_COMPAT_REFACTORING) fun `when initialized and flag disabled the observer is not registered`() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt index 957fdf995776..09ffd946ea19 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt @@ -25,7 +25,6 @@ import android.graphics.Rect import android.os.Binder import android.os.UserManager import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.window.WindowContainerTransaction @@ -62,7 +61,6 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor @@ -89,8 +87,6 @@ import org.mockito.quality.Strictness @ExperimentalCoroutinesApi @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE, FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE) class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @Mock lateinit var transitions: Transitions diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index fae7363e0676..0d5741fccbcc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -23,7 +23,6 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.ContentResolver import android.os.Binder import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.testing.AndroidTestingRunner @@ -51,7 +50,6 @@ import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.isNull @@ -74,9 +72,6 @@ import org.mockito.quality.Strictness @SmallTest @RunWith(AndroidTestingRunner::class) class DesktopDisplayEventHandlerTest : ShellTestCase() { - - @JvmField @Rule val setFlagsRule = SetFlagsRule() - @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var transitions: Transitions @Mock lateinit var displayController: DisplayController diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt index 47d133b974e6..006c3cae121c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt @@ -23,7 +23,6 @@ import android.os.Binder import android.os.IBinder import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.Display.DEFAULT_DISPLAY @@ -73,7 +72,6 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class DesktopImmersiveControllerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() @JvmField @Rule val animatorTestRule = AnimatorTestRule(this) @Mock private lateinit var mockTransitions: Transitions diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index f61ea4a194d6..f48bc99a8cfa 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -27,7 +27,6 @@ import android.os.Handler import android.os.IBinder import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl @@ -57,7 +56,6 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt @@ -82,8 +80,6 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class DesktopMixedTransitionHandlerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - @Mock lateinit var transitions: Transitions @Mock lateinit var userRepositories: DesktopUserRepositories @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt index bddc06204a52..8a5acfa70f50 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo import android.graphics.Rect import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker @@ -65,15 +64,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { val displayLayout = mock<DisplayLayout>() @JvmField - @Rule(order = 0) + @Rule() val extendedMockitoRule = ExtendedMockitoRule.Builder(this) .mockStatic(FrameworkStatsLog::class.java) .mockStatic(EventLogTags::class.java) .build()!! - @JvmField @Rule(order = 1) val setFlagsRule = SetFlagsRule() - @Before fun setUp() { doReturn(displayLayout).whenever(displayController).getDisplayLayout(anyInt()) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt index 016e04039b12..470c110fd49b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt @@ -24,7 +24,6 @@ import android.hardware.input.InputManager import android.hardware.input.InputManager.KeyGestureEventHandler import android.hardware.input.KeyGestureEvent import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.view.KeyEvent @@ -64,7 +63,6 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.anyInt @@ -87,8 +85,6 @@ import org.mockito.quality.Strictness @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class DesktopModeKeyGestureHandlerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() private val shellTaskOrganizer = mock<ShellTaskOrganizer>() private val focusTransitionObserver = mock<FocusTransitionObserver>() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index e46d2c7147ed..13b44977e9c7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -19,22 +19,30 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo import android.graphics.PointF import android.graphics.Rect +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl import androidx.test.filters.SmallTest import com.android.internal.policy.SystemBarUtils +import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.google.common.truth.Truth.assertThat import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.kotlin.any import org.mockito.kotlin.whenever /** @@ -43,9 +51,19 @@ import org.mockito.kotlin.whenever * Usage: atest WMShellUnitTests:DesktopModeVisualIndicatorTest */ @SmallTest +@RunWithLooper @RunWith(AndroidTestingRunner::class) +@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class DesktopModeVisualIndicatorTest : ShellTestCase() { - @Mock private lateinit var taskInfo: RunningTaskInfo + + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + @JvmField + @Rule + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this).mockStatic(DesktopModeStatus::class.java).build()!! + + private lateinit var taskInfo: RunningTaskInfo @Mock private lateinit var syncQueue: SyncTransactionQueue @Mock private lateinit var displayController: DisplayController @Mock private lateinit var taskSurface: SurfaceControl @@ -56,10 +74,13 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { @Before fun setUp() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS) whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayController.getDisplay(anyInt())).thenReturn(mContext.display) + taskInfo = DesktopTestHelpers.createFullscreenTask() } @Test @@ -190,6 +211,39 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) } + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testDefaultIndicatorWithNoDesktop() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + var result = visualIndicator.updateIndicatorType(PointF(500f, 500f)) + assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) + + result = visualIndicator.updateIndicatorType(PointF(10000f, 500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR) + + result = visualIndicator.updateIndicatorType(PointF(-10000f, 500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + result = visualIndicator.updateIndicatorType(PointF(500f, 500f)) + assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) + + result = visualIndicator.updateIndicatorType(PointF(500f, 0f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) + result = visualIndicator.updateIndicatorType(PointF(500f, 500f)) + assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) + } + private fun createVisualIndicator(dragStartState: DesktopModeVisualIndicator.DragStartState) { visualIndicator = DesktopModeVisualIndicator( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index f5c93ee8ffe4..6a343c56d364 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -20,7 +20,6 @@ import android.graphics.Rect import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization -import android.platform.test.flag.junit.SetFlagsRule import android.util.ArraySet import android.view.Display.DEFAULT_DISPLAY import android.view.Display.INVALID_DISPLAY @@ -28,6 +27,7 @@ import androidx.test.filters.SmallTest import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.ShellExecutor @@ -36,6 +36,7 @@ import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat import junit.framework.Assert.fail +import kotlin.test.assertEquals import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -47,7 +48,6 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -71,8 +71,6 @@ import platform.test.runner.parameterized.Parameters @ExperimentalCoroutinesApi class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule(flags) - private lateinit var repo: DesktopRepository private lateinit var shellInit: ShellInit private lateinit var datastoreScope: CoroutineScope @@ -1080,13 +1078,37 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) - val tasksBeforeRemoval = repo.removeDesk(displayId = DEFAULT_DISPLAY) + val tasksBeforeRemoval = repo.removeDesk(deskId = DEFAULT_DISPLAY) assertThat(tasksBeforeRemoval).containsExactly(1, 2, 3).inOrder() assertThat(repo.getActiveTasks(displayId = DEFAULT_DISPLAY)).isEmpty() } @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleDesks_active_removes() { + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + repo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + + repo.removeDesk(deskId = 3) + + assertThat(repo.getDeskIds(displayId = DEFAULT_DISPLAY)).doesNotContain(3) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleDesks_inactive_removes() { + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + repo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + + repo.removeDesk(deskId = 2) + + assertThat(repo.getDeskIds(displayId = DEFAULT_DISPLAY)).doesNotContain(2) + } + + @Test fun getTaskInFullImmersiveState_byDisplay() { repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) @@ -1168,6 +1190,26 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { assertThat(repo.getActiveTaskIdsInDesk(999)).contains(6) } + @Test + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun getDisplayForDesk() { + repo.addDesk(SECOND_DISPLAY, SECOND_DISPLAY) + + assertEquals(SECOND_DISPLAY, repo.getDisplayForDesk(deskId = SECOND_DISPLAY)) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun getDisplayForDesk_multipleDesks() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addDesk(DEFAULT_DISPLAY, deskId = 7) + repo.addDesk(SECOND_DISPLAY, deskId = 8) + repo.addDesk(SECOND_DISPLAY, deskId = 9) + + assertEquals(DEFAULT_DISPLAY, repo.getDisplayForDesk(deskId = 7)) + assertEquals(SECOND_DISPLAY, repo.getDisplayForDesk(deskId = 8)) + } + class TestListener : DesktopRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt index 12c7ff61399f..50590f021a2a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.desktopmode import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION @@ -26,7 +25,6 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTask import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt @@ -44,8 +42,6 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class DesktopTaskChangeListenerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - private lateinit var desktopTaskChangeListener: DesktopTaskChangeListener private val desktopUserRepositories = mock<DesktopUserRepositories>() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index a55cdb34c2fe..8e7545c2a99c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -35,6 +35,7 @@ import android.content.pm.ActivityInfo.CONFIG_DENSITY import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED +import android.content.pm.PackageManager import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.content.res.Resources @@ -49,7 +50,6 @@ import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization -import android.platform.test.flag.junit.SetFlagsRule import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.Gravity @@ -118,7 +118,9 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCR import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler +import com.android.wm.shell.desktopmode.multidesks.DeskTransition import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer @@ -165,7 +167,6 @@ import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assume.assumeTrue import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor @@ -181,6 +182,7 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argThat import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.capture @@ -201,8 +203,6 @@ import platform.test.runner.parameterized.Parameters @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule(flags) - @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var shellCommandHandler: ShellCommandHandler @Mock lateinit var shellController: ShellController @@ -254,6 +254,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() private lateinit var overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver @Mock private lateinit var desksOrganizer: DesksOrganizer @Mock private lateinit var userProfileContexts: UserProfileContexts + @Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit @@ -351,6 +352,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .thenReturn(ExitResult.NoExit) whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken) whenever(userProfileContexts[anyInt()]).thenReturn(context) + whenever(userProfileContexts.getOrCreate(anyInt())).thenReturn(context) controller = createController() controller.setSplitScreenController(splitScreenController) @@ -408,6 +410,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Optional.of(bubbleController), overviewToDesktopTransitionObserver, desksOrganizer, + desksTransitionsObserver, userProfileContexts, desktopModeCompatPolicy, ) @@ -538,6 +541,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskHidden(task1) @@ -726,6 +730,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskVisible(task1) @@ -764,7 +769,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { + fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersOnlyFreeformTasks() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskHidden(task1) @@ -781,6 +787,24 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() wct.assertReorderAt(index = 2, task2) } + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersAll() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertReorderAt(index = 0, wallpaperToken) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() { @@ -796,7 +820,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { + fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_addsDesktopWallpaper() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = @@ -805,6 +831,16 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_reordersDesktopWallpaper() { + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + wct.assertReorderAt(index = 0, wallpaperToken) + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) @@ -828,6 +864,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) @@ -872,6 +909,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val minimizedTask = setUpFreeformTask() @@ -1337,6 +1375,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) @@ -1422,6 +1461,41 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .isEqualTo(WINDOWING_MODE_FREEFORM) } + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun moveRunningTaskToDesktop_defaultHomePackageWithDisplay_doesNothing() { + val packageManager: PackageManager = org.mockito.kotlin.mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = homeActivities + isTopActivityNoDisplay = false + } + mContext.setMockPackageManager(packageManager) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + verifyEnterDesktopWCTNotExecuted() + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun moveRunningTaskToDesktop_defaultHomePackageWithoutDisplay_doesNothing() { + val packageManager: PackageManager = org.mockito.kotlin.mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = homeActivities + isTopActivityNoDisplay = false + } + mContext.setMockPackageManager(packageManager) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() { @@ -1484,6 +1558,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() markTaskHidden(freeformTask) @@ -1586,6 +1661,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } val newTask = setUpFullscreenTask() val homeTask = setUpHomeTask() @@ -1624,6 +1700,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val homeTask = setUpHomeTask() val task = setUpFreeformTask() assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) .configuration @@ -1637,9 +1715,33 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) - assertThat(wct.hierarchyOps).hasSize(1) + assertThat(wct.hierarchyOps).hasSize(3) // Removes wallpaper activity when leaving desktop wct.assertReorderAt(index = 0, wallpaperToken, toTop = false) + // Moves home task behind the fullscreen task + wct.assertReorderAt(index = 1, homeTask.getToken(), toTop = true) + wct.assertReorderAt(index = 2, task.getToken(), toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) + fun moveToFullscreen_tdaFreeform_enforcedDesktop_doesNotReorderHome() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val homeTask = setUpHomeTask() + val task = setUpFreeformTask() + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FREEFORM + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + assertThat(wct.hierarchyOps).hasSize(1) + // Removes wallpaper activity when leaving desktop but doesn't reorder home or the task + wct.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -1658,6 +1760,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { + val homeTask = setUpHomeTask() val task = setUpFreeformTask() assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) @@ -1672,13 +1775,17 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN) verify(desktopModeEnterExitTransitionListener) .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) - assertThat(wct.hierarchyOps).hasSize(1) + assertThat(wct.hierarchyOps).hasSize(3) // Removes wallpaper activity when leaving desktop wct.assertReorderAt(index = 0, wallpaperToken, toTop = false) + // Moves home task behind the fullscreen task + wct.assertReorderAt(index = 1, homeTask.getToken(), toTop = true) + wct.assertReorderAt(index = 2, task.getToken(), toTop = true) } @Test fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() { + val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() // Setup task2 setUpFreeformTask() @@ -1696,7 +1803,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) // Does not remove wallpaper activity, as desktop still has a visible desktop task - assertThat(wct.hierarchyOps).isEmpty() + assertThat(wct.hierarchyOps).hasSize(2) + // Moves home task behind the fullscreen task + wct.assertReorderAt(index = 0, homeTask.getToken(), toTop = true) + wct.assertReorderAt(index = 1, task1.getToken(), toTop = true) } @Test @@ -2572,6 +2682,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM @@ -2703,6 +2814,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() @@ -2737,7 +2849,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) assertNotNull(result, "Should handle request") @@ -2765,6 +2879,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) // Second display task createFreeformTask(displayId = SECOND_DISPLAY) @@ -3018,6 +3133,46 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .isEqualTo(WINDOWING_MODE_FREEFORM) } + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_defaultHomePackageWithDisplay_returnSwitchToFullscreenWCT() { + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + val packageManager: PackageManager = org.mockito.kotlin.mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = homeActivities + isTopActivityNoDisplay = false + } + mContext.setMockPackageManager(packageManager) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_defaultHomePackageWithoutDisplay_returnSwitchToFreeformWCT() { + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + val packageManager: PackageManager = org.mockito.kotlin.mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = homeActivities + isTopActivityNoDisplay = false + } + mContext.setMockPackageManager(packageManager) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + @Test fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT_enforcedDesktop() { whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) @@ -3421,6 +3576,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() { + val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() val task3 = setUpFreeformTask() @@ -3435,7 +3591,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() assertThat(taskChange.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN // Does not remove wallpaper activity, as desktop still has visible desktop tasks - assertThat(wct.hierarchyOps).isEmpty() + assertThat(wct.hierarchyOps).hasSize(2) + // Moves home task behind the fullscreen task + wct.assertReorderAt(index = 0, homeTask.getToken(), toTop = true) + wct.assertReorderAt(index = 1, task2.getToken(), toTop = true) } @Test @@ -3465,13 +3624,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun removeDesktop_multipleTasks_removesAll() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleTasks_removesAll() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() val task3 = setUpFreeformTask() taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) - controller.removeDesktop(displayId = DEFAULT_DISPLAY) + controller.removeDefaultDeskInDisplay(displayId = DEFAULT_DISPLAY) val wct = getLatestWct(TRANSIT_CLOSE) assertThat(wct.hierarchyOps).hasSize(3) @@ -3482,14 +3642,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun removeDesktop_multipleTasksWithBackgroundTask_removesAll() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleTasksWithBackgroundTask_removesAll() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() val task3 = setUpFreeformTask() taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) whenever(shellTaskOrganizer.getRunningTaskInfo(task3.taskId)).thenReturn(null) - controller.removeDesktop(displayId = DEFAULT_DISPLAY) + controller.removeDefaultDeskInDisplay(displayId = DEFAULT_DISPLAY) val wct = getLatestWct(TRANSIT_CLOSE) assertThat(wct.hierarchyOps).hasSize(2) @@ -3499,6 +3660,30 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun removeDesk_multipleDesks_addsPendingTransition() { + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_CLOSE), any(), anyOrNull())) + .thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 2) + + controller.removeDesk(deskId = 2) + + verify(desksOrganizer).removeDesk(any(), eq(2)) + verify(desksTransitionsObserver) + .addPendingTransition( + argThat { + this is DeskTransition.RemoveDesk && + this.token == transition && + this.deskId == 2 + } + ) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { val spyController = spy(controller) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 554b09f130bd..d33209dbc30e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -24,7 +24,6 @@ import android.os.IBinder import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.view.SurfaceControl @@ -66,7 +65,6 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -88,8 +86,6 @@ import org.mockito.quality.Strictness @ExperimentalCoroutinesApi class DesktopTasksLimiterTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @Mock lateinit var transitions: Transitions @Mock lateinit var interactionJankMonitor: InteractionJankMonitor diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index ca1e3edb3fd3..c29edece5537 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -25,7 +25,6 @@ import android.content.Intent import android.os.Binder import android.os.IBinder import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager import android.view.WindowManager.TRANSIT_CLOSE @@ -48,11 +47,13 @@ import com.android.wm.shell.back.BackAnimationController import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP +import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Before @@ -77,9 +78,6 @@ import org.mockito.kotlin.whenever * Build/Install/Run: atest WMShellUnitTests:DesktopTasksTransitionObserverTest */ class DesktopTasksTransitionObserverTest { - - @JvmField @Rule val setFlagsRule = SetFlagsRule() - @JvmField @Rule val extendedMockitoRule = @@ -96,6 +94,7 @@ class DesktopTasksTransitionObserverTest { private val backAnimationController = mock<BackAnimationController>() private val desktopWallpaperActivityTokenProvider = mock<DesktopWallpaperActivityTokenProvider>() + private val desksTransitionObserver = mock<DesksTransitionObserver>() private val wallpaperToken = MockToken().token() private lateinit var transitionObserver: DesktopTasksTransitionObserver @@ -119,6 +118,7 @@ class DesktopTasksTransitionObserverTest { mixedHandler, backAnimationController, desktopWallpaperActivityTokenProvider, + desksTransitionObserver, shellInit, ) } @@ -415,6 +415,21 @@ class DesktopTasksTransitionObserverTest { verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) } + @Test + fun onTransitionReady_forwardsToDesksTransitionObserver() { + val transition = Binder() + val info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0) + + transitionObserver.onTransitionReady( + transition = transition, + info = info, + StubTransaction(), + StubTransaction(), + ) + + verify(desksTransitionObserver).onTransitionReady(transition, info) + } + private fun createBackNavigationTransition( task: RunningTaskInfo?, type: Int = TRANSIT_TO_BACK, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt index b9e307fa5973..83e48728c4f2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt @@ -21,7 +21,6 @@ import android.content.pm.UserInfo import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn @@ -44,7 +43,6 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.spy @@ -56,8 +54,6 @@ import org.mockito.quality.Strictness @RunWith(AndroidTestingRunner::class) @ExperimentalCoroutinesApi class DesktopUserRepositoriesTest : ShellTestCase() { - @get:Rule val setFlagsRule = SetFlagsRule() - private lateinit var userRepositories: DesktopUserRepositories private lateinit var shellInit: ShellInit private lateinit var datastoreScope: CoroutineScope diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index bf9cf00050dc..33dc1aadf548 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -284,6 +284,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { cancelToken, TransitionInfo(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, 0), mock<SurfaceControl.Transaction>(), + mock<SurfaceControl.Transaction>(), startToken, mock<Transitions.TransitionFinishCallback>(), ) @@ -385,21 +386,23 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun mergeAnimation_otherTransition_doesNotMerge() { - val transaction = mock<SurfaceControl.Transaction>() + val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val mergedFinishTransaction = mock<SurfaceControl.Transaction>() val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() startDrag(defaultHandler, task) defaultHandler.mergeAnimation( - transition = mock(), + transition = mock<IBinder>(), info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = task), - t = transaction, - mergeTarget = mock(), + startT = mergedStartTransaction, + finishT = mergedFinishTransaction, + mergeTarget = mock<IBinder>(), finishCallback = finishCallback, ) // Should NOT have any transaction changes - verifyZeroInteractions(transaction) + verifyZeroInteractions(mergedStartTransaction) // Should NOT merge animation verify(finishCallback, never()).onTransitionFinished(any()) } @@ -408,6 +411,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { fun mergeAnimation_endTransition_mergesAnimation() { val playingFinishTransaction = mock<SurfaceControl.Transaction>() val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val mergedFinishTransaction = mock<SurfaceControl.Transaction>() val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() val startTransition = @@ -415,13 +419,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { defaultHandler.onTaskResizeAnimationListener = mock() defaultHandler.mergeAnimation( - transition = mock(), + transition = mock<IBinder>(), info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task, ), - t = mergedStartTransaction, + startT = mergedStartTransaction, + finishT = mergedFinishTransaction, mergeTarget = startTransition, finishCallback = finishCallback, ) @@ -440,6 +445,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { whenever(dragAnimator.computeCurrentVelocity()).thenReturn(PointF()) val playingFinishTransaction = mock<SurfaceControl.Transaction>() val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val mergedFinishTransaction = mock<SurfaceControl.Transaction>() val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() val startTransition = @@ -447,13 +453,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { springHandler.onTaskResizeAnimationListener = mock() springHandler.mergeAnimation( - transition = mock(), + transition = mock<IBinder>(), info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task, ), - t = mergedStartTransaction, + startT = mergedStartTransaction, + finishT = mergedFinishTransaction, mergeTarget = startTransition, finishCallback = finishCallback, ) @@ -564,7 +571,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task, ), - t = mock<SurfaceControl.Transaction>(), + startT = mock<SurfaceControl.Transaction>(), + finishT = mock<SurfaceControl.Transaction>(), mergeTarget = startTransition, finishCallback = mock<Transitions.TransitionFinishCallback>(), ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt index 7d2a8082c43e..86e8142b1aaa 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.desktopmode.education import android.os.SystemProperties import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableContext import androidx.test.filters.SmallTest @@ -75,7 +74,6 @@ class AppHandleEducationControllerTest : ShellTestCase() { .mockStatic(DesktopModeStatus::class.java) .mockStatic(SystemProperties::class.java) .build()!! - @JvmField @Rule val setFlagsRule = SetFlagsRule() private lateinit var educationController: AppHandleEducationController private lateinit var testableContext: TestableContext @@ -175,6 +173,64 @@ class AppHandleEducationControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_noCaptionStateNotified_shouldHideAllTooltips() = + testScope.runTest { + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, times(1)).hideEducationTooltip() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_appHandleHintViewed_shouldNotListenToNoCaptionNotification() = + testScope.runTest { + testDataStoreFlow.value = + createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, never()).hideEducationTooltip() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_enterDesktopModeHintViewed_shouldNotListenToNoCaptionNotification() = + testScope.runTest { + testDataStoreFlow.value = + createWindowingEducationProto(enterDesktopModeHintViewedTimestampMillis = 123L) + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, never()).hideEducationTooltip() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_exitDesktopModeHintViewed_shouldNotListenToNoCaptionNotification() = + testScope.runTest { + testDataStoreFlow.value = + createWindowingEducationProto(exitDesktopModeHintViewedTimestampMillis = 123L) + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, never()).hideEducationTooltip() + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun init_flagDisabled_shouldNotCallShowEducationTooltip() = testScope.runTest { @@ -289,8 +345,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { // Mark app handle hint viewed. testDataStoreFlow.value = createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) - val systemPropertiesKey = "persist.windowing_force_show_desktop_mode_education" - whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())) + whenever(SystemProperties.getBoolean(eq(FORCE_SHOW_EDUCATION_SYSPROP), anyBoolean())) .thenReturn(true) setShouldShowDesktopModeEducation(true) @@ -396,5 +451,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { private companion object { val APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS: Long = APP_HANDLE_EDUCATION_DELAY_MILLIS + 1000L + + val FORCE_SHOW_EDUCATION_SYSPROP = "persist.windowing_force_show_desktop_mode_education" } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt new file mode 100644 index 000000000000..bfbaa84e9312 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.desktopmode.multidesks + +import android.os.Binder +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.TRANSIT_CLOSE +import android.window.TransitionInfo +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.desktopmode.DesktopRepository +import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.sysui.ShellInit +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** + * Tests for [DesksTransitionObserver]. + * + * Build/Install/Run: atest WMShellUnitTests:DesksTransitionObserverTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesksTransitionObserverTest : ShellTestCase() { + + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + private lateinit var desktopUserRepositories: DesktopUserRepositories + private lateinit var observer: DesksTransitionObserver + + private val repository: DesktopRepository + get() = desktopUserRepositories.current + + @Before + fun setUp() { + desktopUserRepositories = + DesktopUserRepositories( + context, + ShellInit(TestShellExecutor()), + /* shellController= */ mock(), + /* persistentRepository= */ mock(), + /* repositoryInitializer= */ mock(), + /* mainCoroutineScope= */ mock(), + /* userManager= */ mock(), + ) + observer = DesksTransitionObserver(desktopUserRepositories) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_removeDesk_removesFromRepository() { + val transition = Binder() + val removeTransition = + DeskTransition.RemoveDesk( + transition, + displayId = DEFAULT_DISPLAY, + deskId = 5, + tasks = setOf(10, 11), + onDeskRemovedListener = null, + ) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(removeTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0), + ) + + assertThat(repository.getDeskIds(DEFAULT_DISPLAY)).doesNotContain(5) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_removeDesk_invokesOnRemoveListener() { + class FakeOnDeskRemovedListener : OnDeskRemovedListener { + var lastDeskRemoved: Int? = null + + override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) { + lastDeskRemoved = deskId + } + } + val transition = Binder() + val removeListener = FakeOnDeskRemovedListener() + val removeTransition = + DeskTransition.RemoveDesk( + transition, + displayId = DEFAULT_DISPLAY, + deskId = 5, + tasks = setOf(10, 11), + onDeskRemovedListener = removeListener, + ) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(removeTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0), + ) + + assertThat(removeListener.lastDeskRemoved).isEqualTo(5) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt index 9a8f264e98a4..dd9e6ca0deae 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.desktopmode.persistence import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import androidx.test.filters.SmallTest @@ -42,7 +41,6 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.spy @@ -54,8 +52,6 @@ import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi class DesktopRepositoryInitializerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - private lateinit var repositoryInitializer: DesktopRepositoryInitializer private lateinit var shellInit: ShellInit private lateinit var datastoreScope: CoroutineScope diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java index 4174bbd89b76..9509aaf20c9b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java @@ -35,7 +35,6 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -56,7 +55,6 @@ import com.android.wm.shell.windowdecor.WindowDecorViewModel; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -72,9 +70,6 @@ import java.util.Optional; @RunWith(AndroidJUnit4.class) public final class FreeformTaskListenerTests extends ShellTestCase { - @Rule - public final SetFlagsRule setFlagsRule = new SetFlagsRule(); - @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java index 5aed4611cdc8..bc918450a3cf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java @@ -34,7 +34,6 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.IBinder; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import android.window.IWindowContainerToken; import android.window.TransitionInfo; @@ -43,6 +42,7 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; @@ -60,9 +60,8 @@ import java.util.Optional; /** Tests for {@link FreeformTaskTransitionObserver}. */ @SmallTest -public class FreeformTaskTransitionObserverTest { +public class FreeformTaskTransitionObserverTest extends ShellTestCase { - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private ShellInit mShellInit; @Mock private Transitions mTransitions; @Mock private DesktopImmersiveController mDesktopImmersiveController; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 836f4c24a979..26688236d5be 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -37,7 +37,6 @@ import android.content.pm.ActivityInfo; import android.graphics.Rect; import android.os.RemoteException; import android.platform.test.annotations.DisableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.Rational; @@ -70,8 +69,6 @@ import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; -import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; @@ -88,11 +85,6 @@ import java.util.Optional; @TestableLooper.RunWithLooper @DisableFlags(Flags.FLAG_ENABLE_PIP2) public class PipTaskOrganizerTest extends ShellTestCase { - @ClassRule - public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); - @Rule - public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); - private PipTaskOrganizer mPipTaskOrganizer; @Mock private DisplayController mMockDisplayController; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 5ef934ce8394..13fce2a27524 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -43,7 +43,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.platform.test.annotations.DisableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -77,8 +76,6 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; -import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -95,9 +92,6 @@ import java.util.Set; @TestableLooper.RunWithLooper @DisableFlags(Flags.FLAG_ENABLE_PIP2) public class PipControllerTest extends ShellTestCase { - @ClassRule public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); - @Rule public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); - private PipController mPipController; private ShellInit mShellInit; private ShellController mShellController; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java index b11715b669f4..273cb2727508 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -25,7 +25,6 @@ import static org.mockito.Mockito.verify; import android.graphics.Rect; import android.platform.test.annotations.DisableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.Size; @@ -49,7 +48,6 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -68,9 +66,6 @@ import java.util.Optional; @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) public class PipTouchHandlerTest extends ShellTestCase { - @Rule - public SetFlagsRule setFlagsRule = new SetFlagsRule(); - private static final int INSET = 10; private static final int PIP_LENGTH = 100; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index 542289db6cfc..5028479b6ace 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -63,7 +63,6 @@ import android.graphics.Rect; import android.os.Bundle; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -89,7 +88,6 @@ import com.android.wm.shell.sysui.ShellInit; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -129,9 +127,6 @@ public class RecentTasksControllerTest extends ShellTestCase { @Mock private DesktopRepository mDesktopRepository; - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private ShellTaskOrganizer mShellTaskOrganizer; private RecentTasksController mRecentTasksController; private RecentTasksController mRecentTasksControllerReal; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java index ab43119b14c0..b50af741b2a6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -47,7 +47,6 @@ import android.content.pm.PackageManager; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; -import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -77,7 +76,6 @@ import com.android.wm.shell.util.StubTransaction; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -115,9 +113,6 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { @Mock private DesktopRepository mDesktopRepository; - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private ShellTaskOrganizer mShellTaskOrganizer; private RecentTasksController mRecentTasksController; private RecentTasksController mRecentTasksControllerReal; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt index 99194620c313..769407b370c3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt @@ -26,7 +26,6 @@ import android.content.Intent import android.os.IBinder import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.SurfaceControl import android.view.WindowManager @@ -42,6 +41,7 @@ import android.window.WindowContainerToken import androidx.test.filters.SmallTest import com.android.window.flags.Flags import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.TestSyncExecutor import com.android.wm.shell.common.ShellExecutor @@ -53,7 +53,6 @@ import com.android.wm.shell.windowdecor.extension.isFullscreen import com.google.common.truth.Truth.assertThat import dagger.Lazy import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor @@ -71,9 +70,7 @@ import org.mockito.kotlin.whenever */ @SmallTest @RunWith(AndroidTestingRunner::class) -class TaskStackTransitionObserverTest { - - @JvmField @Rule val setFlagsRule = SetFlagsRule() +class TaskStackTransitionObserverTest : ShellTestCase() { @Mock private lateinit var shellInit: ShellInit @Mock private lateinit var shellTaskOrganizerLazy: Lazy<ShellTaskOrganizer> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt index 8c78debdc19f..a8a7be8fe7e3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.shared.desktopmode import android.content.ComponentName +import android.content.pm.PackageManager import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.internal.R @@ -27,6 +28,9 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever /** * Tests for [@link DesktopModeCompatPolicy]. @@ -110,4 +114,32 @@ class DesktopModeCompatPolicyTest : CompatUIShellTestCase() { isTopActivityNoDisplay = true })) } + + @Test + fun testIsTopActivityExemptFromDesktopWindowing_defaultHomePackage() { + val packageManager: PackageManager = mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + mContext.setMockPackageManager(packageManager) + assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( + createFreeformTask(/* displayId */ 0) + .apply { + baseActivity = homeActivities + isTopActivityNoDisplay = false + })) + } + + @Test + fun testIsTopActivityExemptFromDesktopWindowing_defaultHomePackage_notDisplayed() { + val packageManager: PackageManager = mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + mContext.setMockPackageManager(packageManager) + assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( + createFreeformTask(/* displayId */ 0) + .apply { + baseActivity = homeActivities + isTopActivityNoDisplay = true + })) + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt new file mode 100644 index 000000000000..4dac99b14aaf --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt @@ -0,0 +1,235 @@ +/* + * 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.wm.shell.shared.desktopmode + +import android.content.Context +import android.content.res.Resources +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.annotations.Presubmit +import android.platform.test.flag.junit.SetFlagsRule +import android.provider.Settings +import android.provider.Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES +import android.window.DesktopModeFlags +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@Presubmit +@EnableFlags(Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) +class DesktopModeStatusTest : ShellTestCase() { + @get:Rule + val mSetFlagsRule = SetFlagsRule() + + private val mockContext = mock<Context>() + private val mockResources = mock<Resources>() + + @Before + fun setUp() { + doReturn(mockResources).whenever(mockContext).getResources() + doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(false).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + doReturn(context.contentResolver).whenever(mockContext).contentResolver + resetDesktopModeFlagsCache() + resetEnforceDeviceRestriction() + resetFlagOverride() + } + + @After + fun tearDown() { + resetDesktopModeFlagsCache() + resetEnforceDeviceRestriction() + resetFlagOverride() + } + + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION + ) + @Test + fun canEnterDesktopMode_DWFlagDisabled_configsOff_returnsFalse() { + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse() + } + + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION + ) + @Test + fun canEnterDesktopMode_DWFlagDisabled_configsOn_disableDeviceRestrictions_returnsFalse() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + disableEnforceDeviceRestriction() + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse() + } + + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION + ) + @Test + fun canEnterDesktopMode_DWFlagDisabled_configDevOptionOn_returnsFalse() { + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse() + } + + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION + ) + @Test + fun canEnterDesktopMode_DWFlagDisabled_configDevOptionOn_flagOverrideOn_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + setFlagOverride(DesktopModeFlags.ToggleOverride.OVERRIDE_ON) + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue() + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + fun canEnterDesktopMode_DWFlagEnabled_configsOff_returnsFalse() { + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse() + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + fun canEnterDesktopMode_DWFlagEnabled_configDesktopModeOff_returnsFalse() { + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse() + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + fun canEnterDesktopMode_DWFlagEnabled_configDesktopModeOn_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue() + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + fun canEnterDesktopMode_DWFlagEnabled_configsOff_disableDeviceRestrictions_returnsTrue() { + disableEnforceDeviceRestriction() + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue() + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + fun canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + setFlagOverride(DesktopModeFlags.ToggleOverride.OVERRIDE_ON) + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue() + } + + @Test + fun isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @Test + fun isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() { + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @Test + fun isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() { + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @Test + fun isDeviceEligibleForDesktopMode_supportFlagOn_configDevOptModeOn_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + } + + private fun resetEnforceDeviceRestriction() { + setEnforceDeviceRestriction(true) + } + + private fun disableEnforceDeviceRestriction() { + setEnforceDeviceRestriction(false) + } + + private fun setEnforceDeviceRestriction(value: Boolean) { + val field = DesktopModeStatus::class.java.getDeclaredField("ENFORCE_DEVICE_RESTRICTIONS") + field.isAccessible = true + field.setBoolean(null, value) + } + + private fun resetDesktopModeFlagsCache() { + val cachedToggleOverride = + DesktopModeFlags::class.java.getDeclaredField("sCachedToggleOverride") + cachedToggleOverride.isAccessible = true + cachedToggleOverride.set(null, null) + } + + private fun resetFlagOverride() { + Settings.Global.putString( + context.contentResolver, + DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, null + ) + } + + private fun setFlagOverride(override: DesktopModeFlags.ToggleOverride) { + Settings.Global.putInt( + context.contentResolver, + DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.setting + ) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java index 6ac34d736f6f..2d454a55a51c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java @@ -51,7 +51,6 @@ import android.graphics.Rect; import android.graphics.Region; import android.os.Looper; import android.platform.test.flag.junit.FlagsParameterization; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.TestableLooper; import android.view.SurfaceControl; import android.view.SurfaceHolder; @@ -72,7 +71,6 @@ import com.android.wm.shell.transition.Transitions; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -96,9 +94,6 @@ public class TaskViewTest extends ShellTestCase { return FlagsParameterization.allCombinationsOf(Flags.FLAG_TASK_VIEW_REPOSITORY); } - @Rule - public final SetFlagsRule mSetFlagsRule; - @Mock TaskView.Listener mViewListener; @Mock @@ -127,9 +122,7 @@ public class TaskViewTest extends ShellTestCase { TaskViewTransitions mTaskViewTransitions; TaskViewTaskController mTaskViewTaskController; - public TaskViewTest(FlagsParameterization flags) { - mSetFlagsRule = new SetFlagsRule(flags); - } + public TaskViewTest(FlagsParameterization flags) {} @Before public void setUp() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java index 326f11e300fd..3a455ba6b5df 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java @@ -34,7 +34,6 @@ import android.app.ActivityManager; import android.graphics.Rect; import android.os.IBinder; import android.platform.test.flag.junit.FlagsParameterization; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.TestableLooper; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -50,7 +49,6 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.transition.Transitions; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -74,9 +72,6 @@ public class TaskViewTransitionsTest extends ShellTestCase { Flags.FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP); } - @Rule - public final SetFlagsRule mSetFlagsRule; - @Mock Transitions mTransitions; @Mock @@ -95,9 +90,7 @@ public class TaskViewTransitionsTest extends ShellTestCase { TaskViewRepository mTaskViewRepository; TaskViewTransitions mTaskViewTransitions; - public TaskViewTransitionsTest(FlagsParameterization flags) { - mSetFlagsRule = new SetFlagsRule(flags); - } + public TaskViewTransitionsTest(FlagsParameterization flags) {} @Before public void setUp() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java index e540322a96a1..82392e0e3bc0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java @@ -292,6 +292,7 @@ public class DefaultTransitionHandlerTest extends ShellTestCase { new Binder(), new TransitionInfoBuilder(TRANSIT_SLEEP, FLAG_SYNC).build(), MockTransactionPool.create(), + MockTransactionPool.create(), token, mock(Transitions.TransitionFinishCallback.class)); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java index 74c2f0e6753a..96e4f4955dc8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java @@ -35,8 +35,6 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager.RunningTaskInfo; import android.os.RemoteException; import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.TransitionInfo.TransitionMode; @@ -50,7 +48,6 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.shared.FocusTransitionListener; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -67,8 +64,6 @@ public class FocusTransitionObserverTest extends ShellTestCase { static final int SECONDARY_DISPLAY_ID = 1; - @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private FocusTransitionListener mListener; private final TestShellExecutor mShellExecutor = new TestShellExecutor(); private FocusTransitionObserver mFocusTransitionObserver; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index 3e53ee5cfb9f..6f28e656d060 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -39,8 +39,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.TransitionInfo.TransitionMode; @@ -60,7 +58,6 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -73,9 +70,6 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) public class HomeTransitionObserverTest extends ShellTestCase { - - @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private final ShellTaskOrganizer mOrganizer = mock(ShellTaskOrganizer.class); private final TransactionPool mTransactionPool = mock(TransactionPool.class); private final Context mContext = diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 0a19be4eb959..6f73db0bacc3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -74,7 +74,8 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; -import android.platform.test.flag.junit.SetFlagsRule; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.util.ArraySet; import android.util.Pair; import android.view.Surface; @@ -117,7 +118,6 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.util.StubTransaction; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; @@ -145,9 +145,6 @@ public class ShellTransitionTests extends ShellTestCase { private final TestTransitionHandler mDefaultHandler = new TestTransitionHandler(); private final Handler mMainHandler = new Handler(Looper.getMainLooper()); - @Rule - public final SetFlagsRule setFlagsRule = new SetFlagsRule(); - @Before public void setUp() { doAnswer(invocation -> new Binder()) @@ -553,7 +550,8 @@ public class ShellTransitionTests extends ShellTestCase { } @Test - public void testRegisteredRemoteTransitionTakeover() { + @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) + public void testRegisteredRemoteTransitionTakeover_flagDisabled() { Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); @@ -608,7 +606,6 @@ public class ShellTransitionTests extends ShellTestCase { mMainExecutor.flushAll(); // Takeover shouldn't happen when the flag is disabled. - setFlagsRule.disableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED); IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); @@ -621,12 +618,69 @@ public class ShellTransitionTests extends ShellTestCase { mDefaultHandler.finishAll(); mMainExecutor.flushAll(); verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any()); + } + + @Test + @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) + public void testRegisteredRemoteTransitionTakeover_flagEnabled() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IRemoteTransition testRemote = new RemoteTransitionStub() { + @Override + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + final Transitions.TransitionHandler takeoverHandler = + transitions.getHandlerForTakeover(token, info); + + if (takeoverHandler == null) { + finishCallback.onTransitionFinished(null /* wct */, null /* sct */); + return; + } + + takeoverHandler.takeOverAnimation(token, info, new SurfaceControl.Transaction(), + wct -> { + try { + finishCallback.onTransitionFinished(wct, null /* sct */); + } catch (RemoteException e) { + // Fail + } + }, new WindowAnimationState[info.getChanges().size()]); + } + }; + final boolean[] takeoverRemoteCalled = new boolean[]{false}; + IRemoteTransition testTakeoverRemote = new RemoteTransitionStub() { + @Override + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback) {} + + @Override + public void takeOverAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction startTransaction, + IRemoteTransitionFinishedCallback finishCallback, WindowAnimationState[] states) + throws RemoteException { + takeoverRemoteCalled[0] = true; + finishCallback.onTransitionFinished(null /* wct */, null /* sct */); + } + }; + + TransitionFilter filter = new TransitionFilter(); + filter.mRequirements = + new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + + transitions.registerRemote(filter, new RemoteTransition(testRemote, "Test")); + transitions.registerRemoteForTakeover( + filter, new RemoteTransition(testTakeoverRemote, "Test")); + mMainExecutor.flushAll(); // Takeover should happen when the flag is enabled. - setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED); + IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); - info = new TransitionInfoBuilder(TRANSIT_OPEN) + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); transitions.onTransitionReady(transitToken, info, new StubTransaction(), new StubTransaction()); @@ -634,7 +688,7 @@ public class ShellTransitionTests extends ShellTestCase { assertTrue(takeoverRemoteCalled[0]); mDefaultHandler.finishAll(); mMainExecutor.flushAll(); - verify(mOrganizer, times(2)).finishTransition(eq(transitToken), any()); + verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any()); } @Test @@ -1705,7 +1759,9 @@ public class ShellTransitionTests extends ShellTestCase { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (mFinishOnSync && info.getType() == TRANSIT_SLEEP) { for (int i = 0; i < mFinishes.size(); ++i) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java index 71af97e5add3..aad18cba4436 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java @@ -43,6 +43,7 @@ import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestSyncExecutor; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.TransactionPool; @@ -61,7 +62,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; -public class UnfoldTransitionHandlerTest { +public class UnfoldTransitionHandlerTest extends ShellTestCase { private UnfoldTransitionHandler mUnfoldTransitionHandler; @@ -169,7 +170,8 @@ public class UnfoldTransitionHandlerTest { // Send fold transition request TransitionFinishCallback mergeFinishCallback = mock(TransitionFinishCallback.class); mUnfoldTransitionHandler.mergeAnimation(new Binder(), createFoldTransitionInfo(), - mock(SurfaceControl.Transaction.class), mTransition, mergeFinishCallback); + mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), + mTransition, mergeFinishCallback); mTestLooper.dispatchAll(); // Verify that fold transition is merged into unfold and that unfold is finished @@ -387,6 +389,7 @@ public class UnfoldTransitionHandlerTest { new Binder(), new TransitionInfoBuilder(TRANSIT_CHANGE, TRANSIT_FLAG_KEYGUARD_GOING_AWAY).build(), mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mTransition, mergeCallback); verify(finishCallback, never()).onTransitionFinished(any()); @@ -396,6 +399,7 @@ public class UnfoldTransitionHandlerTest { new Binder(), new TransitionInfoBuilder(TRANSIT_CHANGE).build(), mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mTransition, mergeCallback); verify(mergeCallback).onTransitionFinished(any()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt index cf6c3a5e03a0..257bbb5603a7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt @@ -19,7 +19,6 @@ import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.SurfaceControl @@ -35,7 +34,6 @@ import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystem import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -51,10 +49,6 @@ import org.mockito.kotlin.mock @RunWith(AndroidTestingRunner::class) class DesktopHeaderManageWindowsMenuTest : ShellTestCase() { - @JvmField - @Rule - val setFlagsRule: SetFlagsRule = SetFlagsRule() - private lateinit var userRepositories: DesktopUserRepositories private lateinit var menu: DesktopHeaderManageWindowsMenu diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index baccbee0893d..737780ed8024 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -28,6 +28,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.Intent.ACTION_MAIN +import android.content.pm.PackageManager import android.graphics.Rect import android.graphics.Region import android.hardware.display.DisplayManager @@ -96,7 +97,6 @@ import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.util.function.Consumer - /** * Tests of [DesktopModeWindowDecorViewModel] * Usage: atest WMShellUnitTests:DesktopModeWindowDecorViewModelTests @@ -307,6 +307,23 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun testDecorationIsNotCreatedForDefaultHomePackage() { + val packageManager: PackageManager = org.mockito.kotlin.mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN).apply { + baseActivity = homeActivities + isTopActivityNoDisplay = false + } + mContext.setMockPackageManager(packageManager) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + + onTaskOpening(task) + + assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) fun testInsetsStateChanged_notifiesAllDecorsInDisplay() { val task1 = createTask(windowingMode = WINDOWING_MODE_FREEFORM, displayId = 1) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index c5c827467c75..7468c54321a0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -25,9 +25,6 @@ import android.graphics.Rect import android.hardware.input.InputManager import android.os.Handler import android.os.UserHandle -import android.platform.test.flag.junit.CheckFlagsRule -import android.platform.test.flag.junit.DeviceFlagsValueProvider -import android.platform.test.flag.junit.SetFlagsRule import android.testing.TestableContext import android.util.SparseArray import android.view.Choreographer @@ -84,7 +81,6 @@ import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder import org.junit.After -import org.junit.Rule import org.mockito.Mockito import org.mockito.Mockito.anyInt import org.mockito.kotlin.any @@ -105,14 +101,6 @@ import kotlinx.coroutines.MainCoroutineDispatcher */ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { - @JvmField - @Rule - val setFlagsRule = SetFlagsRule() - - @JvmField - @Rule - val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() - private val mockDesktopModeWindowDecorFactory = mock<DesktopModeWindowDecoration.Factory>() protected val mockMainHandler = mock<Handler>() protected val mockMainChoreographer = mock<Choreographer>() @@ -162,7 +150,6 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { val display = mock<Display>() protected lateinit var spyContext: TestableContext private lateinit var desktopModeEventLogger: DesktopModeEventLogger - private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy private val transactionFactory = Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() @@ -177,6 +164,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { DisplayChangeController.OnDisplayChangingListener internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener protected lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel + protected lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy fun setUpCommon() { spyContext = spy(mContext) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 87198d14c839..18a780bbb07c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -20,7 +20,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; import static android.view.WindowInsets.Type.captionBar; @@ -69,7 +68,6 @@ import android.os.Handler; import android.os.SystemProperties; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; @@ -129,7 +127,6 @@ import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Ignore; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -171,8 +168,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true; private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false; - @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); - @Mock private DisplayController mMockDisplayController; @Mock @@ -297,7 +292,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { .thenReturn(mMockHandleMenu); when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any(), anyInt())) .thenReturn(false); - when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any())) + when(mMockAppHeaderViewHolderFactory + .create(any(), any(), any(), any(), any(), any(), any(), any(), any())) .thenReturn(mMockAppHeaderViewHolder); when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mDesktopRepository); when(mMockDesktopUserRepositories.getProfile(anyInt())).thenReturn(mDesktopRepository); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt index a20a89c644ed..ab9dbc98e15f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt @@ -23,7 +23,6 @@ import android.graphics.Rect import android.os.IBinder import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display import android.window.WindowContainerToken @@ -44,7 +43,6 @@ import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -85,10 +83,6 @@ class DragPositioningCallbackUtilityTest { @Mock private lateinit var mockResources: Resources - @JvmField - @Rule - val setFlagsRule = SetFlagsRule() - private lateinit var mockitoSession: StaticMockitoSession @Before diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java index 479f1567ed31..3389ec11f9d0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java @@ -30,7 +30,6 @@ import android.graphics.Point; import android.graphics.Region; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.Size; @@ -41,7 +40,6 @@ import com.android.wm.shell.ShellTestCase; import com.google.common.testing.EqualsTester; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -87,9 +85,6 @@ public class DragResizeWindowGeometryTests extends ShellTestCase { private static final Point BOTTOM_INSET_POINT = new Point(TASK_SIZE.getWidth() / 2, TASK_SIZE.getHeight() - EDGE_RESIZE_HANDLE_INSET / 2); - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - /** * Check that both groups of objects satisfy equals/hashcode within each group, and that each * group is distinct from the next. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index f90988e90b22..f984f6db13fc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -26,7 +26,6 @@ import android.graphics.Point import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.Display @@ -60,7 +59,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt @@ -81,10 +79,6 @@ import org.mockito.kotlin.whenever @TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner::class) class HandleMenuTest : ShellTestCase() { - @JvmField - @Rule - val setFlagsRule: SetFlagsRule = SetFlagsRule() - @Mock private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration @Mock diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index d9693460008f..3a8dcd674b74 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -18,7 +18,6 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; import static android.view.WindowInsets.Type.captionBar; @@ -62,7 +61,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.os.Handler; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; import android.view.AttachedSurfaceControl; @@ -93,7 +91,6 @@ import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -120,9 +117,6 @@ public class WindowDecorationTests extends ShellTestCase { private static final int SHADOW_RADIUS = 10; private static final int STATUS_BAR_INSET_SOURCE_ID = 0; - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); - private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult = new WindowDecoration.RelayoutResult<>(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt index 431de896f433..c61e0eb3b5af 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt @@ -86,6 +86,7 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() { spyContext.setMockPackageManager(mockPackageManager) doReturn(spyContext).whenever(spyContext).createContextAsUser(any(), anyInt()) doReturn(spyContext).whenever(mMockUserProfileContexts)[anyInt()] + doReturn(spyContext).whenever(mMockUserProfileContexts).getOrCreate(anyInt()) loader = WindowDecorTaskResourceLoader( shellInit = shellInit, diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index 139ccfd22b0e..7280b12aaca9 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -24,8 +24,8 @@ package com.android.extensions.appfunctions { public final class AppFunctionManager { ctor public AppFunctionManager(android.content.Context); - method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); - method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method @RequiresPermission(android.Manifest.permission.EXECUTE_APP_FUNCTIONS) public void executeAppFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); + method @RequiresPermission(android.Manifest.permission.EXECUTE_APP_FUNCTIONS) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void isAppFunctionEnabled(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0 diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java index 9eb66a33fedc..1e31390854b8 100644 --- a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java @@ -104,12 +104,7 @@ public final class AppFunctionManager { * <p>See {@link android.app.appfunctions.AppFunctionManager#executeAppFunction} for the * documented behaviour of this method. */ - @RequiresPermission( - anyOf = { - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - Manifest.permission.EXECUTE_APP_FUNCTIONS - }, - conditional = true) + @RequiresPermission(Manifest.permission.EXECUTE_APP_FUNCTIONS) public void executeAppFunction( @NonNull ExecuteAppFunctionRequest sidecarRequest, @NonNull @CallbackExecutor Executor executor, @@ -150,12 +145,7 @@ public final class AppFunctionManager { * <p>See {@link android.app.appfunctions.AppFunctionManager#isAppFunctionEnabled} for the * documented behaviour of this method. */ - @RequiresPermission( - anyOf = { - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - Manifest.permission.EXECUTE_APP_FUNCTIONS - }, - conditional = true) + @RequiresPermission(Manifest.permission.EXECUTE_APP_FUNCTIONS) public void isAppFunctionEnabled( @NonNull String functionIdentifier, @NonNull String targetPackage, diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java index 8bc66a048d27..f308ce953680 100644 --- a/media/java/android/media/AudioFormat.java +++ b/media/java/android/media/AudioFormat.java @@ -18,6 +18,7 @@ package android.media; import static android.media.audio.Flags.FLAG_DOLBY_AC4_LEVEL4_ENCODING_API; import static android.media.audio.Flags.FLAG_IAMF_DEFINITIONS_API; +import static android.media.audio.Flags.FLAG_SONY_360RA_MPEGH_3D_FORMAT; import android.annotation.FlaggedApi; import android.annotation.IntDef; @@ -718,8 +719,9 @@ public final class AudioFormat implements Parcelable { * Same as 9.1.4 with the addition of left and right top side channels */ public static final int CHANNEL_OUT_9POINT1POINT6 = (CHANNEL_OUT_9POINT1POINT4 | CHANNEL_OUT_TOP_SIDE_LEFT | CHANNEL_OUT_TOP_SIDE_RIGHT); - /** @hide */ - public static final int CHANNEL_OUT_13POINT_360RA = ( + /** Output channel mask for 13.0 */ + @FlaggedApi(FLAG_SONY_360RA_MPEGH_3D_FORMAT) + public static final int CHANNEL_OUT_13POINT0 = ( CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_FRONT_RIGHT | CHANNEL_OUT_SIDE_LEFT | CHANNEL_OUT_SIDE_RIGHT | CHANNEL_OUT_TOP_FRONT_LEFT | CHANNEL_OUT_TOP_FRONT_CENTER | @@ -915,7 +917,7 @@ public final class AudioFormat implements Parcelable { case CHANNEL_OUT_9POINT1POINT6: result.append("9.1.6"); break; - case CHANNEL_OUT_13POINT_360RA: + case CHANNEL_OUT_13POINT0: result.append("360RA 13ch"); break; case CHANNEL_OUT_22POINT2: diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index c4886836f451..fb1b5b57cce6 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -48,6 +48,7 @@ import android.os.IHwBinder; import android.os.Looper; import android.os.Message; import android.os.PersistableBundle; +import android.os.Trace; import android.view.Surface; import java.io.IOException; @@ -3107,6 +3108,7 @@ final public class MediaCodec { int index, int offset, int size, long presentationTimeUs, int flags) throws CryptoException { + Trace.traceBegin(Trace.TRACE_TAG_VIDEO, "MediaCodec::queueInputBuffer#java"); if ((flags & BUFFER_FLAG_DECODE_ONLY) != 0 && (flags & BUFFER_FLAG_END_OF_STREAM) != 0) { throw new InvalidBufferFlagsException(EOS_AND_DECODE_ONLY_ERROR_MESSAGE); @@ -3126,6 +3128,8 @@ final public class MediaCodec { } catch (CryptoException | IllegalStateException e) { revalidateByteBuffer(mCachedInputBuffers, index, true /* input */); throw e; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIDEO); } } @@ -3167,6 +3171,7 @@ final public class MediaCodec { public final void queueInputBuffers( int index, @NonNull ArrayDeque<BufferInfo> bufferInfos) { + Trace.traceBegin(Trace.TRACE_TAG_VIDEO, "MediaCodec::queueInputBuffers#java"); synchronized(mBufferLock) { if (mBufferMode == BUFFER_MODE_BLOCK) { throw new IncompatibleWithBlockModelException("queueInputBuffers() " @@ -3182,6 +3187,8 @@ final public class MediaCodec { } catch (CryptoException | IllegalStateException | IllegalArgumentException e) { revalidateByteBuffer(mCachedInputBuffers, index, true /* input */); throw e; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIDEO); } } @@ -3442,6 +3449,7 @@ final public class MediaCodec { @NonNull CryptoInfo info, long presentationTimeUs, int flags) throws CryptoException { + Trace.traceBegin(Trace.TRACE_TAG_VIDEO, "MediaCodec::queueSecureInputBuffer#java"); if ((flags & BUFFER_FLAG_DECODE_ONLY) != 0 && (flags & BUFFER_FLAG_END_OF_STREAM) != 0) { throw new InvalidBufferFlagsException(EOS_AND_DECODE_ONLY_ERROR_MESSAGE); @@ -3461,6 +3469,8 @@ final public class MediaCodec { } catch (CryptoException | IllegalStateException e) { revalidateByteBuffer(mCachedInputBuffers, index, true /* input */); throw e; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIDEO); } } @@ -3491,6 +3501,7 @@ final public class MediaCodec { int index, @NonNull ArrayDeque<BufferInfo> bufferInfos, @NonNull ArrayDeque<CryptoInfo> cryptoInfos) { + Trace.traceBegin(Trace.TRACE_TAG_VIDEO, "MediaCodec::queueSecureInputBuffers#java"); synchronized(mBufferLock) { if (mBufferMode == BUFFER_MODE_BLOCK) { throw new IncompatibleWithBlockModelException("queueSecureInputBuffers() " @@ -3506,6 +3517,8 @@ final public class MediaCodec { } catch (CryptoException | IllegalStateException | IllegalArgumentException e) { revalidateByteBuffer(mCachedInputBuffers, index, true /* input */); throw e; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIDEO); } } @@ -3533,6 +3546,7 @@ final public class MediaCodec { * @throws MediaCodec.CodecException upon codec error. */ public final int dequeueInputBuffer(long timeoutUs) { + Trace.traceBegin(Trace.TRACE_TAG_VIDEO, "MediaCodec::dequeueInputBuffer#java"); synchronized (mBufferLock) { if (mBufferMode == BUFFER_MODE_BLOCK) { throw new IncompatibleWithBlockModelException("dequeueInputBuffer() " @@ -3546,6 +3560,7 @@ final public class MediaCodec { validateInputByteBufferLocked(mCachedInputBuffers, res); } } + Trace.traceEnd(Trace.TRACE_TAG_VIDEO); return res; } @@ -3873,7 +3888,9 @@ final public class MediaCodec { /** * Set a hardware graphic buffer to this queue request. Exactly one buffer must - * be set for a queue request before calling {@link #queue}. + * be set for a queue request before calling {@link #queue}. Ownership of the + * hardware buffer is not transferred to this queue request, nor will it be transferred + * to the codec once {@link #queue} is called. * <p> * Note: buffers should have format {@link HardwareBuffer#YCBCR_420_888}, * a single layer, and an appropriate usage ({@link HardwareBuffer#USAGE_CPU_READ_OFTEN} diff --git a/media/java/android/media/OWNERS b/media/java/android/media/OWNERS index 8cc42e0bd9b5..a600017119e2 100644 --- a/media/java/android/media/OWNERS +++ b/media/java/android/media/OWNERS @@ -1,6 +1,7 @@ # Bug component: 1344 -fgoldfain@google.com +pshehane@google.com elaurent@google.com +etalvala@google.com lajos@google.com jmtrivi@google.com @@ -15,4 +16,5 @@ per-file ExifInterface.java,ExifInterfaceUtils.java,IMediaHTTPConnection.aidl,IM # Haptics team also works on Ringtone per-file *Ringtone* = file:/services/core/java/com/android/server/vibrator/OWNERS +per-file flags/media_better_together.aconfig = file:platform/frameworks/av:/media/janitors/better_together_OWNERS per-file flags/projection.aconfig = file:projection/OWNERS diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java index 3b8cf3fb2909..d27d7fc72a38 100644 --- a/media/java/android/media/RoutingSessionInfo.java +++ b/media/java/android/media/RoutingSessionInfo.java @@ -85,8 +85,7 @@ public final class RoutingSessionInfo implements Parcelable { @Retention(RetentionPolicy.SOURCE) public @interface TransferReason {} - @NonNull - final String mId; + @NonNull final String mOriginalId; @Nullable final CharSequence mName; @Nullable @@ -120,7 +119,7 @@ public final class RoutingSessionInfo implements Parcelable { RoutingSessionInfo(@NonNull Builder builder) { Objects.requireNonNull(builder, "builder must not be null."); - mId = builder.mId; + mOriginalId = builder.mOriginalId; mName = builder.mName; mOwnerPackageName = builder.mOwnerPackageName; mClientPackageName = builder.mClientPackageName; @@ -148,8 +147,8 @@ public final class RoutingSessionInfo implements Parcelable { } RoutingSessionInfo(@NonNull Parcel src) { - mId = src.readString(); - Preconditions.checkArgument(!TextUtils.isEmpty(mId)); + mOriginalId = src.readString(); + Preconditions.checkArgument(!TextUtils.isEmpty(mOriginalId)); mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src); mOwnerPackageName = src.readString(); @@ -221,9 +220,9 @@ public final class RoutingSessionInfo implements Parcelable { @NonNull public String getId() { if (!TextUtils.isEmpty(mProviderId)) { - return MediaRouter2Utils.toUniqueId(mProviderId, mId); + return MediaRouter2Utils.toUniqueId(mProviderId, mOriginalId); } else { - return mId; + return mOriginalId; } } @@ -236,12 +235,16 @@ public final class RoutingSessionInfo implements Parcelable { } /** - * Gets the original id set by {@link Builder#Builder(String, String)}. + * Gets the original id as assigned by the {@link MediaRoute2ProviderService route provider}. + * + * <p>This may be different from {@link #getId()}, which may convert this original id into a + * unique one by adding information about the provider that created this session info. + * * @hide */ @NonNull public String getOriginalId() { - return mId; + return mOriginalId; } /** @@ -423,7 +426,7 @@ public final class RoutingSessionInfo implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeString(mId); + dest.writeString(mOriginalId); dest.writeCharSequence(mName); dest.writeString(mOwnerPackageName); dest.writeString(mClientPackageName); @@ -454,7 +457,7 @@ public final class RoutingSessionInfo implements Parcelable { String indent = prefix + " "; - pw.println(indent + "mId=" + mId); + pw.println(indent + "mOriginalId=" + mOriginalId); pw.println(indent + "mName=" + mName); pw.println(indent + "mOwnerPackageName=" + mOwnerPackageName); pw.println(indent + "mClientPackageName=" + mClientPackageName); @@ -485,7 +488,7 @@ public final class RoutingSessionInfo implements Parcelable { } RoutingSessionInfo other = (RoutingSessionInfo) obj; - return Objects.equals(mId, other.mId) + return Objects.equals(mOriginalId, other.mOriginalId) && Objects.equals(mName, other.mName) && Objects.equals(mOwnerPackageName, other.mOwnerPackageName) && Objects.equals(mClientPackageName, other.mClientPackageName) @@ -500,13 +503,13 @@ public final class RoutingSessionInfo implements Parcelable { && (mTransferReason == other.mTransferReason) && Objects.equals(mTransferInitiatorUserHandle, other.mTransferInitiatorUserHandle) && Objects.equals( - mTransferInitiatorPackageName, other.mTransferInitiatorPackageName); + mTransferInitiatorPackageName, other.mTransferInitiatorPackageName); } @Override public int hashCode() { return Objects.hash( - mId, + mOriginalId, mName, mOwnerPackageName, mClientPackageName, @@ -585,8 +588,7 @@ public final class RoutingSessionInfo implements Parcelable { * Builder class for {@link RoutingSessionInfo}. */ public static final class Builder { - @NonNull - private final String mId; + @NonNull private final String mOriginalId; @Nullable private CharSequence mName; @Nullable @@ -616,23 +618,22 @@ public final class RoutingSessionInfo implements Parcelable { /** * Constructor for builder to create {@link RoutingSessionInfo}. - * <p> - * In order to ensure ID uniqueness in {@link MediaRouter2} side, the value of - * {@link RoutingSessionInfo#getId()} can be different from what was set in - * {@link MediaRoute2ProviderService}. - * </p> * - * @param id ID of the session. Must not be empty. - * @param clientPackageName package name of the client app which uses this session. - * If is is unknown, then just use an empty string. + * <p>In order to ensure ID uniqueness in {@link MediaRouter2} side, the value of {@link + * RoutingSessionInfo#getId()} can be different from what was set in {@link + * MediaRoute2ProviderService}. + * + * @param originalId ID of the session. Must not be empty. + * @param clientPackageName package name of the client app which uses this session. If is is + * unknown, then just use an empty string. * @see MediaRoute2Info#getId() */ - public Builder(@NonNull String id, @NonNull String clientPackageName) { - if (TextUtils.isEmpty(id)) { + public Builder(@NonNull String originalId, @NonNull String clientPackageName) { + if (TextUtils.isEmpty(originalId)) { throw new IllegalArgumentException("id must not be empty"); } - mId = id; + mOriginalId = originalId; mClientPackageName = Objects.requireNonNull(clientPackageName, "clientPackageName must not be null"); mSelectedRoutes = new ArrayList<>(); @@ -648,9 +649,19 @@ public final class RoutingSessionInfo implements Parcelable { * @param sessionInfo the existing instance to copy data from. */ public Builder(@NonNull RoutingSessionInfo sessionInfo) { + this(sessionInfo, sessionInfo.getOriginalId()); + } + + /** + * Builds upon the given {@code sessionInfo}, using the given {@link #getOriginalId()} for + * the id. + * + * @hide + */ + public Builder(@NonNull RoutingSessionInfo sessionInfo, String originalId) { Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); - mId = sessionInfo.mId; + mOriginalId = originalId; mName = sessionInfo.mName; mClientPackageName = sessionInfo.mClientPackageName; mProviderId = sessionInfo.mProviderId; diff --git a/media/java/android/media/audio/common/AidlConversion.java b/media/java/android/media/audio/common/AidlConversion.java index 8521d1c472a8..b831e4f83e02 100644 --- a/media/java/android/media/audio/common/AidlConversion.java +++ b/media/java/android/media/audio/common/AidlConversion.java @@ -366,8 +366,8 @@ public class AidlConversion { return AudioFormat.CHANNEL_OUT_9POINT1POINT4; case AudioChannelLayout.LAYOUT_9POINT1POINT6: return AudioFormat.CHANNEL_OUT_9POINT1POINT6; - case AudioChannelLayout.LAYOUT_13POINT_360RA: - return AudioFormat.CHANNEL_OUT_13POINT_360RA; + case AudioChannelLayout.LAYOUT_13POINT0: + return AudioFormat.CHANNEL_OUT_13POINT0; case AudioChannelLayout.LAYOUT_22POINT2: return AudioFormat.CHANNEL_OUT_22POINT2; case AudioChannelLayout.LAYOUT_MONO_HAPTIC_A: diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 94454cf9ab9b..405d292dfafa 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -125,6 +125,13 @@ flag { } flag { + name: "enable_output_switcher_session_grouping" + namespace: "media_better_together" + description: "Enables selected items in Output Switcher to be grouped together." + bug: "388347018" +} + +flag { name: "enable_prevention_of_keep_alive_route_providers" namespace: "media_solutions" description: "Enables mechanisms to prevent route providers from keeping malicious apps alive." diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java index e558209420e0..e4de3e4420fe 100644 --- a/media/java/android/media/quality/MediaQualityContract.java +++ b/media/java/android/media/quality/MediaQualityContract.java @@ -341,6 +341,13 @@ public class MediaQualityContract { public static final String PARAMETER_FILM_MODE = "film_mode"; /** + * Enable/disable black color auto stretch + * + * @hide + */ + public static final String PARAMETER_BLACK_STRETCH = "black_stretch"; + + /** * Enable/disable blue color auto stretch * * <p>Type: BOOLEAN @@ -457,6 +464,27 @@ public class MediaQualityContract { * @hide * */ + public static final String PARAMETER_COLOR_TEMPERATURE_RED_GAIN = + "color_temperature_red_gain"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TEMPERATURE_GREEN_GAIN = + "color_temperature_green_gain"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TEMPERATURE_BLUE_GAIN = + "color_temperature_blue_gain"; + + /** + * @hide + * + */ public static final String PARAMETER_COLOR_TEMPERATURE_RED_OFFSET = "color_temperature_red_offset"; diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 8419ce761a4a..3bc238a812d9 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -16,7 +16,9 @@ //#define LOG_NDEBUG 0 #define LOG_TAG "MediaCodec-JNI" +#define ATRACE_TAG ATRACE_TAG_VIDEO #include <utils/Log.h> +#include <utils/Trace.h> #include <type_traits> @@ -2106,7 +2108,7 @@ static void android_media_MediaCodec_queueInputBuffer( jlong timestampUs, jint flags) { ALOGV("android_media_MediaCodec_queueInputBuffer"); - + ScopedTrace trace(ATRACE_TAG, "MediaCodec::queueInputBuffer#jni"); sp<JMediaCodec> codec = getMediaCodec(env, thiz); if (codec == NULL || codec->initCheck() != OK) { @@ -2192,6 +2194,7 @@ static void android_media_MediaCodec_queueInputBuffers( jint index, jobjectArray objArray) { ALOGV("android_media_MediaCodec_queueInputBuffers"); + ScopedTrace trace(ATRACE_TAG, "MediaCodec::queueInputBuffers#jni"); sp<JMediaCodec> codec = getMediaCodec(env, thiz); if (codec == NULL || codec->initCheck() != OK || objArray == NULL) { throwExceptionAsNecessary(env, INVALID_OPERATION, codec); @@ -2431,6 +2434,7 @@ static void android_media_MediaCodec_queueSecureInputBuffer( jobject cryptoInfoObj, jlong timestampUs, jint flags) { + ScopedTrace trace(ATRACE_TAG, "MediaCodec::queueSecureInputBuffer#jni"); ALOGV("android_media_MediaCodec_queueSecureInputBuffer"); sp<JMediaCodec> codec = getMediaCodec(env, thiz); @@ -2641,6 +2645,7 @@ static void android_media_MediaCodec_queueSecureInputBuffers( jint index, jobjectArray bufferInfosObjs, jobjectArray cryptoInfoObjs) { + ScopedTrace trace(ATRACE_TAG, "MediaCodec::queueSecureInputBuffers#jni"); ALOGV("android_media_MediaCodec_queueSecureInputBuffers"); sp<JMediaCodec> codec = getMediaCodec(env, thiz); @@ -2685,6 +2690,7 @@ static void android_media_MediaCodec_queueSecureInputBuffers( } static jobject android_media_MediaCodec_mapHardwareBuffer(JNIEnv *env, jclass, jobject bufferObj) { + ScopedTrace trace(ATRACE_TAG, "MediaCodec::mapHardwareBuffer#jni"); ALOGV("android_media_MediaCodec_mapHardwareBuffer"); AHardwareBuffer *hardwareBuffer = android_hardware_HardwareBuffer_getNativeHardwareBuffer( env, bufferObj); @@ -3028,6 +3034,7 @@ static void extractBufferFromContext( static void android_media_MediaCodec_native_queueLinearBlock( JNIEnv *env, jobject thiz, jint index, jobject bufferObj, jobjectArray cryptoInfoArray, jobjectArray objArray, jobject keys, jobject values) { + ScopedTrace trace(ATRACE_TAG, "MediaCodec::queueLinearBlock#jni"); ALOGV("android_media_MediaCodec_native_queueLinearBlock"); sp<JMediaCodec> codec = getMediaCodec(env, thiz); @@ -3145,6 +3152,7 @@ static void android_media_MediaCodec_native_queueHardwareBuffer( JNIEnv *env, jobject thiz, jint index, jobject bufferObj, jlong presentationTimeUs, jint flags, jobject keys, jobject values) { ALOGV("android_media_MediaCodec_native_queueHardwareBuffer"); + ScopedTrace trace(ATRACE_TAG, "MediaCodec::queueHardwareBuffer#jni"); sp<JMediaCodec> codec = getMediaCodec(env, thiz); diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java b/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java deleted file mode 100644 index fdb0fc538fdf..000000000000 --- a/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.service.watchdog; - -import static android.os.Parcelable.Creator; - -import android.annotation.CallbackExecutor; -import android.annotation.FlaggedApi; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.SdkConstant; -import android.annotation.SuppressLint; -import android.annotation.SystemApi; -import android.app.Service; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.crashrecovery.flags.Flags; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Parcel; -import android.os.Parcelable; -import android.os.RemoteCallback; -import android.os.RemoteException; -import android.util.Log; - -import com.android.internal.util.Preconditions; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -/** - * A service to provide packages supporting explicit health checks and route checks to these - * packages on behalf of the package watchdog. - * - * <p>To extend this class, you must declare the service in your manifest file with the - * {@link android.Manifest.permission#BIND_EXPLICIT_HEALTH_CHECK_SERVICE} permission, - * and include an intent filter with the {@link #SERVICE_INTERFACE} action. In adddition, - * your implementation must live in - * {@link PackageManager#getServicesSystemSharedLibraryPackageName()}. - * For example:</p> - * <pre> - * <service android:name=".FooExplicitHealthCheckService" - * android:exported="true" - * android:priority="100" - * android:permission="android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"> - * <intent-filter> - * <action android:name="android.service.watchdog.ExplicitHealthCheckService" /> - * </intent-filter> - * </service> - * </pre> - * @hide - */ -@SystemApi -public abstract class ExplicitHealthCheckService extends Service { - - private static final String TAG = "ExplicitHealthCheckService"; - - /** - * {@link Bundle} key for a {@link List} of {@link PackageConfig} value. - * - * {@hide} - */ - public static final String EXTRA_SUPPORTED_PACKAGES = - "android.service.watchdog.extra.supported_packages"; - - /** - * {@link Bundle} key for a {@link List} of {@link String} value. - * - * {@hide} - */ - public static final String EXTRA_REQUESTED_PACKAGES = - "android.service.watchdog.extra.requested_packages"; - - /** - * {@link Bundle} key for a {@link String} value. - */ - @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) - public static final String EXTRA_HEALTH_CHECK_PASSED_PACKAGE = - "android.service.watchdog.extra.HEALTH_CHECK_PASSED_PACKAGE"; - - /** - * The Intent action that a service must respond to. Add it to the intent filter of the service - * in its manifest. - */ - @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) - public static final String SERVICE_INTERFACE = - "android.service.watchdog.ExplicitHealthCheckService"; - - /** - * The permission that a service must require to ensure that only Android system can bind to it. - * If this permission is not enforced in the AndroidManifest of the service, the system will - * skip that service. - */ - public static final String BIND_PERMISSION = - "android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"; - - private final ExplicitHealthCheckServiceWrapper mWrapper = - new ExplicitHealthCheckServiceWrapper(); - - /** - * Called when the system requests an explicit health check for {@code packageName}. - * - * <p> When {@code packageName} passes the check, implementors should call - * {@link #notifyHealthCheckPassed} to inform the system. - * - * <p> It could take many hours before a {@code packageName} passes a check and implementors - * should never drop requests unless {@link onCancel} is called or the service dies. - * - * <p> Requests should not be queued and additional calls while expecting a result for - * {@code packageName} should have no effect. - */ - public abstract void onRequestHealthCheck(@NonNull String packageName); - - /** - * Called when the system cancels the explicit health check request for {@code packageName}. - * Should do nothing if there are is no active request for {@code packageName}. - */ - public abstract void onCancelHealthCheck(@NonNull String packageName); - - /** - * Called when the system requests for all the packages supporting explicit health checks. The - * system may request an explicit health check for any of these packages with - * {@link #onRequestHealthCheck}. - * - * @return all packages supporting explicit health checks - */ - @NonNull public abstract List<PackageConfig> onGetSupportedPackages(); - - /** - * Called when the system requests for all the packages that it has currently requested - * an explicit health check for. - * - * @return all packages expecting an explicit health check result - */ - @NonNull public abstract List<String> onGetRequestedPackages(); - - private final Handler mHandler = Handler.createAsync(Looper.getMainLooper()); - @Nullable private Consumer<Bundle> mHealthCheckResultCallback; - @Nullable private Executor mCallbackExecutor; - - @Override - @NonNull - public final IBinder onBind(@NonNull Intent intent) { - return mWrapper; - } - - /** - * Sets a callback to be invoked when an explicit health check passes for a package. - * <p> - * The callback will receive a {@link Bundle} containing the package name that passed the - * health check, identified by the key {@link #EXTRA_HEALTH_CHECK_PASSED_PACKAGE}. - * <p> - * <b>Note:</b> This API is primarily intended for testing purposes. Calling this outside of a - * test environment will override the default callback mechanism used to notify the system - * about health check results. Use with caution in production code. - * - * @param executor The executor on which the callback should be invoked. If {@code null}, the - * callback will be executed on the main thread. - * @param callback A callback that receives a {@link Bundle} containing the package name that - * passed the health check. - */ - @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) - public final void setHealthCheckPassedCallback(@CallbackExecutor @Nullable Executor executor, - @Nullable Consumer<Bundle> callback) { - mCallbackExecutor = executor; - mHealthCheckResultCallback = callback; - } - - private void executeCallback(@NonNull String packageName) { - if (mHealthCheckResultCallback != null) { - Objects.requireNonNull(packageName, - "Package passing explicit health check must be non-null"); - Bundle bundle = new Bundle(); - bundle.putString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE, packageName); - mHealthCheckResultCallback.accept(bundle); - } else { - Log.wtf(TAG, "System missed explicit health check result for " + packageName); - } - } - - /** - * Implementors should call this to notify the system when explicit health check passes - * for {@code packageName}; - */ - public final void notifyHealthCheckPassed(@NonNull String packageName) { - if (mCallbackExecutor != null) { - mCallbackExecutor.execute(() -> executeCallback(packageName)); - } else { - mHandler.post(() -> executeCallback(packageName)); - } - } - - /** - * A PackageConfig contains a package supporting explicit health checks and the - * timeout in {@link System#uptimeMillis} across reboots after which health - * check requests from clients are failed. - * - * @hide - */ - @SystemApi - public static final class PackageConfig implements Parcelable { - private static final long DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(1); - - private final String mPackageName; - private final long mHealthCheckTimeoutMillis; - - /** - * Creates a new instance. - * - * @param packageName the package name - * @param durationMillis the duration in milliseconds, must be greater than or - * equal to 0. If it is 0, it will use a system default value. - */ - public PackageConfig(@NonNull String packageName, long healthCheckTimeoutMillis) { - mPackageName = Preconditions.checkNotNull(packageName); - if (healthCheckTimeoutMillis == 0) { - mHealthCheckTimeoutMillis = DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS; - } else { - mHealthCheckTimeoutMillis = Preconditions.checkArgumentNonnegative( - healthCheckTimeoutMillis); - } - } - - private PackageConfig(Parcel parcel) { - mPackageName = parcel.readString(); - mHealthCheckTimeoutMillis = parcel.readLong(); - } - - /** - * Gets the package name. - * - * @return the package name - */ - public @NonNull String getPackageName() { - return mPackageName; - } - - /** - * Gets the timeout in milliseconds to evaluate an explicit health check result after a - * request. - * - * @return the duration in {@link System#uptimeMillis} across reboots - */ - public long getHealthCheckTimeoutMillis() { - return mHealthCheckTimeoutMillis; - } - - @NonNull - @Override - public String toString() { - return "PackageConfig{" + mPackageName + ", " + mHealthCheckTimeoutMillis + "}"; - } - - @Override - public boolean equals(@Nullable Object other) { - if (other == this) { - return true; - } - if (!(other instanceof PackageConfig)) { - return false; - } - - PackageConfig otherInfo = (PackageConfig) other; - return Objects.equals(otherInfo.getHealthCheckTimeoutMillis(), - mHealthCheckTimeoutMillis) - && Objects.equals(otherInfo.getPackageName(), mPackageName); - } - - @Override - public int hashCode() { - return Objects.hash(mPackageName, mHealthCheckTimeoutMillis); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(@SuppressLint({"MissingNullability"}) Parcel parcel, int flags) { - parcel.writeString(mPackageName); - parcel.writeLong(mHealthCheckTimeoutMillis); - } - - public static final @NonNull Creator<PackageConfig> CREATOR = new Creator<PackageConfig>() { - @Override - public PackageConfig createFromParcel(Parcel source) { - return new PackageConfig(source); - } - - @Override - public PackageConfig[] newArray(int size) { - return new PackageConfig[size]; - } - }; - } - - - private class ExplicitHealthCheckServiceWrapper extends IExplicitHealthCheckService.Stub { - @Override - public void setCallback(RemoteCallback callback) throws RemoteException { - mHandler.post(() -> mHealthCheckResultCallback = callback::sendResult); - } - - @Override - public void request(String packageName) throws RemoteException { - mHandler.post(() -> ExplicitHealthCheckService.this.onRequestHealthCheck(packageName)); - } - - @Override - public void cancel(String packageName) throws RemoteException { - mHandler.post(() -> ExplicitHealthCheckService.this.onCancelHealthCheck(packageName)); - } - - @Override - public void getSupportedPackages(RemoteCallback callback) throws RemoteException { - mHandler.post(() -> { - List<PackageConfig> packages = - ExplicitHealthCheckService.this.onGetSupportedPackages(); - Objects.requireNonNull(packages, "Supported package list must be non-null"); - Bundle bundle = new Bundle(); - bundle.putParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, new ArrayList<>(packages)); - callback.sendResult(bundle); - }); - } - - @Override - public void getRequestedPackages(RemoteCallback callback) throws RemoteException { - mHandler.post(() -> { - List<String> packages = - ExplicitHealthCheckService.this.onGetRequestedPackages(); - Objects.requireNonNull(packages, "Requested package list must be non-null"); - Bundle bundle = new Bundle(); - bundle.putStringArrayList(EXTRA_REQUESTED_PACKAGES, new ArrayList<>(packages)); - callback.sendResult(bundle); - }); - } - } -} diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS b/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS deleted file mode 100644 index 1c045e10c0ec..000000000000 --- a/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS +++ /dev/null @@ -1,3 +0,0 @@ -narayan@google.com -nandana@google.com -olilan@google.com diff --git a/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java b/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java deleted file mode 100644 index da9a13961f79..000000000000 --- a/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java +++ /dev/null @@ -1,447 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server; - -import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE; -import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_REQUESTED_PACKAGES; -import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_SUPPORTED_PACKAGES; -import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; - -import android.Manifest; -import android.annotation.MainThread; -import android.annotation.Nullable; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.os.IBinder; -import android.os.RemoteCallback; -import android.os.RemoteException; -import android.os.UserHandle; -import android.service.watchdog.ExplicitHealthCheckService; -import android.service.watchdog.IExplicitHealthCheckService; -import android.text.TextUtils; -import android.util.ArraySet; -import android.util.Slog; - -import com.android.internal.annotations.GuardedBy; - -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.function.Consumer; - -// TODO(b/120598832): Add tests -/** - * Controls the connections with {@link ExplicitHealthCheckService}. - */ -class ExplicitHealthCheckController { - private static final String TAG = "ExplicitHealthCheckController"; - private final Object mLock = new Object(); - private final Context mContext; - - // Called everytime a package passes the health check, so the watchdog is notified of the - // passing check. In practice, should never be null after it has been #setEnabled. - // To prevent deadlocks between the controller and watchdog threads, we have - // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. - // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer. - @GuardedBy("mLock") @Nullable private Consumer<String> mPassedConsumer; - // Called everytime after a successful #syncRequest call, so the watchdog can receive packages - // supporting health checks and update its internal state. In practice, should never be null - // after it has been #setEnabled. - // To prevent deadlocks between the controller and watchdog threads, we have - // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. - // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer. - @GuardedBy("mLock") @Nullable private Consumer<List<PackageConfig>> mSupportedConsumer; - // Called everytime we need to notify the watchdog to sync requests between itself and the - // health check service. In practice, should never be null after it has been #setEnabled. - // To prevent deadlocks between the controller and watchdog threads, we have - // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. - // It's easier to just NOT hold #mLock when calling into watchdog code on this runnable. - @GuardedBy("mLock") @Nullable private Runnable mNotifySyncRunnable; - // Actual binder object to the explicit health check service. - @GuardedBy("mLock") @Nullable private IExplicitHealthCheckService mRemoteService; - // Connection to the explicit health check service, necessary to unbind. - // We should only try to bind if mConnection is null, non-null indicates we - // are connected or at least connecting. - @GuardedBy("mLock") @Nullable private ServiceConnection mConnection; - // Bind state of the explicit health check service. - @GuardedBy("mLock") private boolean mEnabled; - - ExplicitHealthCheckController(Context context) { - mContext = context; - } - - /** Enables or disables explicit health checks. */ - public void setEnabled(boolean enabled) { - synchronized (mLock) { - Slog.i(TAG, "Explicit health checks " + (enabled ? "enabled." : "disabled.")); - mEnabled = enabled; - } - } - - /** - * Sets callbacks to listen to important events from the controller. - * - * <p> Should be called once at initialization before any other calls to the controller to - * ensure a happens-before relationship of the set parameters and visibility on other threads. - */ - public void setCallbacks(Consumer<String> passedConsumer, - Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) { - synchronized (mLock) { - if (mPassedConsumer != null || mSupportedConsumer != null - || mNotifySyncRunnable != null) { - Slog.wtf(TAG, "Resetting health check controller callbacks"); - } - - mPassedConsumer = Objects.requireNonNull(passedConsumer); - mSupportedConsumer = Objects.requireNonNull(supportedConsumer); - mNotifySyncRunnable = Objects.requireNonNull(notifySyncRunnable); - } - } - - /** - * Calls the health check service to request or cancel packages based on - * {@code newRequestedPackages}. - * - * <p> Supported packages in {@code newRequestedPackages} that have not been previously - * requested will be requested while supported packages not in {@code newRequestedPackages} - * but were previously requested will be cancelled. - * - * <p> This handles binding and unbinding to the health check service as required. - * - * <p> Note, calling this may modify {@code newRequestedPackages}. - * - * <p> Note, this method is not thread safe, all calls should be serialized. - */ - public void syncRequests(Set<String> newRequestedPackages) { - boolean enabled; - synchronized (mLock) { - enabled = mEnabled; - } - - if (!enabled) { - Slog.i(TAG, "Health checks disabled, no supported packages"); - // Call outside lock - mSupportedConsumer.accept(Collections.emptyList()); - return; - } - - getSupportedPackages(supportedPackageConfigs -> { - // Notify the watchdog without lock held - mSupportedConsumer.accept(supportedPackageConfigs); - getRequestedPackages(previousRequestedPackages -> { - synchronized (mLock) { - // Hold lock so requests and cancellations are sent atomically. - // It is important we don't mix requests from multiple threads. - - Set<String> supportedPackages = new ArraySet<>(); - for (PackageConfig config : supportedPackageConfigs) { - supportedPackages.add(config.getPackageName()); - } - // Note, this may modify newRequestedPackages - newRequestedPackages.retainAll(supportedPackages); - - // Cancel packages no longer requested - actOnDifference(previousRequestedPackages, - newRequestedPackages, p -> cancel(p)); - // Request packages not yet requested - actOnDifference(newRequestedPackages, - previousRequestedPackages, p -> request(p)); - - if (newRequestedPackages.isEmpty()) { - Slog.i(TAG, "No more health check requests, unbinding..."); - unbindService(); - return; - } - } - }); - }); - } - - private void actOnDifference(Collection<String> collection1, Collection<String> collection2, - Consumer<String> action) { - Iterator<String> iterator = collection1.iterator(); - while (iterator.hasNext()) { - String packageName = iterator.next(); - if (!collection2.contains(packageName)) { - action.accept(packageName); - } - } - } - - /** - * Requests an explicit health check for {@code packageName}. - * After this request, the callback registered on {@link #setCallbacks} can receive explicit - * health check passed results. - */ - private void request(String packageName) { - synchronized (mLock) { - if (!prepareServiceLocked("request health check for " + packageName)) { - return; - } - - Slog.i(TAG, "Requesting health check for package " + packageName); - try { - mRemoteService.request(packageName); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to request health check for package " + packageName, e); - } - } - } - - /** - * Cancels all explicit health checks for {@code packageName}. - * After this request, the callback registered on {@link #setCallbacks} can no longer receive - * explicit health check passed results. - */ - private void cancel(String packageName) { - synchronized (mLock) { - if (!prepareServiceLocked("cancel health check for " + packageName)) { - return; - } - - Slog.i(TAG, "Cancelling health check for package " + packageName); - try { - mRemoteService.cancel(packageName); - } catch (RemoteException e) { - // Do nothing, if the service is down, when it comes up, we will sync requests, - // if there's some other error, retrying wouldn't fix anyways. - Slog.w(TAG, "Failed to cancel health check for package " + packageName, e); - } - } - } - - /** - * Returns the packages that we can request explicit health checks for. - * The packages will be returned to the {@code consumer}. - */ - private void getSupportedPackages(Consumer<List<PackageConfig>> consumer) { - synchronized (mLock) { - if (!prepareServiceLocked("get health check supported packages")) { - return; - } - - Slog.d(TAG, "Getting health check supported packages"); - try { - mRemoteService.getSupportedPackages(new RemoteCallback(result -> { - List<PackageConfig> packages = - result.getParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, android.service.watchdog.ExplicitHealthCheckService.PackageConfig.class); - Slog.i(TAG, "Explicit health check supported packages " + packages); - consumer.accept(packages); - })); - } catch (RemoteException e) { - // Request failed, treat as if all observed packages are supported, if any packages - // expire during this period, we may incorrectly treat it as failing health checks - // even if we don't support health checks for the package. - Slog.w(TAG, "Failed to get health check supported packages", e); - } - } - } - - /** - * Returns the packages for which health checks are currently in progress. - * The packages will be returned to the {@code consumer}. - */ - private void getRequestedPackages(Consumer<List<String>> consumer) { - synchronized (mLock) { - if (!prepareServiceLocked("get health check requested packages")) { - return; - } - - Slog.d(TAG, "Getting health check requested packages"); - try { - mRemoteService.getRequestedPackages(new RemoteCallback(result -> { - List<String> packages = result.getStringArrayList(EXTRA_REQUESTED_PACKAGES); - Slog.i(TAG, "Explicit health check requested packages " + packages); - consumer.accept(packages); - })); - } catch (RemoteException e) { - // Request failed, treat as if we haven't requested any packages, if any packages - // were actually requested, they will not be cancelled now. May be cancelled later - Slog.w(TAG, "Failed to get health check requested packages", e); - } - } - } - - /** - * Binds to the explicit health check service if the controller is enabled and - * not already bound. - */ - private void bindService() { - synchronized (mLock) { - if (!mEnabled || mConnection != null || mRemoteService != null) { - if (!mEnabled) { - Slog.i(TAG, "Not binding to service, service disabled"); - } else if (mRemoteService != null) { - Slog.i(TAG, "Not binding to service, service already connected"); - } else { - Slog.i(TAG, "Not binding to service, service already connecting"); - } - return; - } - ComponentName component = getServiceComponentNameLocked(); - if (component == null) { - Slog.wtf(TAG, "Explicit health check service not found"); - return; - } - - Intent intent = new Intent(); - intent.setComponent(component); - mConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - Slog.i(TAG, "Explicit health check service is connected " + name); - initState(service); - } - - @Override - @MainThread - public void onServiceDisconnected(ComponentName name) { - // Service crashed or process was killed, #onServiceConnected will be called. - // Don't need to re-bind. - Slog.i(TAG, "Explicit health check service is disconnected " + name); - synchronized (mLock) { - mRemoteService = null; - } - } - - @Override - public void onBindingDied(ComponentName name) { - // Application hosting service probably got updated - // Need to re-bind. - Slog.i(TAG, "Explicit health check service binding is dead. Rebind: " + name); - unbindService(); - bindService(); - } - - @Override - public void onNullBinding(ComponentName name) { - // Should never happen. Service returned null from #onBind. - Slog.wtf(TAG, "Explicit health check service binding is null?? " + name); - } - }; - - mContext.bindServiceAsUser(intent, mConnection, - Context.BIND_AUTO_CREATE, UserHandle.SYSTEM); - Slog.i(TAG, "Explicit health check service is bound"); - } - } - - /** Unbinds the explicit health check service. */ - private void unbindService() { - synchronized (mLock) { - if (mRemoteService != null) { - mContext.unbindService(mConnection); - mRemoteService = null; - mConnection = null; - } - Slog.i(TAG, "Explicit health check service is unbound"); - } - } - - @GuardedBy("mLock") - @Nullable - private ServiceInfo getServiceInfoLocked() { - final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE); - final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, - PackageManager.GET_SERVICES | PackageManager.GET_META_DATA - | PackageManager.MATCH_SYSTEM_ONLY); - if (resolveInfo == null || resolveInfo.serviceInfo == null) { - Slog.w(TAG, "No valid components found."); - return null; - } - return resolveInfo.serviceInfo; - } - - @GuardedBy("mLock") - @Nullable - private ComponentName getServiceComponentNameLocked() { - final ServiceInfo serviceInfo = getServiceInfoLocked(); - if (serviceInfo == null) { - return null; - } - - final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); - if (!Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE - .equals(serviceInfo.permission)) { - Slog.w(TAG, name.flattenToShortString() + " does not require permission " - + Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE); - return null; - } - return name; - } - - private void initState(IBinder service) { - synchronized (mLock) { - if (!mEnabled) { - Slog.w(TAG, "Attempting to connect disabled service?? Unbinding..."); - // Very unlikely, but we disabled the service after binding but before we connected - unbindService(); - return; - } - mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service); - try { - mRemoteService.setCallback(new RemoteCallback(result -> { - String packageName = result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE); - if (!TextUtils.isEmpty(packageName)) { - if (mPassedConsumer == null) { - Slog.wtf(TAG, "Health check passed for package " + packageName - + "but no consumer registered."); - } else { - // Call without lock held - mPassedConsumer.accept(packageName); - } - } else { - Slog.wtf(TAG, "Empty package passed explicit health check?"); - } - })); - Slog.i(TAG, "Service initialized, syncing requests"); - } catch (RemoteException e) { - Slog.wtf(TAG, "Could not setCallback on explicit health check service"); - } - } - // Calling outside lock - mNotifySyncRunnable.run(); - } - - /** - * Prepares the health check service to receive requests. - * - * @return {@code true} if it is ready and we can proceed with a request, - * {@code false} otherwise. If it is not ready, and the service is enabled, - * we will bind and the request should be automatically attempted later. - */ - @GuardedBy("mLock") - private boolean prepareServiceLocked(String action) { - if (mRemoteService != null && mEnabled) { - return true; - } - Slog.i(TAG, "Service not ready to " + action - + (mEnabled ? ". Binding..." : ". Disabled")); - if (mEnabled) { - bindService(); - } - return false; - } -} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java deleted file mode 100644 index e4f07f9fc213..000000000000 --- a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java +++ /dev/null @@ -1,2253 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server; - -import static android.content.Intent.ACTION_REBOOT; -import static android.content.Intent.ACTION_SHUTDOWN; -import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; -import static android.util.Xml.Encoding.UTF_8; - -import static com.android.server.crashrecovery.CrashRecoveryUtils.dumpCrashRecoveryEvents; - -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.annotation.CallbackExecutor; -import android.annotation.FlaggedApi; -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.SuppressLint; -import android.annotation.SystemApi; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.VersionedPackage; -import android.crashrecovery.flags.Flags; -import android.os.Environment; -import android.os.Handler; -import android.os.Looper; -import android.os.Process; -import android.os.SystemProperties; -import android.provider.DeviceConfig; -import android.sysprop.CrashRecoveryProperties; -import android.text.TextUtils; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.AtomicFile; -import android.util.EventLog; -import android.util.IndentingPrintWriter; -import android.util.LongArrayQueue; -import android.util.Slog; -import android.util.Xml; -import android.util.XmlUtils; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.FastXmlSerializer; -import com.android.modules.utils.BackgroundThread; - -import libcore.io.IoUtils; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.PrintWriter; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; - -/** - * Monitors the health of packages on the system and notifies interested observers when packages - * fail. On failure, the registered observer with the least user impacting mitigation will - * be notified. - * @hide - */ -@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) -@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) -public class PackageWatchdog { - private static final String TAG = "PackageWatchdog"; - - static final String PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS = - "watchdog_trigger_failure_duration_millis"; - static final String PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT = - "watchdog_trigger_failure_count"; - static final String PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED = - "watchdog_explicit_health_check_enabled"; - - // TODO: make the following values configurable via DeviceConfig - private static final long NATIVE_CRASH_POLLING_INTERVAL_MILLIS = - TimeUnit.SECONDS.toMillis(30); - private static final long NUMBER_OF_NATIVE_CRASH_POLLS = 10; - - - /** Reason for package failure could not be determined. */ - public static final int FAILURE_REASON_UNKNOWN = 0; - - /** The package had a native crash. */ - public static final int FAILURE_REASON_NATIVE_CRASH = 1; - - /** The package failed an explicit health check. */ - public static final int FAILURE_REASON_EXPLICIT_HEALTH_CHECK = 2; - - /** The app crashed. */ - public static final int FAILURE_REASON_APP_CRASH = 3; - - /** The app was not responding. */ - public static final int FAILURE_REASON_APP_NOT_RESPONDING = 4; - - /** The device was boot looping. */ - public static final int FAILURE_REASON_BOOT_LOOP = 5; - - /** @hide */ - @IntDef(prefix = { "FAILURE_REASON_" }, value = { - FAILURE_REASON_UNKNOWN, - FAILURE_REASON_NATIVE_CRASH, - FAILURE_REASON_EXPLICIT_HEALTH_CHECK, - FAILURE_REASON_APP_CRASH, - FAILURE_REASON_APP_NOT_RESPONDING, - FAILURE_REASON_BOOT_LOOP - }) - @Retention(RetentionPolicy.SOURCE) - public @interface FailureReasons {} - - // Duration to count package failures before it resets to 0 - @VisibleForTesting - static final int DEFAULT_TRIGGER_FAILURE_DURATION_MS = - (int) TimeUnit.MINUTES.toMillis(1); - // Number of package failures within the duration above before we notify observers - @VisibleForTesting - static final int DEFAULT_TRIGGER_FAILURE_COUNT = 5; - @VisibleForTesting - static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2); - // Sliding window for tracking how many mitigation calls were made for a package. - @VisibleForTesting - static final long DEFAULT_DEESCALATION_WINDOW_MS = TimeUnit.HOURS.toMillis(1); - // Whether explicit health checks are enabled or not - private static final boolean DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED = true; - - @VisibleForTesting - static final int DEFAULT_BOOT_LOOP_TRIGGER_COUNT = 5; - - static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10); - - // Time needed to apply mitigation - private static final String MITIGATION_WINDOW_MS = - "persist.device_config.configuration.mitigation_window_ms"; - @VisibleForTesting - static final long DEFAULT_MITIGATION_WINDOW_MS = TimeUnit.SECONDS.toMillis(5); - - // Threshold level at which or above user might experience significant disruption. - private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD = - "persist.device_config.configuration.major_user_impact_level_threshold"; - private static final int DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD = - PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; - - // Comma separated list of all packages exempt from user impact level threshold. If a package - // in the list is crash looping, all the mitigations including factory reset will be performed. - private static final String PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD = - "persist.device_config.configuration.packages_exempt_from_impact_level_threshold"; - - // Comma separated list of default packages exempt from user impact level threshold. - private static final String DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD = - "com.android.systemui"; - - private long mNumberOfNativeCrashPollsRemaining; - - private static final int DB_VERSION = 1; - private static final String TAG_PACKAGE_WATCHDOG = "package-watchdog"; - private static final String TAG_PACKAGE = "package"; - private static final String TAG_OBSERVER = "observer"; - private static final String ATTR_VERSION = "version"; - private static final String ATTR_NAME = "name"; - private static final String ATTR_DURATION = "duration"; - private static final String ATTR_EXPLICIT_HEALTH_CHECK_DURATION = "health-check-duration"; - private static final String ATTR_PASSED_HEALTH_CHECK = "passed-health-check"; - private static final String ATTR_MITIGATION_CALLS = "mitigation-calls"; - private static final String ATTR_MITIGATION_COUNT = "mitigation-count"; - - // A file containing information about the current mitigation count in the case of a boot loop. - // This allows boot loop information to persist in the case of an fs-checkpoint being - // aborted. - private static final String METADATA_FILE = "/metadata/watchdog/mitigation_count.txt"; - - /** - * EventLog tags used when logging into the event log. Note the values must be sync with - * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct - * name translation. - */ - private static final int LOG_TAG_RESCUE_NOTE = 2900; - - private static final Object sPackageWatchdogLock = new Object(); - @GuardedBy("sPackageWatchdogLock") - private static PackageWatchdog sPackageWatchdog; - - private static final Object sLock = new Object(); - // System server context - private final Context mContext; - // Handler to run short running tasks - private final Handler mShortTaskHandler; - // Handler for processing IO and long running tasks - private final Handler mLongTaskHandler; - // Contains (observer-name -> observer-handle) that have ever been registered from - // previous boots. Observers with all packages expired are periodically pruned. - // It is saved to disk on system shutdown and repouplated on startup so it survives reboots. - @GuardedBy("sLock") - private final ArrayMap<String, ObserverInternal> mAllObservers = new ArrayMap<>(); - // File containing the XML data of monitored packages /data/system/package-watchdog.xml - private final AtomicFile mPolicyFile; - private final ExplicitHealthCheckController mHealthCheckController; - private final Runnable mSyncRequests = this::syncRequests; - private final Runnable mSyncStateWithScheduledReason = this::syncStateWithScheduledReason; - private final Runnable mSaveToFile = this::saveToFile; - private final SystemClock mSystemClock; - private final BootThreshold mBootThreshold; - private final DeviceConfig.OnPropertiesChangedListener - mOnPropertyChangedListener = this::onPropertyChanged; - - private final Set<String> mPackagesExemptFromImpactLevelThreshold = new ArraySet<>(); - - // The set of packages that have been synced with the ExplicitHealthCheckController - @GuardedBy("sLock") - private Set<String> mRequestedHealthCheckPackages = new ArraySet<>(); - @GuardedBy("sLock") - private boolean mIsPackagesReady; - // Flag to control whether explicit health checks are supported or not - @GuardedBy("sLock") - private boolean mIsHealthCheckEnabled = DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED; - @GuardedBy("sLock") - private int mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS; - @GuardedBy("sLock") - private int mTriggerFailureCount = DEFAULT_TRIGGER_FAILURE_COUNT; - // SystemClock#uptimeMillis when we last executed #syncState - // 0 if no prune is scheduled. - @GuardedBy("sLock") - private long mUptimeAtLastStateSync; - // If true, sync explicit health check packages with the ExplicitHealthCheckController. - @GuardedBy("sLock") - private boolean mSyncRequired = false; - - @GuardedBy("sLock") - private long mLastMitigation = -1000000; - - @FunctionalInterface - @VisibleForTesting - interface SystemClock { - long uptimeMillis(); - } - - private PackageWatchdog(Context context) { - // Needs to be constructed inline - this(context, new AtomicFile( - new File(new File(Environment.getDataDirectory(), "system"), - "package-watchdog.xml")), - new Handler(Looper.myLooper()), BackgroundThread.getHandler(), - new ExplicitHealthCheckController(context), - android.os.SystemClock::uptimeMillis); - } - - /** - * Creates a PackageWatchdog that allows injecting dependencies. - */ - @VisibleForTesting - PackageWatchdog(Context context, AtomicFile policyFile, Handler shortTaskHandler, - Handler longTaskHandler, ExplicitHealthCheckController controller, - SystemClock clock) { - mContext = context; - mPolicyFile = policyFile; - mShortTaskHandler = shortTaskHandler; - mLongTaskHandler = longTaskHandler; - mHealthCheckController = controller; - mSystemClock = clock; - mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS; - mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT, - DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS); - - loadFromFile(); - sPackageWatchdog = this; - } - - /** - * Creates or gets singleton instance of PackageWatchdog. - * - * @param context The system server context. - */ - public static @NonNull PackageWatchdog getInstance(@NonNull Context context) { - synchronized (sPackageWatchdogLock) { - if (sPackageWatchdog == null) { - new PackageWatchdog(context); - } - return sPackageWatchdog; - } - } - - /** - * Called during boot to notify when packages are ready on the device so we can start - * binding. - * @hide - */ - public void onPackagesReady() { - synchronized (sLock) { - mIsPackagesReady = true; - mHealthCheckController.setCallbacks(packageName -> onHealthCheckPassed(packageName), - packages -> onSupportedPackages(packages), - this::onSyncRequestNotified); - setPropertyChangedListenerLocked(); - updateConfigs(); - } - } - - /** - * Registers {@code observer} to listen for package failures. Add a new ObserverInternal for - * this observer if it does not already exist. - * For executing mitigations observers will receive callback on the given executor. - * - * <p>Observers are expected to call this on boot. It does not specify any packages but - * it will resume observing any packages requested from a previous boot. - * - * @param observer instance of {@link PackageHealthObserver} for observing package failures - * and boot loops. - * @param executor Executor for the thread on which observers would receive callbacks - */ - public void registerHealthObserver(@NonNull @CallbackExecutor Executor executor, - @NonNull PackageHealthObserver observer) { - synchronized (sLock) { - ObserverInternal internalObserver = mAllObservers.get(observer.getUniqueIdentifier()); - if (internalObserver != null) { - internalObserver.registeredObserver = observer; - internalObserver.observerExecutor = executor; - } else { - internalObserver = new ObserverInternal(observer.getUniqueIdentifier(), - new ArrayList<>()); - internalObserver.registeredObserver = observer; - internalObserver.observerExecutor = executor; - mAllObservers.put(observer.getUniqueIdentifier(), internalObserver); - syncState("added new observer"); - } - } - } - - /** - * Starts observing the health of the {@code packages} for {@code observer}. - * Note: Observer needs to be registered with {@link #registerHealthObserver} before calling - * this API. - * - * <p>If monitoring a package supporting explicit health check, at the end of the monitoring - * duration if {@link #onHealthCheckPassed} was never called, - * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} will be called as if the - * package failed. - * - * <p>If {@code observer} is already monitoring a package in {@code packageNames}, - * the monitoring window of that package will be reset to {@code durationMs} and the health - * check state will be reset to a default. - * - * <p>The {@code observer} must be registered with {@link #registerHealthObserver} before - * calling this method. - * - * @param packageNames The list of packages to check. If this is empty, the call will be a - * no-op. - * - * @param timeoutMs The timeout after which Explicit Health Checks would not run. If this is - * less than 1, a default monitoring duration 2 days will be used. - * - * @throws IllegalStateException if the observer was not previously registered - */ - public void startExplicitHealthCheck(@NonNull List<String> packageNames, long timeoutMs, - @NonNull PackageHealthObserver observer) { - synchronized (sLock) { - if (!mAllObservers.containsKey(observer.getUniqueIdentifier())) { - Slog.wtf(TAG, "No observer found, need to register the observer: " - + observer.getUniqueIdentifier()); - throw new IllegalStateException("Observer not registered"); - } - } - if (packageNames.isEmpty()) { - Slog.wtf(TAG, "No packages to observe, " + observer.getUniqueIdentifier()); - return; - } - if (timeoutMs < 1) { - Slog.wtf(TAG, "Invalid duration " + timeoutMs + "ms for observer " - + observer.getUniqueIdentifier() + ". Not observing packages " + packageNames); - timeoutMs = DEFAULT_OBSERVING_DURATION_MS; - } - - List<MonitoredPackage> packages = new ArrayList<>(); - for (int i = 0; i < packageNames.size(); i++) { - // Health checks not available yet so health check state will start INACTIVE - MonitoredPackage pkg = newMonitoredPackage(packageNames.get(i), timeoutMs, false); - if (pkg != null) { - packages.add(pkg); - } else { - Slog.w(TAG, "Failed to create MonitoredPackage for pkg=" + packageNames.get(i)); - } - } - - if (packages.isEmpty()) { - return; - } - - // Sync before we add the new packages to the observers. This will #pruneObservers, - // causing any elapsed time to be deducted from all existing packages before we add new - // packages. This maintains the invariant that the elapsed time for ALL (new and existing) - // packages is the same. - mLongTaskHandler.post(() -> { - syncState("observing new packages"); - - synchronized (sLock) { - ObserverInternal oldObserver = mAllObservers.get(observer.getUniqueIdentifier()); - if (oldObserver == null) { - Slog.d(TAG, observer.getUniqueIdentifier() + " started monitoring health " - + "of packages " + packageNames); - mAllObservers.put(observer.getUniqueIdentifier(), - new ObserverInternal(observer.getUniqueIdentifier(), packages)); - } else { - Slog.d(TAG, observer.getUniqueIdentifier() + " added the following " - + "packages to monitor " + packageNames); - oldObserver.updatePackagesLocked(packages); - } - } - - // Sync after we add the new packages to the observers. We may have received packges - // requiring an earlier schedule than we are currently scheduled for. - syncState("updated observers"); - }); - - } - - /** - * Unregisters {@code observer} from listening to package failure. - * Additionally, this stops observing any packages that may have previously been observed - * even from a previous boot. - */ - public void unregisterHealthObserver(@NonNull PackageHealthObserver observer) { - mLongTaskHandler.post(() -> { - synchronized (sLock) { - mAllObservers.remove(observer.getUniqueIdentifier()); - } - syncState("unregistering observer: " + observer.getUniqueIdentifier()); - }); - } - - /** - * Called when a process fails due to a crash, ANR or explicit health check. - * - * <p>For each package contained in the process, one registered observer with the least user - * impact will be notified for mitigation. - * - * <p>This method could be called frequently if there is a severe problem on the device. - */ - public void notifyPackageFailure(@NonNull List<VersionedPackage> packages, - @FailureReasons int failureReason) { - if (packages == null) { - Slog.w(TAG, "Could not resolve a list of failing packages"); - return; - } - synchronized (sLock) { - final long now = mSystemClock.uptimeMillis(); - if (Flags.recoverabilityDetection()) { - if (now >= mLastMitigation - && (now - mLastMitigation) < getMitigationWindowMs()) { - Slog.i(TAG, "Skipping notifyPackageFailure mitigation"); - return; - } - } - } - mLongTaskHandler.post(() -> { - synchronized (sLock) { - if (mAllObservers.isEmpty()) { - return; - } - boolean requiresImmediateAction = (failureReason == FAILURE_REASON_NATIVE_CRASH - || failureReason == FAILURE_REASON_EXPLICIT_HEALTH_CHECK); - if (requiresImmediateAction) { - handleFailureImmediately(packages, failureReason); - } else { - for (int pIndex = 0; pIndex < packages.size(); pIndex++) { - VersionedPackage versionedPackage = packages.get(pIndex); - // Observer that will receive failure for versionedPackage - ObserverInternal currentObserverToNotify = null; - int currentObserverImpact = Integer.MAX_VALUE; - MonitoredPackage currentMonitoredPackage = null; - - // Find observer with least user impact - for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) { - ObserverInternal observer = mAllObservers.valueAt(oIndex); - PackageHealthObserver registeredObserver = observer.registeredObserver; - if (registeredObserver != null - && observer.notifyPackageFailureLocked( - versionedPackage.getPackageName())) { - MonitoredPackage p = observer.getMonitoredPackage( - versionedPackage.getPackageName()); - int mitigationCount = 1; - if (p != null) { - mitigationCount = p.getMitigationCountLocked() + 1; - } - int impact = registeredObserver.onHealthCheckFailed( - versionedPackage, failureReason, mitigationCount); - if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0 - && impact < currentObserverImpact) { - currentObserverToNotify = observer; - currentObserverImpact = impact; - currentMonitoredPackage = p; - } - } - } - - // Execute action with least user impact - if (currentObserverToNotify != null) { - int mitigationCount; - if (currentMonitoredPackage != null) { - currentMonitoredPackage.noteMitigationCallLocked(); - mitigationCount = - currentMonitoredPackage.getMitigationCountLocked(); - } else { - mitigationCount = 1; - } - if (Flags.recoverabilityDetection()) { - maybeExecute(currentObserverToNotify, versionedPackage, - failureReason, currentObserverImpact, mitigationCount); - } else { - PackageHealthObserver registeredObserver = - currentObserverToNotify.registeredObserver; - currentObserverToNotify.observerExecutor.execute(() -> - registeredObserver.onExecuteHealthCheckMitigation( - versionedPackage, failureReason, mitigationCount)); - } - } - } - } - } - }); - } - - /** - * For native crashes or explicit health check failures, call directly into each observer to - * mitigate the error without going through failure threshold logic. - */ - @GuardedBy("sLock") - private void handleFailureImmediately(List<VersionedPackage> packages, - @FailureReasons int failureReason) { - VersionedPackage failingPackage = packages.size() > 0 ? packages.get(0) : null; - ObserverInternal currentObserverToNotify = null; - int currentObserverImpact = Integer.MAX_VALUE; - for (ObserverInternal observer: mAllObservers.values()) { - PackageHealthObserver registeredObserver = observer.registeredObserver; - if (registeredObserver != null) { - int impact = registeredObserver.onHealthCheckFailed( - failingPackage, failureReason, 1); - if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0 - && impact < currentObserverImpact) { - currentObserverToNotify = observer; - currentObserverImpact = impact; - } - } - } - if (currentObserverToNotify != null) { - if (Flags.recoverabilityDetection()) { - maybeExecute(currentObserverToNotify, failingPackage, failureReason, - currentObserverImpact, /*mitigationCount=*/ 1); - } else { - PackageHealthObserver registeredObserver = - currentObserverToNotify.registeredObserver; - currentObserverToNotify.observerExecutor.execute(() -> - registeredObserver.onExecuteHealthCheckMitigation(failingPackage, - failureReason, 1)); - - } - } - } - - private void maybeExecute(ObserverInternal currentObserverToNotify, - VersionedPackage versionedPackage, - @FailureReasons int failureReason, - int currentObserverImpact, - int mitigationCount) { - if (allowMitigations(currentObserverImpact, versionedPackage)) { - PackageHealthObserver registeredObserver; - synchronized (sLock) { - mLastMitigation = mSystemClock.uptimeMillis(); - registeredObserver = currentObserverToNotify.registeredObserver; - } - currentObserverToNotify.observerExecutor.execute(() -> - registeredObserver.onExecuteHealthCheckMitigation(versionedPackage, - failureReason, mitigationCount)); - } - } - - private boolean allowMitigations(int currentObserverImpact, - VersionedPackage versionedPackage) { - return currentObserverImpact < getUserImpactLevelLimit() - || getPackagesExemptFromImpactLevelThreshold().contains( - versionedPackage.getPackageName()); - } - - private long getMitigationWindowMs() { - return SystemProperties.getLong(MITIGATION_WINDOW_MS, DEFAULT_MITIGATION_WINDOW_MS); - } - - - /** - * Called when the system server boots. If the system server is detected to be in a boot loop, - * query each observer and perform the mitigation action with the lowest user impact. - * - * Note: PackageWatchdog considers system_server restart loop as bootloop. Full reboots - * are not counted in bootloop. - * @hide - */ - @SuppressWarnings("GuardedBy") - public void noteBoot() { - synchronized (sLock) { - // if boot count has reached threshold, start mitigation. - // We wait until threshold number of restarts only for the first time. Perform - // mitigations for every restart after that. - boolean mitigate = mBootThreshold.incrementAndTest(); - if (mitigate) { - if (!Flags.recoverabilityDetection()) { - mBootThreshold.reset(); - } - int mitigationCount = mBootThreshold.getMitigationCount() + 1; - ObserverInternal currentObserverToNotify = null; - int currentObserverImpact = Integer.MAX_VALUE; - for (int i = 0; i < mAllObservers.size(); i++) { - final ObserverInternal observer = mAllObservers.valueAt(i); - PackageHealthObserver registeredObserver = observer.registeredObserver; - if (registeredObserver != null) { - int impact = Flags.recoverabilityDetection() - ? registeredObserver.onBootLoop( - observer.getBootMitigationCount() + 1) - : registeredObserver.onBootLoop(mitigationCount); - if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0 - && impact < currentObserverImpact) { - currentObserverToNotify = observer; - currentObserverImpact = impact; - } - } - } - - if (currentObserverToNotify != null) { - PackageHealthObserver registeredObserver = - currentObserverToNotify.registeredObserver; - if (Flags.recoverabilityDetection()) { - int currentObserverMitigationCount = - currentObserverToNotify.getBootMitigationCount() + 1; - currentObserverToNotify.setBootMitigationCount( - currentObserverMitigationCount); - saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); - currentObserverToNotify.observerExecutor - .execute(() -> registeredObserver.onExecuteBootLoopMitigation( - currentObserverMitigationCount)); - } else { - mBootThreshold.setMitigationCount(mitigationCount); - mBootThreshold.saveMitigationCountToMetadata(); - currentObserverToNotify.observerExecutor - .execute(() -> registeredObserver.onExecuteBootLoopMitigation( - mitigationCount)); - - } - } - } - } - } - - // TODO(b/120598832): Optimize write? Maybe only write a separate smaller file? Also - // avoid holding lock? - // This currently adds about 7ms extra to shutdown thread - /** @hide Writes the package information to file during shutdown. */ - public void writeNow() { - synchronized (sLock) { - // Must only run synchronous tasks as this runs on the ShutdownThread and no other - // thread is guaranteed to run during shutdown. - if (!mAllObservers.isEmpty()) { - mLongTaskHandler.removeCallbacks(mSaveToFile); - pruneObserversLocked(); - saveToFile(); - Slog.i(TAG, "Last write to update package durations"); - } - } - } - - /** - * Enables or disables explicit health checks. - * <p> If explicit health checks are enabled, the health check service is started. - * <p> If explicit health checks are disabled, pending explicit health check requests are - * passed and the health check service is stopped. - */ - private void setExplicitHealthCheckEnabled(boolean enabled) { - synchronized (sLock) { - mIsHealthCheckEnabled = enabled; - mHealthCheckController.setEnabled(enabled); - mSyncRequired = true; - // Prune to update internal state whenever health check is enabled/disabled - syncState("health check state " + (enabled ? "enabled" : "disabled")); - } - } - - /** - * This method should be only called on mShortTaskHandler, since it modifies - * {@link #mNumberOfNativeCrashPollsRemaining}. - */ - private void checkAndMitigateNativeCrashes() { - mNumberOfNativeCrashPollsRemaining--; - // Check if native watchdog reported a crash - if ("1".equals(SystemProperties.get("sys.init.updatable_crashing"))) { - // We rollback all available low impact rollbacks when crash is unattributable - notifyPackageFailure(Collections.EMPTY_LIST, FAILURE_REASON_NATIVE_CRASH); - // we stop polling after an attempt to execute rollback, regardless of whether the - // attempt succeeds or not - } else { - if (mNumberOfNativeCrashPollsRemaining > 0) { - mShortTaskHandler.postDelayed(() -> checkAndMitigateNativeCrashes(), - NATIVE_CRASH_POLLING_INTERVAL_MILLIS); - } - } - } - - /** - * Since this method can eventually trigger a rollback, it should be called - * only once boot has completed {@code onBootCompleted} and not earlier, because the install - * session must be entirely completed before we try to rollback. - * @hide - */ - public void scheduleCheckAndMitigateNativeCrashes() { - Slog.i(TAG, "Scheduling " + mNumberOfNativeCrashPollsRemaining + " polls to check " - + "and mitigate native crashes"); - mShortTaskHandler.post(()->checkAndMitigateNativeCrashes()); - } - - private int getUserImpactLevelLimit() { - return SystemProperties.getInt(MAJOR_USER_IMPACT_LEVEL_THRESHOLD, - DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD); - } - - private Set<String> getPackagesExemptFromImpactLevelThreshold() { - if (mPackagesExemptFromImpactLevelThreshold.isEmpty()) { - String packageNames = SystemProperties.get(PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD, - DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD); - return Set.of(packageNames.split("\\s*,\\s*")); - } - return mPackagesExemptFromImpactLevelThreshold; - } - - /** - * Indicates that a mitigation was successfully triggered or executed during - * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} or - * {@link PackageHealthObserver#onExecuteBootLoopMitigation}. - */ - public static final int MITIGATION_RESULT_SUCCESS = - ObserverMitigationResult.MITIGATION_RESULT_SUCCESS; - - /** - * Indicates that a mitigation executed during - * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} or - * {@link PackageHealthObserver#onExecuteBootLoopMitigation} was skipped. - */ - public static final int MITIGATION_RESULT_SKIPPED = - ObserverMitigationResult.MITIGATION_RESULT_SKIPPED; - - - /** - * Possible return values of the for mitigations executed during - * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} and - * {@link PackageHealthObserver#onExecuteBootLoopMitigation}. - * @hide - */ - @Retention(SOURCE) - @IntDef(prefix = "MITIGATION_RESULT_", value = { - ObserverMitigationResult.MITIGATION_RESULT_SUCCESS, - ObserverMitigationResult.MITIGATION_RESULT_SKIPPED, - }) - public @interface ObserverMitigationResult { - int MITIGATION_RESULT_SUCCESS = 1; - int MITIGATION_RESULT_SKIPPED = 2; - } - - /** - * The minimum value that can be returned by any observer. - * It represents that no mitigations were available. - */ - public static final int USER_IMPACT_THRESHOLD_NONE = - PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - - /** - * The mitigation impact beyond which the user will start noticing the mitigations. - */ - public static final int USER_IMPACT_THRESHOLD_MEDIUM = - PackageHealthObserverImpact.USER_IMPACT_LEVEL_20; - - /** - * The mitigation impact beyond which the user impact is severely high. - */ - public static final int USER_IMPACT_THRESHOLD_HIGH = - PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; - - /** - * Possible severity values of the user impact of a - * {@link PackageHealthObserver#onExecuteHealthCheckMitigation}. - * @hide - */ - @Retention(SOURCE) - @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_10, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_20, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_30, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_40, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_50, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_70, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_71, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_75, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_80, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_90, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_100}) - public @interface PackageHealthObserverImpact { - /** No action to take. */ - int USER_IMPACT_LEVEL_0 = 0; - /* Action has low user impact, user of a device will barely notice. */ - int USER_IMPACT_LEVEL_10 = 10; - /* Actions having medium user impact, user of a device will likely notice. */ - int USER_IMPACT_LEVEL_20 = 20; - int USER_IMPACT_LEVEL_30 = 30; - int USER_IMPACT_LEVEL_40 = 40; - int USER_IMPACT_LEVEL_50 = 50; - int USER_IMPACT_LEVEL_70 = 70; - /* Action has high user impact, a last resort, user of a device will be very frustrated. */ - int USER_IMPACT_LEVEL_71 = 71; - int USER_IMPACT_LEVEL_75 = 75; - int USER_IMPACT_LEVEL_80 = 80; - int USER_IMPACT_LEVEL_90 = 90; - int USER_IMPACT_LEVEL_100 = 100; - } - - /** Register instances of this interface to receive notifications on package failure. */ - @SuppressLint({"CallbackName"}) - public interface PackageHealthObserver { - /** - * Called when health check fails for the {@code versionedPackage}. - * Note: if the returned user impact is higher than {@link #USER_IMPACT_THRESHOLD_HIGH}, - * then {@link #onExecuteHealthCheckMitigation} would be called only in severe device - * conditions like boot-loop or network failure. - * - * @param versionedPackage the package that is failing. This may be null if a native - * service is crashing. - * @param failureReason the type of failure that is occurring. - * @param mitigationCount the number of times mitigation has been called for this package - * (including this time). - * - * @return any value greater than {@link #USER_IMPACT_THRESHOLD_NONE} to express - * the impact of mitigation on the user in {@link #onExecuteHealthCheckMitigation}. - * Returning {@link #USER_IMPACT_THRESHOLD_NONE} would indicate no mitigations available. - */ - @PackageHealthObserverImpact int onHealthCheckFailed( - @Nullable VersionedPackage versionedPackage, - @FailureReasons int failureReason, - int mitigationCount); - - /** - * This would be called after {@link #onHealthCheckFailed}. - * This is called only if current observer returned least impact mitigation for failed - * health check. - * - * @param versionedPackage the package that is failing. This may be null if a native - * service is crashing. - * @param failureReason the type of failure that is occurring. - * @param mitigationCount the number of times mitigation has been called for this package - * (including this time). - * @return {@link #MITIGATION_RESULT_SUCCESS} if the mitigation was successful, - * or {@link #MITIGATION_RESULT_SKIPPED} if the mitigation was skipped. - */ - @ObserverMitigationResult int onExecuteHealthCheckMitigation( - @Nullable VersionedPackage versionedPackage, - @FailureReasons int failureReason, int mitigationCount); - - - /** - * Called when the system server has booted several times within a window of time, defined - * by {@link #mBootThreshold} - * - * @param mitigationCount the number of times mitigation has been attempted for this - * boot loop (including this time). - * - * @return any value greater than {@link #USER_IMPACT_THRESHOLD_NONE} to express - * the impact of mitigation on the user in {@link #onExecuteBootLoopMitigation}. - * Returning {@link #USER_IMPACT_THRESHOLD_NONE} would indicate no mitigations available. - */ - default @PackageHealthObserverImpact int onBootLoop(int mitigationCount) { - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - } - - /** - * This would be called after {@link #onBootLoop}. - * This is called only if current observer returned least impact mitigation for fixing - * boot loop. - * - * @param mitigationCount the number of times mitigation has been attempted for this - * boot loop (including this time). - * - * @return {@link #MITIGATION_RESULT_SUCCESS} if the mitigation was successful, - * or {@link #MITIGATION_RESULT_SKIPPED} if the mitigation was skipped. - */ - default @ObserverMitigationResult int onExecuteBootLoopMitigation(int mitigationCount) { - return ObserverMitigationResult.MITIGATION_RESULT_SKIPPED; - } - - // TODO(b/120598832): Ensure uniqueness? - /** - * Identifier for the observer, should not change across device updates otherwise the - * watchdog may drop observing packages with the old name. - */ - @NonNull String getUniqueIdentifier(); - - /** - * An observer will not be pruned if this is set, even if the observer is not explicitly - * monitoring any packages. - */ - default boolean isPersistent() { - return false; - } - - /** - * Returns {@code true} if this observer wishes to observe the given package, {@code false} - * otherwise. - * Any failing package can be passed on to the observer. Currently the packages that have - * ANRs and perform {@link android.service.watchdog.ExplicitHealthCheckService} are being - * passed to observers in these API. - * - * <p> A persistent observer may choose to start observing certain failing packages, even if - * it has not explicitly asked to watch the package with {@link #startExplicitHealthCheck}. - */ - default boolean mayObservePackage(@NonNull String packageName) { - return false; - } - } - - @VisibleForTesting - long getTriggerFailureCount() { - synchronized (sLock) { - return mTriggerFailureCount; - } - } - - @VisibleForTesting - long getTriggerFailureDurationMs() { - synchronized (sLock) { - return mTriggerFailureDurationMs; - } - } - - /** - * Serializes and syncs health check requests with the {@link ExplicitHealthCheckController}. - */ - private void syncRequestsAsync() { - mShortTaskHandler.removeCallbacks(mSyncRequests); - mShortTaskHandler.post(mSyncRequests); - } - - /** - * Syncs health check requests with the {@link ExplicitHealthCheckController}. - * Calls to this must be serialized. - * - * @see #syncRequestsAsync - */ - private void syncRequests() { - boolean syncRequired = false; - synchronized (sLock) { - if (mIsPackagesReady) { - Set<String> packages = getPackagesPendingHealthChecksLocked(); - if (mSyncRequired || !packages.equals(mRequestedHealthCheckPackages) - || packages.isEmpty()) { - syncRequired = true; - mRequestedHealthCheckPackages = packages; - } - } // else, we will sync requests when packages become ready - } - - // Call outside lock to avoid holding lock when calling into the controller. - if (syncRequired) { - Slog.i(TAG, "Syncing health check requests for packages: " - + mRequestedHealthCheckPackages); - mHealthCheckController.syncRequests(mRequestedHealthCheckPackages); - mSyncRequired = false; - } - } - - /** - * Updates the observers monitoring {@code packageName} that explicit health check has passed. - * - * <p> This update is strictly for registered observers at the time of the call - * Observers that register after this signal will have no knowledge of prior signals and will - * effectively behave as if the explicit health check hasn't passed for {@code packageName}. - * - * <p> {@code packageName} can still be considered failed if reported by - * {@link #notifyPackageFailureLocked} before the package expires. - * - * <p> Triggered by components outside the system server when they are fully functional after an - * update. - */ - private void onHealthCheckPassed(String packageName) { - Slog.i(TAG, "Health check passed for package: " + packageName); - boolean isStateChanged = false; - - synchronized (sLock) { - for (int observerIdx = 0; observerIdx < mAllObservers.size(); observerIdx++) { - ObserverInternal observer = mAllObservers.valueAt(observerIdx); - MonitoredPackage monitoredPackage = observer.getMonitoredPackage(packageName); - - if (monitoredPackage != null) { - int oldState = monitoredPackage.getHealthCheckStateLocked(); - int newState = monitoredPackage.tryPassHealthCheckLocked(); - isStateChanged |= oldState != newState; - } - } - } - - if (isStateChanged) { - syncState("health check passed for " + packageName); - } - } - - private void onSupportedPackages(List<PackageConfig> supportedPackages) { - boolean isStateChanged = false; - - Map<String, Long> supportedPackageTimeouts = new ArrayMap<>(); - Iterator<PackageConfig> it = supportedPackages.iterator(); - while (it.hasNext()) { - PackageConfig info = it.next(); - supportedPackageTimeouts.put(info.getPackageName(), info.getHealthCheckTimeoutMillis()); - } - - synchronized (sLock) { - Slog.d(TAG, "Received supported packages " + supportedPackages); - Iterator<ObserverInternal> oit = mAllObservers.values().iterator(); - while (oit.hasNext()) { - Iterator<MonitoredPackage> pit = oit.next().getMonitoredPackages() - .values().iterator(); - while (pit.hasNext()) { - MonitoredPackage monitoredPackage = pit.next(); - String packageName = monitoredPackage.getName(); - int oldState = monitoredPackage.getHealthCheckStateLocked(); - int newState; - - if (supportedPackageTimeouts.containsKey(packageName)) { - // Supported packages become ACTIVE if currently INACTIVE - newState = monitoredPackage.setHealthCheckActiveLocked( - supportedPackageTimeouts.get(packageName)); - } else { - // Unsupported packages are marked as PASSED unless already FAILED - newState = monitoredPackage.tryPassHealthCheckLocked(); - } - isStateChanged |= oldState != newState; - } - } - } - - if (isStateChanged) { - syncState("updated health check supported packages " + supportedPackages); - } - } - - private void onSyncRequestNotified() { - synchronized (sLock) { - mSyncRequired = true; - syncRequestsAsync(); - } - } - - @GuardedBy("sLock") - private Set<String> getPackagesPendingHealthChecksLocked() { - Set<String> packages = new ArraySet<>(); - Iterator<ObserverInternal> oit = mAllObservers.values().iterator(); - while (oit.hasNext()) { - ObserverInternal observer = oit.next(); - Iterator<MonitoredPackage> pit = - observer.getMonitoredPackages().values().iterator(); - while (pit.hasNext()) { - MonitoredPackage monitoredPackage = pit.next(); - String packageName = monitoredPackage.getName(); - if (monitoredPackage.isPendingHealthChecksLocked()) { - packages.add(packageName); - } - } - } - return packages; - } - - /** - * Syncs the state of the observers. - * - * <p> Prunes all observers, saves new state to disk, syncs health check requests with the - * health check service and schedules the next state sync. - */ - private void syncState(String reason) { - synchronized (sLock) { - Slog.i(TAG, "Syncing state, reason: " + reason); - pruneObserversLocked(); - - saveToFileAsync(); - syncRequestsAsync(); - - // Done syncing state, schedule the next state sync - scheduleNextSyncStateLocked(); - } - } - - private void syncStateWithScheduledReason() { - syncState("scheduled"); - } - - @GuardedBy("sLock") - private void scheduleNextSyncStateLocked() { - long durationMs = getNextStateSyncMillisLocked(); - mShortTaskHandler.removeCallbacks(mSyncStateWithScheduledReason); - if (durationMs == Long.MAX_VALUE) { - Slog.i(TAG, "Cancelling state sync, nothing to sync"); - mUptimeAtLastStateSync = 0; - } else { - mUptimeAtLastStateSync = mSystemClock.uptimeMillis(); - mShortTaskHandler.postDelayed(mSyncStateWithScheduledReason, durationMs); - } - } - - /** - * Returns the next duration in millis to sync the watchdog state. - * - * @returns Long#MAX_VALUE if there are no observed packages. - */ - @GuardedBy("sLock") - private long getNextStateSyncMillisLocked() { - long shortestDurationMs = Long.MAX_VALUE; - for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) { - ArrayMap<String, MonitoredPackage> packages = mAllObservers.valueAt(oIndex) - .getMonitoredPackages(); - for (int pIndex = 0; pIndex < packages.size(); pIndex++) { - MonitoredPackage mp = packages.valueAt(pIndex); - long duration = mp.getShortestScheduleDurationMsLocked(); - if (duration < shortestDurationMs) { - shortestDurationMs = duration; - } - } - } - return shortestDurationMs; - } - - /** - * Removes {@code elapsedMs} milliseconds from all durations on monitored packages - * and updates other internal state. - */ - @GuardedBy("sLock") - private void pruneObserversLocked() { - long elapsedMs = mUptimeAtLastStateSync == 0 - ? 0 : mSystemClock.uptimeMillis() - mUptimeAtLastStateSync; - if (elapsedMs <= 0) { - Slog.i(TAG, "Not pruning observers, elapsed time: " + elapsedMs + "ms"); - return; - } - - Iterator<ObserverInternal> it = mAllObservers.values().iterator(); - while (it.hasNext()) { - ObserverInternal observer = it.next(); - Set<MonitoredPackage> failedPackages = - observer.prunePackagesLocked(elapsedMs); - if (!failedPackages.isEmpty()) { - onHealthCheckFailed(observer, failedPackages); - } - if (observer.getMonitoredPackages().isEmpty() && (observer.registeredObserver == null - || !observer.registeredObserver.isPersistent())) { - Slog.i(TAG, "Discarding observer " + observer.name + ". All packages expired"); - it.remove(); - } - } - } - - private void onHealthCheckFailed(ObserverInternal observer, - Set<MonitoredPackage> failedPackages) { - mLongTaskHandler.post(() -> { - synchronized (sLock) { - PackageHealthObserver registeredObserver = observer.registeredObserver; - if (registeredObserver != null) { - Iterator<MonitoredPackage> it = failedPackages.iterator(); - while (it.hasNext()) { - VersionedPackage versionedPkg = getVersionedPackage(it.next().getName()); - if (versionedPkg != null) { - Slog.i(TAG, - "Explicit health check failed for package " + versionedPkg); - observer.observerExecutor.execute(() -> - registeredObserver.onExecuteHealthCheckMitigation(versionedPkg, - PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK, - 1)); - } - } - } - } - }); - } - - /** - * Gets PackageInfo for the given package. Matches any user and apex. - * - * @throws PackageManager.NameNotFoundException if no such package is installed. - */ - private PackageInfo getPackageInfo(String packageName) - throws PackageManager.NameNotFoundException { - PackageManager pm = mContext.getPackageManager(); - try { - // The MATCH_ANY_USER flag doesn't mix well with the MATCH_APEX - // flag, so make two separate attempts to get the package info. - // We don't need both flags at the same time because we assume - // apex files are always installed for all users. - return pm.getPackageInfo(packageName, PackageManager.MATCH_ANY_USER); - } catch (PackageManager.NameNotFoundException e) { - return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX); - } - } - - @Nullable - private VersionedPackage getVersionedPackage(String packageName) { - final PackageManager pm = mContext.getPackageManager(); - if (pm == null || TextUtils.isEmpty(packageName)) { - return null; - } - try { - final long versionCode = getPackageInfo(packageName).getLongVersionCode(); - return new VersionedPackage(packageName, versionCode); - } catch (PackageManager.NameNotFoundException e) { - return null; - } - } - - /** - * Loads mAllObservers from file. - * - * <p>Note that this is <b>not</b> thread safe and should only called be called - * from the constructor. - */ - private void loadFromFile() { - InputStream infile = null; - mAllObservers.clear(); - try { - infile = mPolicyFile.openRead(); - final XmlPullParser parser = Xml.newPullParser(); - parser.setInput(infile, UTF_8.name()); - XmlUtils.beginDocument(parser, TAG_PACKAGE_WATCHDOG); - int outerDepth = parser.getDepth(); - while (XmlUtils.nextElementWithin(parser, outerDepth)) { - ObserverInternal observer = ObserverInternal.read(parser, this); - if (observer != null) { - mAllObservers.put(observer.name, observer); - } - } - } catch (FileNotFoundException e) { - // Nothing to monitor - } catch (Exception e) { - Slog.wtf(TAG, "Unable to read monitored packages, deleting file", e); - mPolicyFile.delete(); - } finally { - IoUtils.closeQuietly(infile); - } - } - - private void onPropertyChanged(DeviceConfig.Properties properties) { - try { - updateConfigs(); - } catch (Exception ignore) { - Slog.w(TAG, "Failed to reload device config changes"); - } - } - - /** Adds a {@link DeviceConfig#OnPropertiesChangedListener}. */ - private void setPropertyChangedListenerLocked() { - DeviceConfig.addOnPropertiesChangedListener( - DeviceConfig.NAMESPACE_ROLLBACK, - mContext.getMainExecutor(), - mOnPropertyChangedListener); - } - - @VisibleForTesting - void removePropertyChangedListener() { - DeviceConfig.removeOnPropertiesChangedListener(mOnPropertyChangedListener); - } - - /** - * Health check is enabled or disabled after reading the flags - * from DeviceConfig. - */ - @VisibleForTesting - void updateConfigs() { - synchronized (sLock) { - mTriggerFailureCount = DeviceConfig.getInt( - DeviceConfig.NAMESPACE_ROLLBACK, - PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, - DEFAULT_TRIGGER_FAILURE_COUNT); - if (mTriggerFailureCount <= 0) { - mTriggerFailureCount = DEFAULT_TRIGGER_FAILURE_COUNT; - } - - mTriggerFailureDurationMs = DeviceConfig.getInt( - DeviceConfig.NAMESPACE_ROLLBACK, - PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS, - DEFAULT_TRIGGER_FAILURE_DURATION_MS); - if (mTriggerFailureDurationMs <= 0) { - mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS; - } - - setExplicitHealthCheckEnabled(DeviceConfig.getBoolean( - DeviceConfig.NAMESPACE_ROLLBACK, - PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED, - DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED)); - } - } - - /** - * Persists mAllObservers to file. Threshold information is ignored. - */ - private boolean saveToFile() { - Slog.i(TAG, "Saving observer state to file"); - synchronized (sLock) { - FileOutputStream stream; - try { - stream = mPolicyFile.startWrite(); - } catch (IOException e) { - Slog.w(TAG, "Cannot update monitored packages", e); - return false; - } - - try { - XmlSerializer out = new FastXmlSerializer(); - out.setOutput(stream, UTF_8.name()); - out.startDocument(null, true); - out.startTag(null, TAG_PACKAGE_WATCHDOG); - out.attribute(null, ATTR_VERSION, Integer.toString(DB_VERSION)); - for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) { - mAllObservers.valueAt(oIndex).writeLocked(out); - } - out.endTag(null, TAG_PACKAGE_WATCHDOG); - out.endDocument(); - mPolicyFile.finishWrite(stream); - return true; - } catch (IOException e) { - Slog.w(TAG, "Failed to save monitored packages, restoring backup", e); - mPolicyFile.failWrite(stream); - return false; - } - } - } - - private void saveToFileAsync() { - if (!mLongTaskHandler.hasCallbacks(mSaveToFile)) { - mLongTaskHandler.post(mSaveToFile); - } - } - - /** @hide Convert a {@code LongArrayQueue} to a String of comma-separated values. */ - public static String longArrayQueueToString(LongArrayQueue queue) { - if (queue.size() > 0) { - StringBuilder sb = new StringBuilder(); - sb.append(queue.get(0)); - for (int i = 1; i < queue.size(); i++) { - sb.append(","); - sb.append(queue.get(i)); - } - return sb.toString(); - } - return ""; - } - - /** @hide Parse a comma-separated String of longs into a LongArrayQueue. */ - public static LongArrayQueue parseLongArrayQueue(String commaSeparatedValues) { - LongArrayQueue result = new LongArrayQueue(); - if (!TextUtils.isEmpty(commaSeparatedValues)) { - String[] values = commaSeparatedValues.split(","); - for (String value : values) { - result.addLast(Long.parseLong(value)); - } - } - return result; - } - - - /** Dump status of every observer in mAllObservers. */ - public void dump(@NonNull PrintWriter pw) { - if (Flags.synchronousRebootInRescueParty() && isRecoveryTriggeredReboot()) { - dumpInternal(pw); - } else { - synchronized (sLock) { - dumpInternal(pw); - } - } - } - - /** - * Check if we're currently attempting to reboot during mitigation. This method must return - * true if triggered reboot early during a boot loop, since the device will not be fully booted - * at this time. - */ - public static boolean isRecoveryTriggeredReboot() { - return isFactoryResetPropertySet() || isRebootPropertySet(); - } - - private static boolean isFactoryResetPropertySet() { - return CrashRecoveryProperties.attemptingFactoryReset().orElse(false); - } - - private static boolean isRebootPropertySet() { - return CrashRecoveryProperties.attemptingReboot().orElse(false); - } - - private void dumpInternal(@NonNull PrintWriter pw) { - IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); - ipw.println("Package Watchdog status"); - ipw.increaseIndent(); - synchronized (sLock) { - for (String observerName : mAllObservers.keySet()) { - ipw.println("Observer name: " + observerName); - ipw.increaseIndent(); - ObserverInternal observerInternal = mAllObservers.get(observerName); - observerInternal.dump(ipw); - ipw.decreaseIndent(); - } - } - ipw.decreaseIndent(); - dumpCrashRecoveryEvents(ipw); - } - - @VisibleForTesting - @GuardedBy("sLock") - void registerObserverInternal(ObserverInternal observerInternal) { - mAllObservers.put(observerInternal.name, observerInternal); - } - - /** - * Represents an observer monitoring a set of packages along with the failure thresholds for - * each package. - * - * <p> Note, the PackageWatchdog#sLock must always be held when reading or writing - * instances of this class. - */ - static class ObserverInternal { - public final String name; - @GuardedBy("sLock") - private final ArrayMap<String, MonitoredPackage> mPackages = new ArrayMap<>(); - @Nullable - @GuardedBy("sLock") - public PackageHealthObserver registeredObserver; - public Executor observerExecutor; - private int mMitigationCount; - - ObserverInternal(String name, List<MonitoredPackage> packages) { - this(name, packages, /*mitigationCount=*/ 0); - } - - ObserverInternal(String name, List<MonitoredPackage> packages, int mitigationCount) { - this.name = name; - updatePackagesLocked(packages); - this.mMitigationCount = mitigationCount; - } - - /** - * Writes important {@link MonitoredPackage} details for this observer to file. - * Does not persist any package failure thresholds. - */ - @GuardedBy("sLock") - public boolean writeLocked(XmlSerializer out) { - try { - out.startTag(null, TAG_OBSERVER); - out.attribute(null, ATTR_NAME, name); - if (Flags.recoverabilityDetection()) { - out.attribute(null, ATTR_MITIGATION_COUNT, Integer.toString(mMitigationCount)); - } - for (int i = 0; i < mPackages.size(); i++) { - MonitoredPackage p = mPackages.valueAt(i); - p.writeLocked(out); - } - out.endTag(null, TAG_OBSERVER); - return true; - } catch (IOException e) { - Slog.w(TAG, "Cannot save observer", e); - return false; - } - } - - public int getBootMitigationCount() { - return mMitigationCount; - } - - public void setBootMitigationCount(int mitigationCount) { - mMitigationCount = mitigationCount; - } - - @GuardedBy("sLock") - public void updatePackagesLocked(List<MonitoredPackage> packages) { - for (int pIndex = 0; pIndex < packages.size(); pIndex++) { - MonitoredPackage p = packages.get(pIndex); - MonitoredPackage existingPackage = getMonitoredPackage(p.getName()); - if (existingPackage != null) { - existingPackage.updateHealthCheckDuration(p.mDurationMs); - } else { - putMonitoredPackage(p); - } - } - } - - /** - * Reduces the monitoring durations of all packages observed by this observer by - * {@code elapsedMs}. If any duration is less than 0, the package is removed from - * observation. If any health check duration is less than 0, the health check result - * is evaluated. - * - * @return a {@link Set} of packages that were removed from the observer without explicit - * health check passing, or an empty list if no package expired for which an explicit health - * check was still pending - */ - @GuardedBy("sLock") - private Set<MonitoredPackage> prunePackagesLocked(long elapsedMs) { - Set<MonitoredPackage> failedPackages = new ArraySet<>(); - Iterator<MonitoredPackage> it = mPackages.values().iterator(); - while (it.hasNext()) { - MonitoredPackage p = it.next(); - int oldState = p.getHealthCheckStateLocked(); - int newState = p.handleElapsedTimeLocked(elapsedMs); - if (oldState != HealthCheckState.FAILED - && newState == HealthCheckState.FAILED) { - Slog.i(TAG, "Package " + p.getName() + " failed health check"); - failedPackages.add(p); - } - if (p.isExpiredLocked()) { - it.remove(); - } - } - return failedPackages; - } - - /** - * Increments failure counts of {@code packageName}. - * @returns {@code true} if failure threshold is exceeded, {@code false} otherwise - * @hide - */ - @GuardedBy("sLock") - public boolean notifyPackageFailureLocked(String packageName) { - if (getMonitoredPackage(packageName) == null && registeredObserver.isPersistent() - && registeredObserver.mayObservePackage(packageName)) { - putMonitoredPackage(sPackageWatchdog.newMonitoredPackage( - packageName, DEFAULT_OBSERVING_DURATION_MS, false)); - } - MonitoredPackage p = getMonitoredPackage(packageName); - if (p != null) { - return p.onFailureLocked(); - } - return false; - } - - /** - * Returns the map of packages monitored by this observer. - * - * @return a mapping of package names to {@link MonitoredPackage} objects. - */ - @GuardedBy("sLock") - public ArrayMap<String, MonitoredPackage> getMonitoredPackages() { - return mPackages; - } - - /** - * Returns the {@link MonitoredPackage} associated with a given package name if the - * package is being monitored by this observer. - * - * @param packageName: the name of the package. - * @return the {@link MonitoredPackage} object associated with the package name if one - * exists, {@code null} otherwise. - */ - @GuardedBy("sLock") - @Nullable - public MonitoredPackage getMonitoredPackage(String packageName) { - return mPackages.get(packageName); - } - - /** - * Associates a {@link MonitoredPackage} with the observer. - * - * @param p: the {@link MonitoredPackage} to store. - */ - @GuardedBy("sLock") - public void putMonitoredPackage(MonitoredPackage p) { - mPackages.put(p.getName(), p); - } - - /** - * Returns one ObserverInternal from the {@code parser} and advances its state. - * - * <p>Note that this method is <b>not</b> thread safe. It should only be called from - * #loadFromFile which in turn is only called on construction of the - * singleton PackageWatchdog. - **/ - public static ObserverInternal read(XmlPullParser parser, PackageWatchdog watchdog) { - String observerName = null; - int observerMitigationCount = 0; - if (TAG_OBSERVER.equals(parser.getName())) { - observerName = parser.getAttributeValue(null, ATTR_NAME); - if (TextUtils.isEmpty(observerName)) { - Slog.wtf(TAG, "Unable to read observer name"); - return null; - } - } - List<MonitoredPackage> packages = new ArrayList<>(); - int innerDepth = parser.getDepth(); - try { - if (Flags.recoverabilityDetection()) { - try { - observerMitigationCount = Integer.parseInt( - parser.getAttributeValue(null, ATTR_MITIGATION_COUNT)); - } catch (Exception e) { - Slog.i( - TAG, - "ObserverInternal mitigation count was not present."); - } - } - while (XmlUtils.nextElementWithin(parser, innerDepth)) { - if (TAG_PACKAGE.equals(parser.getName())) { - try { - MonitoredPackage pkg = watchdog.parseMonitoredPackage(parser); - if (pkg != null) { - packages.add(pkg); - } - } catch (NumberFormatException e) { - Slog.wtf(TAG, "Skipping package for observer " + observerName, e); - continue; - } - } - } - } catch (XmlPullParserException | IOException e) { - Slog.wtf(TAG, "Unable to read observer " + observerName, e); - return null; - } - if (packages.isEmpty()) { - return null; - } - return new ObserverInternal(observerName, packages, observerMitigationCount); - } - - /** Dumps information about this observer and the packages it watches. */ - public void dump(IndentingPrintWriter pw) { - boolean isPersistent = registeredObserver != null && registeredObserver.isPersistent(); - pw.println("Persistent: " + isPersistent); - for (String packageName : mPackages.keySet()) { - MonitoredPackage p = getMonitoredPackage(packageName); - pw.println(packageName + ": "); - pw.increaseIndent(); - pw.println("# Failures: " + p.mFailureHistory.size()); - pw.println("Monitoring duration remaining: " + p.mDurationMs + "ms"); - pw.println("Explicit health check duration: " + p.mHealthCheckDurationMs + "ms"); - pw.println("Health check state: " + p.toString(p.mHealthCheckState)); - pw.decreaseIndent(); - } - } - } - - /** @hide */ - @Retention(SOURCE) - @IntDef(value = { - HealthCheckState.ACTIVE, - HealthCheckState.INACTIVE, - HealthCheckState.PASSED, - HealthCheckState.FAILED}) - public @interface HealthCheckState { - // The package has not passed health check but has requested a health check - int ACTIVE = 0; - // The package has not passed health check and has not requested a health check - int INACTIVE = 1; - // The package has passed health check - int PASSED = 2; - // The package has failed health check - int FAILED = 3; - } - - MonitoredPackage newMonitoredPackage( - String name, long durationMs, boolean hasPassedHealthCheck) { - return newMonitoredPackage(name, durationMs, Long.MAX_VALUE, hasPassedHealthCheck, - new LongArrayQueue()); - } - - MonitoredPackage newMonitoredPackage(String name, long durationMs, long healthCheckDurationMs, - boolean hasPassedHealthCheck, LongArrayQueue mitigationCalls) { - return new MonitoredPackage(name, durationMs, healthCheckDurationMs, - hasPassedHealthCheck, mitigationCalls); - } - - MonitoredPackage parseMonitoredPackage(XmlPullParser parser) - throws XmlPullParserException { - String packageName = parser.getAttributeValue(null, ATTR_NAME); - long duration = Long.parseLong(parser.getAttributeValue(null, ATTR_DURATION)); - long healthCheckDuration = Long.parseLong(parser.getAttributeValue(null, - ATTR_EXPLICIT_HEALTH_CHECK_DURATION)); - boolean hasPassedHealthCheck = Boolean.parseBoolean(parser.getAttributeValue(null, - ATTR_PASSED_HEALTH_CHECK)); - LongArrayQueue mitigationCalls = parseLongArrayQueue( - parser.getAttributeValue(null, ATTR_MITIGATION_CALLS)); - return newMonitoredPackage(packageName, - duration, healthCheckDuration, hasPassedHealthCheck, mitigationCalls); - } - - /** - * Represents a package and its health check state along with the time - * it should be monitored for. - * - * <p> Note, the PackageWatchdog#sLock must always be held when reading or writing - * instances of this class. - */ - class MonitoredPackage { - private final String mPackageName; - // Times when package failures happen sorted in ascending order - @GuardedBy("sLock") - private final LongArrayQueue mFailureHistory = new LongArrayQueue(); - // Times when an observer was called to mitigate this package's failure. Sorted in - // ascending order. - @GuardedBy("sLock") - private final LongArrayQueue mMitigationCalls; - // One of STATE_[ACTIVE|INACTIVE|PASSED|FAILED]. Updated on construction and after - // methods that could change the health check state: handleElapsedTimeLocked and - // tryPassHealthCheckLocked - private int mHealthCheckState = HealthCheckState.INACTIVE; - // Whether an explicit health check has passed. - // This value in addition with mHealthCheckDurationMs determines the health check state - // of the package, see #getHealthCheckStateLocked - @GuardedBy("sLock") - private boolean mHasPassedHealthCheck; - // System uptime duration to monitor package. - @GuardedBy("sLock") - private long mDurationMs; - // System uptime duration to check the result of an explicit health check - // Initially, MAX_VALUE until we get a value from the health check service - // and request health checks. - // This value in addition with mHasPassedHealthCheck determines the health check state - // of the package, see #getHealthCheckStateLocked - @GuardedBy("sLock") - private long mHealthCheckDurationMs = Long.MAX_VALUE; - - MonitoredPackage(String packageName, long durationMs, - long healthCheckDurationMs, boolean hasPassedHealthCheck, - LongArrayQueue mitigationCalls) { - mPackageName = packageName; - mDurationMs = durationMs; - mHealthCheckDurationMs = healthCheckDurationMs; - mHasPassedHealthCheck = hasPassedHealthCheck; - mMitigationCalls = mitigationCalls; - updateHealthCheckStateLocked(); - } - - /** Writes the salient fields to disk using {@code out}. - * @hide - */ - @GuardedBy("sLock") - public void writeLocked(XmlSerializer out) throws IOException { - out.startTag(null, TAG_PACKAGE); - out.attribute(null, ATTR_NAME, getName()); - out.attribute(null, ATTR_DURATION, Long.toString(mDurationMs)); - out.attribute(null, ATTR_EXPLICIT_HEALTH_CHECK_DURATION, - Long.toString(mHealthCheckDurationMs)); - out.attribute(null, ATTR_PASSED_HEALTH_CHECK, Boolean.toString(mHasPassedHealthCheck)); - LongArrayQueue normalizedCalls = normalizeMitigationCalls(); - out.attribute(null, ATTR_MITIGATION_CALLS, longArrayQueueToString(normalizedCalls)); - out.endTag(null, TAG_PACKAGE); - } - - /** - * Increment package failures or resets failure count depending on the last package failure. - * - * @return {@code true} if failure count exceeds a threshold, {@code false} otherwise - */ - @GuardedBy("sLock") - public boolean onFailureLocked() { - // Sliding window algorithm: find out if there exists a window containing failures >= - // mTriggerFailureCount. - final long now = mSystemClock.uptimeMillis(); - mFailureHistory.addLast(now); - while (now - mFailureHistory.peekFirst() > mTriggerFailureDurationMs) { - // Prune values falling out of the window - mFailureHistory.removeFirst(); - } - boolean failed = mFailureHistory.size() >= mTriggerFailureCount; - if (failed) { - mFailureHistory.clear(); - } - return failed; - } - - /** - * Notes the timestamp of a mitigation call into the observer. - */ - @GuardedBy("sLock") - public void noteMitigationCallLocked() { - mMitigationCalls.addLast(mSystemClock.uptimeMillis()); - } - - /** - * Prunes any mitigation calls outside of the de-escalation window, and returns the - * number of calls that are in the window afterwards. - * - * @return the number of mitigation calls made in the de-escalation window. - */ - @GuardedBy("sLock") - public int getMitigationCountLocked() { - try { - final long now = mSystemClock.uptimeMillis(); - while (now - mMitigationCalls.peekFirst() > DEFAULT_DEESCALATION_WINDOW_MS) { - mMitigationCalls.removeFirst(); - } - } catch (NoSuchElementException ignore) { - } - - return mMitigationCalls.size(); - } - - /** - * Before writing to disk, make the mitigation call timestamps relative to the current - * system uptime. This is because they need to be relative to the uptime which will reset - * at the next boot. - * - * @return a LongArrayQueue of the mitigation calls relative to the current system uptime. - */ - @GuardedBy("sLock") - public LongArrayQueue normalizeMitigationCalls() { - LongArrayQueue normalized = new LongArrayQueue(); - final long now = mSystemClock.uptimeMillis(); - for (int i = 0; i < mMitigationCalls.size(); i++) { - normalized.addLast(mMitigationCalls.get(i) - now); - } - return normalized; - } - - /** - * Sets the initial health check duration. - * - * @return the new health check state - */ - @GuardedBy("sLock") - public int setHealthCheckActiveLocked(long initialHealthCheckDurationMs) { - if (initialHealthCheckDurationMs <= 0) { - Slog.wtf(TAG, "Cannot set non-positive health check duration " - + initialHealthCheckDurationMs + "ms for package " + getName() - + ". Using total duration " + mDurationMs + "ms instead"); - initialHealthCheckDurationMs = mDurationMs; - } - if (mHealthCheckState == HealthCheckState.INACTIVE) { - // Transitions to ACTIVE - mHealthCheckDurationMs = initialHealthCheckDurationMs; - } - return updateHealthCheckStateLocked(); - } - - /** - * Updates the monitoring durations of the package. - * - * @return the new health check state - */ - @GuardedBy("sLock") - public int handleElapsedTimeLocked(long elapsedMs) { - if (elapsedMs <= 0) { - Slog.w(TAG, "Cannot handle non-positive elapsed time for package " + getName()); - return mHealthCheckState; - } - // Transitions to FAILED if now <= 0 and health check not passed - mDurationMs -= elapsedMs; - if (mHealthCheckState == HealthCheckState.ACTIVE) { - // We only update health check durations if we have #setHealthCheckActiveLocked - // This ensures we don't leave the INACTIVE state for an unexpected elapsed time - // Transitions to FAILED if now <= 0 and health check not passed - mHealthCheckDurationMs -= elapsedMs; - } - return updateHealthCheckStateLocked(); - } - - /** Explicitly update the monitoring duration of the package. */ - @GuardedBy("sLock") - public void updateHealthCheckDuration(long newDurationMs) { - mDurationMs = newDurationMs; - } - - /** - * Marks the health check as passed and transitions to {@link HealthCheckState.PASSED} - * if not yet {@link HealthCheckState.FAILED}. - * - * @return the new {@link HealthCheckState health check state} - */ - @GuardedBy("sLock") - @HealthCheckState - public int tryPassHealthCheckLocked() { - if (mHealthCheckState != HealthCheckState.FAILED) { - // FAILED is a final state so only pass if we haven't failed - // Transition to PASSED - mHasPassedHealthCheck = true; - } - return updateHealthCheckStateLocked(); - } - - /** Returns the monitored package name. */ - private String getName() { - return mPackageName; - } - - /** - * Returns the current {@link HealthCheckState health check state}. - */ - @GuardedBy("sLock") - @HealthCheckState - public int getHealthCheckStateLocked() { - return mHealthCheckState; - } - - /** - * Returns the shortest duration before the package should be scheduled for a prune. - * - * @return the duration or {@link Long#MAX_VALUE} if the package should not be scheduled - */ - @GuardedBy("sLock") - public long getShortestScheduleDurationMsLocked() { - // Consider health check duration only if #isPendingHealthChecksLocked is true - return Math.min(toPositive(mDurationMs), - isPendingHealthChecksLocked() - ? toPositive(mHealthCheckDurationMs) : Long.MAX_VALUE); - } - - /** - * Returns {@code true} if the total duration left to monitor the package is less than or - * equal to 0 {@code false} otherwise. - */ - @GuardedBy("sLock") - public boolean isExpiredLocked() { - return mDurationMs <= 0; - } - - /** - * Returns {@code true} if the package, {@link #getName} is expecting health check results - * {@code false} otherwise. - */ - @GuardedBy("sLock") - public boolean isPendingHealthChecksLocked() { - return mHealthCheckState == HealthCheckState.ACTIVE - || mHealthCheckState == HealthCheckState.INACTIVE; - } - - /** - * Updates the health check state based on {@link #mHasPassedHealthCheck} - * and {@link #mHealthCheckDurationMs}. - * - * @return the new {@link HealthCheckState health check state} - */ - @GuardedBy("sLock") - @HealthCheckState - private int updateHealthCheckStateLocked() { - int oldState = mHealthCheckState; - if (mHasPassedHealthCheck) { - // Set final state first to avoid ambiguity - mHealthCheckState = HealthCheckState.PASSED; - } else if (mHealthCheckDurationMs <= 0 || mDurationMs <= 0) { - // Set final state first to avoid ambiguity - mHealthCheckState = HealthCheckState.FAILED; - } else if (mHealthCheckDurationMs == Long.MAX_VALUE) { - mHealthCheckState = HealthCheckState.INACTIVE; - } else { - mHealthCheckState = HealthCheckState.ACTIVE; - } - - if (oldState != mHealthCheckState) { - Slog.i(TAG, "Updated health check state for package " + getName() + ": " - + toString(oldState) + " -> " + toString(mHealthCheckState)); - } - return mHealthCheckState; - } - - /** Returns a {@link String} representation of the current health check state. */ - private String toString(@HealthCheckState int state) { - switch (state) { - case HealthCheckState.ACTIVE: - return "ACTIVE"; - case HealthCheckState.INACTIVE: - return "INACTIVE"; - case HealthCheckState.PASSED: - return "PASSED"; - case HealthCheckState.FAILED: - return "FAILED"; - default: - return "UNKNOWN"; - } - } - - /** Returns {@code value} if it is greater than 0 or {@link Long#MAX_VALUE} otherwise. */ - private long toPositive(long value) { - return value > 0 ? value : Long.MAX_VALUE; - } - - /** Compares the equality of this object with another {@link MonitoredPackage}. */ - @VisibleForTesting - boolean isEqualTo(MonitoredPackage pkg) { - return (getName().equals(pkg.getName())) - && mDurationMs == pkg.mDurationMs - && mHasPassedHealthCheck == pkg.mHasPassedHealthCheck - && mHealthCheckDurationMs == pkg.mHealthCheckDurationMs - && (mMitigationCalls.toString()).equals(pkg.mMitigationCalls.toString()); - } - } - - @GuardedBy("sLock") - @SuppressWarnings("GuardedBy") - void saveAllObserversBootMitigationCountToMetadata(String filePath) { - HashMap<String, Integer> bootMitigationCounts = new HashMap<>(); - for (int i = 0; i < mAllObservers.size(); i++) { - final ObserverInternal observer = mAllObservers.valueAt(i); - bootMitigationCounts.put(observer.name, observer.getBootMitigationCount()); - } - - FileOutputStream fileStream = null; - ObjectOutputStream objectStream = null; - try { - fileStream = new FileOutputStream(new File(filePath)); - objectStream = new ObjectOutputStream(fileStream); - objectStream.writeObject(bootMitigationCounts); - objectStream.flush(); - } catch (Exception e) { - Slog.i(TAG, "Could not save observers metadata to file: " + e); - return; - } finally { - IoUtils.closeQuietly(objectStream); - IoUtils.closeQuietly(fileStream); - } - } - - /** - * Handles the thresholding logic for system server boots. - */ - class BootThreshold { - - private final int mBootTriggerCount; - private final long mTriggerWindow; - - BootThreshold(int bootTriggerCount, long triggerWindow) { - this.mBootTriggerCount = bootTriggerCount; - this.mTriggerWindow = triggerWindow; - } - - public void reset() { - setStart(0); - setCount(0); - } - - protected int getCount() { - return CrashRecoveryProperties.rescueBootCount().orElse(0); - } - - protected void setCount(int count) { - CrashRecoveryProperties.rescueBootCount(count); - } - - public long getStart() { - return CrashRecoveryProperties.rescueBootStart().orElse(0L); - } - - public int getMitigationCount() { - return CrashRecoveryProperties.bootMitigationCount().orElse(0); - } - - public void setStart(long start) { - CrashRecoveryProperties.rescueBootStart(getStartTime(start)); - } - - public void setMitigationStart(long start) { - CrashRecoveryProperties.bootMitigationStart(getStartTime(start)); - } - - public long getMitigationStart() { - return CrashRecoveryProperties.bootMitigationStart().orElse(0L); - } - - public void setMitigationCount(int count) { - CrashRecoveryProperties.bootMitigationCount(count); - } - - private static long constrain(long amount, long low, long high) { - return amount < low ? low : (amount > high ? high : amount); - } - - public long getStartTime(long start) { - final long now = mSystemClock.uptimeMillis(); - return constrain(start, 0, now); - } - - public void saveMitigationCountToMetadata() { - try (BufferedWriter writer = new BufferedWriter(new FileWriter(METADATA_FILE))) { - writer.write(String.valueOf(getMitigationCount())); - } catch (Exception e) { - Slog.e(TAG, "Could not save metadata to file: " + e); - } - } - - public void readMitigationCountFromMetadataIfNecessary() { - File bootPropsFile = new File(METADATA_FILE); - if (bootPropsFile.exists()) { - try (BufferedReader reader = new BufferedReader(new FileReader(METADATA_FILE))) { - String mitigationCount = reader.readLine(); - setMitigationCount(Integer.parseInt(mitigationCount)); - bootPropsFile.delete(); - } catch (Exception e) { - Slog.i(TAG, "Could not read metadata file: " + e); - } - } - } - - - /** Increments the boot counter, and returns whether the device is bootlooping. */ - @GuardedBy("sLock") - public boolean incrementAndTest() { - if (Flags.recoverabilityDetection()) { - readAllObserversBootMitigationCountIfNecessary(METADATA_FILE); - } else { - readMitigationCountFromMetadataIfNecessary(); - } - - final long now = mSystemClock.uptimeMillis(); - if (now - getStart() < 0) { - Slog.e(TAG, "Window was less than zero. Resetting start to current time."); - setStart(now); - setMitigationStart(now); - } - if (now - getMitigationStart() > DEFAULT_DEESCALATION_WINDOW_MS) { - setMitigationStart(now); - if (Flags.recoverabilityDetection()) { - resetAllObserversBootMitigationCount(); - } else { - setMitigationCount(0); - } - } - final long window = now - getStart(); - if (window >= mTriggerWindow) { - setCount(1); - setStart(now); - return false; - } else { - int count = getCount() + 1; - setCount(count); - EventLog.writeEvent(LOG_TAG_RESCUE_NOTE, Process.ROOT_UID, count, window); - if (Flags.recoverabilityDetection()) { - // After a reboot (e.g. by WARM_REBOOT or mainline rollback) we apply - // mitigations without waiting for DEFAULT_BOOT_LOOP_TRIGGER_COUNT. - return (count >= mBootTriggerCount) - || (performedMitigationsDuringWindow() && count > 1); - } - return count >= mBootTriggerCount; - } - } - - @GuardedBy("sLock") - private boolean performedMitigationsDuringWindow() { - for (ObserverInternal observerInternal: mAllObservers.values()) { - if (observerInternal.getBootMitigationCount() > 0) { - return true; - } - } - return false; - } - - @GuardedBy("sLock") - private void resetAllObserversBootMitigationCount() { - for (int i = 0; i < mAllObservers.size(); i++) { - final ObserverInternal observer = mAllObservers.valueAt(i); - observer.setBootMitigationCount(0); - } - saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); - } - - @GuardedBy("sLock") - @SuppressWarnings("GuardedBy") - void readAllObserversBootMitigationCountIfNecessary(String filePath) { - File metadataFile = new File(filePath); - if (metadataFile.exists()) { - FileInputStream fileStream = null; - ObjectInputStream objectStream = null; - HashMap<String, Integer> bootMitigationCounts = null; - try { - fileStream = new FileInputStream(metadataFile); - objectStream = new ObjectInputStream(fileStream); - bootMitigationCounts = - (HashMap<String, Integer>) objectStream.readObject(); - } catch (Exception e) { - Slog.i(TAG, "Could not read observer metadata file: " + e); - return; - } finally { - IoUtils.closeQuietly(objectStream); - IoUtils.closeQuietly(fileStream); - } - - if (bootMitigationCounts == null || bootMitigationCounts.isEmpty()) { - Slog.i(TAG, "No observer in metadata file"); - return; - } - for (int i = 0; i < mAllObservers.size(); i++) { - final ObserverInternal observer = mAllObservers.valueAt(i); - if (bootMitigationCounts.containsKey(observer.name)) { - observer.setBootMitigationCount( - bootMitigationCounts.get(observer.name)); - } - } - } - } - } - - /** - * Register broadcast receiver for shutdown. - * We would save the observer state to persist across boots. - * - * @hide - */ - public void registerShutdownBroadcastReceiver() { - BroadcastReceiver shutdownEventReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - // Only write if intent is relevant to device reboot or shutdown. - String intentAction = intent.getAction(); - if (ACTION_REBOOT.equals(intentAction) - || ACTION_SHUTDOWN.equals(intentAction)) { - writeNow(); - } - } - }; - - // Setup receiver for device reboots or shutdowns. - IntentFilter filter = new IntentFilter(ACTION_REBOOT); - filter.addAction(ACTION_SHUTDOWN); - mContext.registerReceiverForAllUsers(shutdownEventReceiver, filter, null, - /* run on main thread */ null); - } -} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java deleted file mode 100644 index 846da194b3c3..000000000000 --- a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java +++ /dev/null @@ -1,861 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server; - -import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SKIPPED; -import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SUCCESS; -import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.VersionedPackage; -import android.crashrecovery.flags.Flags; -import android.os.Build; -import android.os.PowerManager; -import android.os.RecoverySystem; -import android.os.SystemClock; -import android.os.SystemProperties; -import android.provider.Settings; -import android.sysprop.CrashRecoveryProperties; -import android.text.TextUtils; -import android.util.EventLog; -import android.util.FileUtils; -import android.util.Log; -import android.util.Slog; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; -import com.android.server.PackageWatchdog.FailureReasons; -import com.android.server.PackageWatchdog.PackageHealthObserver; -import com.android.server.PackageWatchdog.PackageHealthObserverImpact; -import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; - -import java.io.File; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Utilities to help rescue the system from crash loops. Callers are expected to - * report boot events and persistent app crashes, and if they happen frequently - * enough this class will slowly escalate through several rescue operations - * before finally rebooting and prompting the user if they want to wipe data as - * a last resort. - * - * @hide - */ -public class RescueParty { - @VisibleForTesting - static final String PROP_ENABLE_RESCUE = "persist.sys.enable_rescue"; - @VisibleForTesting - static final int LEVEL_NONE = 0; - @VisibleForTesting - static final int LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 1; - @VisibleForTesting - static final int LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 2; - @VisibleForTesting - static final int LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 3; - @VisibleForTesting - static final int LEVEL_WARM_REBOOT = 4; - @VisibleForTesting - static final int LEVEL_FACTORY_RESET = 5; - @VisibleForTesting - static final int RESCUE_LEVEL_NONE = 0; - @VisibleForTesting - static final int RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET = 1; - @VisibleForTesting - static final int RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET = 2; - @VisibleForTesting - static final int RESCUE_LEVEL_WARM_REBOOT = 3; - @VisibleForTesting - static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 4; - @VisibleForTesting - static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 5; - @VisibleForTesting - static final int RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 6; - @VisibleForTesting - static final int RESCUE_LEVEL_FACTORY_RESET = 7; - - @IntDef(prefix = { "RESCUE_LEVEL_" }, value = { - RESCUE_LEVEL_NONE, - RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET, - RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET, - RESCUE_LEVEL_WARM_REBOOT, - RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS, - RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES, - RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS, - RESCUE_LEVEL_FACTORY_RESET - }) - @Retention(RetentionPolicy.SOURCE) - @interface RescueLevels {} - - @VisibleForTesting - static final String RESCUE_NON_REBOOT_LEVEL_LIMIT = "persist.sys.rescue_non_reboot_level_limit"; - @VisibleForTesting - static final int DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT = RESCUE_LEVEL_WARM_REBOOT - 1; - @VisibleForTesting - static final String TAG = "RescueParty"; - @VisibleForTesting - static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2); - @VisibleForTesting - static final int DEVICE_CONFIG_RESET_MODE = Settings.RESET_MODE_TRUSTED_DEFAULTS; - // The DeviceConfig namespace containing all RescueParty switches. - @VisibleForTesting - static final String NAMESPACE_CONFIGURATION = "configuration"; - @VisibleForTesting - static final String NAMESPACE_TO_PACKAGE_MAPPING_FLAG = - "namespace_to_package_mapping"; - @VisibleForTesting - static final long DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN = 1440; - - private static final String NAME = "rescue-party-observer"; - - private static final String PROP_DISABLE_RESCUE = "persist.sys.disable_rescue"; - private static final String PROP_VIRTUAL_DEVICE = "ro.hardware.virtual_device"; - private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG = - "persist.device_config.configuration.disable_rescue_party"; - private static final String PROP_DISABLE_FACTORY_RESET_FLAG = - "persist.device_config.configuration.disable_rescue_party_factory_reset"; - private static final String PROP_THROTTLE_DURATION_MIN_FLAG = - "persist.device_config.configuration.rescue_party_throttle_duration_min"; - - private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT - | ApplicationInfo.FLAG_SYSTEM; - - /** - * EventLog tags used when logging into the event log. Note the values must be sync with - * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct - * name translation. - */ - private static final int LOG_TAG_RESCUE_SUCCESS = 2902; - private static final int LOG_TAG_RESCUE_FAILURE = 2903; - - /** Register the Rescue Party observer as a Package Watchdog health observer */ - public static void registerHealthObserver(Context context) { - PackageWatchdog.getInstance(context).registerHealthObserver( - context.getMainExecutor(), RescuePartyObserver.getInstance(context)); - } - - private static boolean isDisabled() { - // Check if we're explicitly enabled for testing - if (SystemProperties.getBoolean(PROP_ENABLE_RESCUE, false)) { - return false; - } - - // We're disabled if the DeviceConfig disable flag is set to true. - // This is in case that an emergency rollback of the feature is needed. - if (SystemProperties.getBoolean(PROP_DEVICE_CONFIG_DISABLE_FLAG, false)) { - Slog.v(TAG, "Disabled because of DeviceConfig flag"); - return true; - } - - // We're disabled on all engineering devices - if (Build.TYPE.equals("eng")) { - Slog.v(TAG, "Disabled because of eng build"); - return true; - } - - // We're disabled on userdebug devices connected over USB, since that's - // a decent signal that someone is actively trying to debug the device, - // or that it's in a lab environment. - if (Build.TYPE.equals("userdebug") && isUsbActive()) { - Slog.v(TAG, "Disabled because of active USB connection"); - return true; - } - - // One last-ditch check - if (SystemProperties.getBoolean(PROP_DISABLE_RESCUE, false)) { - Slog.v(TAG, "Disabled because of manual property"); - return true; - } - - return false; - } - - /** - * Check if we're currently attempting to reboot for a factory reset. This method must - * return true if RescueParty tries to reboot early during a boot loop, since the device - * will not be fully booted at this time. - */ - public static boolean isRecoveryTriggeredReboot() { - return isFactoryResetPropertySet() || isRebootPropertySet(); - } - - static boolean isFactoryResetPropertySet() { - return CrashRecoveryProperties.attemptingFactoryReset().orElse(false); - } - - static boolean isRebootPropertySet() { - return CrashRecoveryProperties.attemptingReboot().orElse(false); - } - - protected static long getLastFactoryResetTimeMs() { - return CrashRecoveryProperties.lastFactoryResetTimeMs().orElse(0L); - } - - protected static int getMaxRescueLevelAttempted() { - return CrashRecoveryProperties.maxRescueLevelAttempted().orElse(LEVEL_NONE); - } - - protected static void setFactoryResetProperty(boolean value) { - CrashRecoveryProperties.attemptingFactoryReset(value); - } - protected static void setRebootProperty(boolean value) { - CrashRecoveryProperties.attemptingReboot(value); - } - - protected static void setLastFactoryResetTimeMs(long value) { - CrashRecoveryProperties.lastFactoryResetTimeMs(value); - } - - protected static void setMaxRescueLevelAttempted(int level) { - CrashRecoveryProperties.maxRescueLevelAttempted(level); - } - - @VisibleForTesting - static long getElapsedRealtime() { - return SystemClock.elapsedRealtime(); - } - - private static int getMaxRescueLevel(boolean mayPerformReboot) { - if (Flags.recoverabilityDetection()) { - if (!mayPerformReboot - || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { - return SystemProperties.getInt(RESCUE_NON_REBOOT_LEVEL_LIMIT, - DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT); - } - return RESCUE_LEVEL_FACTORY_RESET; - } else { - if (!mayPerformReboot - || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { - return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS; - } - return LEVEL_FACTORY_RESET; - } - } - - private static int getMaxRescueLevel() { - if (!SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { - return Level.factoryReset(); - } - return Level.reboot(); - } - - /** - * Get the rescue level to perform if this is the n-th attempt at mitigating failure. - * - * @param mitigationCount: the mitigation attempt number (1 = first attempt etc.) - * @param mayPerformReboot: whether or not a reboot and factory reset may be performed - * for the given failure. - * @return the rescue level for the n-th mitigation attempt. - */ - private static int getRescueLevel(int mitigationCount, boolean mayPerformReboot) { - if (!Flags.deprecateFlagsAndSettingsResets()) { - if (mitigationCount == 1) { - return LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS; - } else if (mitigationCount == 2) { - return LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES; - } else if (mitigationCount == 3) { - return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS; - } else if (mitigationCount == 4) { - return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_WARM_REBOOT); - } else if (mitigationCount >= 5) { - return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_FACTORY_RESET); - } else { - Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount); - return LEVEL_NONE; - } - } else { - if (mitigationCount == 1) { - return Level.reboot(); - } else if (mitigationCount >= 2) { - return Math.min(getMaxRescueLevel(), Level.factoryReset()); - } else { - Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount); - return LEVEL_NONE; - } - } - } - - /** - * Get the rescue level to perform if this is the n-th attempt at mitigating failure. - * When failedPackage is null then 1st and 2nd mitigation counts are redundant (scoped and - * all device config reset). Behaves as if one mitigation attempt was already done. - * - * @param mitigationCount the mitigation attempt number (1 = first attempt etc.). - * @param mayPerformReboot whether or not a reboot and factory reset may be performed - * for the given failure. - * @param failedPackage in case of bootloop this is null. - * @return the rescue level for the n-th mitigation attempt. - */ - private static @RescueLevels int getRescueLevel(int mitigationCount, boolean mayPerformReboot, - @Nullable VersionedPackage failedPackage) { - // Skipping RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET since it's not defined without a failed - // package. - if (failedPackage == null && mitigationCount > 0) { - mitigationCount += 1; - } - if (mitigationCount == 1) { - return RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET; - } else if (mitigationCount == 2) { - return RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET; - } else if (mitigationCount == 3) { - return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_WARM_REBOOT); - } else if (mitigationCount == 4) { - return Math.min(getMaxRescueLevel(mayPerformReboot), - RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS); - } else if (mitigationCount == 5) { - return Math.min(getMaxRescueLevel(mayPerformReboot), - RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES); - } else if (mitigationCount == 6) { - return Math.min(getMaxRescueLevel(mayPerformReboot), - RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS); - } else if (mitigationCount >= 7) { - return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_FACTORY_RESET); - } else { - return RESCUE_LEVEL_NONE; - } - } - - /** - * Get the rescue level to perform if this is the n-th attempt at mitigating failure. - * - * @param mitigationCount the mitigation attempt number (1 = first attempt etc.). - * @return the rescue level for the n-th mitigation attempt. - */ - private static @RescueLevels int getRescueLevel(int mitigationCount) { - if (mitigationCount == 1) { - return Level.reboot(); - } else if (mitigationCount >= 2) { - return Math.min(getMaxRescueLevel(), Level.factoryReset()); - } else { - return Level.none(); - } - } - - private static void executeRescueLevel(Context context, @Nullable String failedPackage, - int level) { - Slog.w(TAG, "Attempting rescue level " + levelToString(level)); - try { - executeRescueLevelInternal(context, level, failedPackage); - EventLog.writeEvent(LOG_TAG_RESCUE_SUCCESS, level); - String successMsg = "Finished rescue level " + levelToString(level); - if (!TextUtils.isEmpty(failedPackage)) { - successMsg += " for package " + failedPackage; - } - logCrashRecoveryEvent(Log.DEBUG, successMsg); - } catch (Throwable t) { - logRescueException(level, failedPackage, t); - } - } - - private static void executeRescueLevelInternal(Context context, int level, @Nullable - String failedPackage) throws Exception { - if (Flags.recoverabilityDetection()) { - executeRescueLevelInternalNew(context, level, failedPackage); - } else { - executeRescueLevelInternalOld(context, level, failedPackage); - } - } - - private static void executeRescueLevelInternalOld(Context context, int level, @Nullable - String failedPackage) throws Exception { - CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED, - level, levelToString(level)); - // Try our best to reset all settings possible, and once finished - // rethrow any exception that we encountered - Exception res = null; - switch (level) { - case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: - break; - case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: - break; - case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: - break; - case LEVEL_WARM_REBOOT: - executeWarmReboot(context, level, failedPackage); - break; - case LEVEL_FACTORY_RESET: - // Before the completion of Reboot, if any crash happens then PackageWatchdog - // escalates to next level i.e. factory reset, as they happen in separate threads. - // Adding a check to prevent factory reset to execute before above reboot completes. - // Note: this reboot property is not persistent resets after reboot is completed. - if (isRebootPropertySet()) { - return; - } - executeFactoryReset(context, level, failedPackage); - break; - } - - if (res != null) { - throw res; - } - } - - private static void executeRescueLevelInternalNew(Context context, @RescueLevels int level, - @Nullable String failedPackage) throws Exception { - CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED, - level, levelToString(level)); - switch (level) { - case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: - break; - case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: - break; - case RESCUE_LEVEL_WARM_REBOOT: - executeWarmReboot(context, level, failedPackage); - break; - case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: - // do nothing - break; - case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: - // do nothing - break; - case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: - // do nothing - break; - case RESCUE_LEVEL_FACTORY_RESET: - // Before the completion of Reboot, if any crash happens then PackageWatchdog - // escalates to next level i.e. factory reset, as they happen in separate threads. - // Adding a check to prevent factory reset to execute before above reboot completes. - // Note: this reboot property is not persistent resets after reboot is completed. - if (isRebootPropertySet()) { - return; - } - executeFactoryReset(context, level, failedPackage); - break; - } - } - - private static void executeWarmReboot(Context context, int level, - @Nullable String failedPackage) { - if (Flags.deprecateFlagsAndSettingsResets()) { - if (shouldThrottleReboot()) { - return; - } - } - - // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog - // when device shutting down. - setRebootProperty(true); - - if (Flags.synchronousRebootInRescueParty()) { - try { - PowerManager pm = context.getSystemService(PowerManager.class); - if (pm != null) { - pm.reboot(TAG); - } - } catch (Throwable t) { - logRescueException(level, failedPackage, t); - } - } else { - Runnable runnable = () -> { - try { - PowerManager pm = context.getSystemService(PowerManager.class); - if (pm != null) { - pm.reboot(TAG); - } - } catch (Throwable t) { - logRescueException(level, failedPackage, t); - } - }; - Thread thread = new Thread(runnable); - thread.start(); - } - } - - private static void executeFactoryReset(Context context, int level, - @Nullable String failedPackage) { - if (Flags.deprecateFlagsAndSettingsResets()) { - if (shouldThrottleReboot()) { - return; - } - } - setFactoryResetProperty(true); - long now = System.currentTimeMillis(); - setLastFactoryResetTimeMs(now); - - if (Flags.synchronousRebootInRescueParty()) { - try { - RecoverySystem.rebootPromptAndWipeUserData(context, TAG + "," + failedPackage); - } catch (Throwable t) { - logRescueException(level, failedPackage, t); - } - } else { - Runnable runnable = new Runnable() { - @Override - public void run() { - try { - RecoverySystem.rebootPromptAndWipeUserData(context, - TAG + "," + failedPackage); - } catch (Throwable t) { - logRescueException(level, failedPackage, t); - } - } - }; - Thread thread = new Thread(runnable); - thread.start(); - } - } - - - private static String getCompleteMessage(Throwable t) { - final StringBuilder builder = new StringBuilder(); - builder.append(t.getMessage()); - while ((t = t.getCause()) != null) { - builder.append(": ").append(t.getMessage()); - } - return builder.toString(); - } - - private static void logRescueException(int level, @Nullable String failedPackageName, - Throwable t) { - final String msg = getCompleteMessage(t); - EventLog.writeEvent(LOG_TAG_RESCUE_FAILURE, level, msg); - String failureMsg = "Failed rescue level " + levelToString(level); - if (!TextUtils.isEmpty(failedPackageName)) { - failureMsg += " for package " + failedPackageName; - } - logCrashRecoveryEvent(Log.ERROR, failureMsg + ": " + msg); - } - - private static int mapRescueLevelToUserImpact(int rescueLevel) { - if (Flags.recoverabilityDetection()) { - switch (rescueLevel) { - case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; - case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_40; - case RESCUE_LEVEL_WARM_REBOOT: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; - case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; - case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_75; - case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_80; - case RESCUE_LEVEL_FACTORY_RESET: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; - default: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - } - } else { - switch (rescueLevel) { - case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: - case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; - case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: - case LEVEL_WARM_REBOOT: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; - case LEVEL_FACTORY_RESET: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; - default: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - } - } - } - - /** - * Handle mitigation action for package failures. This observer will be register to Package - * Watchdog and will receive calls about package failures. This observer is persistent so it - * may choose to mitigate failures for packages it has not explicitly asked to observe. - */ - public static class RescuePartyObserver implements PackageHealthObserver { - - private final Context mContext; - private final Map<String, Set<String>> mCallingPackageNamespaceSetMap = new HashMap<>(); - private final Map<String, Set<String>> mNamespaceCallingPackageSetMap = new HashMap<>(); - - @GuardedBy("RescuePartyObserver.class") - static RescuePartyObserver sRescuePartyObserver; - - private RescuePartyObserver(Context context) { - mContext = context; - } - - /** Creates or gets singleton instance of RescueParty. */ - public static RescuePartyObserver getInstance(Context context) { - synchronized (RescuePartyObserver.class) { - if (sRescuePartyObserver == null) { - sRescuePartyObserver = new RescuePartyObserver(context); - } - return sRescuePartyObserver; - } - } - - /** Gets singleton instance. It returns null if the instance is not created yet.*/ - @Nullable - public static RescuePartyObserver getInstanceIfCreated() { - synchronized (RescuePartyObserver.class) { - return sRescuePartyObserver; - } - } - - @VisibleForTesting - static void reset() { - synchronized (RescuePartyObserver.class) { - sRescuePartyObserver = null; - } - } - - @Override - public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage, - @FailureReasons int failureReason, int mitigationCount) { - int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH - || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) { - if (Flags.recoverabilityDetection()) { - if (!Flags.deprecateFlagsAndSettingsResets()) { - impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, - mayPerformReboot(failedPackage), failedPackage)); - } else { - impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount)); - } - } else { - impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, - mayPerformReboot(failedPackage))); - } - } - - Slog.i(TAG, "Checking available remediations for health check failure." - + " failedPackage: " - + (failedPackage == null ? null : failedPackage.getPackageName()) - + " failureReason: " + failureReason - + " available impact: " + impact); - return impact; - } - - @Override - public int onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage, - @FailureReasons int failureReason, int mitigationCount) { - if (isDisabled()) { - return MITIGATION_RESULT_SKIPPED; - } - Slog.i(TAG, "Executing remediation." - + " failedPackage: " - + (failedPackage == null ? null : failedPackage.getPackageName()) - + " failureReason: " + failureReason - + " mitigationCount: " + mitigationCount); - if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH - || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) { - final int level; - if (Flags.recoverabilityDetection()) { - if (!Flags.deprecateFlagsAndSettingsResets()) { - level = getRescueLevel(mitigationCount, mayPerformReboot(failedPackage), - failedPackage); - } else { - level = getRescueLevel(mitigationCount); - } - } else { - level = getRescueLevel(mitigationCount, mayPerformReboot(failedPackage)); - } - executeRescueLevel(mContext, - failedPackage == null ? null : failedPackage.getPackageName(), level); - return MITIGATION_RESULT_SUCCESS; - } else { - return MITIGATION_RESULT_SKIPPED; - } - } - - @Override - public boolean isPersistent() { - return true; - } - - @Override - public boolean mayObservePackage(String packageName) { - PackageManager pm = mContext.getPackageManager(); - try { - // A package is a module if this is non-null - if (pm.getModuleInfo(packageName, 0) != null) { - return true; - } - } catch (PackageManager.NameNotFoundException | IllegalStateException ignore) { - } - - return isPersistentSystemApp(packageName); - } - - @Override - public int onBootLoop(int mitigationCount) { - if (isDisabled()) { - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - } - if (Flags.recoverabilityDetection()) { - if (!Flags.deprecateFlagsAndSettingsResets()) { - return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, - true, /*failedPackage=*/ null)); - } else { - return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount)); - } - } else { - return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true)); - } - } - - @Override - public int onExecuteBootLoopMitigation(int mitigationCount) { - if (isDisabled()) { - return MITIGATION_RESULT_SKIPPED; - } - boolean mayPerformReboot = !shouldThrottleReboot(); - final int level; - if (Flags.recoverabilityDetection()) { - if (!Flags.deprecateFlagsAndSettingsResets()) { - level = getRescueLevel(mitigationCount, mayPerformReboot, - /*failedPackage=*/ null); - } else { - level = getRescueLevel(mitigationCount); - } - } else { - level = getRescueLevel(mitigationCount, mayPerformReboot); - } - executeRescueLevel(mContext, /*failedPackage=*/ null, level); - return MITIGATION_RESULT_SUCCESS; - } - - @Override - public String getUniqueIdentifier() { - return NAME; - } - - /** - * Returns {@code true} if the failing package is non-null and performing a reboot or - * prompting a factory reset is an acceptable mitigation strategy for the package's - * failure, {@code false} otherwise. - */ - private boolean mayPerformReboot(@Nullable VersionedPackage failingPackage) { - if (failingPackage == null) { - return false; - } - if (shouldThrottleReboot()) { - return false; - } - - return isPersistentSystemApp(failingPackage.getPackageName()); - } - - private boolean isPersistentSystemApp(@NonNull String packageName) { - PackageManager pm = mContext.getPackageManager(); - try { - ApplicationInfo info = pm.getApplicationInfo(packageName, 0); - return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK; - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - - private synchronized Set<String> getCallingPackagesSet(String namespace) { - return mNamespaceCallingPackageSetMap.get(namespace); - } - } - - /** - * Returns {@code true} if Rescue Party is allowed to attempt a reboot or factory reset. - * Will return {@code false} if a factory reset was already offered recently. - */ - private static boolean shouldThrottleReboot() { - Long lastResetTime = getLastFactoryResetTimeMs(); - long now = System.currentTimeMillis(); - long throttleDurationMin = SystemProperties.getLong(PROP_THROTTLE_DURATION_MIN_FLAG, - DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN); - return now < lastResetTime + TimeUnit.MINUTES.toMillis(throttleDurationMin); - } - - /** - * Hacky test to check if the device has an active USB connection, which is - * a good proxy for someone doing local development work. - */ - private static boolean isUsbActive() { - if (SystemProperties.getBoolean(PROP_VIRTUAL_DEVICE, false)) { - Slog.v(TAG, "Assuming virtual device is connected over USB"); - return true; - } - try { - final String state = FileUtils - .readTextFile(new File("/sys/class/android_usb/android0/state"), 128, ""); - return "CONFIGURED".equals(state.trim()); - } catch (Throwable t) { - Slog.w(TAG, "Failed to determine if device was on USB", t); - return false; - } - } - - private static class Level { - static int none() { - return Flags.recoverabilityDetection() ? RESCUE_LEVEL_NONE : LEVEL_NONE; - } - - static int reboot() { - return Flags.recoverabilityDetection() ? RESCUE_LEVEL_WARM_REBOOT : LEVEL_WARM_REBOOT; - } - - static int factoryReset() { - return Flags.recoverabilityDetection() - ? RESCUE_LEVEL_FACTORY_RESET - : LEVEL_FACTORY_RESET; - } - } - - private static String levelToString(int level) { - if (Flags.recoverabilityDetection()) { - switch (level) { - case RESCUE_LEVEL_NONE: - return "NONE"; - case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: - return "SCOPED_DEVICE_CONFIG_RESET"; - case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: - return "ALL_DEVICE_CONFIG_RESET"; - case RESCUE_LEVEL_WARM_REBOOT: - return "WARM_REBOOT"; - case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: - return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; - case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: - return "RESET_SETTINGS_UNTRUSTED_CHANGES"; - case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: - return "RESET_SETTINGS_TRUSTED_DEFAULTS"; - case RESCUE_LEVEL_FACTORY_RESET: - return "FACTORY_RESET"; - default: - return Integer.toString(level); - } - } else { - switch (level) { - case LEVEL_NONE: - return "NONE"; - case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: - return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; - case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: - return "RESET_SETTINGS_UNTRUSTED_CHANGES"; - case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: - return "RESET_SETTINGS_TRUSTED_DEFAULTS"; - case LEVEL_WARM_REBOOT: - return "WARM_REBOOT"; - case LEVEL_FACTORY_RESET: - return "FACTORY_RESET"; - default: - return Integer.toString(level); - } - } - } -} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java deleted file mode 100644 index 8a81aaa1e636..000000000000 --- a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.crashrecovery; - -import android.content.Context; - -import com.android.server.PackageWatchdog; -import com.android.server.RescueParty; -import com.android.server.SystemService; - - -/** This class encapsulate the lifecycle methods of CrashRecovery module. - * - * @hide - */ -public class CrashRecoveryModule { - private static final String TAG = "CrashRecoveryModule"; - - /** Lifecycle definition for CrashRecovery module. */ - public static class Lifecycle extends SystemService { - private Context mSystemContext; - private PackageWatchdog mPackageWatchdog; - - public Lifecycle(Context context) { - super(context); - mSystemContext = context; - mPackageWatchdog = PackageWatchdog.getInstance(context); - } - - @Override - public void onStart() { - RescueParty.registerHealthObserver(mSystemContext); - mPackageWatchdog.registerShutdownBroadcastReceiver(); - mPackageWatchdog.noteBoot(); - } - - @Override - public void onBootPhase(int phase) { - if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { - mPackageWatchdog.onPackagesReady(); - } - } - } -} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java deleted file mode 100644 index 2e2a93776f9d..000000000000 --- a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.crashrecovery; - -import android.os.Environment; -import android.util.IndentingPrintWriter; -import android.util.Log; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.time.LocalDateTime; -import java.time.ZoneId; - -/** - * Class containing helper methods for the CrashRecoveryModule. - * - * @hide - */ -public class CrashRecoveryUtils { - private static final String TAG = "CrashRecoveryUtils"; - private static final long MAX_CRITICAL_INFO_DUMP_SIZE = 1000 * 1000; // ~1MB - private static final Object sFileLock = new Object(); - - /** Persist recovery related events in crashrecovery events file.**/ - public static void logCrashRecoveryEvent(int priority, String msg) { - Log.println(priority, TAG, msg); - try { - File fname = getCrashRecoveryEventsFile(); - synchronized (sFileLock) { - FileOutputStream out = new FileOutputStream(fname, true); - PrintWriter pw = new PrintWriter(out); - String dateString = LocalDateTime.now(ZoneId.systemDefault()).toString(); - pw.println(dateString + ": " + msg); - pw.close(); - } - } catch (IOException e) { - Log.e(TAG, "Unable to log CrashRecoveryEvents " + e.getMessage()); - } - } - - /** Dump recovery related events from crashrecovery events file.**/ - public static void dumpCrashRecoveryEvents(IndentingPrintWriter pw) { - pw.println("CrashRecovery Events: "); - pw.increaseIndent(); - final File file = getCrashRecoveryEventsFile(); - final long skipSize = file.length() - MAX_CRITICAL_INFO_DUMP_SIZE; - synchronized (sFileLock) { - try (BufferedReader in = new BufferedReader(new FileReader(file))) { - if (skipSize > 0) { - in.skip(skipSize); - } - String line; - while ((line = in.readLine()) != null) { - pw.println(line); - } - } catch (IOException e) { - Log.e(TAG, "Unable to dump CrashRecoveryEvents " + e.getMessage()); - } - } - pw.decreaseIndent(); - } - - private static File getCrashRecoveryEventsFile() { - File systemDir = new File(Environment.getDataDirectory(), "system"); - return new File(systemDir, "crashrecovery-events.txt"); - } -} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java deleted file mode 100644 index 4978df491c62..000000000000 --- a/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java +++ /dev/null @@ -1,785 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.rollback; - -import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SKIPPED; -import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SUCCESS; -import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent; - -import android.annotation.AnyThread; -import android.annotation.FlaggedApi; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.SuppressLint; -import android.annotation.SystemApi; -import android.annotation.WorkerThread; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.VersionedPackage; -import android.content.rollback.PackageRollbackInfo; -import android.content.rollback.RollbackInfo; -import android.content.rollback.RollbackManager; -import android.crashrecovery.flags.Flags; -import android.os.Environment; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.PowerManager; -import android.os.SystemProperties; -import android.sysprop.CrashRecoveryProperties; -import android.util.ArraySet; -import android.util.FileUtils; -import android.util.Log; -import android.util.Slog; -import android.util.SparseArray; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.Preconditions; -import com.android.server.PackageWatchdog; -import com.android.server.PackageWatchdog.FailureReasons; -import com.android.server.PackageWatchdog.PackageHealthObserver; -import com.android.server.PackageWatchdog.PackageHealthObserverImpact; -import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; - -/** - * {@link PackageHealthObserver} for {@link RollbackManagerService}. - * This class monitors crashes and triggers RollbackManager rollback accordingly. - * It also monitors native crashes for some short while after boot. - * - * @hide - */ -@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) -@SuppressLint({"CallbackName"}) -@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) -public final class RollbackPackageHealthObserver implements PackageHealthObserver { - private static final String TAG = "RollbackPackageHealthObserver"; - private static final String NAME = "rollback-observer"; - private static final String CLASS_NAME = RollbackPackageHealthObserver.class.getName(); - - private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT - | ApplicationInfo.FLAG_SYSTEM; - - private static final String PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG = - "persist.device_config.configuration.disable_high_impact_rollback"; - - private final Context mContext; - private final Handler mHandler; - private final File mLastStagedRollbackIdsFile; - private final File mTwoPhaseRollbackEnabledFile; - // Staged rollback ids that have been committed but their session is not yet ready - private final Set<Integer> mPendingStagedRollbackIds = new ArraySet<>(); - // True if needing to roll back only rebootless apexes when native crash happens - private boolean mTwoPhaseRollbackEnabled; - - @VisibleForTesting - public RollbackPackageHealthObserver(@NonNull Context context) { - mContext = context; - HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver"); - handlerThread.start(); - mHandler = new Handler(handlerThread.getLooper()); - File dataDir = new File(Environment.getDataDirectory(), "rollback-observer"); - dataDir.mkdirs(); - mLastStagedRollbackIdsFile = new File(dataDir, "last-staged-rollback-ids"); - mTwoPhaseRollbackEnabledFile = new File(dataDir, "two-phase-rollback-enabled"); - PackageWatchdog.getInstance(mContext).registerHealthObserver(context.getMainExecutor(), - this); - - if (SystemProperties.getBoolean("sys.boot_completed", false)) { - // Load the value from the file if system server has crashed and restarted - mTwoPhaseRollbackEnabled = readBoolean(mTwoPhaseRollbackEnabledFile); - } else { - // Disable two-phase rollback for a normal reboot. We assume the rebootless apex - // installed before reboot is stable if native crash didn't happen. - mTwoPhaseRollbackEnabled = false; - writeBoolean(mTwoPhaseRollbackEnabledFile, false); - } - } - - @Override - public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage, - @FailureReasons int failureReason, int mitigationCount) { - int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - if (Flags.recoverabilityDetection()) { - List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); - List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel( - availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW); - if (!lowImpactRollbacks.isEmpty()) { - if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { - // For native crashes, we will directly roll back any available rollbacks at low - // impact level - impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; - } else if (getRollbackForPackage(failedPackage, lowImpactRollbacks) != null) { - // Rollback is available for crashing low impact package - impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; - } else { - impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70; - } - } - } else { - boolean anyRollbackAvailable = !mContext.getSystemService(RollbackManager.class) - .getAvailableRollbacks().isEmpty(); - - if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH - && anyRollbackAvailable) { - // For native crashes, we will directly roll back any available rollbacks - // Note: For non-native crashes the rollback-all step has higher impact - impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; - } else if (getAvailableRollback(failedPackage) != null) { - // Rollback is available, we may get a callback into #onExecuteHealthCheckMitigation - impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; - } else if (anyRollbackAvailable) { - // If any rollbacks are available, we will commit them - impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70; - } - } - - Slog.i(TAG, "Checking available remediations for health check failure." - + " failedPackage: " - + (failedPackage == null ? null : failedPackage.getPackageName()) - + " failureReason: " + failureReason - + " available impact: " + impact); - return impact; - } - - @Override - public int onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage, - @FailureReasons int rollbackReason, int mitigationCount) { - Slog.i(TAG, "Executing remediation." - + " failedPackage: " - + (failedPackage == null ? null : failedPackage.getPackageName()) - + " rollbackReason: " + rollbackReason - + " mitigationCount: " + mitigationCount); - if (Flags.recoverabilityDetection()) { - List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); - if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { - mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason)); - return MITIGATION_RESULT_SUCCESS; - } - - List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel( - availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW); - RollbackInfo rollback = getRollbackForPackage(failedPackage, lowImpactRollbacks); - if (rollback != null) { - mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason)); - } else if (!lowImpactRollbacks.isEmpty()) { - // Apply all available low impact rollbacks. - mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason)); - } - } else { - if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { - mHandler.post(() -> rollbackAll(rollbackReason)); - return MITIGATION_RESULT_SUCCESS; - } - - RollbackInfo rollback = getAvailableRollback(failedPackage); - if (rollback != null) { - mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason)); - } else { - mHandler.post(() -> rollbackAll(rollbackReason)); - } - } - - // Assume rollbacks executed successfully - return MITIGATION_RESULT_SUCCESS; - } - - @Override - public int onBootLoop(int mitigationCount) { - int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - if (Flags.recoverabilityDetection()) { - List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); - if (!availableRollbacks.isEmpty()) { - impact = getUserImpactBasedOnRollbackImpactLevel(availableRollbacks); - } - } - return impact; - } - - @Override - public int onExecuteBootLoopMitigation(int mitigationCount) { - if (Flags.recoverabilityDetection()) { - List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); - - triggerLeastImpactLevelRollback(availableRollbacks, - PackageWatchdog.FAILURE_REASON_BOOT_LOOP); - return MITIGATION_RESULT_SUCCESS; - } - return MITIGATION_RESULT_SKIPPED; - } - - @Override - @NonNull - public String getUniqueIdentifier() { - return NAME; - } - - @Override - public boolean isPersistent() { - return true; - } - - @Override - public boolean mayObservePackage(@NonNull String packageName) { - if (getAvailableRollbacks().isEmpty()) { - return false; - } - return isPersistentSystemApp(packageName); - } - - private List<RollbackInfo> getAvailableRollbacks() { - return mContext.getSystemService(RollbackManager.class).getAvailableRollbacks(); - } - - private boolean isPersistentSystemApp(@NonNull String packageName) { - PackageManager pm = mContext.getPackageManager(); - try { - ApplicationInfo info = pm.getApplicationInfo(packageName, 0); - return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK; - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - - private void assertInWorkerThread() { - Preconditions.checkState(mHandler.getLooper().isCurrentThread()); - } - - @AnyThread - @NonNull - public void notifyRollbackAvailable(@NonNull RollbackInfo rollback) { - mHandler.post(() -> { - // Enable two-phase rollback when a rebootless apex rollback is made available. - // We assume the rebootless apex is stable and is less likely to be the cause - // if native crash doesn't happen before reboot. So we will clear the flag and disable - // two-phase rollback after reboot. - if (isRebootlessApex(rollback)) { - mTwoPhaseRollbackEnabled = true; - writeBoolean(mTwoPhaseRollbackEnabledFile, true); - } - }); - } - - private static boolean isRebootlessApex(RollbackInfo rollback) { - if (!rollback.isStaged()) { - for (PackageRollbackInfo info : rollback.getPackages()) { - if (info.isApex()) { - return true; - } - } - } - return false; - } - - /** Verifies the rollback state after a reboot and schedules polling for sometime after reboot - * to check for native crashes and mitigate them if needed. - */ - @AnyThread - public void onBootCompletedAsync() { - mHandler.post(()->onBootCompleted()); - } - - @WorkerThread - private void onBootCompleted() { - assertInWorkerThread(); - - RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); - if (!rollbackManager.getAvailableRollbacks().isEmpty()) { - // TODO(gavincorkery): Call into Package Watchdog from outside the observer - PackageWatchdog.getInstance(mContext).scheduleCheckAndMitigateNativeCrashes(); - } - - SparseArray<String> rollbackIds = popLastStagedRollbackIds(); - for (int i = 0; i < rollbackIds.size(); i++) { - WatchdogRollbackLogger.logRollbackStatusOnBoot(mContext, - rollbackIds.keyAt(i), rollbackIds.valueAt(i), - rollbackManager.getRecentlyCommittedRollbacks()); - } - } - - @AnyThread - private RollbackInfo getAvailableRollback(VersionedPackage failedPackage) { - RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); - for (RollbackInfo rollback : rollbackManager.getAvailableRollbacks()) { - for (PackageRollbackInfo packageRollback : rollback.getPackages()) { - if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) { - return rollback; - } - // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have - // to rely on complicated reasoning as below - - // Due to b/147666157, for apk in apex, we do not know the version we are rolling - // back from. But if a package X is embedded in apex A exclusively (not embedded in - // any other apex), which is not guaranteed, then it is sufficient to check only - // package names here, as the version of failedPackage and the PackageRollbackInfo - // can't be different. If failedPackage has a higher version, then it must have - // been updated somehow. There are two ways: it was updated by an update of apex A - // or updated directly as apk. In both cases, this rollback would have gotten - // expired when onPackageReplaced() was called. Since the rollback exists, it has - // same version as failedPackage. - if (packageRollback.isApkInApex() - && packageRollback.getVersionRolledBackFrom().getPackageName() - .equals(failedPackage.getPackageName())) { - return rollback; - } - } - } - return null; - } - - @AnyThread - private RollbackInfo getRollbackForPackage(@Nullable VersionedPackage failedPackage, - List<RollbackInfo> availableRollbacks) { - if (failedPackage == null) { - return null; - } - - for (RollbackInfo rollback : availableRollbacks) { - for (PackageRollbackInfo packageRollback : rollback.getPackages()) { - if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) { - return rollback; - } - // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have - // to rely on complicated reasoning as below - - // Due to b/147666157, for apk in apex, we do not know the version we are rolling - // back from. But if a package X is embedded in apex A exclusively (not embedded in - // any other apex), which is not guaranteed, then it is sufficient to check only - // package names here, as the version of failedPackage and the PackageRollbackInfo - // can't be different. If failedPackage has a higher version, then it must have - // been updated somehow. There are two ways: it was updated by an update of apex A - // or updated directly as apk. In both cases, this rollback would have gotten - // expired when onPackageReplaced() was called. Since the rollback exists, it has - // same version as failedPackage. - if (packageRollback.isApkInApex() - && packageRollback.getVersionRolledBackFrom().getPackageName() - .equals(failedPackage.getPackageName())) { - return rollback; - } - } - } - return null; - } - - /** - * Returns {@code true} if staged session associated with {@code rollbackId} was marked - * as handled, {@code false} if already handled. - */ - @WorkerThread - private boolean markStagedSessionHandled(int rollbackId) { - assertInWorkerThread(); - return mPendingStagedRollbackIds.remove(rollbackId); - } - - /** - * Returns {@code true} if all pending staged rollback sessions were marked as handled, - * {@code false} if there is any left. - */ - @WorkerThread - private boolean isPendingStagedSessionsEmpty() { - assertInWorkerThread(); - return mPendingStagedRollbackIds.isEmpty(); - } - - private static boolean readBoolean(File file) { - try (FileInputStream fis = new FileInputStream(file)) { - return fis.read() == 1; - } catch (IOException ignore) { - return false; - } - } - - private static void writeBoolean(File file, boolean value) { - try (FileOutputStream fos = new FileOutputStream(file)) { - fos.write(value ? 1 : 0); - fos.flush(); - FileUtils.sync(fos); - } catch (IOException ignore) { - } - } - - @WorkerThread - private void saveStagedRollbackId(int stagedRollbackId, @Nullable VersionedPackage logPackage) { - assertInWorkerThread(); - writeStagedRollbackId(mLastStagedRollbackIdsFile, stagedRollbackId, logPackage); - } - - static void writeStagedRollbackId(File file, int stagedRollbackId, - @Nullable VersionedPackage logPackage) { - try { - FileOutputStream fos = new FileOutputStream(file, true); - PrintWriter pw = new PrintWriter(fos); - String logPackageName = logPackage != null ? logPackage.getPackageName() : ""; - pw.append(String.valueOf(stagedRollbackId)).append(",").append(logPackageName); - pw.println(); - pw.flush(); - FileUtils.sync(fos); - pw.close(); - } catch (IOException e) { - Slog.e(TAG, "Failed to save last staged rollback id", e); - file.delete(); - } - } - - @WorkerThread - private SparseArray<String> popLastStagedRollbackIds() { - assertInWorkerThread(); - try { - return readStagedRollbackIds(mLastStagedRollbackIdsFile); - } finally { - mLastStagedRollbackIdsFile.delete(); - } - } - - static SparseArray<String> readStagedRollbackIds(File file) { - SparseArray<String> result = new SparseArray<>(); - try { - String line; - BufferedReader reader = new BufferedReader(new FileReader(file)); - while ((line = reader.readLine()) != null) { - // Each line is of the format: "id,logging_package" - String[] values = line.trim().split(","); - String rollbackId = values[0]; - String logPackageName = ""; - if (values.length > 1) { - logPackageName = values[1]; - } - result.put(Integer.parseInt(rollbackId), logPackageName); - } - } catch (Exception ignore) { - return new SparseArray<>(); - } - return result; - } - - - /** - * Returns true if the package name is the name of a module. - */ - @AnyThread - private boolean isModule(String packageName) { - // Check if the package is listed among the system modules or is an - // APK inside an updatable APEX. - try { - PackageManager pm = mContext.getPackageManager(); - final PackageInfo pkg = pm.getPackageInfo(packageName, 0 /* flags */); - String apexPackageName = pkg.getApexPackageName(); - if (apexPackageName != null) { - packageName = apexPackageName; - } - - return pm.getModuleInfo(packageName, 0 /* flags */) != null; - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - - /** - * Rolls back the session that owns {@code failedPackage} - * - * @param rollback {@code rollbackInfo} of the {@code failedPackage} - * @param failedPackage the package that needs to be rolled back - */ - @WorkerThread - private void rollbackPackage(RollbackInfo rollback, VersionedPackage failedPackage, - @FailureReasons int rollbackReason) { - assertInWorkerThread(); - String failedPackageName = (failedPackage == null ? null : failedPackage.getPackageName()); - - Slog.i(TAG, "Rolling back package. RollbackId: " + rollback.getRollbackId() - + " failedPackage: " + failedPackageName - + " rollbackReason: " + rollbackReason); - logCrashRecoveryEvent(Log.DEBUG, String.format("Rolling back %s. Reason: %s", - failedPackageName, rollbackReason)); - final RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); - int reasonToLog = WatchdogRollbackLogger.mapFailureReasonToMetric(rollbackReason); - final String failedPackageToLog; - if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { - failedPackageToLog = SystemProperties.get( - "sys.init.updatable_crashing_process_name", ""); - } else { - failedPackageToLog = failedPackage.getPackageName(); - } - VersionedPackage logPackageTemp = null; - if (isModule(failedPackage.getPackageName())) { - logPackageTemp = WatchdogRollbackLogger.getLogPackage(mContext, failedPackage); - } - - final VersionedPackage logPackage = logPackageTemp; - WatchdogRollbackLogger.logEvent(logPackage, - CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE, - reasonToLog, failedPackageToLog); - - Consumer<Intent> onResult = result -> { - assertInWorkerThread(); - int status = result.getIntExtra(RollbackManager.EXTRA_STATUS, - RollbackManager.STATUS_FAILURE); - if (status == RollbackManager.STATUS_SUCCESS) { - if (rollback.isStaged()) { - int rollbackId = rollback.getRollbackId(); - saveStagedRollbackId(rollbackId, logPackage); - WatchdogRollbackLogger.logEvent(logPackage, - CrashRecoveryStatsLog - .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED, - reasonToLog, failedPackageToLog); - - } else { - WatchdogRollbackLogger.logEvent(logPackage, - CrashRecoveryStatsLog - .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS, - reasonToLog, failedPackageToLog); - } - } else { - WatchdogRollbackLogger.logEvent(logPackage, - CrashRecoveryStatsLog - .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE, - reasonToLog, failedPackageToLog); - } - if (rollback.isStaged()) { - markStagedSessionHandled(rollback.getRollbackId()); - // Wait for all pending staged sessions to get handled before rebooting. - if (isPendingStagedSessionsEmpty()) { - CrashRecoveryProperties.attemptingReboot(true); - mContext.getSystemService(PowerManager.class).reboot("Rollback staged install"); - } - } - }; - - // Define a BroadcastReceiver to handle the result - BroadcastReceiver rollbackReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent result) { - mHandler.post(() -> onResult.accept(result)); - } - }; - - String intentActionName = CLASS_NAME + rollback.getRollbackId(); - // Register the BroadcastReceiver - mContext.registerReceiver(rollbackReceiver, - new IntentFilter(intentActionName), - Context.RECEIVER_NOT_EXPORTED); - - Intent intentReceiver = new Intent(intentActionName); - intentReceiver.putExtra("rollbackId", rollback.getRollbackId()); - intentReceiver.setPackage(mContext.getPackageName()); - intentReceiver.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); - - PendingIntent rollbackPendingIntent = PendingIntent.getBroadcast(mContext, - rollback.getRollbackId(), - intentReceiver, - PendingIntent.FLAG_MUTABLE); - - rollbackManager.commitRollback(rollback.getRollbackId(), - Collections.singletonList(failedPackage), - rollbackPendingIntent.getIntentSender()); - } - - /** - * Two-phase rollback: - * 1. roll back rebootless apexes first - * 2. roll back all remaining rollbacks if native crash doesn't stop after (1) is done - * - * This approach gives us a better chance to correctly attribute native crash to rebootless - * apex update without rolling back Mainline updates which might contains critical security - * fixes. - */ - @WorkerThread - private boolean useTwoPhaseRollback(List<RollbackInfo> rollbacks) { - assertInWorkerThread(); - if (!mTwoPhaseRollbackEnabled) { - return false; - } - - Slog.i(TAG, "Rolling back all rebootless APEX rollbacks"); - boolean found = false; - for (RollbackInfo rollback : rollbacks) { - if (isRebootlessApex(rollback)) { - VersionedPackage firstRollback = - rollback.getPackages().get(0).getVersionRolledBackFrom(); - rollbackPackage(rollback, firstRollback, - PackageWatchdog.FAILURE_REASON_NATIVE_CRASH); - found = true; - } - } - return found; - } - - /** - * Rollback the package that has minimum rollback impact level. - * @param availableRollbacks all available rollbacks - * @param rollbackReason reason to rollback - */ - private void triggerLeastImpactLevelRollback(List<RollbackInfo> availableRollbacks, - @FailureReasons int rollbackReason) { - int minRollbackImpactLevel = getMinRollbackImpactLevel(availableRollbacks); - - if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_LOW) { - // Apply all available low impact rollbacks. - mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason)); - } else if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_HIGH) { - // Check disable_high_impact_rollback device config before performing rollback - if (SystemProperties.getBoolean(PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG, false)) { - return; - } - // Rollback one package at a time. If that doesn't resolve the issue, rollback - // next with same impact level. - mHandler.post(() -> rollbackHighImpact(availableRollbacks, rollbackReason)); - } - } - - /** - * sort the available high impact rollbacks by first package name to have a deterministic order. - * Apply the first available rollback. - * @param availableRollbacks all available rollbacks - * @param rollbackReason reason to rollback - */ - @WorkerThread - private void rollbackHighImpact(List<RollbackInfo> availableRollbacks, - @FailureReasons int rollbackReason) { - assertInWorkerThread(); - List<RollbackInfo> highImpactRollbacks = - getRollbacksAvailableForImpactLevel( - availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_HIGH); - - // sort rollbacks based on package name of the first package. This is to have a - // deterministic order of rollbacks. - List<RollbackInfo> sortedHighImpactRollbacks = highImpactRollbacks.stream().sorted( - Comparator.comparing(a -> a.getPackages().get(0).getPackageName())).toList(); - VersionedPackage firstRollback = - sortedHighImpactRollbacks - .get(0) - .getPackages() - .get(0) - .getVersionRolledBackFrom(); - Slog.i(TAG, "Rolling back high impact rollback for package: " - + firstRollback.getPackageName()); - rollbackPackage(sortedHighImpactRollbacks.get(0), firstRollback, rollbackReason); - } - - @WorkerThread - private void rollbackAll(@FailureReasons int rollbackReason) { - assertInWorkerThread(); - RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); - List<RollbackInfo> rollbacks = rollbackManager.getAvailableRollbacks(); - if (useTwoPhaseRollback(rollbacks)) { - return; - } - - Slog.i(TAG, "Rolling back all available rollbacks"); - // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all - // pending staged rollbacks are handled. - for (RollbackInfo rollback : rollbacks) { - if (rollback.isStaged()) { - mPendingStagedRollbackIds.add(rollback.getRollbackId()); - } - } - - for (RollbackInfo rollback : rollbacks) { - VersionedPackage firstRollback = - rollback.getPackages().get(0).getVersionRolledBackFrom(); - rollbackPackage(rollback, firstRollback, rollbackReason); - } - } - - /** - * Rollback all available low impact rollbacks - * @param availableRollbacks all available rollbacks - * @param rollbackReason reason to rollbacks - */ - @WorkerThread - private void rollbackAllLowImpact( - List<RollbackInfo> availableRollbacks, @FailureReasons int rollbackReason) { - assertInWorkerThread(); - - List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel( - availableRollbacks, - PackageManager.ROLLBACK_USER_IMPACT_LOW); - if (useTwoPhaseRollback(lowImpactRollbacks)) { - return; - } - - Slog.i(TAG, "Rolling back all available low impact rollbacks"); - logCrashRecoveryEvent(Log.DEBUG, "Rolling back all available. Reason: " + rollbackReason); - // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all - // pending staged rollbacks are handled. - for (RollbackInfo rollback : lowImpactRollbacks) { - if (rollback.isStaged()) { - mPendingStagedRollbackIds.add(rollback.getRollbackId()); - } - } - - for (RollbackInfo rollback : lowImpactRollbacks) { - VersionedPackage firstRollback = - rollback.getPackages().get(0).getVersionRolledBackFrom(); - rollbackPackage(rollback, firstRollback, rollbackReason); - } - } - - private List<RollbackInfo> getRollbacksAvailableForImpactLevel( - List<RollbackInfo> availableRollbacks, int impactLevel) { - return availableRollbacks.stream() - .filter(rollbackInfo -> rollbackInfo.getRollbackImpactLevel() == impactLevel) - .toList(); - } - - private int getMinRollbackImpactLevel(List<RollbackInfo> availableRollbacks) { - return availableRollbacks.stream() - .mapToInt(RollbackInfo::getRollbackImpactLevel) - .min() - .orElse(-1); - } - - private int getUserImpactBasedOnRollbackImpactLevel(List<RollbackInfo> availableRollbacks) { - int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - int minImpact = getMinRollbackImpactLevel(availableRollbacks); - switch (minImpact) { - case PackageManager.ROLLBACK_USER_IMPACT_LOW: - impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70; - break; - case PackageManager.ROLLBACK_USER_IMPACT_HIGH: - if (!SystemProperties.getBoolean(PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG, false)) { - impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_90; - } - break; - default: - impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - } - return impact; - } - - @VisibleForTesting - Handler getHandler() { - return mHandler; - } -} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java deleted file mode 100644 index 9cfed02f9355..000000000000 --- a/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java +++ /dev/null @@ -1,255 +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. - */ - -package com.android.server.rollback; - -import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent; -import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH; -import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING; -import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING; -import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK; -import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH; -import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH_DURING_BOOT; -import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN; -import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED; -import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE; -import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE; -import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInstaller; -import android.content.pm.PackageManager; -import android.content.pm.VersionedPackage; -import android.content.rollback.PackageRollbackInfo; -import android.content.rollback.RollbackInfo; -import android.os.SystemProperties; -import android.text.TextUtils; -import android.util.Log; -import android.util.Slog; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.server.PackageWatchdog; -import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; - -import java.util.List; - -/** - * This class handles the logic for logging Watchdog-triggered rollback events. - * @hide - */ -public final class WatchdogRollbackLogger { - private static final String TAG = "WatchdogRollbackLogger"; - - private static final String LOGGING_PARENT_KEY = "android.content.pm.LOGGING_PARENT"; - - private WatchdogRollbackLogger() { - } - - @Nullable - private static String getLoggingParentName(Context context, @NonNull String packageName) { - PackageManager packageManager = context.getPackageManager(); - try { - int flags = PackageManager.MATCH_APEX | PackageManager.GET_META_DATA; - ApplicationInfo ai = packageManager.getPackageInfo(packageName, flags).applicationInfo; - if (ai.metaData == null) { - return null; - } - return ai.metaData.getString(LOGGING_PARENT_KEY); - } catch (Exception e) { - Slog.w(TAG, "Unable to discover logging parent package: " + packageName, e); - return null; - } - } - - /** - * Returns the logging parent of a given package if it exists, {@code null} otherwise. - * - * The logging parent is defined by the {@code android.content.pm.LOGGING_PARENT} field in the - * metadata of a package's AndroidManifest.xml. - */ - @VisibleForTesting - @Nullable - static VersionedPackage getLogPackage(Context context, - @NonNull VersionedPackage failingPackage) { - String logPackageName; - VersionedPackage loggingParent; - logPackageName = getLoggingParentName(context, failingPackage.getPackageName()); - if (logPackageName == null) { - return null; - } - try { - loggingParent = new VersionedPackage(logPackageName, context.getPackageManager() - .getPackageInfo(logPackageName, 0 /* flags */).getLongVersionCode()); - } catch (PackageManager.NameNotFoundException e) { - return null; - } - return loggingParent; - } - - static void logRollbackStatusOnBoot(Context context, int rollbackId, String logPackageName, - List<RollbackInfo> recentlyCommittedRollbacks) { - PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); - - RollbackInfo rollback = null; - for (RollbackInfo info : recentlyCommittedRollbacks) { - if (rollbackId == info.getRollbackId()) { - rollback = info; - break; - } - } - - if (rollback == null) { - Slog.e(TAG, "rollback info not found for last staged rollback: " + rollbackId); - return; - } - - // Use the version of the logging parent that was installed before - // we rolled back for logging purposes. - VersionedPackage oldLoggingPackage = null; - if (!TextUtils.isEmpty(logPackageName)) { - for (PackageRollbackInfo packageRollback : rollback.getPackages()) { - if (logPackageName.equals(packageRollback.getPackageName())) { - oldLoggingPackage = packageRollback.getVersionRolledBackFrom(); - break; - } - } - } - - int sessionId = rollback.getCommittedSessionId(); - PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId); - if (sessionInfo == null) { - Slog.e(TAG, "On boot completed, could not load session id " + sessionId); - return; - } - - if (sessionInfo.isStagedSessionApplied()) { - logEvent(oldLoggingPackage, - WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS, - WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN, ""); - } else if (sessionInfo.isStagedSessionFailed()) { - logEvent(oldLoggingPackage, - WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE, - WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN, ""); - } - } - - /** - * Log a Watchdog rollback event to statsd. - * - * @param logPackage the package to associate the rollback with. - * @param type the state of the rollback. - * @param rollbackReason the reason Watchdog triggered a rollback, if known. - * @param failingPackageName the failing package or process which triggered the rollback. - */ - public static void logEvent(@Nullable VersionedPackage logPackage, int type, - int rollbackReason, @NonNull String failingPackageName) { - String logMsg = "Watchdog event occurred with type: " + rollbackTypeToString(type) - + " logPackage: " + logPackage - + " rollbackReason: " + rollbackReasonToString(rollbackReason) - + " failedPackageName: " + failingPackageName; - Slog.i(TAG, logMsg); - if (logPackage != null) { - CrashRecoveryStatsLog.write( - CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED, - type, - logPackage.getPackageName(), - logPackage.getVersionCode(), - rollbackReason, - failingPackageName, - new byte[]{}); - } else { - // In the case that the log package is null, still log an empty string as an - // indication that retrieving the logging parent failed. - CrashRecoveryStatsLog.write( - CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED, - type, - "", - 0, - rollbackReason, - failingPackageName, - new byte[]{}); - } - - logTestProperties(logMsg); - } - - /** - * Writes properties which will be used by rollback tests to check if particular rollback - * events have occurred. - */ - private static void logTestProperties(String logMsg) { - // This property should be on only during the tests - if (!SystemProperties.getBoolean("persist.sys.rollbacktest.enabled", false)) { - return; - } - logCrashRecoveryEvent(Log.DEBUG, logMsg); - } - - @VisibleForTesting - static int mapFailureReasonToMetric(@PackageWatchdog.FailureReasons int failureReason) { - switch (failureReason) { - case PackageWatchdog.FAILURE_REASON_NATIVE_CRASH: - return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH; - case PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK: - return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK; - case PackageWatchdog.FAILURE_REASON_APP_CRASH: - return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH; - case PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING: - return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING; - case PackageWatchdog.FAILURE_REASON_BOOT_LOOP: - return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING; - default: - return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN; - } - } - - private static String rollbackTypeToString(int type) { - switch (type) { - case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE: - return "ROLLBACK_INITIATE"; - case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS: - return "ROLLBACK_SUCCESS"; - case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE: - return "ROLLBACK_FAILURE"; - case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED: - return "ROLLBACK_BOOT_TRIGGERED"; - default: - return "UNKNOWN"; - } - } - - private static String rollbackReasonToString(int reason) { - switch (reason) { - case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH: - return "REASON_NATIVE_CRASH"; - case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK: - return "REASON_EXPLICIT_HEALTH_CHECK"; - case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH: - return "REASON_APP_CRASH"; - case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING: - return "REASON_APP_NOT_RESPONDING"; - case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH_DURING_BOOT: - return "REASON_NATIVE_CRASH_DURING_BOOT"; - case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING: - return "REASON_BOOT_LOOP"; - default: - return "UNKNOWN"; - } - } -} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java deleted file mode 100644 index 29ff7cced897..000000000000 --- a/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -import android.annotation.Nullable; - -/** - * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java - * - * @hide - */ -public class ArrayUtils { - private ArrayUtils() { /* cannot be instantiated */ } - - /** - * Checks if given array is null or has zero elements. - */ - public static boolean isEmpty(@Nullable int[] array) { - return array == null || array.length == 0; - } - - /** - * True if the byte array is null or has length 0. - */ - public static boolean isEmpty(@Nullable byte[] array) { - return array == null || array.length == 0; - } -} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java deleted file mode 100644 index d60a9b9847ca..000000000000 --- a/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -import android.annotation.Nullable; - -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * Bits and pieces copied from hidden API of android.os.FileUtils. - * - * @hide - */ -public class FileUtils { - /** - * Read a text file into a String, optionally limiting the length. - * - * @param file to read (will not seek, so things like /proc files are OK) - * @param max length (positive for head, negative of tail, 0 for no limit) - * @param ellipsis to add of the file was truncated (can be null) - * @return the contents of the file, possibly truncated - * @throws IOException if something goes wrong reading the file - * @hide - */ - public static @Nullable String readTextFile(@Nullable File file, @Nullable int max, - @Nullable String ellipsis) throws IOException { - InputStream input = new FileInputStream(file); - // wrapping a BufferedInputStream around it because when reading /proc with unbuffered - // input stream, bytes read not equal to buffer size is not necessarily the correct - // indication for EOF; but it is true for BufferedInputStream due to its implementation. - BufferedInputStream bis = new BufferedInputStream(input); - try { - long size = file.length(); - if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes - if (size > 0 && (max == 0 || size < max)) max = (int) size; - byte[] data = new byte[max + 1]; - int length = bis.read(data); - if (length <= 0) return ""; - if (length <= max) return new String(data, 0, length); - if (ellipsis == null) return new String(data, 0, max); - return new String(data, 0, max) + ellipsis; - } else if (max < 0) { // "tail" mode: keep the last N - int len; - boolean rolled = false; - byte[] last = null; - byte[] data = null; - do { - if (last != null) rolled = true; - byte[] tmp = last; - last = data; - data = tmp; - if (data == null) data = new byte[-max]; - len = bis.read(data); - } while (len == data.length); - - if (last == null && len <= 0) return ""; - if (last == null) return new String(data, 0, len); - if (len > 0) { - rolled = true; - System.arraycopy(last, len, last, 0, last.length - len); - System.arraycopy(data, 0, last, last.length - len, len); - } - if (ellipsis == null || !rolled) return new String(last); - return ellipsis + new String(last); - } else { // "cat" mode: size unknown, read it all in streaming fashion - ByteArrayOutputStream contents = new ByteArrayOutputStream(); - int len; - byte[] data = new byte[1024]; - do { - len = bis.read(data); - if (len > 0) contents.write(data, 0, len); - } while (len == data.length); - return contents.toString(); - } - } finally { - bis.close(); - input.close(); - } - } - - /** - * Perform an fsync on the given FileOutputStream. The stream at this - * point must be flushed but not yet closed. - * - * @hide - */ - public static boolean sync(FileOutputStream stream) { - try { - if (stream != null) { - stream.getFD().sync(); - } - return true; - } catch (IOException e) { - } - return false; - } -} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java b/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java deleted file mode 100644 index 9a24ada8b69a..000000000000 --- a/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -import libcore.util.EmptyArray; - -import java.util.NoSuchElementException; - -/** - * Copied from frameworks/base/core/java/android/util/LongArrayQueue.java - * - * @hide - */ -public class LongArrayQueue { - - private long[] mValues; - private int mSize; - private int mHead; - private int mTail; - - private long[] newUnpaddedLongArray(int num) { - return new long[num]; - } - /** - * Initializes a queue with the given starting capacity. - * - * @param initialCapacity the capacity. - */ - public LongArrayQueue(int initialCapacity) { - if (initialCapacity == 0) { - mValues = EmptyArray.LONG; - } else { - mValues = newUnpaddedLongArray(initialCapacity); - } - mSize = 0; - mHead = mTail = 0; - } - - /** - * Initializes a queue with default starting capacity. - */ - public LongArrayQueue() { - this(16); - } - - /** @hide */ - public static int growSize(int currentSize) { - return currentSize <= 4 ? 8 : currentSize * 2; - } - - private void grow() { - if (mSize < mValues.length) { - throw new IllegalStateException("Queue not full yet!"); - } - final int newSize = growSize(mSize); - final long[] newArray = newUnpaddedLongArray(newSize); - final int r = mValues.length - mHead; // Number of elements on and to the right of head. - System.arraycopy(mValues, mHead, newArray, 0, r); - System.arraycopy(mValues, 0, newArray, r, mHead); - mValues = newArray; - mHead = 0; - mTail = mSize; - } - - /** - * Returns the number of elements in the queue. - */ - public int size() { - return mSize; - } - - /** - * Removes all elements from this queue. - */ - public void clear() { - mSize = 0; - mHead = mTail = 0; - } - - /** - * Adds a value to the tail of the queue. - * - * @param value the value to be added. - */ - public void addLast(long value) { - if (mSize == mValues.length) { - grow(); - } - mValues[mTail] = value; - mTail = (mTail + 1) % mValues.length; - mSize++; - } - - /** - * Removes an element from the head of the queue. - * - * @return the element at the head of the queue. - * @throws NoSuchElementException if the queue is empty. - */ - public long removeFirst() { - if (mSize == 0) { - throw new NoSuchElementException("Queue is empty!"); - } - final long ret = mValues[mHead]; - mHead = (mHead + 1) % mValues.length; - mSize--; - return ret; - } - - /** - * Returns the element at the given position from the head of the queue, where 0 represents the - * head of the queue. - * - * @param position the position from the head of the queue. - * @return the element found at the given position. - * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or - * {@code position} >= {@link #size()} - */ - public long get(int position) { - if (position < 0 || position >= mSize) { - throw new IndexOutOfBoundsException("Index " + position - + " not valid for a queue of size " + mSize); - } - final int index = (mHead + position) % mValues.length; - return mValues[index]; - } - - /** - * Returns the element at the head of the queue, without removing it. - * - * @return the element at the head of the queue. - * @throws NoSuchElementException if the queue is empty - */ - public long peekFirst() { - if (mSize == 0) { - throw new NoSuchElementException("Queue is empty!"); - } - return mValues[mHead]; - } - - /** - * Returns the element at the tail of the queue. - * - * @return the element at the tail of the queue. - * @throws NoSuchElementException if the queue is empty. - */ - public long peekLast() { - if (mSize == 0) { - throw new NoSuchElementException("Queue is empty!"); - } - final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1; - return mValues[index]; - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - if (mSize <= 0) { - return "{}"; - } - - final StringBuilder buffer = new StringBuilder(mSize * 64); - buffer.append('{'); - buffer.append(get(0)); - for (int i = 1; i < mSize; i++) { - buffer.append(", "); - buffer.append(get(i)); - } - buffer.append('}'); - return buffer.toString(); - } -} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java deleted file mode 100644 index 488b531c2b8a..000000000000 --- a/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; - -/** - * Bits and pieces copied from hidden API of - * frameworks/base/core/java/com/android/internal/util/XmlUtils.java - * - * @hide - */ -public class XmlUtils { - - /** @hide */ - public static final void beginDocument(XmlPullParser parser, String firstElementName) - throws XmlPullParserException, IOException { - int type; - while ((type = parser.next()) != parser.START_TAG - && type != parser.END_DOCUMENT) { - // Do nothing - } - - if (type != parser.START_TAG) { - throw new XmlPullParserException("No start tag found"); - } - - if (!parser.getName().equals(firstElementName)) { - throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() - + ", expected " + firstElementName); - } - } - - /** @hide */ - public static boolean nextElementWithin(XmlPullParser parser, int outerDepth) - throws IOException, XmlPullParserException { - for (;;) { - int type = parser.next(); - if (type == XmlPullParser.END_DOCUMENT - || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) { - return false; - } - if (type == XmlPullParser.START_TAG - && parser.getDepth() == outerDepth + 1) { - return true; - } - } - } -} diff --git a/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.java index cae8db27a435..428997e0c446 100644 --- a/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.java @@ -19,6 +19,7 @@ package android.app.ondeviceintelligence; import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE; import android.annotation.CurrentTimeMillisLong; +import android.annotation.DurationMillisLong; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SystemApi; @@ -51,7 +52,8 @@ public final class InferenceInfo implements Parcelable { private final long endTimeMs; /** - * Suspended time in milliseconds. + * The total duration of the period(s) during which the inference was + * suspended (i.e. not running), in milliseconds. */ private final long suspendedTimeMs; @@ -61,7 +63,7 @@ public final class InferenceInfo implements Parcelable { * @param uid Uid for the caller app. * @param startTimeMs Inference start time (milliseconds from the epoch time). * @param endTimeMs Inference end time (milliseconds from the epoch time). - * @param suspendedTimeMs Suspended time in milliseconds. + * @param suspendedTimeMs Suspended duration, in milliseconds. */ InferenceInfo(int uid, long startTimeMs, long endTimeMs, long suspendedTimeMs) { @@ -128,11 +130,12 @@ public final class InferenceInfo implements Parcelable { } /** - * Returns the suspended time in milliseconds. + * Returns the suspended duration, in milliseconds. * - * @return the suspended time in milliseconds. + * @return the total duration of the period(s) during which the inference + * was suspended (i.e. not running), in milliseconds. */ - @CurrentTimeMillisLong + @DurationMillisLong public long getSuspendedTimeMillis() { return suspendedTimeMs; } @@ -197,12 +200,14 @@ public final class InferenceInfo implements Parcelable { } /** - * Sets the suspended time in milliseconds. + * Sets the suspended duration, in milliseconds. * - * @param suspendedTimeMs the suspended time in milliseconds. + * @param suspendedTimeMs the total duration of the period(s) in which + * the request was suspended (i.e. not running), + * in milliseconds. * @return the Builder instance. */ - public @NonNull Builder setSuspendedTimeMillis(@CurrentTimeMillisLong long suspendedTimeMs) { + public @NonNull Builder setSuspendedTimeMillis(@DurationMillisLong long suspendedTimeMs) { this.suspendedTimeMs = suspendedTimeMs; return this; } diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.java index cae8db27a435..64524fb096cb 100644 --- a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.java +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.java @@ -19,6 +19,7 @@ package android.app.ondeviceintelligence; import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE; import android.annotation.CurrentTimeMillisLong; +import android.annotation.DurationMillisLong; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SystemApi; @@ -50,8 +51,9 @@ public final class InferenceInfo implements Parcelable { */ private final long endTimeMs; - /** - * Suspended time in milliseconds. + /** + * The total duration of the period(s) during which the inference was + * suspended (i.e. not running), in milliseconds. */ private final long suspendedTimeMs; @@ -61,7 +63,7 @@ public final class InferenceInfo implements Parcelable { * @param uid Uid for the caller app. * @param startTimeMs Inference start time (milliseconds from the epoch time). * @param endTimeMs Inference end time (milliseconds from the epoch time). - * @param suspendedTimeMs Suspended time in milliseconds. + * @param suspendedTimeMs Suspended duration, in milliseconds. */ InferenceInfo(int uid, long startTimeMs, long endTimeMs, long suspendedTimeMs) { @@ -128,11 +130,12 @@ public final class InferenceInfo implements Parcelable { } /** - * Returns the suspended time in milliseconds. + * Returns the suspended duration, in milliseconds. * - * @return the suspended time in milliseconds. + * @return the total duration of the period(s) during which the inference + * was suspended (i.e. not running), in milliseconds. */ - @CurrentTimeMillisLong + @DurationMillisLong public long getSuspendedTimeMillis() { return suspendedTimeMs; } @@ -197,12 +200,14 @@ public final class InferenceInfo implements Parcelable { } /** - * Sets the suspended time in milliseconds. + * Sets the suspended duration, in milliseconds. * - * @param suspendedTimeMs the suspended time in milliseconds. + * @param suspendedTimeMs the total duration of the period(s) in which + * the request was suspended (i.e. not running), + * in milliseconds. * @return the Builder instance. */ - public @NonNull Builder setSuspendedTimeMillis(@CurrentTimeMillisLong long suspendedTimeMs) { + public @NonNull Builder setSuspendedTimeMillis(@DurationMillisLong long suspendedTimeMs) { this.suspendedTimeMs = suspendedTimeMs; return this; } diff --git a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java index 60a9ebd6f98b..c82829d6ccea 100644 --- a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java +++ b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java @@ -116,8 +116,8 @@ public class BannerMessagePreference extends Preference implements GroupSectionD // Default attention level is High. private AttentionLevel mAttentionLevel = AttentionLevel.HIGH; - private String mSubtitle; - private String mHeader; + private CharSequence mSubtitle; + private CharSequence mHeader; private int mButtonOrientation; public BannerMessagePreference(Context context) { @@ -351,7 +351,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the text to be displayed in positive button. */ - public BannerMessagePreference setPositiveButtonText(String positiveButtonText) { + public BannerMessagePreference setPositiveButtonText(CharSequence positiveButtonText) { if (!TextUtils.equals(positiveButtonText, mPositiveButtonInfo.mText)) { mPositiveButtonInfo.mText = positiveButtonText; notifyChanged(); @@ -369,7 +369,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the text to be displayed in negative button. */ - public BannerMessagePreference setNegativeButtonText(String negativeButtonText) { + public BannerMessagePreference setNegativeButtonText(CharSequence negativeButtonText) { if (!TextUtils.equals(negativeButtonText, mNegativeButtonInfo.mText)) { mNegativeButtonInfo.mText = negativeButtonText; notifyChanged(); @@ -401,7 +401,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD * Sets the subtitle. */ @RequiresApi(Build.VERSION_CODES.S) - public BannerMessagePreference setSubtitle(String subtitle) { + public BannerMessagePreference setSubtitle(CharSequence subtitle) { if (!TextUtils.equals(subtitle, mSubtitle)) { mSubtitle = subtitle; notifyChanged(); @@ -421,8 +421,8 @@ public class BannerMessagePreference extends Preference implements GroupSectionD * Sets the header. */ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) - public BannerMessagePreference setHeader(String header) { - if (!TextUtils.equals(header, mSubtitle)) { + public BannerMessagePreference setHeader(CharSequence header) { + if (!TextUtils.equals(header, mHeader)) { mHeader = header; notifyChanged(); } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt index a140eb8424a8..a8483308556d 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt @@ -35,6 +35,7 @@ interface BlockedByAdmin : RestrictedMode { interface BlockedByEcm : RestrictedMode { fun showRestrictedSettingsDetails() + fun isBlockedByPhoneCall() = false } internal data class BlockedByAdminImpl( @@ -72,8 +73,13 @@ internal data class BlockedByEcmImpl( private val context: Context, private val intent: Intent, ) : BlockedByEcm { + private val reasonPhoneState = "phone_state" override fun showRestrictedSettingsDetails() { context.startActivity(intent) } + + override fun isBlockedByPhoneCall(): Boolean { + return intent.getStringExtra(Intent.EXTRA_REASON) == reasonPhoneState + } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt index 0bb92ce72595..fb4880f10d3e 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt @@ -163,9 +163,11 @@ internal class RestrictedSwitchPreferenceModel( is BlockedByAdmin -> restrictedMode.getSummary(checkedIfBlockedByAdmin ?: checkedIfNoRestricted()) - is BlockedByEcm -> + is BlockedByEcm -> if (restrictedMode.isBlockedByPhoneCall()) { + context.getString(com.android.settingslib.R.string.disabled_in_phone_call_text) + } else { context.getString(com.android.settingslib.R.string.disabled) - + } null -> context.getPlaceholder() } } diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 3da2271431f8..a3e42f1d1e51 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1228,6 +1228,8 @@ <!-- Summary for settings preference disabled by app ops [CHAR LIMIT=50] --> <string name="disabled_by_app_ops_text">Controlled by Restricted Setting</string> + <!-- Summary for settings preference disabled while the device is in a phone call [CHAR LIMIT=50] --> + <string name="disabled_in_phone_call_text">Unavailable during calls</string> <!-- [CHAR LIMIT=25] Manage applications, text telling using an application is disabled. --> <string name="disabled">Disabled</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java index 212e43aa4044..1044750bae25 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java @@ -44,6 +44,8 @@ import androidx.preference.PreferenceViewHolder; public class RestrictedPreferenceHelper { private static final String TAG = "RestrictedPreferenceHelper"; + private static final String REASON_PHONE_STATE = "phone_state"; + private final Context mContext; private final Preference mPreference; String packageName; @@ -121,7 +123,7 @@ public class RestrictedPreferenceHelper { if (mDisabledByAdmin) { summaryView.setText(disabledText); } else if (mDisabledByEcm) { - summaryView.setText(R.string.disabled_by_app_ops_text); + summaryView.setText(getEcmTextResId()); } else if (TextUtils.equals(disabledText, summaryView.getText())) { // It's previously set to disabled text, clear it. summaryView.setText(null); @@ -323,7 +325,16 @@ public class RestrictedPreferenceHelper { } if (!isEnabled && mDisabledByEcm) { - mPreference.setSummary(R.string.disabled_by_app_ops_text); + mPreference.setSummary(getEcmTextResId()); + } + } + + private int getEcmTextResId() { + if (mDisabledByEcmIntent != null && REASON_PHONE_STATE.equals( + mDisabledByEcmIntent.getStringExtra(Intent.EXTRA_REASON))) { + return R.string.disabled_in_phone_call_text; + } else { + return R.string.disabled_by_app_ops_text; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index ad196b8c1f7b..4ee9ff059502 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -658,12 +658,9 @@ public abstract class InfoMediaManager { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { RouteListingPreference routeListingPreference = getRouteListingPreference(); if (routeListingPreference != null) { - final List<RouteListingPreference.Item> preferenceRouteListing = - Api34Impl.composePreferenceRouteListing( - routeListingPreference); availableRoutes = Api34Impl.arrangeRouteListByPreference(selectedRoutes, getAvailableRoutesFromRouter(), - preferenceRouteListing); + routeListingPreference); } return Api34Impl.filterDuplicatedIds(availableRoutes); } else { @@ -760,11 +757,15 @@ public abstract class InfoMediaManager { @DoNotInline static List<RouteListingPreference.Item> composePreferenceRouteListing( RouteListingPreference routeListingPreference) { + boolean preferRouteListingOrdering = + com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping() + && preferRouteListingOrdering(routeListingPreference); List<RouteListingPreference.Item> finalizedItemList = new ArrayList<>(); List<RouteListingPreference.Item> itemList = routeListingPreference.getItems(); for (RouteListingPreference.Item item : itemList) { // Put suggested devices on the top first before further organization - if ((item.getFlags() & RouteListingPreference.Item.FLAG_SUGGESTED) != 0) { + if (!preferRouteListingOrdering + && (item.getFlags() & RouteListingPreference.Item.FLAG_SUGGESTED) != 0) { finalizedItemList.add(0, item); } else { finalizedItemList.add(item); @@ -792,7 +793,7 @@ public abstract class InfoMediaManager { * Returns an ordered list of available devices based on the provided {@code * routeListingPreferenceItems}. * - * <p>The result has the following order: + * <p>The resulting order if enableOutputSwitcherSessionGrouping is disabled is: * * <ol> * <li>Selected routes. @@ -800,22 +801,54 @@ public abstract class InfoMediaManager { * <li>Not-selected, non-system, available routes sorted by route listing preference. * </ol> * + * <p>The resulting order if enableOutputSwitcherSessionGrouping is enabled is: + * + * <ol> + * <li>Selected routes sorted by route listing preference. + * <li>Selected routes not defined by route listing preference. + * <li>Not-selected system routes. + * <li>Not-selected, non-system, available routes sorted by route listing preference. + * </ol> + * + * * @param selectedRoutes List of currently selected routes. * @param availableRoutes List of available routes that match the app's requested route * features. - * @param routeListingPreferenceItems Ordered list of {@link RouteListingPreference.Item} to - * sort routes with. + * @param routeListingPreference Preferences provided by the app to determine route order. */ @DoNotInline static List<MediaRoute2Info> arrangeRouteListByPreference( List<MediaRoute2Info> selectedRoutes, List<MediaRoute2Info> availableRoutes, - List<RouteListingPreference.Item> routeListingPreferenceItems) { + RouteListingPreference routeListingPreference) { + final List<RouteListingPreference.Item> routeListingPreferenceItems = + Api34Impl.composePreferenceRouteListing(routeListingPreference); + Set<String> sortedRouteIds = new LinkedHashSet<>(); + boolean addSelectedRlpItemsFirst = + com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping() + && preferRouteListingOrdering(routeListingPreference); + Set<String> selectedRouteIds = new HashSet<>(); + + if (addSelectedRlpItemsFirst) { + // Add selected RLP items first + for (MediaRoute2Info selectedRoute : selectedRoutes) { + selectedRouteIds.add(selectedRoute.getId()); + } + for (RouteListingPreference.Item item: routeListingPreferenceItems) { + if (selectedRouteIds.contains(item.getRouteId())) { + sortedRouteIds.add(item.getRouteId()); + } + } + } + // Add selected routes first. - for (MediaRoute2Info selectedRoute : selectedRoutes) { - sortedRouteIds.add(selectedRoute.getId()); + if (com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping() + && sortedRouteIds.size() != selectedRoutes.size()) { + for (MediaRoute2Info selectedRoute : selectedRoutes) { + sortedRouteIds.add(selectedRoute.getId()); + } } // Add not-yet-added system routes. diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index 64a2de5025de..ecea5fd35150 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -33,10 +33,12 @@ import android.service.notification.ZenPolicy; import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; -import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; public class TestModeBuilder { + private static final AtomicInteger sNextId = new AtomicInteger(0); + private String mId; private AutomaticZenRule mRule; private ZenModeConfig.ZenRule mConfigZenRule; @@ -47,7 +49,7 @@ public class TestModeBuilder { public TestModeBuilder() { // Reasonable defaults - int id = new Random().nextInt(1000); + int id = sNextId.incrementAndGet(); mId = "rule_" + id; mRule = new AutomaticZenRule.Builder("Test Rule #" + id, Uri.parse("rule://" + id)) .setPackage("some_package") diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index e1447dc8410c..1a83f0a2e775 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -48,15 +48,20 @@ import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; import android.media.session.MediaSessionManager; import android.os.Build; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import com.android.media.flags.Flags; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.media.InfoMediaManager.Api34Impl; import com.android.settingslib.testutils.shadow.ShadowRouter2Manager; import com.google.common.collect.ImmutableList; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -122,6 +127,8 @@ public class InfoMediaManagerTest { .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO) .build(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private MediaRouter2Manager mRouterManager; @Mock @@ -377,21 +384,26 @@ public class InfoMediaManagerTest { } private RouteListingPreference setUpPreferenceList(String packageName) { + return setUpPreferenceList(packageName, false); + } + + private RouteListingPreference setUpPreferenceList( + String packageName, boolean useSystemOrdering) { ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.UPSIDE_DOWN_CAKE); final List<RouteListingPreference.Item> preferenceItemList = new ArrayList<>(); - RouteListingPreference.Item item1 = + RouteListingPreference.Item item1 = new RouteListingPreference.Item.Builder( + TEST_ID_3).build(); + RouteListingPreference.Item item2 = new RouteListingPreference.Item.Builder(TEST_ID_4) .setFlags(RouteListingPreference.Item.FLAG_SUGGESTED) .build(); - RouteListingPreference.Item item2 = new RouteListingPreference.Item.Builder( - TEST_ID_3).build(); preferenceItemList.add(item1); preferenceItemList.add(item2); RouteListingPreference routeListingPreference = new RouteListingPreference.Builder().setItems( - preferenceItemList).setUseSystemOrdering(false).build(); + preferenceItemList).setUseSystemOrdering(useSystemOrdering).build(); when(mRouterManager.getRouteListingPreference(packageName)) .thenReturn(routeListingPreference); return routeListingPreference; @@ -908,4 +920,66 @@ public class InfoMediaManagerTest { assertThat(device.getState()).isEqualTo(STATE_SELECTED); assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(device); } + + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void composePreferenceRouteListing_useSystemOrderingIsFalse() { + RouteListingPreference routeListingPreference = + setUpPreferenceList(TEST_PACKAGE_NAME, false); + + List<RouteListingPreference.Item> routeOrder = + Api34Impl.composePreferenceRouteListing(routeListingPreference); + + assertThat(routeOrder.get(0).getRouteId()).isEqualTo(TEST_ID_3); + assertThat(routeOrder.get(1).getRouteId()).isEqualTo(TEST_ID_4); + } + + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void composePreferenceRouteListing_useSystemOrderingIsTrue() { + RouteListingPreference routeListingPreference = + setUpPreferenceList(TEST_PACKAGE_NAME, true); + + List<RouteListingPreference.Item> routeOrder = + Api34Impl.composePreferenceRouteListing(routeListingPreference); + + assertThat(routeOrder.get(0).getRouteId()).isEqualTo(TEST_ID_4); + assertThat(routeOrder.get(1).getRouteId()).isEqualTo(TEST_ID_3); + } + + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void arrangeRouteListByPreference_useSystemOrderingIsFalse() { + RouteListingPreference routeListingPreference = + setUpPreferenceList(TEST_PACKAGE_NAME, false); + List<MediaRoute2Info> routes = setAvailableRoutesList(TEST_PACKAGE_NAME); + when(mRouterManager.getSelectedRoutes(any())).thenReturn(routes); + + List<MediaRoute2Info> routeOrder = + Api34Impl.arrangeRouteListByPreference( + routes, routes, routeListingPreference); + + assertThat(routeOrder.get(0).getId()).isEqualTo(TEST_ID_3); + assertThat(routeOrder.get(1).getId()).isEqualTo(TEST_ID_4); + assertThat(routeOrder.get(2).getId()).isEqualTo(TEST_ID_2); + assertThat(routeOrder.get(3).getId()).isEqualTo(TEST_ID_1); + } + + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void arrangeRouteListByPreference_useSystemOrderingIsTrue() { + RouteListingPreference routeListingPreference = + setUpPreferenceList(TEST_PACKAGE_NAME, true); + List<MediaRoute2Info> routes = setAvailableRoutesList(TEST_PACKAGE_NAME); + when(mRouterManager.getSelectedRoutes(any())).thenReturn(routes); + + List<MediaRoute2Info> routeOrder = + Api34Impl.arrangeRouteListByPreference( + routes, routes, routeListingPreference); + + assertThat(routeOrder.get(0).getId()).isEqualTo(TEST_ID_2); + assertThat(routeOrder.get(1).getId()).isEqualTo(TEST_ID_3); + assertThat(routeOrder.get(2).getId()).isEqualTo(TEST_ID_4); + assertThat(routeOrder.get(3).getId()).isEqualTo(TEST_ID_1); + } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespaces.java b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespaces.java index d504f83877c5..6d30f492075e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespaces.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespaces.java @@ -35,6 +35,7 @@ final class WritableNamespaces { new ArraySet<String>(Arrays.asList( "adservices", "autofill", + "app_compat_overrides", "captive_portal_login", "connectivity", "exo", @@ -42,6 +43,7 @@ final class WritableNamespaces { "netd_native", "network_security", "on_device_personalization", + "testing", "tethering", "tethering_u_or_later_native", "thread_network" diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 2b4e65f2415c..8fe3a0c4b4ae 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -952,7 +952,6 @@ <uses-permission android:name="android.permission.SETUP_FSVERITY" /> <!-- Permissions required for CTS test - AppFunctionManagerTest --> - <uses-permission android:name="android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED" /> <uses-permission android:name="android.permission.EXECUTE_APP_FUNCTIONS" /> <!-- Permission required for CTS test - CtsNfcTestCases --> @@ -998,6 +997,12 @@ <uses-permission android:name="android.permission.health.READ_SKIN_TEMPERATURE" android:featureFlag="android.permission.flags.replace_body_sensor_permission_enabled"/> + <!-- Permissions required for CTS test - CtsHealthFitnessDeviceTestCases--> + <uses-permission android:name="android.permission.BACKUP_HEALTH_CONNECT_DATA_AND_SETTINGS" + android:featureFlag="android.permission.flags.health_connect_backup_restore_permission_enabled"/> + <uses-permission android:name="android.permission.RESTORE_HEALTH_CONNECT_DATA_AND_SETTINGS" + android:featureFlag="android.permission.flags.health_connect_backup_restore_permission_enabled"/> + <!-- Permission for TestClassifier tests to get access to classifier by type --> <uses-permission android:name="android.permission.ACCESS_TEXT_CLASSIFIER_BY_TYPE" android:featureFlag="android.permission.flags.text_classifier_choice_api_enabled"/> diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index f4181e1c69ef..6c96279711d0 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -196,6 +196,18 @@ flag { } flag { + name: "notification_undo_guts_on_config_changed" + namespace: "systemui" + description: "Fixes a bug where a theme or font change while notification guts were open" + " (e.g. the snooze options or notification info) would show an empty notification by" + " closing the guts and undoing changes." + bug: "379267630" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "pss_app_selector_recents_split_screen" namespace: "systemui" description: "Allows recent apps selected for partial screenshare to be launched in split screen mode" @@ -575,20 +587,6 @@ flag { } flag { - name: "clock_reactive_variants" - namespace: "systemui" - description: "Add reactive variant fonts to some clocks" - bug: "343495953" -} - -flag { - name: "lockscreen_custom_clocks" - namespace: "systemui" - description: "Enable lockscreen custom clocks" - bug: "378486437" -} - -flag { name: "faster_unlock_transition" namespace: "systemui" description: "Faster wallpaper unlock transition" @@ -1699,13 +1697,6 @@ flag { } flag { - name: "magic_portrait_wallpapers" - namespace: "systemui" - description: "Magic Portrait related changes in systemui" - bug: "370863642" -} - -flag { name: "notes_role_qs_tile" namespace: "systemui" description: "Enables notes role qs tile which opens default notes role app in app bubbles" @@ -1828,6 +1819,13 @@ flag { } flag { + name: "notification_row_transparency" + namespace: "systemui" + description: "Enables transparency on the Notification Shade." + bug: "392187268" +} + +flag { name: "shade_expands_on_status_bar_long_press" namespace: "systemui" description: "Expands the shade on long press of any status bar" @@ -2008,3 +2006,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "show_locked_by_your_watch_keyguard_indicator" + namespace: "systemui" + description: "Show a Locked by your watch indicator on the keyguard when the device is locked by the watch." + bug: "387322459" +} diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt index 0f6e6a7c4383..f490968b7a7c 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt @@ -34,7 +34,6 @@ import dagger.Module import dagger.Provides import dagger.multibindings.IntoSet import javax.inject.Provider -import kotlinx.coroutines.ExperimentalCoroutinesApi @Module(includes = [LockscreenSceneBlueprintModule::class]) interface LockscreenSceneModule { @@ -43,7 +42,6 @@ interface LockscreenSceneModule { companion object { - @OptIn(ExperimentalCoroutinesApi::class) @Provides @SysUISingleton @KeyguardRootView diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/ContentDescription.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/ContentDescription.kt index 4a5ad6554dc6..b254963cc5e9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/ContentDescription.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/ContentDescription.kt @@ -17,11 +17,13 @@ package com.android.systemui.common.ui.compose import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.res.stringResource import com.android.systemui.common.shared.model.ContentDescription /** Returns the loaded [String] or `null` if there isn't one. */ @Composable +@ReadOnlyComposable fun ContentDescription.load(): String? { return when (this) { is ContentDescription.Loaded -> description diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt index 82d14369f239..8b0c00535262 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt @@ -21,9 +21,8 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.res.painterResource -import androidx.core.graphics.drawable.toBitmap +import com.android.compose.ui.graphics.painter.rememberDrawablePainter import com.android.systemui.common.shared.model.Icon /** @@ -36,7 +35,7 @@ fun Icon(icon: Icon, modifier: Modifier = Modifier, tint: Color = LocalContentCo val contentDescription = icon.contentDescription?.load() when (icon) { is Icon.Loaded -> { - Icon(icon.drawable.toBitmap().asImageBitmap(), contentDescription, modifier, tint) + Icon(rememberDrawablePainter(icon.drawable), contentDescription, modifier, tint) } is Icon.Resource -> Icon(painterResource(icon.res), contentDescription, modifier, tint) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt index 4e8121f14e4b..19adba0f39de 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt @@ -19,6 +19,7 @@ package com.android.systemui.common.ui.compose import android.content.Context import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import com.android.systemui.common.shared.model.Text @@ -26,6 +27,7 @@ import com.android.systemui.common.shared.model.Text.Companion.loadText /** Returns the loaded [String] or `null` if there isn't one. */ @Composable +@ReadOnlyComposable fun Text.load(): String? { return when (this) { is Text.Loaded -> text diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt index 30dfa5bb826a..2c6d09a4593a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt @@ -19,13 +19,11 @@ package com.android.systemui.communal.ui.compose import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntRect @@ -35,6 +33,7 @@ import com.android.compose.animation.scene.ContentScope import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.smartspace.SmartspaceInteractionHandler import com.android.systemui.communal.ui.compose.section.AmbientStatusBarSection +import com.android.systemui.communal.ui.compose.section.CommunalLockSection import com.android.systemui.communal.ui.compose.section.CommunalPopupSection import com.android.systemui.communal.ui.compose.section.CommunalToDreamButtonSection import com.android.systemui.communal.ui.compose.section.HubOnboardingSection @@ -43,7 +42,6 @@ import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection import com.android.systemui.keyguard.ui.composable.section.LockSection -import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialogFactory import javax.inject.Inject import kotlin.math.min @@ -58,6 +56,7 @@ constructor( private val communalSettingsInteractor: CommunalSettingsInteractor, private val dialogFactory: SystemUIDialogFactory, private val lockSection: LockSection, + private val communalLockSection: CommunalLockSection, private val bottomAreaSection: BottomAreaSection, private val ambientStatusBarSection: AmbientStatusBarSection, private val communalPopupSection: CommunalPopupSection, @@ -88,12 +87,9 @@ constructor( with(hubOnboardingSection) { BottomSheet() } } if (communalSettingsInteractor.isV2FlagEnabled()) { - Icon( - painter = painterResource(id = R.drawable.ic_lock), - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.element(Communal.Elements.LockIcon), - ) + with(communalLockSection) { + LockIcon(modifier = Modifier.element(Communal.Elements.LockIcon)) + } } else { with(lockSection) { LockIcon( @@ -156,14 +152,8 @@ constructor( val bottomAreaPlaceable = bottomAreaMeasurable.measure(noMinConstraints) - val screensaverButtonSizeInt = screensaverButtonSize.roundToPx() val screensaverButtonPlaceable = - screensaverButtonMeasurable?.measure( - Constraints.fixed( - width = screensaverButtonSizeInt, - height = screensaverButtonSizeInt, - ) - ) + screensaverButtonMeasurable?.measure(noMinConstraints) val communalGridPlaceable = communalGridMeasurable.measure( @@ -181,12 +171,12 @@ constructor( screensaverButtonPlaceable?.place( x = constraints.maxWidth - - screensaverButtonSizeInt - - screensaverButtonPaddingInt, + screensaverButtonPaddingInt - + screensaverButtonPlaceable.width, y = constraints.maxHeight - - screensaverButtonSizeInt - - screensaverButtonPaddingInt, + screensaverButtonPaddingInt - + screensaverButtonPlaceable.height, ) } } @@ -194,7 +184,6 @@ constructor( } companion object { - private val screensaverButtonSize: Dp = 64.dp private val screensaverButtonPadding: Dp = 24.dp // TODO(b/382739998): Remove these hardcoded values once lock icon size and bottom area diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalLockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalLockSection.kt new file mode 100644 index 000000000000..eab2b8717405 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalLockSection.kt @@ -0,0 +1,150 @@ +/* + * 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.communal.ui.compose.section + +import android.content.Context +import android.util.DisplayMetrics +import android.view.WindowManager +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.viewinterop.AndroidView +import com.android.compose.animation.scene.ContentScope +import com.android.compose.animation.scene.ElementKey +import com.android.systemui.biometrics.AuthController +import com.android.systemui.communal.ui.binder.CommunalLockIconViewBinder +import com.android.systemui.communal.ui.viewmodel.CommunalLockIconViewModel +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines +import com.android.systemui.keyguard.ui.view.DeviceEntryIconView +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LongPressHandlingViewLogger +import com.android.systemui.log.dagger.LongPressTouchLog +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.res.R +import com.android.systemui.statusbar.VibratorHelper +import dagger.Lazy +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +class CommunalLockSection +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val windowManager: WindowManager, + private val authController: AuthController, + private val viewModel: Lazy<CommunalLockIconViewModel>, + private val falsingManager: Lazy<FalsingManager>, + private val vibratorHelper: Lazy<VibratorHelper>, + private val featureFlags: FeatureFlagsClassic, + @LongPressTouchLog private val logBuffer: LogBuffer, +) { + @Composable + fun ContentScope.LockIcon(modifier: Modifier = Modifier) { + val context = LocalContext.current + + AndroidView( + factory = { context -> + DeviceEntryIconView( + context, + null, + logger = LongPressHandlingViewLogger(logBuffer, tag = TAG), + ) + .apply { + id = R.id.device_entry_icon_view + CommunalLockIconViewBinder.bind( + applicationScope, + this, + viewModel.get(), + falsingManager.get(), + vibratorHelper.get(), + ) + } + }, + modifier = + modifier.element(LockIconElementKey).layout { measurable, _ -> + val lockIconBounds = lockIconBounds(context) + val placeable = + measurable.measure( + Constraints.fixed( + width = lockIconBounds.width, + height = lockIconBounds.height, + ) + ) + layout( + width = placeable.width, + height = placeable.height, + alignmentLines = + mapOf( + BlueprintAlignmentLines.LockIcon.Left to lockIconBounds.left, + BlueprintAlignmentLines.LockIcon.Top to lockIconBounds.top, + BlueprintAlignmentLines.LockIcon.Right to lockIconBounds.right, + BlueprintAlignmentLines.LockIcon.Bottom to lockIconBounds.bottom, + ), + ) { + placeable.place(0, 0) + } + }, + ) + } + + /** Returns the bounds of the lock icon, in window view coordinates. */ + private fun lockIconBounds(context: Context): IntRect { + val windowViewBounds = windowManager.currentWindowMetrics.bounds + var widthPx = windowViewBounds.right.toFloat() + if (featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) { + val insets = windowManager.currentWindowMetrics.windowInsets + // Assumed to be initially neglected as there are no left or right insets in portrait. + // However, on landscape, these insets need to included when calculating the midpoint. + @Suppress("DEPRECATION") + widthPx -= (insets.systemWindowInsetLeft + insets.systemWindowInsetRight).toFloat() + } + val defaultDensity = + DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() / + DisplayMetrics.DENSITY_DEFAULT.toFloat() + val lockIconRadiusPx = (defaultDensity * 36).toInt() + + val scaleFactor = authController.scaleFactor + val bottomPaddingPx = + context.resources.getDimensionPixelSize( + com.android.systemui.customization.R.dimen.lock_icon_margin_bottom + ) + val heightPx = windowViewBounds.bottom.toFloat() + val (center, radius) = + Pair( + IntOffset( + x = (widthPx / 2).toInt(), + y = (heightPx - ((bottomPaddingPx + lockIconRadiusPx) * scaleFactor)).toInt(), + ), + (lockIconRadiusPx * scaleFactor).toInt(), + ) + + return IntRect(center, radius) + } + + companion object { + private const val TAG = "CommunalLockSection" + } +} + +private val LockIconElementKey = ElementKey("CommunalLockIcon") diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt index 9421596f7116..13d551aef4c2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt @@ -16,14 +16,37 @@ package com.android.systemui.communal.ui.compose.section +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.toRect +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp import com.android.compose.PlatformIconButton import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor +import com.android.systemui.communal.ui.compose.extensions.observeTaps import com.android.systemui.communal.ui.viewmodel.CommunalToDreamButtonViewModel import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R @@ -43,23 +66,111 @@ constructor( val viewModel = rememberViewModel("CommunalToDreamButtonSection") { viewModelFactory.create() } - val shouldShowDreamButtonOnHub by - viewModel.shouldShowDreamButtonOnHub.collectAsStateWithLifecycle(false) - if (!shouldShowDreamButtonOnHub) { + if (!viewModel.shouldShowDreamButtonOnHub) { return } - PlatformIconButton( - onClick = { viewModel.onShowDreamButtonTap() }, - iconResource = R.drawable.ic_screensaver_auto, - contentDescription = - stringResource(R.string.accessibility_glanceable_hub_to_dream_button), - colors = - IconButtonDefaults.filledIconButtonColors( - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - containerColor = MaterialTheme.colorScheme.primaryContainer, - ), + if (viewModel.shouldShowTooltip) { + Column( + modifier = + Modifier.widthIn(max = tooltipMaxWidth).pointerInput(Unit) { + observeTaps { viewModel.setDreamButtonTooltipDismissed() } + } + ) { + Tooltip( + pointerOffsetDp = buttonSize.div(2), + text = stringResource(R.string.glanceable_hub_to_dream_button_tooltip), + ) + GoToDreamButton( + modifier = Modifier.width(buttonSize).height(buttonSize).align(Alignment.End) + ) { + viewModel.onShowDreamButtonTap() + } + } + } else { + GoToDreamButton(modifier = Modifier.width(buttonSize).height(buttonSize)) { + viewModel.onShowDreamButtonTap() + } + } + } + + companion object { + private val buttonSize = 64.dp + private val tooltipMaxWidth = 350.dp + } +} + +@Composable +private fun GoToDreamButton(modifier: Modifier, onClick: () -> Unit) { + PlatformIconButton( + modifier = modifier, + onClick = onClick, + iconResource = R.drawable.ic_screensaver_auto, + contentDescription = stringResource(R.string.accessibility_glanceable_hub_to_dream_button), + colors = + IconButtonDefaults.filledIconButtonColors( + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = MaterialTheme.colorScheme.primaryContainer, + ), + ) +} + +@Composable +private fun Tooltip(pointerOffsetDp: Dp, text: String) { + Surface( + color = MaterialTheme.colorScheme.surface, + shape = TooltipShape(pointerSizeDp = 12.dp, pointerOffsetDp = pointerOffsetDp), + ) { + Text( + modifier = Modifier.padding(start = 32.dp, top = 16.dp, end = 32.dp, bottom = 32.dp), + color = MaterialTheme.colorScheme.onSurface, + text = text, ) } + + Spacer(modifier = Modifier.height(4.dp)) +} + +private class TooltipShape(private val pointerSizeDp: Dp, private val pointerOffsetDp: Dp) : Shape { + + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density, + ): Outline { + + val pointerSizePx = with(density) { pointerSizeDp.toPx() } + val pointerOffsetPx = with(density) { pointerOffsetDp.toPx() } + val cornerRadius = CornerRadius(CornerSize(16.dp).toPx(size, density)) + val bubbleSize = size.copy(height = size.height - pointerSizePx) + + val path = + Path().apply { + addRoundRect( + RoundRect( + rect = bubbleSize.toRect(), + topLeft = cornerRadius, + topRight = cornerRadius, + bottomRight = cornerRadius, + bottomLeft = cornerRadius, + ) + ) + addPath( + Path().apply { + moveTo(0f, 0f) + lineTo(pointerSizePx / 2f, pointerSizePx) + lineTo(pointerSizePx, 0f) + close() + }, + offset = + Offset( + x = bubbleSize.width - pointerOffsetPx - pointerSizePx / 2f, + y = bubbleSize.height, + ), + ) + } + + return Outline.Generic(path) + } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt index d02215083679..500527f4a508 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt @@ -57,9 +57,7 @@ import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaVie import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel import com.android.systemui.log.LongPressHandlingViewLogger import com.android.systemui.res.R -import kotlinx.coroutines.ExperimentalCoroutinesApi -@ExperimentalCoroutinesApi @Composable fun AlternateBouncer( alternateBouncerDependencies: AlternateBouncerDependencies, @@ -127,7 +125,6 @@ fun AlternateBouncer( } } -@ExperimentalCoroutinesApi @Composable private fun StatusMessage( viewModel: AlternateBouncerMessageAreaViewModel, @@ -156,7 +153,6 @@ private fun StatusMessage( } } -@ExperimentalCoroutinesApi @Composable private fun DeviceEntryIcon( viewModel: AlternateBouncerUdfpsIconViewModel, @@ -179,7 +175,6 @@ private fun DeviceEntryIcon( } /** TODO (b/353955910): Validate accessibility CUJs */ -@ExperimentalCoroutinesApi @Composable private fun UdfpsA11yOverlay( viewModel: AlternateBouncerUdfpsAccessibilityOverlayViewModel, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt index 478970f4e210..d3417022565b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -45,7 +45,6 @@ import com.android.systemui.keyguard.ui.composable.section.StatusBarSection import com.android.systemui.keyguard.ui.composable.section.TopAreaSection import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import com.android.systemui.res.R -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod import java.util.Optional import javax.inject.Inject import kotlin.math.roundToInt @@ -130,9 +129,7 @@ constructor( if (!isShadeLayoutWide && !isBypassEnabled) { Box(modifier = Modifier.weight(weight = 1f)) { Column(Modifier.align(alignment = Alignment.TopStart)) { - if (PromotedNotificationUiAod.isEnabled) { - AodPromotedNotification() - } + AodPromotedNotificationArea() AodNotificationIcons( modifier = Modifier.padding(start = aodIconPadding) ) @@ -145,9 +142,7 @@ constructor( } } else { Column { - if (PromotedNotificationUiAod.isEnabled) { - AodPromotedNotification() - } + AodPromotedNotificationArea() AodNotificationIcons( modifier = Modifier.padding(start = aodIconPadding) ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index b66690c2fe89..abf7fdc05f2e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -54,6 +54,7 @@ import com.android.systemui.statusbar.notification.icon.ui.viewbinder.Notificati import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel import com.android.systemui.statusbar.notification.promoted.AODPromotedNotification +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod import com.android.systemui.statusbar.notification.promoted.ui.viewmodel.AODPromotedNotificationViewModel import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView @@ -110,8 +111,23 @@ constructor( } @Composable - fun AodPromotedNotification() { - AODPromotedNotification(aodPromotedNotificationViewModelFactory) + fun AodPromotedNotificationArea(modifier: Modifier = Modifier) { + if (!PromotedNotificationUiAod.isEnabled) { + return + } + + val isVisible by + keyguardRootViewModel.isAodPromotedNotifVisible.collectAsStateWithLifecycle() + val burnIn = rememberBurnIn(clockInteractor) + + AnimatedVisibility( + visible = isVisible, + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier.burnInAware(aodBurnInViewModel, burnIn.parameters), + ) { + AODPromotedNotification(aodPromotedNotificationViewModelFactory) + } } @Composable diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt index 6738b97de015..1423d4acca21 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package com.android.systemui.scene.ui.composable import androidx.compose.runtime.snapshotFlow @@ -26,7 +24,6 @@ import com.android.compose.animation.scene.TransitionKey import com.android.compose.animation.scene.observableTransitionState import com.android.systemui.scene.shared.model.SceneDataSource import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flatMapLatest diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt index b4c60037b426..73b0750f4a54 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt @@ -17,11 +17,8 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween -import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder -import com.android.compose.animation.scene.UserActionDistance -import com.android.compose.animation.scene.UserActionDistanceScope import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.notifications.ui.composable.NotificationsShade @@ -31,9 +28,6 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - distance = UserActionDistance { _, shadeContentKey, _ -> - calculateShadePanelTargetPositionY(shadeContentKey) - } // Ensure the clock isn't clipped by the shade outline during the transition from lockscreen. sharedElement( @@ -50,12 +44,4 @@ fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0 fractionRange(start = .5f) { fade(Notifications.Elements.NotificationScrim) } } -/** Returns the Y position of the bottom of the shade container panel within [shadeOverlayKey]. */ -fun UserActionDistanceScope.calculateShadePanelTargetPositionY(shadeOverlayKey: ContentKey): Float { - val marginTop = OverlayShade.Elements.Panel.targetOffset(shadeOverlayKey)?.y ?: 0f - val panelHeight = - OverlayShade.Elements.Panel.targetSize(shadeOverlayKey)?.height?.toFloat() ?: 0f - return marginTop + panelHeight -} - private val DefaultDuration = 300.milliseconds diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt index c9fbb4da9ffb..43aa35854542 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt @@ -19,18 +19,13 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder -import com.android.compose.animation.scene.UserActionDistance import com.android.systemui.shade.ui.composable.OverlayShade import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toQuickSettingsShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - distance = UserActionDistance { _, shadeContentKey, _ -> - calculateShadePanelTargetPositionY(shadeContentKey) - } translate(OverlayShade.Elements.Panel, Edge.Top) - fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt index 51483a894e1e..358d01874229 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package com.android.compose.nestedscroll import androidx.compose.foundation.gestures.FlingBehavior @@ -28,7 +26,6 @@ import androidx.compose.ui.unit.Velocity import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.test.runMonotonicClockTest import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt index 0245cf2a26b8..97fba3d0dc8e 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/RunMonotonicClockTest.kt @@ -3,7 +3,6 @@ package com.android.compose.test import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.TestMonotonicFrameClock import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -18,7 +17,7 @@ import kotlinx.coroutines.withContext * Note: Please refer to the documentation for [runTest], as this feature utilizes it. This will * provide a comprehensive understanding of all its behaviors. */ -@OptIn(ExperimentalTestApi::class, ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalTestApi::class) fun runMonotonicClockTest(block: suspend MonotonicClockTestScope.() -> Unit) = runTest { val testScope: TestScope = this diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index 12b20a53df81..ab31286b78b4 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -299,8 +299,7 @@ open class ClockRegistry( Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, ) } - - ClockSettings.fromJson(JSONObject(json)) + json?.let { ClockSettings.fromJson(JSONObject(it)) } } catch (ex: Exception) { logger.e("Failed to parse clock settings", ex) null diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt index e2bbe0fef3c0..d7d8d28a71e0 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt @@ -37,6 +37,7 @@ import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData import com.android.systemui.shared.clocks.view.FlexClockView import com.android.systemui.shared.clocks.view.HorizontalAlignment +import com.android.systemui.shared.clocks.view.VerticalAlignment import java.util.Locale import java.util.TimeZone import kotlin.math.max @@ -255,7 +256,7 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: timespec = DigitalTimespec.TIME_FULL_FORMAT, style = FontTextStyle(fontSizeScale = 0.98f), aodStyle = FontTextStyle(), - alignment = DigitalAlignment(HorizontalAlignment.LEFT, null), + alignment = DigitalAlignment(HorizontalAlignment.LEFT, VerticalAlignment.CENTER), dateTimeFormat = "h:mm", ) } diff --git a/packages/SystemUI/flag_check.py b/packages/SystemUI/flag_check.py deleted file mode 100755 index d78ef5a5f1bf..000000000000 --- a/packages/SystemUI/flag_check.py +++ /dev/null @@ -1,138 +0,0 @@ -#! /usr/bin/env python3 - -import sys -import re -import argparse - -# partially copied from tools/repohooks/rh/hooks.py - -TEST_MSG = """Commit message is missing a "Flag:" line. It must match one of the -following case-sensitive regex: - - %s - -The Flag: stanza is regex matched and should describe whether your change is behind a flag or flags. -As a CL author, you'll have a consistent place to describe the risk of the proposed change by explicitly calling out the name of the flag. -For legacy flags use EXEMPT with your flag name. - -Some examples below: - -Flag: NONE Repohook Update -Flag: TEST_ONLY -Flag: EXEMPT resource only update -Flag: EXEMPT bugfix -Flag: EXEMPT refactor -Flag: com.android.launcher3.enable_twoline_allapps -Flag: com.google.android.apps.nexuslauncher.zero_state_web_data_loader - -Check the git history for more examples. It's a regex matched field. See go/android-flag-directive for more details on various formats. -""" - -def main(): - """Check the commit message for a 'Flag:' line.""" - parser = argparse.ArgumentParser( - description='Check the commit message for a Flag: line.') - parser.add_argument('--msg', - metavar='msg', - type=str, - nargs='?', - default='HEAD', - help='commit message to process.') - parser.add_argument( - '--files', - metavar='files', - nargs='?', - default='', - help= - 'PREUPLOAD_FILES in repo upload to determine whether the check should run for the files.') - parser.add_argument( - '--project', - metavar='project', - type=str, - nargs='?', - default='', - help= - 'REPO_PROJECT in repo upload to determine whether the check should run for this project.') - - # Parse the arguments - args = parser.parse_args() - desc = args.msg - files = args.files - project = args.project - - if not should_run_path(project, files): - return - - field = 'Flag' - none = 'NONE' - testOnly = 'TEST_ONLY' - docsOnly = 'DOCS_ONLY' - exempt = 'EXEMPT' - justification = '<justification>' - - # Aconfig Flag name format = <packageName>.<flagName> - # package name - Contains only lowercase alphabets + digits + '.' - Ex: com.android.launcher3 - # For now alphabets, digits, "_", "." characters are allowed in flag name. - # Checks if there is "one dot" between packageName and flagName and not adding stricter format check - #common_typos_disable - flagName = '([a-zA-Z0-9.]+)([.]+)([a-zA-Z0-9_.]+)' - - # None and Exempt needs justification - exemptRegex = fr'{exempt}\s*[a-zA-Z]+' - noneRegex = fr'{none}\s*[a-zA-Z]+' - #common_typos_enable - - readableRegexMsg = '\n\tFlag: '+none+' '+justification+'\n\tFlag: <packageName>.<flagName>\n\tFlag: ' +exempt+' '+justification+'\n\tFlag: '+testOnly+'\n\tFlag: '+docsOnly - - flagRegex = fr'^{field}: .*$' - check_flag = re.compile(flagRegex) #Flag: - - # Ignore case for flag name format. - flagNameRegex = fr'(?i)^{field}:\s*({noneRegex}|{flagName}|{testOnly}|{docsOnly}|{exemptRegex})\s*' - check_flagName = re.compile(flagNameRegex) #Flag: <flag name format> - - flagError = False - foundFlag = [] - # Check for multiple "Flag:" lines and all lines should match this format - for line in desc.splitlines(): - if check_flag.match(line): - if not check_flagName.match(line): - flagError = True - break - foundFlag.append(line) - - # Throw error if - # 1. No "Flag:" line is found - # 2. "Flag:" doesn't follow right format. - if (not foundFlag) or (flagError): - error = TEST_MSG % (readableRegexMsg) - print(error) - sys.exit(1) - - sys.exit(0) - - -def should_run_path(project, files): - """Returns a boolean if this check should run with these paths. - If you want to check for a particular subdirectory under the path, - add a check here, call should_run_files and check for a specific sub dir path in should_run_files. - """ - if not project: - return False - if project == 'platform/frameworks/base': - return should_run_files(files) - # Default case, run for all other projects which calls this script. - return True - - -def should_run_files(files): - """Returns a boolean if this check should run with these files.""" - if not files: - return False - if 'packages/SystemUI' in files: - return True - return False - - -if __name__ == '__main__': - main() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index b8d4bb4b8e77..50762edc1179 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.biometrics import android.app.ActivityTaskManager -import android.app.admin.DevicePolicyManager import android.content.pm.PackageManager import android.content.res.Configuration import android.hardware.biometrics.BiometricAuthenticator @@ -43,6 +42,8 @@ import androidx.test.filters.SmallTest import com.android.app.viewcapture.ViewCapture import com.android.internal.jank.InteractionJankMonitor import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN +import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN import com.android.launcher3.icons.IconProvider import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository @@ -432,8 +433,7 @@ open class AuthContainerViewTest : SysuiTestCase() { .setMoreOptionsButtonListener(fakeExecutor) { _, _ -> isButtonClicked = true } .build() - val container = - initializeFingerprintContainer(contentViewWithMoreOptionsButton = contentView) + val container = initializeFingerprintContainer() waitForIdleSync() @@ -488,8 +488,7 @@ open class AuthContainerViewTest : SysuiTestCase() { .build() val container = initializeFingerprintContainer( - authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL, - contentViewWithMoreOptionsButton = contentView, + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL ) waitForIdleSync() @@ -500,8 +499,8 @@ open class AuthContainerViewTest : SysuiTestCase() { @Test fun testCredentialViewUsesEffectiveUserId() { whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(200) - whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(200))) - .thenReturn(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING) + whenever(lockPatternUtils.getCredentialTypeForUser(eq(200))) + .thenReturn(CREDENTIAL_TYPE_PATTERN) val container = initializeFingerprintContainer( @@ -578,8 +577,7 @@ open class AuthContainerViewTest : SysuiTestCase() { addToView: Boolean = true ): TestAuthContainerView { whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20) - whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20))) - .thenReturn(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC) + whenever(lockPatternUtils.getCredentialTypeForUser(eq(20))).thenReturn(CREDENTIAL_TYPE_PIN) // In the credential view, clicking on the background (to cancel authentication) is not // valid. Thus, the listener should be null, and it should not be in the accessibility @@ -599,7 +597,6 @@ open class AuthContainerViewTest : SysuiTestCase() { authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, addToView: Boolean = true, verticalListContentView: PromptVerticalListContentView? = null, - contentViewWithMoreOptionsButton: PromptContentViewWithMoreOptionsButton? = null, ) = initializeContainer( TestAuthContainerView( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt index 58fe2c9cbe57..fde847897f72 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt @@ -135,9 +135,9 @@ class CredentialInteractorImplTest : SysuiTestCase() { private fun pinCredential(result: VerifyCredentialResponse, credentialOwner: Int = USER_ID) = runTest { val usedAttempts = 1 - whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID))) + whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(credentialOwner))) .thenReturn(usedAttempts) - whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())) + whenever(lockPatternUtils.verifyCredential(any(), eq(credentialOwner), anyInt())) .thenReturn(result) whenever(lockPatternUtils.verifyTiedProfileChallenge(any(), eq(USER_ID), anyInt())) .thenReturn(result) @@ -170,7 +170,7 @@ class CredentialInteractorImplTest : SysuiTestCase() { assertThat(successfulResult).isNotNull() assertThat(successfulResult!!.hat).isEqualTo(result.gatekeeperHAT) - verify(lockPatternUtils).userPresent(eq(USER_ID)) + verify(lockPatternUtils).userPresent(eq(credentialOwner)) verify(lockPatternUtils) .removeGatekeeperPasswordHandle(eq(result.gatekeeperPasswordHandle)) } else { @@ -190,13 +190,13 @@ class CredentialInteractorImplTest : SysuiTestCase() { .hasSize(statusList.size) verify(lockPatternUtils) - .setLockoutAttemptDeadline(eq(USER_ID), eq(result.timeout)) + .setLockoutAttemptDeadline(eq(credentialOwner), eq(result.timeout)) } else { // failed assertThat(failedResult.error) .matches(Regex("(.*)try again(.*)", RegexOption.IGNORE_CASE).toPattern()) assertThat(statusList).isEmpty() - verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID)) + verify(lockPatternUtils).reportFailedPasswordAttempt(eq(credentialOwner)) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt index 4c329dcf2f2b..cebd05d92537 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt @@ -18,20 +18,12 @@ package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothLeBroadcast import android.bluetooth.BluetoothLeBroadcastMetadata -import android.content.ContentResolver -import android.content.applicationContext import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.settingslib.bluetooth.BluetoothEventManager import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast -import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant -import com.android.settingslib.bluetooth.LocalBluetoothProfileManager -import com.android.settingslib.bluetooth.VolumeControlProfile -import com.android.settingslib.volume.shared.AudioSharingLogger import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -46,14 +38,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.whenever @@ -78,7 +66,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testIsAudioSharingOn_flagOff_false() = + fun isAudioSharingOn_flagOff_false() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false) @@ -90,7 +78,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testIsAudioSharingOn_flagOn_notInAudioSharing_false() = + fun isAudioSharingOn_flagOn_notInAudioSharing_false() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) @@ -103,7 +91,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testIsAudioSharingOn_flagOn_inAudioSharing_true() = + fun isAudioSharingOn_flagOn_inAudioSharing_true() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) @@ -116,7 +104,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testAudioSourceStateUpdate_notInAudioSharing_returnEmpty() = + fun audioSourceStateUpdate_notInAudioSharing_returnEmpty() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) @@ -129,7 +117,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testAudioSourceStateUpdate_inAudioSharing_returnUnit() = + fun audioSourceStateUpdate_inAudioSharing_returnUnit() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) @@ -144,7 +132,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testHandleAudioSourceWhenReady_flagOff_sourceNotAdded() = + fun handleAudioSourceWhenReady_flagOff_sourceNotAdded() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false) @@ -157,7 +145,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testHandleAudioSourceWhenReady_noProfile_sourceNotAdded() = + fun handleAudioSourceWhenReady_noProfile_sourceNotAdded() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) @@ -171,36 +159,41 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() = + fun handleAudioSourceWhenReady_hasProfileButAudioSharingNeverTriggered_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) - runCurrent() + // Verify callback registered for onBroadcastStartedOrStopped + verify(localBluetoothLeBroadcast).registerServiceCallBack(any(), any()) assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() job.cancel() } } @Test - fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() = + fun handleAudioSourceWhenReady_audioSharingTriggeredButFailed_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + // Verify callback registered for onBroadcastStartedOrStopped + verify(localBluetoothLeBroadcast) + .registerServiceCallBack(any(), callbackCaptor.capture()) + // Audio sharing started failed, trigger onBroadcastStartFailed + whenever(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(false) + underTest.startAudioSharing() + runCurrent() + callbackCaptor.value.onBroadcastStartFailed(0) runCurrent() assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() @@ -209,122 +202,59 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() = + fun handleAudioSourceWhenReady_audioSharingTriggeredButMetadataNotReady_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) - runCurrent() + // Verify callback registered for onBroadcastStartedOrStopped verify(localBluetoothLeBroadcast) .registerServiceCallBack(any(), callbackCaptor.capture()) runCurrent() - callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata) + underTest.startAudioSharing() runCurrent() + // Verify callback registered for onBroadcastMetadataChanged + verify(localBluetoothLeBroadcast, times(2)) + .registerServiceCallBack(any(), callbackCaptor.capture()) - assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isTrue() - job.cancel() - } - } - - @Test - fun testHandleAudioSourceWhenReady_skipInitialValue_noAudioSharing_sourceNotAdded() = - with(kosmos) { - testScope.runTest { - val (broadcast, repository) = setupRepositoryImpl() - val interactor = - object : - AudioSharingInteractorImpl( - applicationContext, - localBluetoothManager, - repository, - testDispatcher, - ) { - override suspend fun audioSharingAvailable() = true - } - val job = launch { interactor.handleAudioSourceWhenReady() } - runCurrent() - // Verify callback registered for onBroadcastStartedOrStopped - verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture()) - runCurrent() - // Verify source is not added - verify(repository, never()).addSource() + assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() job.cancel() } } @Test - fun testHandleAudioSourceWhenReady_skipInitialValue_newAudioSharing_sourceAdded() = + fun handleAudioSourceWhenReady_audioSharingTriggeredAndMetadataReady_sourceAdded() = with(kosmos) { testScope.runTest { - val (broadcast, repository) = setupRepositoryImpl() - val interactor = - object : - AudioSharingInteractorImpl( - applicationContext, - localBluetoothManager, - repository, - testDispatcher, - ) { - override suspend fun audioSharingAvailable() = true - } - val job = launch { interactor.handleAudioSourceWhenReady() } + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( + localBluetoothLeBroadcast + ) + val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() // Verify callback registered for onBroadcastStartedOrStopped - verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture()) + verify(localBluetoothLeBroadcast) + .registerServiceCallBack(any(), callbackCaptor.capture()) // Audio sharing started, trigger onBroadcastStarted - whenever(broadcast.isEnabled(null)).thenReturn(true) + whenever(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(true) + underTest.startAudioSharing() + runCurrent() callbackCaptor.value.onBroadcastStarted(0, 0) runCurrent() // Verify callback registered for onBroadcastMetadataChanged - verify(broadcast, times(2)).registerServiceCallBack(any(), callbackCaptor.capture()) + verify(localBluetoothLeBroadcast, times(2)) + .registerServiceCallBack(any(), callbackCaptor.capture()) runCurrent() // Trigger onBroadcastMetadataChanged (ready to add source) callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata) runCurrent() - // Verify source added - verify(repository).addSource() + + assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isTrue() job.cancel() } } - - private fun setupRepositoryImpl(): Pair<LocalBluetoothLeBroadcast, AudioSharingRepositoryImpl> { - with(kosmos) { - val broadcast = - mock<LocalBluetoothLeBroadcast> { - on { isProfileReady } doReturn true - on { isEnabled(null) } doReturn false - } - val assistant = - mock<LocalBluetoothLeBroadcastAssistant> { on { isProfileReady } doReturn true } - val volumeControl = mock<VolumeControlProfile> { on { isProfileReady } doReturn true } - val profileManager = - mock<LocalBluetoothProfileManager> { - on { leAudioBroadcastProfile } doReturn broadcast - on { leAudioBroadcastAssistantProfile } doReturn assistant - on { volumeControlProfile } doReturn volumeControl - } - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(localBluetoothManager.eventManager).thenReturn(mock<BluetoothEventManager> {}) - - val repository = - AudioSharingRepositoryImpl( - localBluetoothManager, - com.android.settingslib.volume.data.repository.AudioSharingRepositoryImpl( - mock<ContentResolver> {}, - localBluetoothManager, - testScope.backgroundScope, - testScope.testScheduler, - mock<AudioSharingLogger> {}, - ), - testDispatcher, - ) - return Pair(broadcast, spy(repository)) - } - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt index 0d410cff5ff6..b6359c7f8da5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt @@ -116,6 +116,34 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() { } @Test + fun isDreamButtonTooltipDismissedValue_byDefault_isFalse() = + testScope.runTest { + val isDreamButtonTooltipDismissed by + collectLastValue(underTest.isDreamButtonTooltipDismissed(MAIN_USER)) + assertThat(isDreamButtonTooltipDismissed).isFalse() + } + + @Test + fun isDreamButtonTooltipDismissedValue_onSet_isTrue() = + testScope.runTest { + val isDreamButtonTooltipDismissed by + collectLastValue(underTest.isDreamButtonTooltipDismissed(MAIN_USER)) + + underTest.setDreamButtonTooltipDismissed(MAIN_USER) + assertThat(isDreamButtonTooltipDismissed).isTrue() + } + + @Test + fun isDreamButtonTooltipDismissedValue_onSetForDifferentUser_isStillFalse() = + testScope.runTest { + val isDreamButtonTooltipDismissed by + collectLastValue(underTest.isDreamButtonTooltipDismissed(MAIN_USER)) + + underTest.setDreamButtonTooltipDismissed(SECONDARY_USER) + assertThat(isDreamButtonTooltipDismissed).isFalse() + } + + @Test fun getSharedPreferences_whenFileRestored() = testScope.runTest { val isCtaDismissed by collectLastValue(underTest.isCtaDismissed(MAIN_USER)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt index 809099e0d464..eb1f1d9c52f4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt @@ -38,19 +38,19 @@ import com.android.systemui.communal.data.model.DisabledReason import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_BACKGROUND_SETTING import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled import com.android.systemui.communal.shared.model.CommunalBackgroundType -import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.testKosmos import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -62,9 +62,11 @@ import platform.test.runner.parameterized.Parameters @RunWith(ParameterizedAndroidJunit4::class) class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiTestCase() { private val kosmos = - testKosmos().apply { mainResources = mContext.orCreateTestableResources.resources } - private val testScope = kosmos.testScope - private lateinit var underTest: CommunalSettingsRepository + testKosmos() + .apply { mainResources = mContext.orCreateTestableResources.resources } + .useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Kosmos.Fixture { communalSettingsRepository } init { mSetFlagsRule.setFlagsParameterization(flags!!) @@ -76,98 +78,105 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_FEATURES_NONE) setKeyguardFeaturesDisabled(SECONDARY_USER, KEYGUARD_DISABLE_FEATURES_NONE) setKeyguardFeaturesDisabled(WORK_PROFILE, KEYGUARD_DISABLE_FEATURES_NONE) - underTest = kosmos.communalSettingsRepository } @EnableFlags(FLAG_COMMUNAL_HUB) @DisableFlags(FLAG_GLANCEABLE_HUB_V2) @Test - fun getFlagEnabled_bothEnabled() { - kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) + fun getFlagEnabled_bothEnabled() = + kosmos.runTest { + fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) - assertThat(underTest.getFlagEnabled()).isTrue() - } + assertThat(underTest.getFlagEnabled()).isTrue() + } @DisableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) @Test - fun getFlagEnabled_bothDisabled() { - kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false) + fun getFlagEnabled_bothDisabled() = + kosmos.runTest { + fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false) - assertThat(underTest.getFlagEnabled()).isFalse() - } + assertThat(underTest.getFlagEnabled()).isFalse() + } @DisableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) @Test - fun getFlagEnabled_onlyClassicFlagEnabled() { - kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) + fun getFlagEnabled_onlyClassicFlagEnabled() = + kosmos.runTest { + fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) - assertThat(underTest.getFlagEnabled()).isFalse() - } + assertThat(underTest.getFlagEnabled()).isFalse() + } @EnableFlags(FLAG_COMMUNAL_HUB) @DisableFlags(FLAG_GLANCEABLE_HUB_V2) @Test - fun getFlagEnabled_onlyTrunkFlagEnabled() { - kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false) + fun getFlagEnabled_onlyTrunkFlagEnabled() = + kosmos.runTest { + fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false) - assertThat(underTest.getFlagEnabled()).isFalse() - } + assertThat(underTest.getFlagEnabled()).isFalse() + } @EnableFlags(FLAG_GLANCEABLE_HUB_V2) @DisableFlags(FLAG_COMMUNAL_HUB) @Test - fun getFlagEnabled_mobileConfigEnabled() { - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_glanceableHubEnabled, - true, - ) + fun getFlagEnabled_mobileConfigEnabled() = + kosmos.runTest { + mContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_glanceableHubEnabled, + true, + ) - assertThat(underTest.getFlagEnabled()).isTrue() - } + assertThat(underTest.getFlagEnabled()).isTrue() + } @DisableFlags(FLAG_GLANCEABLE_HUB_V2, FLAG_COMMUNAL_HUB) @Test - fun getFlagEnabled_onlyMobileConfigEnabled() { - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_glanceableHubEnabled, - true, - ) + fun getFlagEnabled_onlyMobileConfigEnabled() = + kosmos.runTest { + mContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_glanceableHubEnabled, + true, + ) - assertThat(underTest.getFlagEnabled()).isFalse() - } + assertThat(underTest.getFlagEnabled()).isFalse() + } @EnableFlags(FLAG_GLANCEABLE_HUB_V2) @DisableFlags(FLAG_COMMUNAL_HUB) @Test - fun getFlagEnabled_onlyMobileFlagEnabled() { - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_glanceableHubEnabled, - false, - ) + fun getFlagEnabled_onlyMobileFlagEnabled() = + kosmos.runTest { + mContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_glanceableHubEnabled, + false, + ) - assertThat(underTest.getFlagEnabled()).isFalse() - } + assertThat(underTest.getFlagEnabled()).isFalse() + } @EnableFlags(FLAG_GLANCEABLE_HUB_V2) @DisableFlags(FLAG_COMMUNAL_HUB) @Test - fun getFlagEnabled_oldFlagIgnored() { - // New config flag enabled. - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_glanceableHubEnabled, - true, - ) + fun getFlagEnabled_oldFlagIgnored() = + kosmos.runTest { + // New config flag enabled. + mContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_glanceableHubEnabled, + true, + ) - // Old config flag disabled. - kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false) + // Old config flag disabled. + fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false) - assertThat(underTest.getFlagEnabled()).isTrue() - } + assertThat(underTest.getFlagEnabled()).isTrue() + } @EnableFlags(FLAG_COMMUNAL_HUB) @Test fun secondaryUserIsInvalid() = - testScope.runTest { + kosmos.runTest { val enabledState by collectLastValue(underTest.getEnabledState(SECONDARY_USER)) assertThat(enabledState?.enabled).isFalse() @@ -187,7 +196,7 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT @DisableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) @Test fun communalHubFlagIsDisabled() = - testScope.runTest { + kosmos.runTest { val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) assertThat(enabledState?.enabled).isFalse() assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG) @@ -196,35 +205,23 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT @EnableFlags(FLAG_COMMUNAL_HUB) @Test fun hubIsDisabledByUser() = - testScope.runTest { - kosmos.fakeSettings.putIntForUser( - Settings.Secure.GLANCEABLE_HUB_ENABLED, - 0, - PRIMARY_USER.id, - ) + kosmos.runTest { + fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id) val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) assertThat(enabledState?.enabled).isFalse() assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_USER_SETTING) - kosmos.fakeSettings.putIntForUser( - Settings.Secure.GLANCEABLE_HUB_ENABLED, - 1, - SECONDARY_USER.id, - ) + fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, SECONDARY_USER.id) assertThat(enabledState?.enabled).isFalse() - kosmos.fakeSettings.putIntForUser( - Settings.Secure.GLANCEABLE_HUB_ENABLED, - 1, - PRIMARY_USER.id, - ) + fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, PRIMARY_USER.id) assertThat(enabledState?.enabled).isTrue() } @EnableFlags(FLAG_COMMUNAL_HUB) @Test fun hubIsDisabledByDevicePolicy() = - testScope.runTest { + kosmos.runTest { val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) assertThat(enabledState?.enabled).isTrue() @@ -236,7 +233,7 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT @EnableFlags(FLAG_COMMUNAL_HUB) @Test fun widgetsAllowedForWorkProfile_isFalse_whenDisallowedByDevicePolicy() = - testScope.runTest { + kosmos.runTest { val widgetsAllowedForWorkProfile by collectLastValue(underTest.getAllowedByDevicePolicy(WORK_PROFILE)) assertThat(widgetsAllowedForWorkProfile).isTrue() @@ -248,7 +245,7 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT @EnableFlags(FLAG_COMMUNAL_HUB) @Test fun hubIsEnabled_whenDisallowedByDevicePolicyForWorkProfile() = - testScope.runTest { + kosmos.runTest { val enabledStateForPrimaryUser by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) assertThat(enabledStateForPrimaryUser?.enabled).isTrue() @@ -260,15 +257,11 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT @EnableFlags(FLAG_COMMUNAL_HUB) @Test fun hubIsDisabledByUserAndDevicePolicy() = - testScope.runTest { + kosmos.runTest { val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) assertThat(enabledState?.enabled).isTrue() - kosmos.fakeSettings.putIntForUser( - Settings.Secure.GLANCEABLE_HUB_ENABLED, - 0, - PRIMARY_USER.id, - ) + fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id) setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL) assertThat(enabledState?.enabled).isFalse() @@ -282,17 +275,17 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT @Test @DisableFlags(FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND) fun backgroundType_defaultValue() = - testScope.runTest { + kosmos.runTest { val backgroundType by collectLastValue(underTest.getBackground(PRIMARY_USER)) assertThat(backgroundType).isEqualTo(CommunalBackgroundType.ANIMATED) } @Test fun backgroundType_verifyAllValues() = - testScope.runTest { + kosmos.runTest { val backgroundType by collectLastValue(underTest.getBackground(PRIMARY_USER)) for (type in CommunalBackgroundType.entries) { - kosmos.fakeSettings.putIntForUser( + fakeSettings.putIntForUser( GLANCEABLE_HUB_BACKGROUND_SETTING, type.value, PRIMARY_USER.id, @@ -308,30 +301,71 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT @Test fun screensaverDisabledByUser() = - testScope.runTest { + kosmos.runTest { val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER)) - kosmos.fakeSettings.putIntForUser( - Settings.Secure.SCREENSAVER_ENABLED, - 0, - PRIMARY_USER.id, - ) + fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ENABLED, 0, PRIMARY_USER.id) assertThat(enabledState).isFalse() } @Test fun screensaverEnabledByUser() = - testScope.runTest { + kosmos.runTest { val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER)) - kosmos.fakeSettings.putIntForUser( - Settings.Secure.SCREENSAVER_ENABLED, + fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ENABLED, 1, PRIMARY_USER.id) + + assertThat(enabledState).isTrue() + } + + @Test + fun whenToDream_charging() = + kosmos.runTest { + val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER)) + + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1, PRIMARY_USER.id, ) - assertThat(enabledState).isTrue() + assertThat(whenToDreamState).isEqualTo(WhenToDream.WHILE_CHARGING) + } + + @Test + fun whenToDream_docked() = + kosmos.runTest { + val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER)) + + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, + 1, + PRIMARY_USER.id, + ) + + assertThat(whenToDreamState).isEqualTo(WhenToDream.WHILE_DOCKED) + } + + @Test + fun whenToDream_postured() = + kosmos.runTest { + val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER)) + + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + 1, + PRIMARY_USER.id, + ) + + assertThat(whenToDreamState).isEqualTo(WhenToDream.WHILE_POSTURED) + } + + @Test + fun whenToDream_default() = + kosmos.runTest { + val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER)) + assertThat(whenToDreamState).isEqualTo(WhenToDream.NEVER) } private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 7ae0577bd289..ff137b768b65 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -38,13 +38,9 @@ import com.android.systemui.Flags.FLAG_COMMUNAL_WIDGET_RESIZING import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.model.CommunalSmartspaceTimer -import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository -import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository -import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository -import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository -import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository -import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository import com.android.systemui.communal.data.repository.fakeCommunalPrefsRepository import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository @@ -53,52 +49,49 @@ import com.android.systemui.communal.data.repository.fakeCommunalTutorialReposit import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.domain.model.CommunalTransitionProgressModel +import com.android.systemui.communal.posturing.data.repository.fake +import com.android.systemui.communal.posturing.data.repository.posturingRepository +import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.EditModeState -import com.android.systemui.communal.widgets.EditWidgetsActivityStarter -import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dock.DockManager +import com.android.systemui.dock.fakeDockManager import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope -import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.plugins.activityStarter -import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.fakeUserTracker import com.android.systemui.statusbar.phone.fakeManagedProfileController import com.android.systemui.testKosmos -import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever -import com.android.systemui.utils.leaks.FakeManagedProfileController +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq -import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @@ -107,32 +100,15 @@ import platform.test.runner.parameterized.Parameters * [CommunalInteractorCommunalDisabledTest]. */ @SmallTest -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(ParameterizedAndroidJunit4::class) class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { - @Mock private lateinit var mainUser: UserInfo - @Mock private lateinit var secondaryUser: UserInfo - - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - - private lateinit var tutorialRepository: FakeCommunalTutorialRepository - private lateinit var communalRepository: FakeCommunalSceneRepository - private lateinit var mediaRepository: FakeCommunalMediaRepository - private lateinit var widgetRepository: FakeCommunalWidgetRepository - private lateinit var smartspaceRepository: FakeCommunalSmartspaceRepository - private lateinit var userRepository: FakeUserRepository - private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository - private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter - private lateinit var sceneInteractor: SceneInteractor - private lateinit var communalSceneInteractor: CommunalSceneInteractor - private lateinit var userTracker: FakeUserTracker - private lateinit var activityStarter: ActivityStarter - private lateinit var userManager: UserManager - private lateinit var managedProfileController: FakeManagedProfileController - - private lateinit var underTest: CommunalInteractor + private val mainUser = + UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN) + private val secondaryUser = UserInfo(/* id= */ 1, /* name= */ "secondary user", /* flags= */ 0) + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Kosmos.Fixture { communalInteractor } init { mSetFlagsRule.setFlagsParameterization(flags) @@ -140,128 +116,104 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Before fun setUp() { - MockitoAnnotations.initMocks(this) - - tutorialRepository = kosmos.fakeCommunalTutorialRepository - communalRepository = kosmos.fakeCommunalSceneRepository - mediaRepository = kosmos.fakeCommunalMediaRepository - widgetRepository = kosmos.fakeCommunalWidgetRepository - smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository - userRepository = kosmos.fakeUserRepository - keyguardRepository = kosmos.fakeKeyguardRepository - editWidgetsActivityStarter = kosmos.editWidgetsActivityStarter - communalPrefsRepository = kosmos.fakeCommunalPrefsRepository - sceneInteractor = kosmos.sceneInteractor - communalSceneInteractor = kosmos.communalSceneInteractor - userTracker = kosmos.fakeUserTracker - activityStarter = kosmos.activityStarter - userManager = kosmos.userManager - managedProfileController = kosmos.fakeManagedProfileController - - whenever(mainUser.isMain).thenReturn(true) - whenever(secondaryUser.isMain).thenReturn(false) - whenever(userManager.isQuietModeEnabled(any<UserHandle>())).thenReturn(false) - whenever(userManager.isManagedProfile(anyInt())).thenReturn(false) - userRepository.setUserInfos(listOf(mainUser, secondaryUser)) + whenever(kosmos.userManager.isQuietModeEnabled(any<UserHandle>())).thenReturn(false) + whenever(kosmos.userManager.isManagedProfile(anyInt())).thenReturn(false) + kosmos.fakeUserRepository.setUserInfos(listOf(mainUser, secondaryUser)) kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) - - underTest = kosmos.communalInteractor } @Test fun communalEnabled_true() = - testScope.runTest { - userRepository.setSelectedUserInfo(mainUser) - runCurrent() + kosmos.runTest { + fakeUserRepository.setSelectedUserInfo(mainUser) assertThat(underTest.isCommunalEnabled.value).isTrue() } @Test fun isCommunalAvailable_storageUnlockedAndMainUser_true() = - testScope.runTest { + kosmos.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - keyguardRepository.setIsEncryptedOrLockdown(false) - userRepository.setSelectedUserInfo(mainUser) - keyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setSelectedUserInfo(mainUser) + fakeKeyguardRepository.setKeyguardShowing(true) assertThat(isAvailable).isTrue() } @Test fun isCommunalAvailable_storageLockedAndMainUser_false() = - testScope.runTest { + kosmos.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - keyguardRepository.setIsEncryptedOrLockdown(true) - userRepository.setSelectedUserInfo(mainUser) - keyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setIsEncryptedOrLockdown(true) + fakeUserRepository.setSelectedUserInfo(mainUser) + fakeKeyguardRepository.setKeyguardShowing(true) assertThat(isAvailable).isFalse() } @Test fun isCommunalAvailable_storageUnlockedAndSecondaryUser_false() = - testScope.runTest { + kosmos.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - keyguardRepository.setIsEncryptedOrLockdown(false) - userRepository.setSelectedUserInfo(secondaryUser) - keyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setSelectedUserInfo(secondaryUser) + fakeKeyguardRepository.setKeyguardShowing(true) assertThat(isAvailable).isFalse() } @Test fun isCommunalAvailable_whenKeyguardShowing_true() = - testScope.runTest { + kosmos.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - keyguardRepository.setIsEncryptedOrLockdown(false) - userRepository.setSelectedUserInfo(mainUser) - keyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setSelectedUserInfo(mainUser) + fakeKeyguardRepository.setKeyguardShowing(true) assertThat(isAvailable).isTrue() } @Test fun isCommunalAvailable_communalDisabled_false() = - testScope.runTest { + kosmos.runTest { mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - keyguardRepository.setIsEncryptedOrLockdown(false) - userRepository.setSelectedUserInfo(mainUser) - keyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setSelectedUserInfo(mainUser) + fakeKeyguardRepository.setKeyguardShowing(true) assertThat(isAvailable).isFalse() } @Test fun widget_tutorialCompletedAndWidgetsAvailable_showWidgetContent() = - testScope.runTest { + kosmos.runTest { // Keyguard showing, and tutorial completed. - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + fakeKeyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setKeyguardOccluded(false) + fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK) - userRepository.setUserInfos(userInfos) - userTracker.set(userInfos = userInfos, selectedUserIndex = 0) - runCurrent() + fakeUserRepository.setUserInfos(userInfos) + fakeUserTracker.set(userInfos = userInfos, selectedUserIndex = 0) // Widgets available. - widgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id) - widgetRepository.addWidget(appWidgetId = 2, userId = MAIN_USER_INFO.id) - widgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 2, userId = MAIN_USER_INFO.id) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id) val widgetContent by collectLastValue(underTest.widgetContent) @@ -356,18 +308,18 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { totalTargets: Int, expectedSizes: List<CommunalContentSize>, ) = - testScope.runTest { + kosmos.runTest { // Keyguard showing, and tutorial completed. - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + fakeKeyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setKeyguardOccluded(false) + fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) val targets = mutableListOf<CommunalSmartspaceTimer>() for (index in 0 until totalTargets) { targets.add(smartspaceTimer(index.toString())) } - smartspaceRepository.setTimers(targets) + fakeCommunalSmartspaceRepository.setTimers(targets) val smartspaceContent by collectLastValue(underTest.ongoingContent(false)) assertThat(smartspaceContent?.size).isEqualTo(totalTargets) @@ -378,12 +330,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun umo_mediaPlaying_showsUmo() = - testScope.runTest { + kosmos.runTest { // Tutorial completed. - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) // Media is playing. - mediaRepository.mediaActive() + fakeCommunalMediaRepository.mediaActive() val umoContent by collectLastValue(underTest.ongoingContent(true)) @@ -394,12 +346,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun umo_mediaPlaying_mediaHostNotVisible_hidesUmo() = - testScope.runTest { + kosmos.runTest { // Tutorial completed. - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) // Media is playing. - mediaRepository.mediaActive() + fakeCommunalMediaRepository.mediaActive() val umoContent by collectLastValue(underTest.ongoingContent(false)) assertThat(umoContent?.size).isEqualTo(0) @@ -409,26 +361,26 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test @DisableFlags(FLAG_COMMUNAL_RESPONSIVE_GRID) fun ongoing_shouldOrderAndSizeByTimestamp() = - testScope.runTest { + kosmos.runTest { // Keyguard showing, and tutorial completed. - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + fakeKeyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setKeyguardOccluded(false) + fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) // Timer1 started val timer1 = smartspaceTimer("timer1", timestamp = 1L) - smartspaceRepository.setTimers(listOf(timer1)) + fakeCommunalSmartspaceRepository.setTimers(listOf(timer1)) // Umo started - mediaRepository.mediaActive(timestamp = 2L) + fakeCommunalMediaRepository.mediaActive(timestamp = 2L) // Timer2 started val timer2 = smartspaceTimer("timer2", timestamp = 3L) - smartspaceRepository.setTimers(listOf(timer1, timer2)) + fakeCommunalSmartspaceRepository.setTimers(listOf(timer1, timer2)) // Timer3 started val timer3 = smartspaceTimer("timer3", timestamp = 4L) - smartspaceRepository.setTimers(listOf(timer1, timer2, timer3)) + fakeCommunalSmartspaceRepository.setTimers(listOf(timer1, timer2, timer3)) val ongoingContent by collectLastValue(underTest.ongoingContent(true)) assertThat(ongoingContent?.size).isEqualTo(4) @@ -446,9 +398,10 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun ctaTile_showsByDefault() = - testScope.runTest { - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + kosmos.runTest { + fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) val ctaTileContent by collectLastValue(underTest.ctaTileContent) @@ -460,15 +413,15 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun ctaTile_afterDismiss_doesNotShow() = - testScope.runTest { + kosmos.runTest { // Set to main user, so we can dismiss the tile for the main user. - val user = userRepository.asMainUser() - userTracker.set(userInfos = listOf(user), selectedUserIndex = 0) - runCurrent() + val user = fakeUserRepository.asMainUser() + fakeUserTracker.set(userInfos = listOf(user), selectedUserIndex = 0) - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - communalPrefsRepository.setCtaDismissed(user) + fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + fakeCommunalPrefsRepository.setCtaDismissed(user) val ctaTileContent by collectLastValue(underTest.ctaTileContent) @@ -477,36 +430,30 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun listensToSceneChange() = - testScope.runTest { + kosmos.runTest { kosmos.setCommunalAvailable(true) - runCurrent() - var desiredScene = collectLastValue(underTest.desiredScene) - runCurrent() - assertThat(desiredScene()).isEqualTo(CommunalScenes.Blank) + val desiredScene by collectLastValue(underTest.desiredScene) + assertThat(desiredScene).isEqualTo(CommunalScenes.Blank) val targetScene = CommunalScenes.Communal - communalRepository.changeScene(targetScene) - desiredScene = collectLastValue(underTest.desiredScene) - runCurrent() - assertThat(desiredScene()).isEqualTo(targetScene) + fakeCommunalSceneRepository.changeScene(targetScene) + assertThat(desiredScene).isEqualTo(targetScene) } @Test fun updatesScene() = - testScope.runTest { + kosmos.runTest { val targetScene = CommunalScenes.Communal - underTest.changeScene(targetScene, "test") - val desiredScene = collectLastValue(communalRepository.currentScene) - runCurrent() - assertThat(desiredScene()).isEqualTo(targetScene) + val desiredScene by collectLastValue(fakeCommunalSceneRepository.currentScene) + assertThat(desiredScene).isEqualTo(targetScene) } @Test fun transitionProgress_onTargetScene_fullProgress() = - testScope.runTest { + kosmos.runTest { val targetScene = CommunalScenes.Blank val transitionProgressFlow = underTest.transitionProgressToScene(targetScene) val transitionProgress by collectLastValue(transitionProgressFlow) @@ -524,7 +471,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun transitionProgress_notOnTargetScene_noProgress() = - testScope.runTest { + kosmos.runTest { val targetScene = CommunalScenes.Blank val currentScene = CommunalScenes.Communal val transitionProgressFlow = underTest.transitionProgressToScene(targetScene) @@ -543,7 +490,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun transitionProgress_transitioningToTrackedScene() = - testScope.runTest { + kosmos.runTest { val currentScene = CommunalScenes.Communal val targetScene = CommunalScenes.Blank val transitionProgressFlow = underTest.transitionProgressToScene(targetScene) @@ -591,7 +538,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun transitionProgress_transitioningAwayFromTrackedScene() = - testScope.runTest { + kosmos.runTest { val currentScene = CommunalScenes.Blank val targetScene = CommunalScenes.Communal val transitionProgressFlow = underTest.transitionProgressToScene(currentScene) @@ -642,52 +589,42 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun isCommunalShowing() = - testScope.runTest { + kosmos.runTest { kosmos.setCommunalAvailable(true) - runCurrent() - var isCommunalShowing = collectLastValue(underTest.isCommunalShowing) - runCurrent() - assertThat(isCommunalShowing()).isEqualTo(false) + val isCommunalShowing by collectLastValue(underTest.isCommunalShowing) + assertThat(isCommunalShowing).isEqualTo(false) underTest.changeScene(CommunalScenes.Communal, "test") - - isCommunalShowing = collectLastValue(underTest.isCommunalShowing) - runCurrent() - assertThat(isCommunalShowing()).isEqualTo(true) + assertThat(isCommunalShowing).isEqualTo(true) } @Test fun isCommunalShowing_whenSceneContainerDisabled() = - testScope.runTest { + kosmos.runTest { kosmos.setCommunalAvailable(true) - runCurrent() // Verify default is false val isCommunalShowing by collectLastValue(underTest.isCommunalShowing) - runCurrent() assertThat(isCommunalShowing).isFalse() // Verify scene changes with the flag doesn't have any impact sceneInteractor.changeScene(Scenes.Communal, loggingReason = "") - runCurrent() assertThat(isCommunalShowing).isFalse() // Verify scene changes (without the flag) to communal sets the value to true underTest.changeScene(CommunalScenes.Communal, "test") - runCurrent() assertThat(isCommunalShowing).isTrue() // Verify scene changes (without the flag) to blank sets the value back to false underTest.changeScene(CommunalScenes.Blank, "test") - runCurrent() assertThat(isCommunalShowing).isFalse() } @Test @EnableSceneContainer fun isCommunalShowing_whenSceneContainerEnabled() = - testScope.runTest { + kosmos.runTest { // Verify default is false val isCommunalShowing by collectLastValue(underTest.isCommunalShowing) assertThat(isCommunalShowing).isFalse() @@ -704,7 +641,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test @EnableSceneContainer fun isCommunalShowing_whenSceneContainerEnabledAndChangeToLegacyScene() = - testScope.runTest { + kosmos.runTest { // Verify default is false val isCommunalShowing by collectLastValue(underTest.isCommunalShowing) assertThat(isCommunalShowing).isFalse() @@ -720,21 +657,19 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun isIdleOnCommunal() = - testScope.runTest { + kosmos.runTest { val transitionState = MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(CommunalScenes.Blank) ) - communalRepository.setTransitionState(transitionState) + fakeCommunalSceneRepository.setTransitionState(transitionState) // isIdleOnCommunal is false when not on communal. val isIdleOnCommunal by collectLastValue(underTest.isIdleOnCommunal) - runCurrent() assertThat(isIdleOnCommunal).isEqualTo(false) // Transition to communal. transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Communal) - runCurrent() // isIdleOnCommunal is now true since we're on communal. assertThat(isIdleOnCommunal).isEqualTo(true) @@ -749,7 +684,6 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { isInitiatedByUserInput = false, isUserInputOngoing = flowOf(false), ) - runCurrent() // isIdleOnCommunal turns false as soon as transition away starts. assertThat(isIdleOnCommunal).isEqualTo(false) @@ -757,12 +691,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun isCommunalVisible() = - testScope.runTest { + kosmos.runTest { val transitionState = MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(CommunalScenes.Blank) ) - communalRepository.setTransitionState(transitionState) + fakeCommunalSceneRepository.setTransitionState(transitionState) // isCommunalVisible is false when not on communal. val isCommunalVisible by collectLastValue(underTest.isCommunalVisible) @@ -805,7 +739,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testShowWidgetEditorStartsActivity() = - testScope.runTest { + kosmos.runTest { val editModeState by collectLastValue(communalSceneInteractor.editModeState) underTest.showWidgetEditor() @@ -816,14 +750,14 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun showWidgetEditor_openWidgetPickerOnStart_startsActivity() = - testScope.runTest { + kosmos.runTest { underTest.showWidgetEditor(shouldOpenWidgetPickerOnStart = true) verify(editWidgetsActivityStarter).startActivity(shouldOpenWidgetPickerOnStart = true) } @Test fun navigateToCommunalWidgetSettings_startsActivity() = - testScope.runTest { + kosmos.runTest { underTest.navigateToCommunalWidgetSettings() val intentCaptor = argumentCaptor<Intent>() verify(activityStarter) @@ -833,23 +767,22 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun filterWidgets_whenUserProfileRemoved() = - testScope.runTest { + kosmos.runTest { // Keyguard showing, and tutorial completed. - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + fakeKeyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setKeyguardOccluded(false) + fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) // Only main user exists. val userInfos = listOf(MAIN_USER_INFO) - userRepository.setUserInfos(userInfos) - userTracker.set(userInfos = userInfos, selectedUserIndex = 0) - runCurrent() + fakeUserRepository.setUserInfos(userInfos) + fakeUserTracker.set(userInfos = userInfos, selectedUserIndex = 0) val widgetContent by collectLastValue(underTest.widgetContent) // Given three widgets, and one of them is associated with pre-existing work profile. - widgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id) - widgetRepository.addWidget(appWidgetId = 2, userId = MAIN_USER_INFO.id) - widgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 2, userId = MAIN_USER_INFO.id) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id) // One widget is filtered out and the remaining two link to main user id. assertThat(checkNotNull(widgetContent).size).isEqualTo(2) @@ -867,17 +800,16 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun widgetContent_inQuietMode() = - testScope.runTest { + kosmos.runTest { // Keyguard showing, and tutorial completed. - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + fakeKeyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setKeyguardOccluded(false) + fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) // Work profile is set up. val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK) - userRepository.setUserInfos(userInfos) - userTracker.set(userInfos = userInfos, selectedUserIndex = 0) - runCurrent() + fakeUserRepository.setUserInfos(userInfos) + fakeUserTracker.set(userInfos = userInfos, selectedUserIndex = 0) // When work profile is paused. whenever(userManager.isQuietModeEnabled(eq(UserHandle.of(USER_INFO_WORK.id)))) @@ -885,9 +817,9 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { whenever(userManager.isManagedProfile(eq(USER_INFO_WORK.id))).thenReturn(true) val widgetContent by collectLastValue(underTest.widgetContent) - widgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id) - widgetRepository.addWidget(appWidgetId = 2, userId = MAIN_USER_INFO.id) - widgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 2, userId = MAIN_USER_INFO.id) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id) // The work profile widget is in quiet mode, while other widgets are not. assertThat(widgetContent).hasSize(3) @@ -911,23 +843,25 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun filterWidgets_whenDisallowedByDevicePolicyForWorkProfile() = - testScope.runTest { + kosmos.runTest { // Keyguard showing, and tutorial completed. - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + fakeKeyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setKeyguardOccluded(false) + fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK) - userRepository.setUserInfos(userInfos) - userTracker.set(userInfos = userInfos, selectedUserIndex = 0) - userRepository.setSelectedUserInfo(MAIN_USER_INFO) - runCurrent() + fakeUserRepository.setUserInfos(userInfos) + fakeUserTracker.set(userInfos = userInfos, selectedUserIndex = 0) + fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) val widgetContent by collectLastValue(underTest.widgetContent) // One available work widget, one pending work widget, and one regular available widget. - widgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id) - widgetRepository.addPendingWidget(appWidgetId = 2, userId = USER_INFO_WORK.id) - widgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id) + fakeCommunalWidgetRepository.addPendingWidget( + appWidgetId = 2, + userId = USER_INFO_WORK.id, + ) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id) setKeyguardFeaturesDisabled( USER_INFO_WORK, @@ -941,23 +875,25 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun filterWidgets_whenAllowedByDevicePolicyForWorkProfile() = - testScope.runTest { + kosmos.runTest { // Keyguard showing, and tutorial completed. - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + fakeKeyguardRepository.setKeyguardShowing(true) + fakeKeyguardRepository.setKeyguardOccluded(false) + fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK) - userRepository.setUserInfos(userInfos) - userTracker.set(userInfos = userInfos, selectedUserIndex = 0) - userRepository.setSelectedUserInfo(MAIN_USER_INFO) - runCurrent() + fakeUserRepository.setUserInfos(userInfos) + fakeUserTracker.set(userInfos = userInfos, selectedUserIndex = 0) + fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) val widgetContent by collectLastValue(underTest.widgetContent) // Given three widgets, and one of them is associated with work profile. - widgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id) - widgetRepository.addPendingWidget(appWidgetId = 2, userId = USER_INFO_WORK.id) - widgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id) + fakeCommunalWidgetRepository.addPendingWidget( + appWidgetId = 2, + userId = USER_INFO_WORK.id, + ) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id) setKeyguardFeaturesDisabled( USER_INFO_WORK, @@ -973,7 +909,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun showCommunalFromOccluded_enteredOccludedFromHub() = - testScope.runTest { + kosmos.runTest { kosmos.setCommunalAvailable(true) val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded) assertThat(showCommunalFromOccluded).isFalse() @@ -989,7 +925,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun showCommunalFromOccluded_enteredOccludedFromLockscreen() = - testScope.runTest { + kosmos.runTest { kosmos.setCommunalAvailable(true) val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded) assertThat(showCommunalFromOccluded).isFalse() @@ -1005,7 +941,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun showCommunalFromOccluded_communalBecomesUnavailableWhileOccluded() = - testScope.runTest { + kosmos.runTest { kosmos.setCommunalAvailable(true) val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded) assertThat(showCommunalFromOccluded).isFalse() @@ -1015,7 +951,6 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { to = KeyguardState.OCCLUDED, testScope, ) - runCurrent() kosmos.setCommunalAvailable(false) assertThat(showCommunalFromOccluded).isFalse() @@ -1023,7 +958,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun showCommunalFromOccluded_showBouncerWhileOccluded() = - testScope.runTest { + kosmos.runTest { kosmos.setCommunalAvailable(true) val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded) assertThat(showCommunalFromOccluded).isFalse() @@ -1033,7 +968,6 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { to = KeyguardState.OCCLUDED, testScope, ) - runCurrent() kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.OCCLUDED, to = KeyguardState.PRIMARY_BOUNCER, @@ -1045,7 +979,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun showCommunalFromOccluded_enteredOccludedFromDreaming() = - testScope.runTest { + kosmos.runTest { kosmos.setCommunalAvailable(true) val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded) assertThat(showCommunalFromOccluded).isFalse() @@ -1069,7 +1003,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun dismissDisclaimerSetsDismissedFlag() = - testScope.runTest { + kosmos.runTest { val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) assertThat(disclaimerDismissed).isFalse() underTest.setDisclaimerDismissed() @@ -1078,17 +1012,17 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun dismissDisclaimerTimeoutResetsDismissedFlag() = - testScope.runTest { + kosmos.runTest { val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) underTest.setDisclaimerDismissed() assertThat(disclaimerDismissed).isTrue() - advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS) + testScope.advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS) assertThat(disclaimerDismissed).isFalse() } @Test fun settingSelectedKey_flowUpdated() { - testScope.runTest { + kosmos.runTest { val key = "test" val selectedKey by collectLastValue(underTest.selectedKey) underTest.setSelectedKey(key) @@ -1098,36 +1032,35 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun unpauseWorkProfileEnablesWorkMode() = - testScope.runTest { + kosmos.runTest { underTest.unpauseWorkProfile() - assertThat(managedProfileController.isWorkModeEnabled()).isTrue() + assertThat(fakeManagedProfileController.isWorkModeEnabled()).isTrue() } @Test @EnableFlags(FLAG_COMMUNAL_WIDGET_RESIZING) @DisableFlags(FLAG_COMMUNAL_RESPONSIVE_GRID) fun resizeWidget_withoutUpdatingOrder() = - testScope.runTest { + kosmos.runTest { val userInfos = listOf(MAIN_USER_INFO) - userRepository.setUserInfos(userInfos) - userTracker.set(userInfos = userInfos, selectedUserIndex = 0) - runCurrent() + fakeUserRepository.setUserInfos(userInfos) + fakeUserTracker.set(userInfos = userInfos, selectedUserIndex = 0) // Widgets available. - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 1, userId = MAIN_USER_INFO.id, rank = 0, spanY = CommunalContentSize.FixedSize.HALF.span, ) - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 2, userId = MAIN_USER_INFO.id, rank = 1, spanY = CommunalContentSize.FixedSize.HALF.span, ) - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 3, userId = MAIN_USER_INFO.id, rank = 2, @@ -1159,26 +1092,25 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test @EnableFlags(FLAG_COMMUNAL_WIDGET_RESIZING, FLAG_COMMUNAL_RESPONSIVE_GRID) fun resizeWidget_withoutUpdatingOrder_responsive() = - testScope.runTest { + kosmos.runTest { val userInfos = listOf(MAIN_USER_INFO) - userRepository.setUserInfos(userInfos) - userTracker.set(userInfos = userInfos, selectedUserIndex = 0) - runCurrent() + fakeUserRepository.setUserInfos(userInfos) + fakeUserTracker.set(userInfos = userInfos, selectedUserIndex = 0) // Widgets available. - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 1, userId = MAIN_USER_INFO.id, rank = 0, spanY = 1, ) - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 2, userId = MAIN_USER_INFO.id, rank = 1, spanY = 1, ) - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 3, userId = MAIN_USER_INFO.id, rank = 2, @@ -1211,26 +1143,25 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @EnableFlags(FLAG_COMMUNAL_WIDGET_RESIZING) @DisableFlags(FLAG_COMMUNAL_RESPONSIVE_GRID) fun resizeWidget_andUpdateOrder() = - testScope.runTest { + kosmos.runTest { val userInfos = listOf(MAIN_USER_INFO) - userRepository.setUserInfos(userInfos) - userTracker.set(userInfos = userInfos, selectedUserIndex = 0) - runCurrent() + fakeUserRepository.setUserInfos(userInfos) + fakeUserTracker.set(userInfos = userInfos, selectedUserIndex = 0) // Widgets available. - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 1, userId = MAIN_USER_INFO.id, rank = 0, spanY = CommunalContentSize.FixedSize.HALF.span, ) - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 2, userId = MAIN_USER_INFO.id, rank = 1, spanY = CommunalContentSize.FixedSize.HALF.span, ) - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 3, userId = MAIN_USER_INFO.id, rank = 2, @@ -1266,26 +1197,25 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test @EnableFlags(FLAG_COMMUNAL_WIDGET_RESIZING, FLAG_COMMUNAL_RESPONSIVE_GRID) fun resizeWidget_andUpdateOrder_responsive() = - testScope.runTest { + kosmos.runTest { val userInfos = listOf(MAIN_USER_INFO) - userRepository.setUserInfos(userInfos) - userTracker.set(userInfos = userInfos, selectedUserIndex = 0) - runCurrent() + fakeUserRepository.setUserInfos(userInfos) + fakeUserTracker.set(userInfos = userInfos, selectedUserIndex = 0) // Widgets available. - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 1, userId = MAIN_USER_INFO.id, rank = 0, spanY = 1, ) - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 2, userId = MAIN_USER_INFO.id, rank = 1, spanY = 1, ) - widgetRepository.addWidget( + fakeCommunalWidgetRepository.addWidget( appWidgetId = 3, userId = MAIN_USER_INFO.id, rank = 2, @@ -1318,6 +1248,66 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { .inOrder() } + @Test + fun showCommunalWhileCharging() = + kosmos.runTest { + fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setSelectedUserInfo(mainUser) + fakeKeyguardRepository.setKeyguardShowing(true) + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + 1, + mainUser.id, + ) + + val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal) + batteryRepository.fake.setDevicePluggedIn(false) + assertThat(shouldShowCommunal).isFalse() + + batteryRepository.fake.setDevicePluggedIn(true) + assertThat(shouldShowCommunal).isTrue() + } + + @Test + fun showCommunalWhilePosturedAndCharging() = + kosmos.runTest { + fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setSelectedUserInfo(mainUser) + fakeKeyguardRepository.setKeyguardShowing(true) + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + 1, + mainUser.id, + ) + + val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal) + batteryRepository.fake.setDevicePluggedIn(true) + posturingRepository.fake.setPosturedState(PosturedState.NotPostured) + assertThat(shouldShowCommunal).isFalse() + + posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) + assertThat(shouldShowCommunal).isTrue() + } + + @Test + fun showCommunalWhileDocked() = + kosmos.runTest { + fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setSelectedUserInfo(mainUser) + fakeKeyguardRepository.setKeyguardShowing(true) + fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, 1, mainUser.id) + + batteryRepository.fake.setDevicePluggedIn(true) + fakeDockManager.setIsDocked(false) + + val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal) + assertThat(shouldShowCommunal).isFalse() + + fakeDockManager.setIsDocked(true) + fakeDockManager.setDockEvent(DockManager.STATE_DOCKED) + assertThat(shouldShowCommunal).isTrue() + } + private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id))) .thenReturn(disabledFlags) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt index 1fef6932ecca..1f5f8cedab02 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt @@ -108,6 +108,43 @@ class CommunalPrefsInteractorTest : SysuiTestCase() { assertThat(isHubOnboardingDismissed).isFalse() } + @Test + fun setDreamButtonTooltipDismissed_currentUser() = + testScope.runTest { + setSelectedUser(MAIN_USER) + val isDreamButtonTooltipDismissed by + collectLastValue(underTest.isDreamButtonTooltipDismissed) + + assertThat(isDreamButtonTooltipDismissed).isFalse() + underTest.setDreamButtonTooltipDismissed(MAIN_USER) + assertThat(isDreamButtonTooltipDismissed).isTrue() + } + + @Test + fun setDreamButtonTooltipDismissed_anotherUser() = + testScope.runTest { + setSelectedUser(MAIN_USER) + val isDreamButtonTooltipDismissed by + collectLastValue(underTest.isDreamButtonTooltipDismissed) + + assertThat(isDreamButtonTooltipDismissed).isFalse() + underTest.setDreamButtonTooltipDismissed(SECONDARY_USER) + assertThat(isDreamButtonTooltipDismissed).isFalse() + } + + @Test + fun isDreamButtonTooltipDismissed_userSwitch() = + testScope.runTest { + setSelectedUser(MAIN_USER) + underTest.setDreamButtonTooltipDismissed(MAIN_USER) + val isDreamButtonTooltipDismissed by + collectLastValue(underTest.isDreamButtonTooltipDismissed) + + assertThat(isDreamButtonTooltipDismissed).isTrue() + setSelectedUser(SECONDARY_USER) + assertThat(isDreamButtonTooltipDismissed).isFalse() + } + private suspend fun setSelectedUser(user: UserInfo) { with(kosmos.fakeUserRepository) { setUserInfos(listOf(user)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt index e4916b1a7e46..310bf6486413 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt @@ -21,19 +21,17 @@ import android.app.admin.devicePolicyManager import android.content.Intent import android.content.pm.UserInfo import android.os.UserManager -import android.os.userManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testScope -import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos -import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository -import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -48,34 +46,20 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class CommunalSettingsInteractorTest : SysuiTestCase() { - private lateinit var userManager: UserManager - private lateinit var userRepository: FakeUserRepository - private lateinit var userTracker: FakeUserTracker + private val kosmos = testKosmos().useUnconfinedTestDispatcher() - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - - private lateinit var underTest: CommunalSettingsInteractor + private val Kosmos.underTest by Kosmos.Fixture { communalSettingsInteractor } @Before fun setUp() { - userManager = kosmos.userManager - userRepository = kosmos.fakeUserRepository - userTracker = kosmos.fakeUserTracker - val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK) - userRepository.setUserInfos(userInfos) - userTracker.set( - userInfos = userInfos, - selectedUserIndex = 0, - ) - - underTest = kosmos.communalSettingsInteractor + kosmos.fakeUserRepository.setUserInfos(userInfos) + kosmos.fakeUserTracker.set(userInfos = userInfos, selectedUserIndex = 0) } @Test fun filterUsers_dontFilteredUsersWhenAllAreAllowed() = - testScope.runTest { + kosmos.runTest { // If no users have any keyguard features disabled... val disallowedUser by collectLastValue(underTest.workProfileUserDisallowedByDevicePolicy) @@ -85,11 +69,11 @@ class CommunalSettingsInteractorTest : SysuiTestCase() { @Test fun filterUsers_filterWorkProfileUserWhenDisallowed() = - testScope.runTest { + kosmos.runTest { // If the work profile user has keyguard widgets disabled... setKeyguardFeaturesDisabled( USER_INFO_WORK, - DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL + DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL, ) // ...then the disallowed user match the work profile val disallowedUser by @@ -102,7 +86,7 @@ class CommunalSettingsInteractorTest : SysuiTestCase() { whenever( kosmos.devicePolicyManager.getKeyguardDisabledFeatures( anyOrNull(), - ArgumentMatchers.eq(user.id) + ArgumentMatchers.eq(user.id), ) ) .thenReturn(disabledFlags) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt index 012ae8f12d4a..b747705fa3a2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.ui.viewmodel +import android.content.pm.UserInfo import android.platform.test.annotations.EnableFlags import android.provider.Settings import android.service.dream.dreamManager @@ -24,15 +25,17 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.uiEventLoggerFake import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.fakeCommunalPrefsRepository +import com.android.systemui.communal.domain.interactor.HubOnboardingInteractorTest.Companion.MAIN_USER import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED import com.android.systemui.flags.fakeFeatureFlagsClassic -import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.plugins.activityStarter +import com.android.systemui.settings.fakeUserTracker import com.android.systemui.statusbar.policy.batteryController import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository @@ -52,7 +55,6 @@ import org.mockito.kotlin.whenever class CommunalToDreamButtonViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val uiEventLoggerFake = kosmos.uiEventLoggerFake private val underTest: CommunalToDreamButtonViewModel by lazy { kosmos.communalToDreamButtonViewModel } @@ -68,9 +70,9 @@ class CommunalToDreamButtonViewModelTest : SysuiTestCase() { with(kosmos) { runTest { whenever(batteryController.isPluggedIn()).thenReturn(true) + runCurrent() - val shouldShowButton by collectLastValue(underTest.shouldShowDreamButtonOnHub) - assertThat(shouldShowButton).isTrue() + assertThat(underTest.shouldShowDreamButtonOnHub).isTrue() } } @@ -79,9 +81,9 @@ class CommunalToDreamButtonViewModelTest : SysuiTestCase() { with(kosmos) { runTest { whenever(batteryController.isPluggedIn()).thenReturn(false) + runCurrent() - val shouldShowButton by collectLastValue(underTest.shouldShowDreamButtonOnHub) - assertThat(shouldShowButton).isFalse() + assertThat(underTest.shouldShowDreamButtonOnHub).isFalse() } } @@ -124,6 +126,23 @@ class CommunalToDreamButtonViewModelTest : SysuiTestCase() { } @Test + fun shouldShowDreamButtonTooltip_trueWhenNotDismissed() = + kosmos.runTest { + runCurrent() + assertThat(underTest.shouldShowTooltip).isTrue() + } + + @Test + fun shouldShowDreamButtonTooltip_falseWhenDismissed() = + kosmos.runTest { + setSelectedUser(MAIN_USER) + fakeCommunalPrefsRepository.setDreamButtonTooltipDismissed(MAIN_USER) + runCurrent() + + assertThat(underTest.shouldShowTooltip).isFalse() + } + + @Test fun onShowDreamButtonTap_eventLogged() = with(kosmos) { runTest { @@ -134,4 +153,12 @@ class CommunalToDreamButtonViewModelTest : SysuiTestCase() { .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_SHOW_DREAM_BUTTON_TAP.id) } } + + private suspend fun setSelectedUser(user: UserInfo) { + with(kosmos.fakeUserRepository) { + setUserInfos(listOf(user)) + setSelectedUserInfo(user) + } + kosmos.fakeUserTracker.set(userInfos = listOf(user), selectedUserIndex = 0) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalLockIconViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalLockIconViewModelTest.kt new file mode 100644 index 000000000000..c535831a27e7 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalLockIconViewModelTest.kt @@ -0,0 +1,138 @@ +/* + * 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.communal.view.viewmodel + +import android.platform.test.flag.junit.FlagsParameterization +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.accessibility.domain.interactor.accessibilityInteractor +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.AuthenticationResult +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.communal.ui.viewmodel.CommunalLockIconViewModel +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntrySourceInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.flags.andSceneContainer +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.ui.view.DeviceEntryIconView +import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +class CommunalLockIconViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by + Kosmos.Fixture { + CommunalLockIconViewModel( + context = context, + configurationInteractor = configurationInteractor, + deviceEntryInteractor = deviceEntryInteractor, + keyguardInteractor = keyguardInteractor, + keyguardViewController = { statusBarKeyguardViewManager }, + deviceEntrySourceInteractor = deviceEntrySourceInteractor, + accessibilityInteractor = accessibilityInteractor, + ) + } + + @Test + fun isLongPressEnabled_unlocked() = + kosmos.runTest { + val isLongPressEnabled by collectLastValue(underTest.isLongPressEnabled) + setLockscreenDismissible() + assertThat(isLongPressEnabled).isTrue() + } + + @Test + fun isLongPressEnabled_lock() = + kosmos.runTest { + val isLongPressEnabled by collectLastValue(underTest.isLongPressEnabled) + if (!SceneContainerFlag.isEnabled) { + fakeKeyguardRepository.setKeyguardDismissible(false) + } + assertThat(isLongPressEnabled).isFalse() + } + + @Test + fun iconType_locked() = + kosmos.runTest { + val viewAttributes by collectLastValue(underTest.viewAttributes) + if (!SceneContainerFlag.isEnabled) { + fakeKeyguardRepository.setKeyguardDismissible(false) + } + assertThat(viewAttributes?.type).isEqualTo(DeviceEntryIconView.IconType.LOCK) + } + + @Test + fun iconType_unlocked() = + kosmos.runTest { + val viewAttributes by collectLastValue(underTest.viewAttributes) + setLockscreenDismissible() + assertThat(viewAttributes?.type).isEqualTo(DeviceEntryIconView.IconType.UNLOCK) + } + + private suspend fun Kosmos.setLockscreenDismissible() { + if (SceneContainerFlag.isEnabled) { + // Need to set up a collection for the authentication to be propagated. + backgroundScope.launch { kosmos.deviceUnlockedInteractor.deviceUnlockStatus.collect {} } + assertThat( + kosmos.authenticationInteractor.authenticate( + FakeAuthenticationRepository.DEFAULT_PIN + ) + ) + .isEqualTo(AuthenticationResult.SUCCEEDED) + } else { + fakeKeyguardRepository.setKeyguardDismissible(true) + } + testScope.advanceTimeBy( + DeviceEntryIconViewModel.UNLOCKED_DELAY_MS * 2 + ) // wait for unlocked delay + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index b70f46c4b01c..7866a7f01658 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.Flags.FLAG_COMMUNAL_RESPONSIVE_GRID import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_DIRECT_EDIT_MODE +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.communal.data.model.CommunalSmartspaceTimer @@ -219,6 +220,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun ordering_smartspaceBeforeUmoBeforeWidgetsBeforeCtaTile() = testScope.runTest { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) @@ -258,7 +260,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { /** TODO(b/378171351): Handle ongoing content in responsive grid. */ @Test - @DisableFlags(FLAG_COMMUNAL_RESPONSIVE_GRID) + @DisableFlags(FLAG_COMMUNAL_RESPONSIVE_GRID, FLAG_GLANCEABLE_HUB_V2) fun ongoingContent_umoAndOneTimer_sizedAppropriately() = testScope.runTest { // Widgets available. @@ -296,7 +298,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { /** TODO(b/378171351): Handle ongoing content in responsive grid. */ @Test - @DisableFlags(FLAG_COMMUNAL_RESPONSIVE_GRID) + @DisableFlags(FLAG_COMMUNAL_RESPONSIVE_GRID, FLAG_GLANCEABLE_HUB_V2) fun ongoingContent_umoAndTwoTimers_sizedAppropriately() = testScope.runTest { // Widgets available. @@ -342,6 +344,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun communalContent_mediaHostVisible_umoIncluded() = testScope.runTest { // Media playing. @@ -353,6 +356,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun communalContent_mediaHostVisible_umoExcluded() = testScope.runTest { whenever(mediaHost.visible).thenReturn(false) @@ -408,6 +412,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun dismissCta_hidesCtaTileAndShowsPopup_thenHidesPopupAfterTimeout() = testScope.runTest { setIsMainUser(true) @@ -734,6 +739,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun communalContent_emitsFrozenContent_whenFrozen() = testScope.runTest { val communalContent by collectLastValue(underTest.communalContent) @@ -790,6 +796,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun communalContent_emitsLatestContent_whenNotFrozen() = testScope.runTest { val communalContent by collectLastValue(underTest.communalContent) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index 6c955bf1818d..5fd480f90ac9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -176,14 +176,14 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { } @Test - fun nonPowerButtonFPS_coExFaceFailure_doNotVibrateError() = + fun nonPowerButtonFPS_coExFaceFailure_vibrateError() = testScope.runTest { val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) enrollFace() runCurrent() faceFailure() - assertThat(playErrorHaptic).isNull() + assertThat(playErrorHaptic).isNotNull() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt index 030233625027..39baa01e07d6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt @@ -570,7 +570,7 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { unlockDevice() assertThat(isUnlocked).isTrue() - underTest.lockNow() + underTest.lockNow("test") runCurrent() assertThat(isUnlocked).isFalse() @@ -597,6 +597,62 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { assertThat(deviceUnlockStatus?.isUnlocked).isFalse() } + @Test + fun deviceUnlockStatus_staysUnlocked_whenDeviceGoesToSleep_whileIsTrusted() = + testScope.runTest { + setLockAfterScreenTimeout(5000) + kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) + + kosmos.fakeTrustRepository.setCurrentUserTrusted(true) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + + kosmos.powerInteractor.setAsleepForTest( + sleepReason = PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON + ) + runCurrent() + + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + } + + @Test + fun deviceUnlockStatus_staysUnlocked_whileIsTrusted() = + testScope.runTest { + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) + kosmos.fakeTrustRepository.setCurrentUserTrusted(true) + unlockDevice() + + kosmos.powerInteractor.setAsleepForTest( + sleepReason = PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON + ) + runCurrent() + + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + } + + @Test + fun deviceUnlockStatus_becomesLocked_whenNoLongerTrusted_whileAsleep() = + testScope.runTest { + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) + kosmos.fakeTrustRepository.setCurrentUserTrusted(true) + unlockDevice() + kosmos.powerInteractor.setAsleepForTest( + sleepReason = PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON + ) + runCurrent() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + + kosmos.fakeTrustRepository.setCurrentUserTrusted(false) + runCurrent() + + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + } + private fun TestScope.unlockDevice() { val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt index a276f514b779..5f5d80c01292 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt @@ -794,6 +794,122 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() { TransitionStep( transitionState = TransitionState.STARTED, from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + + assertEquals( + listOf( + true, + // Still not visible during GONE -> LOCKSCREEN. + false, + ), + values, + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + + assertEquals( + listOf( + true, + false, + // Visible now that we're FINISHED in LOCKSCREEN. + true, + ), + values, + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + runCurrent() + + assertEquals( + listOf( + true, + false, + // Remains true until the transition ends. + true, + ), + values, + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + + runCurrent() + assertEquals( + listOf( + true, + false, + true, + // Until we're finished in GONE again. + false, + ), + values, + ) + } + + @Test + @DisableSceneContainer + fun testLockscreenVisibility_falseDuringWakeAndUnlockToGone_fromNotCanceledGone() = + testScope.runTest { + val values by collectValues(underTest.value.lockscreenVisibility) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope, + ) + + runCurrent() + assertEquals( + listOf( + true, + // Not visible when finished in GONE. + false, + ), + values, + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.GONE, to = KeyguardState.AOD, ) ) @@ -857,8 +973,9 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() { listOf( true, false, - // Remains visible from AOD during transition. true, + // Becomes false immediately since we're wake and unlocking. + false, ), values, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java index 7478464772a4..57ac90648f33 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -117,8 +117,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); mMediaDevices.add(mMediaDevice1); mMediaDevices.add(mMediaDevice2); - mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1)); - mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2)); + mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1, true)); + mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2, false)); mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); @@ -779,4 +779,120 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mViewHolder.getDrawableId(false /* isInputDevice */, false /* isMutedVolumeIcon */)) .isEqualTo(R.drawable.media_output_icon_volume); } + + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void multipleSelectedDevices_verifySessionView() { + initializeSession(); + + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder( + new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); + + assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mEndClickIcon.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_SESSION_NAME); + assertThat(mViewHolder.mSeekBar.getVolume()).isEqualTo(TEST_CURRENT_VOLUME); + } + + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void multipleSelectedDevices_verifyCollapsedView() { + initializeSession(); + + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder( + new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); + + assertThat(mViewHolder.mItemLayout.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.GONE); + } + + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void multipleSelectedDevices_expandIconClicked_verifyInitialView() { + initializeSession(); + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder( + new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); + + mViewHolder.mEndTouchArea.performClick(); + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder( + new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); + + assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mEndClickIcon.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1); + } + + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void multipleSelectedDevices_expandIconClicked_verifyCollapsedView() { + initializeSession(); + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder( + new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); + + mViewHolder.mEndTouchArea.performClick(); + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder( + new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); + + assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mEndClickIcon.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + } + + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void deviceCanNotBeDeselected_verifyView() { + List<MediaDevice> selectedDevices = new ArrayList<>(); + selectedDevices.add(mMediaDevice1); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectedDevices); + when(mMediaSwitchingController.getSelectedMediaDevice()).thenReturn(selectedDevices); + when(mMediaSwitchingController.getDeselectableMediaDevice()).thenReturn(new ArrayList<>()); + + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder( + new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); + + assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mEndClickIcon.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1); + } + + private void initializeSession() { + when(mMediaSwitchingController.getSessionVolumeMax()).thenReturn(TEST_MAX_VOLUME); + when(mMediaSwitchingController.getSessionVolume()).thenReturn(TEST_CURRENT_VOLUME); + when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME); + + List<MediaDevice> selectedDevices = new ArrayList<>(); + selectedDevices.add(mMediaDevice1); + selectedDevices.add(mMediaDevice2); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectedDevices); + when(mMediaSwitchingController.getSelectedMediaDevice()).thenReturn(selectedDevices); + when(mMediaSwitchingController.getDeselectableMediaDevice()).thenReturn(selectedDevices); + + mMediaOutputAdapter.updateItems(); + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt index ff00bfb540c6..63942072f3a3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.mediaprojection.data.repository import android.hardware.display.displayManager +import android.media.projection.MediaProjectionEvent import android.media.projection.MediaProjectionInfo import android.media.projection.StopReason import android.os.Binder @@ -32,8 +33,11 @@ import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHI import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask @@ -42,7 +46,7 @@ import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionMana import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeTasksRepository import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager -import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test @@ -55,7 +59,7 @@ import org.mockito.kotlin.whenever @SmallTest class MediaProjectionManagerRepositoryTest : SysuiTestCase() { - private val kosmos = taskSwitcherKosmos() + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager @@ -345,4 +349,40 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { verify(fakeMediaProjectionManager.mediaProjectionManager) .stopActiveProjection(StopReason.STOP_QS_TILE) } + + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun projectionStartedDuringCallAndActivePostCallEvent_flagEnabled_emitsUnit() = + kosmos.runTest { + val projectionStartedDuringCallAndActivePostCallEvent by + collectLastValue(repo.projectionStartedDuringCallAndActivePostCallEvent) + + fakeMediaProjectionManager.dispatchEvent( + PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL_EVENT + ) + + assertThat(projectionStartedDuringCallAndActivePostCallEvent).isEqualTo(Unit) + } + + @Test + @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun projectionStartedDuringCallAndActivePostCallEvent_flagDisabled_doesNotEmit() = + testScope.runTest { + val projectionStartedDuringCallAndActivePostCallEvent by + collectLastValue(repo.projectionStartedDuringCallAndActivePostCallEvent) + + fakeMediaProjectionManager.dispatchEvent( + PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL_EVENT + ) + + assertThat(projectionStartedDuringCallAndActivePostCallEvent).isNull() + } + + companion object { + private val PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL_EVENT = + MediaProjectionEvent( + MediaProjectionEvent.PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL, + /* timestampMillis= */ 100L, + ) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt index ebc00c3897cb..9283455d3e54 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt @@ -38,7 +38,7 @@ class FakeNoteTaskBubbleController( ) : NoteTaskBubblesController(unUsed1, unsUsed2) { override suspend fun areBubblesAvailable() = optionalBubbles.isPresent - override suspend fun showOrHideAppBubble( + override suspend fun showOrHideNoteBubble( intent: Intent, userHandle: UserHandle, icon: Icon, @@ -49,12 +49,12 @@ class FakeNoteTaskBubbleController( if ( bubbleExpandBehavior == NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED && bubbles.isBubbleExpanded( - Bubble.getAppBubbleKeyForApp(intent.`package`, userHandle) + Bubble.getNoteBubbleKeyForApp(intent.`package`, userHandle) ) ) { return@ifPresentOrElse } - bubbles.showOrHideAppBubble(intent, userHandle, icon) + bubbles.showOrHideNoteBubble(intent, userHandle, icon) }, { throw IllegalAccessException() }, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt index e55d6ad6c5a0..7b9af90ff228 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt @@ -64,41 +64,41 @@ internal class NoteTaskBubblesServiceTest : SysuiTestCase() { } @Test - fun showOrHideAppBubble_defaultExpandBehavior_shouldCallBubblesApi() { + fun showOrHideNoteBubble_defaultExpandBehavior_shouldCallBubblesApi() { val intent = Intent() val user = UserHandle.SYSTEM val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.DEFAULT whenever(bubbles.isBubbleExpanded(any())).thenReturn(false) - createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior) + createServiceBinder().showOrHideNoteBubble(intent, user, icon, bubbleExpandBehavior) - verify(bubbles).showOrHideAppBubble(intent, user, icon) + verify(bubbles).showOrHideNoteBubble(intent, user, icon) } @Test - fun showOrHideAppBubble_keepIfExpanded_bubbleShown_shouldNotCallBubblesApi() { + fun showOrHideNoteBubble_keepIfExpanded_bubbleShown_shouldNotCallBubblesApi() { val intent = Intent().apply { setPackage("test") } val user = UserHandle.SYSTEM val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED whenever(bubbles.isBubbleExpanded(any())).thenReturn(true) - createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior) + createServiceBinder().showOrHideNoteBubble(intent, user, icon, bubbleExpandBehavior) - verify(bubbles, never()).showOrHideAppBubble(intent, user, icon) + verify(bubbles, never()).showOrHideNoteBubble(intent, user, icon) } @Test - fun showOrHideAppBubble_keepIfExpanded_bubbleNotShown_shouldCallBubblesApi() { + fun showOrHideNoteBubble_keepIfExpanded_bubbleNotShown_shouldCallBubblesApi() { val intent = Intent().apply { setPackage("test") } val user = UserHandle.SYSTEM val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED whenever(bubbles.isBubbleExpanded(any())).thenReturn(false) - createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior) + createServiceBinder().showOrHideNoteBubble(intent, user, icon, bubbleExpandBehavior) - verify(bubbles).showOrHideAppBubble(intent, user, icon) + verify(bubbles).showOrHideNoteBubble(intent, user, icon) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt index 98770c724126..c5d679f5df05 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt @@ -62,8 +62,8 @@ class DetailsViewModelTest : SysuiTestCase() { val tiles = currentTilesInteractor.currentTiles.value assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2) - assertThat(tiles!![1].spec).isEqualTo(specNoDetails) - (tiles!![1].tile as FakeQSTile).hasDetailsViewModel = false + assertThat(tiles[1].spec).isEqualTo(specNoDetails) + (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false assertThat(underTest.activeTileDetails).isNull() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractorTest.kt index a29289a7409a..b5aaadcb7e0c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractorTest.kt @@ -40,6 +40,7 @@ import com.android.systemui.qs.tiles.impl.custom.customTileRepository import com.android.systemui.qs.tiles.impl.custom.customTileServiceInteractor import com.android.systemui.qs.tiles.impl.custom.customTileSpec import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults +import com.android.systemui.qs.tiles.impl.custom.qsTileLogger import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.user.data.repository.userRepository @@ -72,6 +73,7 @@ class CustomTileDataInteractorTest : SysuiTestCase() { packageUpdatesRepository = customTilePackagesUpdatesRepository, userRepository = userRepository, tileScope = testScope.backgroundScope, + qsTileLogger = kosmos.qsTileLogger, ) } @@ -152,7 +154,7 @@ class CustomTileDataInteractorTest : SysuiTestCase() { collectLastValue( underTest.tileData( TEST_USER_1.userHandle, - flowOf(DataUpdateTrigger.InitialRequest) + flowOf(DataUpdateTrigger.InitialRequest), ) ) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index ef70305a3f47..5648c62fb439 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -1149,7 +1149,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) - fun skipsFaceErrorHaptics_nonSfps_coEx() = + fun playsFaceErrorHaptics_nonSfps_coEx() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) @@ -1161,14 +1161,15 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() updateFaceAuthStatus(isSuccess = false) - assertThat(playErrorHaptic).isNull() - verify(vibratorHelper, never()).vibrateAuthError(anyString()) + assertThat(playErrorHaptic).isNotNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + verify(vibratorHelper).vibrateAuthError(anyString()) verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) } @Test @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) - fun skipsMSDLFaceErrorHaptics_nonSfps_coEx() = + fun playsMSDLFaceErrorHaptics_nonSfps_coEx() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) @@ -1180,9 +1181,10 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() updateFaceAuthStatus(isSuccess = false) - assertThat(playErrorHaptic).isNull() - assertThat(msdlPlayer.latestTokenPlayed).isNull() - assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + assertThat(playErrorHaptic).isNotNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE) + assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties) } @Test @@ -2695,6 +2697,34 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(isVisible).isFalse() } + @Test + fun deviceLocks_whenNoLongerTrusted_whileDeviceNotEntered() = + testScope.runTest { + prepareState(isDeviceUnlocked = true, initialSceneKey = Scenes.Gone) + underTest.start() + + val isDeviceEntered by collectLastValue(kosmos.deviceEntryInteractor.isDeviceEntered) + val deviceUnlockStatus by + collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus) + val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene) + assertThat(isDeviceEntered).isTrue() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + assertThat(currentScene).isEqualTo(Scenes.Gone) + kosmos.fakeTrustRepository.setCurrentUserTrusted(true) + kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + runCurrent() + assertThat(isDeviceEntered).isFalse() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + + kosmos.fakeTrustRepository.setCurrentUserTrusted(false) + runCurrent() + + assertThat(isDeviceEntered).isFalse() + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java index d8897e9048c3..81301b3886f1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java @@ -183,14 +183,14 @@ public final class AppClipsServiceTest extends SysuiTestCase { when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); when(mOptionalBubbles.isEmpty()).thenReturn(false); when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false); + when(mBubbles.isNoteBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false); } private void mockForScreenshotBlocked() { when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); when(mOptionalBubbles.isEmpty()).thenReturn(false); when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); + when(mBubbles.isNoteBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true); } @@ -199,7 +199,7 @@ public final class AppClipsServiceTest extends SysuiTestCase { when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); when(mOptionalBubbles.isEmpty()).thenReturn(false); when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); + when(mBubbles.isNoteBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); } @@ -208,7 +208,7 @@ public final class AppClipsServiceTest extends SysuiTestCase { when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); when(mOptionalBubbles.isEmpty()).thenReturn(false); when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); + when(mBubbles.isNoteBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 555c717e1e65..93d1f593e81f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -78,6 +78,7 @@ import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -220,6 +221,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { mock(), { configurationForwarder }, brightnessMirrorShowingInteractor, + UnconfinedTestDispatcher(), ) controller.setupExpandedStatusBar() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java index 2020d0dcb041..3d3178793a09 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -460,17 +460,17 @@ public class CommandQueueTest extends SysuiTestCase { } @Test - public void testOnDisplayReady() { - mCommandQueue.onDisplayReady(DEFAULT_DISPLAY); + public void testonDisplayAddSystemDecorations() { + mCommandQueue.onDisplayAddSystemDecorations(DEFAULT_DISPLAY); waitForIdleSync(); - verify(mCallbacks).onDisplayReady(eq(DEFAULT_DISPLAY)); + verify(mCallbacks).onDisplayAddSystemDecorations(eq(DEFAULT_DISPLAY)); } @Test - public void testOnDisplayReadyForSecondaryDisplay() { - mCommandQueue.onDisplayReady(SECONDARY_DISPLAY); + public void testonDisplayAddSystemDecorationsForSecondaryDisplay() { + mCommandQueue.onDisplayAddSystemDecorations(SECONDARY_DISPLAY); waitForIdleSync(); - verify(mCallbacks).onDisplayReady(eq(SECONDARY_DISPLAY)); + verify(mCallbacks).onDisplayAddSystemDecorations(eq(SECONDARY_DISPLAY)); } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index 0a0564994e69..a79f4085ec6d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -45,6 +45,7 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import com.android.wm.shell.appzoomout.AppZoomOut import com.google.common.truth.Truth.assertThat +import java.util.Optional import java.util.function.Consumer import org.junit.Before import org.junit.Rule @@ -66,7 +67,6 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit -import java.util.Optional @RunWith(AndroidJUnit4::class) @RunWithLooper @@ -150,24 +150,23 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun setupListeners() { - verify(dumpManager).registerCriticalDumpable( - anyString(), eq(notificationShadeDepthController) - ) + verify(dumpManager) + .registerCriticalDumpable(anyString(), eq(notificationShadeDepthController)) } @Test fun onPanelExpansionChanged_apliesBlur_ifShade() { notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 1f, expanded = true, tracking = false)) + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) verify(shadeAnimation).animateTo(eq(maxBlur)) } @Test fun onPanelExpansionChanged_animatesBlurIn_ifShade() { notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 0.01f, expanded = false, tracking = false)) + ShadeExpansionChangeEvent(fraction = 0.01f, expanded = false, tracking = false) + ) verify(shadeAnimation).animateTo(eq(maxBlur)) } @@ -176,27 +175,27 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { onPanelExpansionChanged_animatesBlurIn_ifShade() clearInvocations(shadeAnimation) notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 0f, expanded = false, tracking = false)) + ShadeExpansionChangeEvent(fraction = 0f, expanded = false, tracking = false) + ) verify(shadeAnimation).animateTo(eq(0)) } @Test fun onPanelExpansionChanged_animatesBlurOut_ifFlick() { - val event = - ShadeExpansionChangeEvent( - fraction = 1f, expanded = true, tracking = false) + val event = ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) onPanelExpansionChanged_apliesBlur_ifShade() clearInvocations(shadeAnimation) notificationShadeDepthController.onPanelExpansionChanged(event) verify(shadeAnimation, never()).animateTo(anyInt()) notificationShadeDepthController.onPanelExpansionChanged( - event.copy(fraction = 0.9f, tracking = true)) + event.copy(fraction = 0.9f, tracking = true) + ) verify(shadeAnimation, never()).animateTo(anyInt()) notificationShadeDepthController.onPanelExpansionChanged( - event.copy(fraction = 0.8f, tracking = false)) + event.copy(fraction = 0.8f, tracking = false) + ) verify(shadeAnimation).animateTo(eq(0)) } @@ -205,16 +204,14 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { onPanelExpansionChanged_animatesBlurOut_ifFlick() clearInvocations(shadeAnimation) notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 0.6f, expanded = true, tracking = true)) + ShadeExpansionChangeEvent(fraction = 0.6f, expanded = true, tracking = true) + ) verify(shadeAnimation).animateTo(eq(maxBlur)) } @Test fun onPanelExpansionChanged_respectsMinPanelPullDownFraction() { - val event = - ShadeExpansionChangeEvent( - fraction = 0.5f, expanded = true, tracking = true) + val event = ShadeExpansionChangeEvent(fraction = 0.5f, expanded = true, tracking = true) notificationShadeDepthController.panelPullDownMinFraction = 0.5f notificationShadeDepthController.onPanelExpansionChanged(event) assertThat(notificationShadeDepthController.shadeExpansion).isEqualTo(0f) @@ -241,8 +238,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { statusBarState = StatusBarState.KEYGUARD notificationShadeDepthController.qsPanelExpansion = 1f notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 1f, expanded = true, tracking = false)) + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false)) } @@ -252,8 +249,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { statusBarState = StatusBarState.KEYGUARD notificationShadeDepthController.qsPanelExpansion = 0.25f notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 1f, expanded = true, tracking = false)) + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(wallpaperController) .setNotificationShadeZoom(eq(ShadeInterpolation.getNotificationScrimAlpha(0.25f))) @@ -264,8 +261,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { enableSplitShade() notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 1f, expanded = true, tracking = false)) + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(wallpaperController).setNotificationShadeZoom(0f) @@ -276,8 +273,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { disableSplitShade() notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 1f, expanded = true, tracking = false)) + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(wallpaperController).setNotificationShadeZoom(floatThat { it > 0 }) @@ -354,8 +351,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun updateBlurCallback_setsBlur_whenExpanded() { notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 1f, expanded = true, tracking = false)) + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat()) notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false)) @@ -364,8 +361,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun updateBlurCallback_ignoreShadeBlurUntilHidden_overridesZoom() { notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 1f, expanded = true, tracking = false)) + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat()) notificationShadeDepthController.blursDisabledForAppLaunch = true notificationShadeDepthController.updateBlurCallback.doFrame(0) @@ -373,7 +370,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } @Test - @DisableFlags(Flags.FLAG_BOUNCER_UI_REVAMP) + @DisableFlags(Flags.FLAG_BOUNCER_UI_REVAMP, Flags.FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND) fun ignoreShadeBlurUntilHidden_schedulesFrame() { notificationShadeDepthController.blursDisabledForAppLaunch = true verify(blurUtils).prepareBlur(any(), anyInt()) @@ -391,8 +388,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun ignoreBlurForUnlock_ignores() { notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 1f, expanded = true, tracking = false)) + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat()) notificationShadeDepthController.blursDisabledForAppLaunch = false @@ -408,8 +405,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun ignoreBlurForUnlock_doesNotIgnore() { notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 1f, expanded = true, tracking = false)) + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat()) notificationShadeDepthController.blursDisabledForAppLaunch = false @@ -435,14 +432,14 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } @Test - @DisableFlags(Flags.FLAG_BOUNCER_UI_REVAMP) + @DisableFlags(Flags.FLAG_BOUNCER_UI_REVAMP, Flags.FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND) fun brightnessMirror_hidesShadeBlur() { // Brightness mirror is fully visible `when`(brightnessSpring.ratio).thenReturn(1f) // And shade is blurred notificationShadeDepthController.onPanelExpansionChanged( - ShadeExpansionChangeEvent( - fraction = 1f, expanded = true, tracking = false)) + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat()) notificationShadeDepthController.updateBlurCallback.doFrame(0) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index 75262a4d058d..03c07510ce53 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -24,9 +24,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.res.R @@ -47,6 +47,7 @@ import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernizat import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel +import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test @@ -60,7 +61,7 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class CallChipViewModelTest : SysuiTestCase() { - private val kosmos = Kosmos() + private val kosmos = testKosmos() private val notificationListRepository = kosmos.activeNotificationListRepository private val testScope = kosmos.testScope private val repo = kosmos.ongoingCallRepository @@ -162,25 +163,34 @@ class CallChipViewModelTest : SysuiTestCase() { @Test @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun chip_zeroStartTime_cdFlagOff_iconIsNotifIcon() = + fun chip_zeroStartTime_cdFlagOff_iconIsNotifIcon_withContentDescription() = testScope.runTest { val latest by collectLastValue(underTest.chip) val notifIcon = createStatusBarIconViewOrNull() - repo.setOngoingCallState(inCallModel(startTimeMs = 0, notificationIcon = notifIcon)) + repo.setOngoingCallState( + inCallModel( + startTimeMs = 0, + notificationIcon = notifIcon, + appName = "Fake app name", + ) + ) assertThat((latest as OngoingActivityChipModel.Shown).icon) .isInstanceOf(OngoingActivityChipModel.ChipIcon.StatusBarView::class.java) val actualIcon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.StatusBarView) - .impl - assertThat(actualIcon).isEqualTo(notifIcon) + (latest as OngoingActivityChipModel.Shown).icon + as OngoingActivityChipModel.ChipIcon.StatusBarView + assertThat(actualIcon.impl).isEqualTo(notifIcon) + assertThat(actualIcon.contentDescription.loadContentDescription(context)) + .contains("Ongoing call") + assertThat(actualIcon.contentDescription.loadContentDescription(context)) + .contains("Fake app name") } @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun chip_zeroStartTime_cdFlagOn_iconIsNotifKeyIcon() = + fun chip_zeroStartTime_cdFlagOn_iconIsNotifKeyIcon_withContentDescription() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -189,11 +199,22 @@ class CallChipViewModelTest : SysuiTestCase() { startTimeMs = 0, notificationIcon = createStatusBarIconViewOrNull(), notificationKey = "notifKey", + appName = "Fake app name", ) ) assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey")) + .isInstanceOf( + OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon::class.java + ) + val actualIcon = + (latest as OngoingActivityChipModel.Shown).icon + as OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon + assertThat(actualIcon.notificationKey).isEqualTo("notifKey") + assertThat(actualIcon.contentDescription.loadContentDescription(context)) + .contains("Ongoing call") + assertThat(actualIcon.contentDescription.loadContentDescription(context)) + .contains("Fake app name") } @Test @@ -216,7 +237,7 @@ class CallChipViewModelTest : SysuiTestCase() { @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun chip_notifIconFlagOn_butNullNotifIcon_iconNotifKey() = + fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOn_iconIsNotifKeyIcon_withContentDescription() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -225,11 +246,22 @@ class CallChipViewModelTest : SysuiTestCase() { startTimeMs = 1000, notificationIcon = null, notificationKey = "notifKey", + appName = "Fake app name", ) ) assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey")) + .isInstanceOf( + OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon::class.java + ) + val actualIcon = + (latest as OngoingActivityChipModel.Shown).icon + as OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon + assertThat(actualIcon.notificationKey).isEqualTo("notifKey") + assertThat(actualIcon.contentDescription.loadContentDescription(context)) + .contains("Ongoing call") + assertThat(actualIcon.contentDescription.loadContentDescription(context)) + .contains("Fake app name") } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt index dea3d1f68ce5..0dc2759d9a4c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt @@ -22,21 +22,26 @@ import android.content.packageManager import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testCase +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.doAnswer import org.mockito.kotlin.any @@ -44,8 +49,9 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @SmallTest +@RunWith(AndroidJUnit4::class) class MediaProjectionChipInteractorTest : SysuiTestCase() { - private val kosmos = Kosmos().also { it.testCase = this } + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository @@ -57,6 +63,26 @@ class MediaProjectionChipInteractorTest : SysuiTestCase() { private val underTest = kosmos.mediaProjectionChipInteractor @Test + fun projectionStartedDuringCallAndActivePostCallEvent_eventEmitted_isUnit() = + kosmos.runTest { + val latest by + collectLastValue(underTest.projectionStartedDuringCallAndActivePostCallEvent) + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + assertThat(latest).isEqualTo(Unit) + } + + @Test + fun projectionStartedDuringCallAndActivePostCallEvent_noEventEmitted_isNull() = + kosmos.runTest { + val latest by + collectLastValue(underTest.projectionStartedDuringCallAndActivePostCallEvent) + + assertThat(latest).isNull() + } + + @Test fun projection_notProjectingState_isNotProjecting() = testScope.runTest { val latest by collectLastValue(underTest.projection) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt index fe15eac46e2d..05f2585cfaa5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt @@ -49,6 +49,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { val startingNotif = activeNotificationModel( key = "notif1", + appName = "Fake Name", statusBarChipIcon = icon, promotedContent = PROMOTED_CONTENT, ) @@ -58,6 +59,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.notificationChip) assertThat(latest!!.key).isEqualTo("notif1") + assertThat(latest!!.appName).isEqualTo("Fake Name") assertThat(latest!!.statusBarChipIconView).isEqualTo(icon) assertThat(latest!!.promotedContent).isEqualTo(PROMOTED_CONTENT) } @@ -70,6 +72,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { factory.create( activeNotificationModel( key = "notif1", + appName = "Fake Name", statusBarChipIcon = originalIconView, promotedContent = PROMOTED_CONTENT, ), @@ -82,12 +85,14 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { underTest.setNotification( activeNotificationModel( key = "notif1", + appName = "New Name", statusBarChipIcon = newIconView, promotedContent = PROMOTED_CONTENT, ) ) assertThat(latest!!.key).isEqualTo("notif1") + assertThat(latest!!.appName).isEqualTo("New Name") assertThat(latest!!.statusBarChipIconView).isEqualTo(newIconView) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index 942e6554e5d9..1f77ddc73aa5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel +import android.content.Context import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.View @@ -23,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.kosmos.collectLastValue @@ -125,7 +127,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_onePromotedNotif_statusBarIconViewMatches() = + fun chips_onePromotedNotif_connectedDisplaysFlagDisabled_statusBarIconViewMatches() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -134,6 +136,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", + appName = "Fake App Name", statusBarChipIcon = icon, promotedContent = PromotedNotificationContentModel.Builder("notif").build(), ) @@ -142,7 +145,13 @@ class NotifChipsViewModelTest : SysuiTestCase() { assertThat(latest).hasSize(1) val chip = latest!![0] - assertIsNotifChip(chip, icon, "notif") + assertIsNotifChip( + chip, + context, + icon, + expectedNotificationKey = "notif", + expectedContentDescriptionSubstrings = listOf("Ongoing", "Fake App Name"), + ) } @Test @@ -157,6 +166,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = notifKey, + appName = "Fake App Name", statusBarChipIcon = null, promotedContent = PromotedNotificationContentModel.Builder(notifKey).build(), ) @@ -165,9 +175,13 @@ class NotifChipsViewModelTest : SysuiTestCase() { assertThat(latest).hasSize(1) val chip = latest!![0] - assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown::class.java) - assertThat(chip.icon) - .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(notifKey)) + assertIsNotifChip( + chip, + context, + expectedIcon = null, + expectedNotificationKey = "notif", + expectedContentDescriptionSubstrings = listOf("Ongoing", "Fake App Name"), + ) } @Test @@ -230,8 +244,8 @@ class NotifChipsViewModelTest : SysuiTestCase() { ) assertThat(latest).hasSize(2) - assertIsNotifChip(latest!![0], firstIcon, "notif1") - assertIsNotifChip(latest!![1], secondIcon, "notif2") + assertIsNotifChip(latest!![0], context, firstIcon, "notif1") + assertIsNotifChip(latest!![1], context, secondIcon, "notif2") } @Test @@ -590,7 +604,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { // THEN the "notif" chip keeps showing time val chip = latest!![0] assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java) - assertIsNotifChip(chip, icon, "notif") + assertIsNotifChip(chip, context, icon, "notif") } @Test @@ -705,24 +719,41 @@ class NotifChipsViewModelTest : SysuiTestCase() { companion object { fun assertIsNotifChip( latest: OngoingActivityChipModel?, + context: Context, expectedIcon: StatusBarIconView?, - notificationKey: String, + expectedNotificationKey: String, + expectedContentDescriptionSubstrings: List<String> = emptyList(), ) { val shown = latest as OngoingActivityChipModel.Shown if (StatusBarConnectedDisplays.isEnabled) { assertThat(shown.icon) - .isEqualTo( - OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(notificationKey) + .isInstanceOf( + OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon::class.java ) + val icon = shown.icon as OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon + + assertThat(icon.notificationKey).isEqualTo(expectedNotificationKey) + expectedContentDescriptionSubstrings.forEach { + assertThat(icon.contentDescription.loadContentDescription(context)).contains(it) + } } else { - assertThat(latest.icon) - .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(expectedIcon!!)) + assertThat(shown.icon) + .isInstanceOf(OngoingActivityChipModel.ChipIcon.StatusBarView::class.java) + val icon = shown.icon as OngoingActivityChipModel.ChipIcon.StatusBarView + assertThat(icon.impl).isEqualTo(expectedIcon!!) + expectedContentDescriptionSubstrings.forEach { + assertThat(icon.contentDescription.loadContentDescription(context)).contains(it) + } } } fun assertIsNotifKey(latest: OngoingActivityChipModel?, expectedKey: String) { - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(expectedKey)) + assertThat( + ((latest as OngoingActivityChipModel.Shown).icon + as OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon) + .notificationKey + ) + .isEqualTo(expectedKey) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt index 36fc5aa16407..5a66888e4da4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt @@ -31,9 +31,10 @@ import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testCase +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask @@ -41,6 +42,7 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.CAST_TO_OTHER_DEVICES_PACKAGE import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection +import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndGenericShareToAppDialogDelegate import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareScreenToAppDialogDelegate import com.android.systemui.statusbar.chips.ui.model.ColorsModel @@ -51,6 +53,7 @@ import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test @@ -58,6 +61,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mockito.times import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq @@ -68,7 +72,7 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class ShareToAppChipViewModelTest : SysuiTestCase() { - private val kosmos = Kosmos().also { it.testCase = this } + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository private val systemClock = kosmos.fakeSystemClock @@ -89,9 +93,11 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { mock<Expandable>().apply { whenever(dialogTransitionController(any())).thenReturn(mock()) } private val underTest = kosmos.shareToAppChipViewModel + private val mockDialog = mock<SystemUIDialog>() @Before fun setUp() { + underTest.start() setUpPackageManagerForMediaProjection(kosmos) whenever(kosmos.mockSystemUIDialogFactory.create(any<EndShareScreenToAppDialogDelegate>())) @@ -101,6 +107,196 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun chip_flagEnabled_projectionStartedDuringCallAndActivePostCallEventEmitted_chipHidden() = + kosmos.runTest { + val latestChip by collectLastValue(underTest.chip) + + // Set mediaProjectionState to Projecting + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + + // Verify the chip is initially shown + assertThat(latestChip).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + // Verify the chip is hidden + assertThat(latestChip).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + } + + @Test + @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun chip_flagDisabled_projectionStartedDuringCallAndActivePostCallEventEmitted_chipRemainsVisible() = + kosmos.runTest { + val latestChip by collectLastValue(underTest.chip) + + // Set mediaProjectionState to Projecting + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + + // Verify the chip is initially shown + assertThat(latestChip).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + // Chip is still shown + assertThat(latestChip).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + } + + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun stopDialog_flagEnabled_initialState_isHidden() = + kosmos.runTest { + val latest by collectLastValue(underTest.stopDialogToShow) + + assertThat(latest).isEqualTo(MediaProjectionStopDialogModel.Hidden) + } + + @Test + @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun stopDialog_flagDisabled_projectionStartedDuringCallAndActivePostCallEventEmitted_dialogRemainsHidden() = + kosmos.runTest { + val latestStopDialogModel by collectLastValue(underTest.stopDialogToShow) + + // Set mediaProjectionRepo state to Projecting + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + // Verify that no dialog is shown + assertThat(latestStopDialogModel).isEqualTo(MediaProjectionStopDialogModel.Hidden) + } + + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun stopDialog_notProjectingState_flagEnabled_remainsHidden() = + kosmos.runTest { + val latest by collectLastValue(underTest.stopDialogToShow) + + // Set the state to not projecting + mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + // Verify that the dialog remains hidden + assertThat(latest).isEqualTo(MediaProjectionStopDialogModel.Hidden) + } + + @Test + @EnableFlags( + com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END, + FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP, + ) + fun stopDialog_projectingAudio_flagEnabled_eventEmitted_showsGenericStopDialog() = + kosmos.runTest { + val latest by collectLastValue(underTest.stopDialogToShow) + + // Set the state to projecting audio + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE) + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + // Verify that the generic dialog is shown + assertThat(latest).isInstanceOf(MediaProjectionStopDialogModel.Shown::class.java) + val dialogDelegate = (latest as MediaProjectionStopDialogModel.Shown).dialogDelegate + assertThat(dialogDelegate).isInstanceOf(EndGenericShareToAppDialogDelegate::class.java) + } + + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun stopDialog_projectingEntireScreen_flagEnabled_eventEmitted_showsShareScreenToAppStopDialog() = + kosmos.runTest { + val latest by collectLastValue(underTest.stopDialogToShow) + + // Set the state to projecting the entire screen + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + + assertThat(latest).isInstanceOf(MediaProjectionStopDialogModel.Hidden::class.java) + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + // Verify that the dialog is shown + assertThat(latest).isInstanceOf(MediaProjectionStopDialogModel.Shown::class.java) + val dialogDelegate = (latest as MediaProjectionStopDialogModel.Shown).dialogDelegate + assertThat(dialogDelegate).isInstanceOf(EndShareScreenToAppDialogDelegate::class.java) + } + + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun stopDialog_projectingEntireScreen_eventEmitted_hasCancelBehaviour() = + kosmos.runTest { + val latestDialogModel by collectLastValue(underTest.stopDialogToShow) + + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + // Verify that the dialog is shown + assertThat(latestDialogModel) + .isInstanceOf(MediaProjectionStopDialogModel.Shown::class.java) + + val dialogModel = latestDialogModel as MediaProjectionStopDialogModel.Shown + + whenever(dialogModel.dialogDelegate.createDialog()).thenReturn(mockDialog) + + dialogModel.createAndShowDialog() + + // Verify dialog is shown + verify(mockDialog).show() + + // Verify dialog is hidden when dialog is cancelled + argumentCaptor<DialogInterface.OnCancelListener>().apply { + verify(mockDialog).setOnCancelListener(capture()) + firstValue.onCancel(mockDialog) + } + assertThat(underTest.stopDialogToShow.value) + .isEqualTo(MediaProjectionStopDialogModel.Hidden) + + verify(mockDialog, times(1)).setOnCancelListener(any()) + } + + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun stopDialog_projectingEntireScreen_eventEmitted_hasDismissBehaviour() = + kosmos.runTest { + val latestDialogModel by collectLastValue(underTest.stopDialogToShow) + + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + // Verify that the dialog is shown + assertThat(latestDialogModel) + .isInstanceOf(MediaProjectionStopDialogModel.Shown::class.java) + + val dialogModel = latestDialogModel as MediaProjectionStopDialogModel.Shown + + whenever(dialogModel.dialogDelegate.createDialog()).thenReturn(mockDialog) + + // Simulate showing the dialog + dialogModel.createAndShowDialog() + + // Verify dialog is shown + verify(mockDialog).show() + + // Verify dialog is hidden when dialog is dismissed + argumentCaptor<DialogInterface.OnDismissListener>().apply { + verify(mockDialog).setOnDismissListener(capture()) + firstValue.onDismiss(mockDialog) + } + assertThat(underTest.stopDialogToShow.value) + .isEqualTo(MediaProjectionStopDialogModel.Hidden) + + verify(mockDialog, times(1)).setOnDismissListener(any()) + } + + @Test fun chip_notProjectingState_isHidden() = testScope.runTest { val latest by collectLastValue(underTest.chip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index a4b6a841d61b..7a33cbe761c1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -289,7 +289,11 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { fun primaryChip_screenRecordStoppedViaDialog_chipHiddenWithoutAnimation() = testScope.runTest { screenRecordState.value = ScreenRecordModel.Recording - mediaProjectionState.value = MediaProjectionState.NotProjecting + mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen( + NORMAL_PACKAGE, + hostDeviceName = "Recording Display", + ) callRepo.setOngoingCallState(OngoingCallModel.NoCall) val latest by collectLastValue(underTest.primaryChip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 28f360108e50..78103a9906c4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -311,7 +311,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) ) - assertIsNotifChip(latest!!.primary, icon, "notif") + assertIsNotifChip(latest!!.primary, context, icon, "notif") assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) } @@ -339,8 +339,8 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) ) - assertIsNotifChip(latest!!.primary, firstIcon, "firstNotif") - assertIsNotifChip(latest!!.secondary, secondIcon, "secondNotif") + assertIsNotifChip(latest!!.primary, context, firstIcon, "firstNotif") + assertIsNotifChip(latest!!.secondary, context, secondIcon, "secondNotif") } @Test @@ -374,8 +374,8 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) ) - assertIsNotifChip(latest!!.primary, firstIcon, "firstNotif") - assertIsNotifChip(latest!!.secondary, secondIcon, "secondNotif") + assertIsNotifChip(latest!!.primary, context, firstIcon, "firstNotif") + assertIsNotifChip(latest!!.secondary, context, secondIcon, "secondNotif") } @Test @@ -407,7 +407,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) assertIsCallChip(latest!!.primary, callNotificationKey) - assertIsNotifChip(latest!!.secondary, firstIcon, "firstNotif") + assertIsNotifChip(latest!!.secondary, context, firstIcon, "firstNotif") } @Test @@ -456,7 +456,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.primaryChip) - assertIsNotifChip(latest, notifIcon, "notif") + assertIsNotifChip(latest, context, notifIcon, "notif") // WHEN the higher priority call chip is added callRepo.setOngoingCallState( @@ -527,7 +527,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { callRepo.setOngoingCallState(OngoingCallModel.NoCall) // THEN the lower priority notif is used - assertIsNotifChip(latest, notifIcon, "notif") + assertIsNotifChip(latest, context, notifIcon, "notif") } @Test @@ -552,7 +552,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chips) - assertIsNotifChip(latest!!.primary, notifIcon, "notif") + assertIsNotifChip(latest!!.primary, context, notifIcon, "notif") assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) // WHEN the higher priority call chip is added @@ -563,7 +563,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // THEN the higher priority call chip is used as primary and notif is demoted to // secondary assertIsCallChip(latest!!.primary, callNotificationKey) - assertIsNotifChip(latest!!.secondary, notifIcon, "notif") + assertIsNotifChip(latest!!.secondary, context, notifIcon, "notif") // WHEN the higher priority media projection chip is added mediaProjectionState.value = @@ -590,13 +590,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // THEN media projection and notif remain assertIsShareToAppChip(latest!!.primary) - assertIsNotifChip(latest!!.secondary, notifIcon, "notif") + assertIsNotifChip(latest!!.secondary, context, notifIcon, "notif") // WHEN media projection is dropped mediaProjectionState.value = MediaProjectionState.NotProjecting // THEN notif is promoted to primary - assertIsNotifChip(latest!!.primary, notifIcon, "notif") + assertIsNotifChip(latest!!.primary, context, notifIcon, "notif") assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarterTest.kt index c06da4bc5080..dc65a9e24407 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarterTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.core import android.platform.test.annotations.EnableFlags import android.view.Display +import android.view.mockIWindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -31,11 +32,13 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -51,75 +54,110 @@ class MultiDisplayStatusBarStarterTest : SysuiTestCase() { private val fakeInitializerStore = kosmos.fakeStatusBarInitializerStore private val fakePrivacyDotStore = kosmos.fakePrivacyDotWindowControllerStore private val fakeLightBarStore = kosmos.fakeLightBarControllerStore + private val windowManager = kosmos.mockIWindowManager + // Lazy, so that @EnableFlags is set before initializer is instantiated. private val underTest by lazy { kosmos.multiDisplayStatusBarStarter } + @Before + fun setup() { + whenever(windowManager.shouldShowSystemDecors(Display.DEFAULT_DISPLAY)).thenReturn(true) + whenever(windowManager.shouldShowSystemDecors(DISPLAY_1)).thenReturn(true) + whenever(windowManager.shouldShowSystemDecors(DISPLAY_2)).thenReturn(true) + whenever(windowManager.shouldShowSystemDecors(DISPLAY_3)).thenReturn(true) + whenever(windowManager.shouldShowSystemDecors(DISPLAY_4_NO_SYSTEM_DECOR)).thenReturn(false) + } + @Test fun start_startsInitializersForCurrentDisplays() = testScope.runTest { - fakeDisplayRepository.addDisplay(displayId = 1) - fakeDisplayRepository.addDisplay(displayId = 2) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_1) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_2) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) underTest.start() runCurrent() expect - .that(fakeInitializerStore.forDisplay(displayId = 1).startedByCoreStartable) + .that(fakeInitializerStore.forDisplay(displayId = DISPLAY_1).startedByCoreStartable) .isTrue() expect - .that(fakeInitializerStore.forDisplay(displayId = 2).startedByCoreStartable) + .that(fakeInitializerStore.forDisplay(displayId = DISPLAY_2).startedByCoreStartable) .isTrue() + expect + .that( + fakeInitializerStore + .forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) + .startedByCoreStartable + ) + .isFalse() } @Test fun start_startsOrchestratorForCurrentDisplays() = testScope.runTest { - fakeDisplayRepository.addDisplay(displayId = 1) - fakeDisplayRepository.addDisplay(displayId = 2) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_1) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_2) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) underTest.start() runCurrent() - verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = 1)!!).start() - verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = 2)!!).start() + verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = DISPLAY_1)!!) + .start() + verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = DISPLAY_2)!!) + .start() + assertThat( + fakeOrchestratorFactory.createdOrchestratorForDisplay( + displayId = DISPLAY_4_NO_SYSTEM_DECOR + ) + ) + .isNull() } @Test fun start_startsPrivacyDotForCurrentDisplays() = testScope.runTest { - fakeDisplayRepository.addDisplay(displayId = 1) - fakeDisplayRepository.addDisplay(displayId = 2) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_1) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_2) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) underTest.start() runCurrent() - verify(fakePrivacyDotStore.forDisplay(displayId = 1)).start() - verify(fakePrivacyDotStore.forDisplay(displayId = 2)).start() + verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_1)).start() + verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_2)).start() + verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) + .start() } @Test fun start_doesNotStartLightBarControllerForCurrentDisplays() = testScope.runTest { - fakeDisplayRepository.addDisplay(displayId = 1) - fakeDisplayRepository.addDisplay(displayId = 2) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_1) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_2) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) underTest.start() runCurrent() - verify(fakeLightBarStore.forDisplay(displayId = 1), never()).start() - verify(fakeLightBarStore.forDisplay(displayId = 2), never()).start() + verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_1), never()).start() + verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_2), never()).start() + verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) + .start() } @Test fun start_createsLightBarControllerForCurrentDisplays() = testScope.runTest { - fakeDisplayRepository.addDisplay(displayId = 1) - fakeDisplayRepository.addDisplay(displayId = 2) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_1) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_2) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) underTest.start() runCurrent() - assertThat(fakeLightBarStore.perDisplayMocks.keys).containsExactly(1, 2) + assertThat(fakeLightBarStore.perDisplayMocks.keys).containsExactly(1, DISPLAY_2) } @Test @@ -135,121 +173,174 @@ class MultiDisplayStatusBarStarterTest : SysuiTestCase() { } @Test - fun displayAdded_orchestratorForNewDisplayIsStarted() = + fun displayAdded_orchestratorForNewDisplay() = testScope.runTest { underTest.start() runCurrent() - fakeDisplayRepository.addDisplay(displayId = 3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() - verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = 3)!!).start() + verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = DISPLAY_3)!!) + .start() + assertThat( + fakeOrchestratorFactory.createdOrchestratorForDisplay( + displayId = DISPLAY_4_NO_SYSTEM_DECOR + ) + ) + .isNull() } @Test - fun displayAdded_initializerForNewDisplayIsStarted() = + fun displayAdded_initializerForNewDisplay() = testScope.runTest { underTest.start() runCurrent() - fakeDisplayRepository.addDisplay(displayId = 3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() expect - .that(fakeInitializerStore.forDisplay(displayId = 3).startedByCoreStartable) + .that(fakeInitializerStore.forDisplay(displayId = DISPLAY_3).startedByCoreStartable) .isTrue() + expect + .that( + fakeInitializerStore + .forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) + .startedByCoreStartable + ) + .isFalse() } @Test - fun displayAdded_privacyDotForNewDisplayIsStarted() = + fun displayAdded_privacyDotForNewDisplay() = testScope.runTest { underTest.start() runCurrent() - fakeDisplayRepository.addDisplay(displayId = 3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() - verify(fakePrivacyDotStore.forDisplay(displayId = 3)).start() + verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_3)).start() + verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) + .start() } @Test - fun displayAdded_lightBarForNewDisplayIsCreated() = + fun displayAdded_lightBarForNewDisplayCreate() = testScope.runTest { underTest.start() runCurrent() - fakeDisplayRepository.addDisplay(displayId = 3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() - assertThat(fakeLightBarStore.perDisplayMocks.keys).containsExactly(3) + assertThat(fakeLightBarStore.perDisplayMocks.keys).containsExactly(DISPLAY_3) } @Test - fun displayAdded_lightBarForNewDisplayIsNotStarted() = + fun displayAdded_lightBarForNewDisplayStart() = testScope.runTest { underTest.start() runCurrent() - fakeDisplayRepository.addDisplay(displayId = 3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() - verify(fakeLightBarStore.forDisplay(displayId = 3), never()).start() + verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_3), never()).start() + verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) + .start() } @Test - fun displayAddedDuringStart_initializerForNewDisplayIsStarted() = + fun displayAddedDuringStart_initializerForNewDisplay() = testScope.runTest { underTest.start() - fakeDisplayRepository.addDisplay(displayId = 3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() expect - .that(fakeInitializerStore.forDisplay(displayId = 3).startedByCoreStartable) + .that(fakeInitializerStore.forDisplay(displayId = DISPLAY_3).startedByCoreStartable) .isTrue() + expect + .that( + fakeInitializerStore + .forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) + .startedByCoreStartable + ) + .isFalse() } @Test - fun displayAddedDuringStart_orchestratorForNewDisplayIsStarted() = + fun displayAddedDuringStart_orchestratorForNewDisplay() = testScope.runTest { underTest.start() - fakeDisplayRepository.addDisplay(displayId = 3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() - verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = 3)!!).start() + verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = DISPLAY_3)!!) + .start() + assertThat( + fakeOrchestratorFactory.createdOrchestratorForDisplay( + displayId = DISPLAY_4_NO_SYSTEM_DECOR + ) + ) + .isNull() } @Test - fun displayAddedDuringStart_privacyDotForNewDisplayIsStarted() = + fun displayAddedDuringStart_privacyDotForNewDisplay() = testScope.runTest { underTest.start() - fakeDisplayRepository.addDisplay(displayId = 3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() - verify(fakePrivacyDotStore.forDisplay(displayId = 3)).start() + verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_3)).start() + verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) + .start() } @Test - fun displayAddedDuringStart_lightBarForNewDisplayIsCreated() = + fun displayAddedDuringStart_lightBarForNewDisplayCreate() = testScope.runTest { underTest.start() - fakeDisplayRepository.addDisplay(displayId = 3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() - assertThat(fakeLightBarStore.perDisplayMocks.keys).containsExactly(3) + assertThat(fakeLightBarStore.perDisplayMocks.keys).containsExactly(DISPLAY_3) } @Test - fun displayAddedDuringStart_lightBarForNewDisplayIsNotStarted() = + fun displayAddedDuringStart_lightBarForNewDisplayStart() = testScope.runTest { underTest.start() - fakeDisplayRepository.addDisplay(displayId = 3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) + fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() - verify(fakeLightBarStore.forDisplay(displayId = 3), never()).start() + verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_3), never()).start() + verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) + .start() } + + companion object { + const val DISPLAY_1 = 1 + const val DISPLAY_2 = 2 + const val DISPLAY_3 = 3 + const val DISPLAY_4_NO_SYSTEM_DECOR = 4 + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt index 5d9aa71c5d89..35b19c19d5ce 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt @@ -47,7 +47,7 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() { private val notifsRepository = kosmos.activeNotificationListRepository private val notifsInteractor = kosmos.activeNotificationsInteractor private val underTest = - RenderNotificationListInteractor(notifsRepository, sectionStyleProvider = mock()) + RenderNotificationListInteractor(notifsRepository, sectionStyleProvider = mock(), context) @Test fun setRenderedList_preservesOrdering() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt index 9a42f5b02395..163ae47ad78a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel import android.app.Flags import android.app.NotificationManager.Policy +import android.content.res.Configuration +import android.os.LocaleList import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization @@ -27,6 +29,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.notification.data.repository.updateNotificationPolicy import com.android.settingslib.notification.modes.TestModeBuilder import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.andSceneContainer import com.android.systemui.kosmos.testScope @@ -36,9 +39,12 @@ import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyS import com.android.systemui.statusbar.policy.data.repository.zenModeRepository import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import java.util.Locale import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import platform.test.runner.parameterized.ParameterizedAndroidJunit4 @@ -53,6 +59,10 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private val zenModeRepository = kosmos.zenModeRepository private val activeNotificationListRepository = kosmos.activeNotificationListRepository private val fakeSecureSettingsRepository = kosmos.fakeSecureSettingsRepository + private val fakeConfigurationRepository = kosmos.fakeConfigurationRepository + + /** Backup of the current locales, to be restored at the end of the test if they are changed. */ + private lateinit var originalLocales: LocaleList private val underTest by lazy { kosmos.emptyShadeViewModel } @@ -68,6 +78,18 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { mSetFlagsRule.setFlagsParameterization(flags) } + @Before + fun setUp() { + originalLocales = context.resources.configuration.locales + updateLocales(LocaleList(Locale.US)) + } + + @After + fun tearDown() { + // Make sure we restore the original locale even if a test fails after changing it + updateLocales(originalLocales) + } + @Test fun areNotificationsHiddenInShade_true() = testScope.runTest { @@ -144,6 +166,29 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @Test @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + fun text_changesWhenLocaleChanges() = + testScope.runTest { + val text by collectLastValue(underTest.text) + + zenModeRepository.updateNotificationPolicy( + suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST + ) + zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF) + runCurrent() + + assertThat(text).isEqualTo("No notifications") + + updateLocales(LocaleList(Locale.GERMAN)) + runCurrent() + + assertThat(text).isEqualTo("Keine Benachrichtigungen") + + // Make sure we restore the original locales + updateLocales(originalLocales) + } + + @Test + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) fun text_reflectsModesHidingNotifications() = testScope.runTest { val text by collectLastValue(underTest.text) @@ -285,4 +330,11 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(onClick?.targetIntent?.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) assertThat(onClick?.backStack).isEmpty() } + + private fun updateLocales(locales: LocaleList) { + val configuration = Configuration() + configuration.setLocales(locales) + context.resources.updateConfiguration(configuration, context.resources.displayMetrics) + fakeConfigurationRepository.onConfigurationChange(configuration) + } } 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 39c42f183481..28b2ee8dde06 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 @@ -269,6 +269,36 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( } @Test + fun testOpenAndCloseGutsWithoutSave() { + val guts = spy(NotificationGuts(mContext)) + whenever(guts.post(any())).thenAnswer { invocation: InvocationOnMock -> + handler.post(((invocation.arguments[0] as Runnable))) + null + } + + // Test doesn't support animation since the guts view is not attached. + doNothing().whenever(guts).openControls(anyInt(), anyInt(), anyBoolean(), any()) + + val realRow = createTestNotificationRow() + val menuItem = createTestMenuItem(realRow) + + val row = spy(realRow) + whenever(row.windowToken).thenReturn(Binder()) + whenever(row.guts).thenReturn(guts) + + assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem)) + executor.runAllReady() + verify(guts).openControls(anyInt(), anyInt(), anyBoolean(), any<Runnable>()) + + gutsManager.closeAndUndoGuts() + + verify(guts).closeControls(anyInt(), anyInt(), eq(false), eq(false)) + verify(row, times(1)).setGutsView(any<MenuItem>()) + executor.runAllReady() + verify(headsUpManager).setGutsShown(realRow.entry, false) + } + + @Test fun testLockscreenShadeVisible_visible_gutsNotClosed() = testScope.runTest { // First, start out lockscreen or shade as not visible @@ -377,52 +407,6 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( } @Test - fun testChangeDensityOrFontScale() { - val guts = spy(NotificationGuts(mContext)) - whenever(guts.post(any())).thenAnswer { invocation: InvocationOnMock -> - handler.post(((invocation.arguments[0] as Runnable))) - null - } - - // Test doesn't support animation since the guts view is not attached. - doNothing().whenever(guts).openControls(anyInt(), anyInt(), anyBoolean(), any<Runnable>()) - - val realRow = createTestNotificationRow() - val menuItem = createTestMenuItem(realRow) - - val row = spy(realRow) - - whenever(row.windowToken).thenReturn(Binder()) - whenever(row.guts).thenReturn(guts) - doNothing().whenever(row).ensureGutsInflated() - - val realEntry = realRow.entry - val entry = spy(realEntry) - - whenever(entry.row).thenReturn(row) - whenever(entry.guts).thenReturn(guts) - - assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem)) - executor.runAllReady() - verify(guts).openControls(anyInt(), anyInt(), anyBoolean(), any<Runnable>()) - - // called once by mGutsManager.bindGuts() in mGutsManager.openGuts() - verify(row).setGutsView(any<MenuItem>()) - - row.onDensityOrFontScaleChanged() - gutsManager.onDensityOrFontScaleChanged(entry) - - executor.runAllReady() - - gutsManager.closeAndSaveGuts(false, false, false, 0, 0, false) - - verify(guts).closeControls(anyBoolean(), anyBoolean(), anyInt(), anyInt(), anyBoolean()) - - // called again by mGutsManager.bindGuts(), in mGutsManager.onDensityOrFontScaleChanged() - verify(row, times(2)).setGutsView(any<MenuItem>()) - } - - @Test fun testAppOpsSettingsIntent_camera() { val row = createTestNotificationRow() val ops = ArraySet<Int>() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java index 6435e8203d3d..af67a04d2f2a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java @@ -16,29 +16,44 @@ package com.android.systemui.statusbar.notification.row; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.testing.TestableResources; -import android.util.KeyValueListParser; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; import androidx.test.annotation.UiThreadTest; 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.animation.AnimatorTestRule; +import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption; import com.android.systemui.res.R; +import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; +import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) @@ -46,8 +61,12 @@ import java.util.ArrayList; public class NotificationSnoozeTest extends SysuiTestCase { private static final int RES_DEFAULT = 2; private static final int[] RES_OPTIONS = {1, 2, 3}; - private NotificationSnooze mNotificationSnooze; - private KeyValueListParser mMockParser; + private final NotificationSwipeActionHelper mSnoozeListener = mock( + NotificationSwipeActionHelper.class); + private NotificationSnooze mUnderTest; + + @Rule + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this); @Before public void setUp() throws Exception { @@ -56,62 +75,117 @@ public class NotificationSnoozeTest extends SysuiTestCase { TestableResources resources = mContext.getOrCreateTestableResources(); resources.addOverride(R.integer.config_notification_snooze_time_default, RES_DEFAULT); resources.addOverride(R.array.config_notification_snooze_times, RES_OPTIONS); - mNotificationSnooze = new NotificationSnooze(mContext, null); - mMockParser = mock(KeyValueListParser.class); + + mUnderTest = new NotificationSnooze(mContext, null); + mUnderTest.setSnoozeListener(mSnoozeListener); + mUnderTest.mExpandButton = mock(ImageView.class); + mUnderTest.mSnoozeView = mock(View.class); + mUnderTest.mSelectedOptionText = mock(TextView.class); + mUnderTest.mDivider = mock(View.class); + mUnderTest.mSnoozeOptionContainer = mock(ViewGroup.class); + mUnderTest.mSnoozeOptions = mock(List.class); + } + + @After + public void tearDown() { + // Make sure all animations are finished + mAnimatorTestRule.advanceTimeBy(1000L); + } + + @Test + @EnableFlags(Flags.FLAG_NOTIFICATION_UNDO_GUTS_ON_CONFIG_CHANGED) + public void closeControls_withoutSave_performsUndo() { + ArrayList<SnoozeOption> options = mUnderTest.getDefaultSnoozeOptions(); + mUnderTest.mSelectedOption = options.getFirst(); + mUnderTest.showSnoozeOptions(true); + + assertThat( + mUnderTest.handleCloseControls(/* save = */ false, /* force = */ false)).isFalse(); + + assertThat(mUnderTest.mSelectedOption).isNull(); + assertThat(mUnderTest.isExpanded()).isFalse(); + verify(mSnoozeListener, times(0)).snooze(any(), any()); + } + + @Test + public void closeControls_whenExpanded_collapsesOptions() { + ArrayList<SnoozeOption> options = mUnderTest.getDefaultSnoozeOptions(); + mUnderTest.mSelectedOption = options.getFirst(); + mUnderTest.showSnoozeOptions(true); + + assertThat(mUnderTest.handleCloseControls(/* save = */ true, /* force = */ false)).isTrue(); + + assertThat(mUnderTest.mSelectedOption).isNotNull(); + assertThat(mUnderTest.isExpanded()).isFalse(); + } + + @Test + public void closeControls_whenCollapsed_commitsChanges() { + ArrayList<SnoozeOption> options = mUnderTest.getDefaultSnoozeOptions(); + mUnderTest.mSelectedOption = options.getFirst(); + + assertThat(mUnderTest.handleCloseControls(/* save = */ true, /* force = */ false)).isTrue(); + + verify(mSnoozeListener).snooze(any(), any()); + } + + @Test + public void closeControls_withForce_returnsFalse() { + assertThat(mUnderTest.handleCloseControls(/* save = */ true, /* force = */ true)).isFalse(); } @Test - public void testGetOptionsWithNoConfig() throws Exception { - ArrayList<SnoozeOption> result = mNotificationSnooze.getDefaultSnoozeOptions(); + public void testGetOptionsWithNoConfig() { + ArrayList<SnoozeOption> result = mUnderTest.getDefaultSnoozeOptions(); assertEquals(3, result.size()); assertEquals(1, result.get(0).getMinutesToSnoozeFor()); // respect order assertEquals(2, result.get(1).getMinutesToSnoozeFor()); assertEquals(3, result.get(2).getMinutesToSnoozeFor()); - assertEquals(2, mNotificationSnooze.getDefaultOption().getMinutesToSnoozeFor()); + assertEquals(2, mUnderTest.getDefaultOption().getMinutesToSnoozeFor()); } @Test - public void testGetOptionsWithInvalidConfig() throws Exception { + public void testGetOptionsWithInvalidConfig() { Settings.Global.putString(mContext.getContentResolver(), Settings.Global.NOTIFICATION_SNOOZE_OPTIONS, "this is garbage"); - ArrayList<SnoozeOption> result = mNotificationSnooze.getDefaultSnoozeOptions(); + ArrayList<SnoozeOption> result = mUnderTest.getDefaultSnoozeOptions(); assertEquals(3, result.size()); assertEquals(1, result.get(0).getMinutesToSnoozeFor()); // respect order assertEquals(2, result.get(1).getMinutesToSnoozeFor()); assertEquals(3, result.get(2).getMinutesToSnoozeFor()); - assertEquals(2, mNotificationSnooze.getDefaultOption().getMinutesToSnoozeFor()); + assertEquals(2, mUnderTest.getDefaultOption().getMinutesToSnoozeFor()); } @Test - public void testGetOptionsWithValidDefault() throws Exception { + public void testGetOptionsWithValidDefault() { Settings.Global.putString(mContext.getContentResolver(), Settings.Global.NOTIFICATION_SNOOZE_OPTIONS, "default=10,options_array=4:5:6:7"); - ArrayList<SnoozeOption> result = mNotificationSnooze.getDefaultSnoozeOptions(); - assertNotNull(mNotificationSnooze.getDefaultOption()); // pick one + ArrayList<SnoozeOption> result = mUnderTest.getDefaultSnoozeOptions(); + assertNotNull(mUnderTest.getDefaultOption()); // pick one } @Test - public void testGetOptionsWithValidConfig() throws Exception { + public void testGetOptionsWithValidConfig() { Settings.Global.putString(mContext.getContentResolver(), Settings.Global.NOTIFICATION_SNOOZE_OPTIONS, "default=6,options_array=4:5:6:7"); - ArrayList<SnoozeOption> result = mNotificationSnooze.getDefaultSnoozeOptions(); + ArrayList<SnoozeOption> result = mUnderTest.getDefaultSnoozeOptions(); assertEquals(4, result.size()); assertEquals(4, result.get(0).getMinutesToSnoozeFor()); // respect order assertEquals(5, result.get(1).getMinutesToSnoozeFor()); assertEquals(6, result.get(2).getMinutesToSnoozeFor()); assertEquals(7, result.get(3).getMinutesToSnoozeFor()); - assertEquals(6, mNotificationSnooze.getDefaultOption().getMinutesToSnoozeFor()); + assertEquals(6, mUnderTest.getDefaultOption().getMinutesToSnoozeFor()); } @Test - public void testGetOptionsWithLongConfig() throws Exception { + public void testGetOptionsWithLongConfig() { Settings.Global.putString(mContext.getContentResolver(), Settings.Global.NOTIFICATION_SNOOZE_OPTIONS, "default=6,options_array=4:5:6:7:8:9:10:11:12:13:14:15:16:17"); - ArrayList<SnoozeOption> result = mNotificationSnooze.getDefaultSnoozeOptions(); + ArrayList<SnoozeOption> result = mUnderTest.getDefaultSnoozeOptions(); assertTrue(result.size() > 3); assertEquals(4, result.get(0).getMinutesToSnoozeFor()); // respect order assertEquals(5, result.get(1).getMinutesToSnoozeFor()); 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 a1c910d48cef..0223484dae41 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 @@ -20,6 +20,7 @@ import android.graphics.Color import android.graphics.Rect import android.view.View import com.android.systemui.plugins.DarkIconDispatcher +import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle @@ -46,6 +47,9 @@ class FakeHomeStatusBarViewModel( override val statusBarPopupChips = MutableStateFlow(emptyList<PopupChipModel.Shown>()) + override val mediaProjectionStopDialogDueToCallEndedState = + MutableStateFlow(MediaProjectionStopDialogModel.Hidden) + override val isHomeStatusBarAllowedByScene = MutableStateFlow(false) override val shouldHomeStatusBarBeVisible = MutableStateFlow(false) 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 e74d009bb909..46f625fd9ba8 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 @@ -45,8 +45,8 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.collectValues import com.android.systemui.kosmos.runTest -import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.assertLogsWtf import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository @@ -58,7 +58,9 @@ import com.android.systemui.screenrecord.data.repository.screenRecordRepository import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection +import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip @@ -89,7 +91,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -98,9 +100,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class HomeStatusBarViewModelImplTest : SysuiTestCase() { - private val kosmos by lazy { - testKosmos().also { it.testDispatcher = UnconfinedTestDispatcher() } - } + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val Kosmos.underTest by Kosmos.Fixture { kosmos.homeStatusBarViewModel } @Before @@ -112,6 +112,55 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { fun addDisplays() = runBlocking { kosmos.displayRepository.fake.addDisplay(DEFAULT_DISPLAY) } @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun mediaProjectionStopDialogDueToCallEndedState_initiallyHidden() = + kosmos.runTest { + shareToAppChipViewModel.start() + val latest by collectLastValue(underTest.mediaProjectionStopDialogDueToCallEndedState) + + // Verify that the stop dialog is initially hidden + assertThat(latest).isInstanceOf(MediaProjectionStopDialogModel.Hidden::class.java) + } + + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun mediaProjectionStopDialogDueToCallEndedState_flagEnabled_mediaIsProjecting_projectionStartedDuringCallAndActivePostCallEventEmitted_isShown() = + kosmos.runTest { + shareToAppChipViewModel.start() + + val latest by + collectLastValue( + homeStatusBarViewModel.mediaProjectionStopDialogDueToCallEndedState + ) + + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + assertThat(latest).isInstanceOf(MediaProjectionStopDialogModel.Shown::class.java) + } + + @Test + @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun mediaProjectionStopDialogDueToCallEndedState_flagDisabled_mediaIsProjecting_projectionStartedDuringCallAndActivePostCallEventEmitted_isHidden() = + kosmos.runTest { + shareToAppChipViewModel.start() + + val latest by + collectLastValue( + homeStatusBarViewModel.mediaProjectionStopDialogDueToCallEndedState + ) + + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + assertThat(latest).isInstanceOf(MediaProjectionStopDialogModel.Hidden::class.java) + } + + @Test fun isTransitioningFromLockscreenToOccluded_started_isTrue() = kosmos.runTest { val latest by collectLastValue(underTest.isTransitioningFromLockscreenToOccluded) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt index 09be93de9f3e..ea91b7a9d6e2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.unfold import android.content.Context import android.content.res.Resources import android.hardware.devicestate.DeviceStateManager +import android.os.PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R @@ -27,16 +28,20 @@ import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImp import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl import com.android.systemui.defaultDeviceState import com.android.systemui.deviceStateManager -import com.android.systemui.display.data.repository.DeviceStateRepository import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState.FOLDED +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState.HALF_FOLDED +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState.UNFOLDED +import com.android.systemui.display.data.repository.fakeDeviceStateRepository import com.android.systemui.foldedDeviceStateList import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.kosmos.Kosmos -import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.power.shared.model.ScreenPowerState -import com.android.systemui.power.shared.model.WakeSleepReason -import com.android.systemui.power.shared.model.WakefulnessModel -import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setScreenPowerState +import com.android.systemui.power.domain.interactor.PowerInteractorFactory +import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_OFF +import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON import com.android.systemui.shared.system.SysUiStatsLog import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_CLOSED @@ -45,7 +50,7 @@ import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLate import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import com.android.systemui.unfoldedDeviceState -import com.android.systemui.util.animation.data.repository.AnimationStatusRepository +import com.android.systemui.util.animation.data.repository.fakeAnimationStatusRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.time.FakeSystemClock @@ -77,14 +82,15 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { private lateinit var displaySwitchLatencyTracker: DisplaySwitchLatencyTracker @Captor private lateinit var loggerArgumentCaptor: ArgumentCaptor<DisplaySwitchLatencyEvent> + private val kosmos = Kosmos() private val mockContext = mock<Context>() private val resources = mock<Resources>() - private val foldStateRepository = mock<DeviceStateRepository>() - private val powerInteractor = mock<PowerInteractor>() - private val animationStatusRepository = mock<AnimationStatusRepository>() + private val foldStateRepository = kosmos.fakeDeviceStateRepository + private val powerInteractor = PowerInteractorFactory.create().powerInteractor + private val animationStatusRepository = kosmos.fakeAnimationStatusRepository private val keyguardInteractor = mock<KeyguardInteractor>() private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>() - private val kosmos = Kosmos() + private val deviceStateManager = kosmos.deviceStateManager private val closedDeviceState = kosmos.foldedDeviceStateList.first() private val openDeviceState = kosmos.unfoldedDeviceState @@ -94,12 +100,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { private val testDispatcher: TestDispatcher = StandardTestDispatcher() private val testScope: TestScope = TestScope(testDispatcher) - private val isAsleep = MutableStateFlow(false) private val isAodAvailable = MutableStateFlow(false) - private val deviceState = MutableStateFlow(DeviceState.UNFOLDED) - private val screenPowerState = MutableStateFlow(ScreenPowerState.SCREEN_ON) - private val areAnimationEnabled = MutableStateFlow(true) - private val lastWakefulnessEvent = MutableStateFlow(WakefulnessModel()) private val systemClock = FakeSystemClock() private val configurationController = FakeConfigurationController() private val configurationRepository = @@ -126,13 +127,10 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { .thenReturn(listOf(closedDeviceState, openDeviceState)) whenever(resources.getIntArray(R.array.config_foldedDeviceStates)) .thenReturn(nonEmptyClosedDeviceStatesArray) - whenever(foldStateRepository.state).thenReturn(deviceState) - whenever(powerInteractor.isAsleep).thenReturn(isAsleep) - whenever(animationStatusRepository.areAnimationsEnabled()).thenReturn(areAnimationEnabled) - whenever(powerInteractor.screenPowerState).thenReturn(screenPowerState) whenever(keyguardInteractor.isAodAvailable).thenReturn(isAodAvailable) - whenever(powerInteractor.detailedWakefulness).thenReturn(lastWakefulnessEvent) - + animationStatusRepository.onAnimationStatusChanged(true) + powerInteractor.setAwakeForTest() + powerInteractor.setScreenPowerState(SCREEN_ON) displaySwitchLatencyTracker = DisplaySwitchLatencyTracker( mockContext, @@ -152,21 +150,19 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { @Test fun unfold_logsLatencyTillTransitionStarted() { testScope.runTest { - areAnimationEnabled.emit(true) - displaySwitchLatencyTracker.start() - deviceState.emit(DeviceState.FOLDED) - screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + setDeviceState(FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) systemClock.advanceTime(50) runCurrent() - deviceState.emit(DeviceState.HALF_FOLDED) + setDeviceState(HALF_FOLDED) runCurrent() systemClock.advanceTime(50) - screenPowerState.emit(ScreenPowerState.SCREEN_ON) + powerInteractor.setScreenPowerState(SCREEN_ON) systemClock.advanceTime(200) unfoldTransitionProgressProvider.onTransitionStarted() runCurrent() - deviceState.emit(DeviceState.UNFOLDED) + setDeviceState(UNFOLDED) verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) val loggedEvent = loggerArgumentCaptor.value @@ -202,23 +198,22 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { systemClock, deviceStateManager, ) - areAnimationEnabled.emit(true) displaySwitchLatencyTracker.start() - deviceState.emit(DeviceState.FOLDED) - screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + setDeviceState(FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) systemClock.advanceTime(50) runCurrent() - deviceState.emit(DeviceState.HALF_FOLDED) + setDeviceState(HALF_FOLDED) systemClock.advanceTime(50) runCurrent() - screenPowerState.emit(ScreenPowerState.SCREEN_ON) + powerInteractor.setScreenPowerState(SCREEN_ON) systemClock.advanceTime(50) runCurrent() systemClock.advanceTime(200) unfoldTransitionProgressProvider.onTransitionStarted() runCurrent() - deviceState.emit(DeviceState.UNFOLDED) + setDeviceState(UNFOLDED) verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) val loggedEvent = loggerArgumentCaptor.value @@ -235,23 +230,23 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { @Test fun unfold_animationDisabled_logsLatencyTillScreenTurnedOn() { testScope.runTest { - areAnimationEnabled.emit(false) + animationStatusRepository.onAnimationStatusChanged(false) displaySwitchLatencyTracker.start() - deviceState.emit(DeviceState.FOLDED) - screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + setDeviceState(FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) systemClock.advanceTime(50) runCurrent() - deviceState.emit(DeviceState.HALF_FOLDED) + setDeviceState(HALF_FOLDED) systemClock.advanceTime(50) runCurrent() - screenPowerState.emit(ScreenPowerState.SCREEN_ON) + powerInteractor.setScreenPowerState(SCREEN_ON) systemClock.advanceTime(50) runCurrent() unfoldTransitionProgressProvider.onTransitionStarted() systemClock.advanceTime(200) runCurrent() - deviceState.emit(DeviceState.UNFOLDED) + setDeviceState(UNFOLDED) verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) val loggedEvent = loggerArgumentCaptor.value @@ -268,19 +263,18 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { @Test fun foldWhileStayingAwake_logsLatency() { testScope.runTest { - areAnimationEnabled.emit(true) - deviceState.emit(DeviceState.UNFOLDED) - screenPowerState.emit(ScreenPowerState.SCREEN_ON) + setDeviceState(UNFOLDED) + powerInteractor.setScreenPowerState(SCREEN_ON) displaySwitchLatencyTracker.start() - deviceState.emit(DeviceState.HALF_FOLDED) + setDeviceState(HALF_FOLDED) systemClock.advanceTime(50) runCurrent() - deviceState.emit(DeviceState.FOLDED) - screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + setDeviceState(FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) runCurrent() systemClock.advanceTime(200) - screenPowerState.emit(ScreenPowerState.SCREEN_ON) + powerInteractor.setScreenPowerState(SCREEN_ON) runCurrent() verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) @@ -298,25 +292,19 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { @Test fun foldToAod_capturesToStateAsAod() { testScope.runTest { - areAnimationEnabled.emit(true) - deviceState.emit(DeviceState.UNFOLDED) + setDeviceState(UNFOLDED) isAodAvailable.emit(true) displaySwitchLatencyTracker.start() - deviceState.emit(DeviceState.HALF_FOLDED) + setDeviceState(HALF_FOLDED) systemClock.advanceTime(50) runCurrent() - deviceState.emit(DeviceState.FOLDED) - lastWakefulnessEvent.emit( - WakefulnessModel( - internalWakefulnessState = WakefulnessState.ASLEEP, - lastSleepReason = WakeSleepReason.FOLD, - ) - ) - screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + setDeviceState(FOLDED) + powerInteractor.setAsleepForTest(sleepReason = GO_TO_SLEEP_REASON_DEVICE_FOLD) + powerInteractor.setScreenPowerState(SCREEN_OFF) runCurrent() systemClock.advanceTime(200) - screenPowerState.emit(ScreenPowerState.SCREEN_ON) + powerInteractor.setScreenPowerState(SCREEN_ON) runCurrent() verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) @@ -335,22 +323,21 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { @Test fun fold_notAFoldable_shouldNotLogLatency() { testScope.runTest { - areAnimationEnabled.emit(true) - deviceState.emit(DeviceState.UNFOLDED) + setDeviceState(UNFOLDED) whenever(resources.getIntArray(R.array.config_foldedDeviceStates)) .thenReturn(IntArray(0)) whenever(deviceStateManager.supportedDeviceStates) .thenReturn(listOf(defaultDeviceState)) displaySwitchLatencyTracker.start() - deviceState.emit(DeviceState.HALF_FOLDED) + setDeviceState(HALF_FOLDED) systemClock.advanceTime(50) runCurrent() - deviceState.emit(DeviceState.FOLDED) - screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + setDeviceState(FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) runCurrent() systemClock.advanceTime(200) - screenPowerState.emit(ScreenPowerState.SCREEN_ON) + powerInteractor.setScreenPowerState(SCREEN_ON) runCurrent() verify(displaySwitchLatencyLogger, never()).log(any()) @@ -360,22 +347,16 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { @Test fun foldToScreenOff_capturesToStateAsScreenOff() { testScope.runTest { - areAnimationEnabled.emit(true) - deviceState.emit(DeviceState.UNFOLDED) + setDeviceState(UNFOLDED) isAodAvailable.emit(false) displaySwitchLatencyTracker.start() - deviceState.emit(DeviceState.HALF_FOLDED) + setDeviceState(HALF_FOLDED) systemClock.advanceTime(50) runCurrent() - deviceState.emit(DeviceState.FOLDED) - lastWakefulnessEvent.emit( - WakefulnessModel( - internalWakefulnessState = WakefulnessState.ASLEEP, - lastSleepReason = WakeSleepReason.FOLD, - ) - ) - screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + setDeviceState(FOLDED) + powerInteractor.setAsleepForTest(sleepReason = GO_TO_SLEEP_REASON_DEVICE_FOLD) + powerInteractor.setScreenPowerState(SCREEN_OFF) runCurrent() verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) @@ -390,4 +371,8 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { assertThat(loggedEvent).isEqualTo(expectedLoggedEvent) } } + + private suspend fun setDeviceState(state: DeviceState) { + foldStateRepository.emit(state) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt index 89410593fe62..b4fbaad6ab37 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.wallpapers import android.app.Flags import android.content.Context +import android.content.res.Resources import android.graphics.Canvas import android.graphics.Paint import android.graphics.Rect @@ -31,14 +32,18 @@ import android.view.SurfaceHolder import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.spy import org.mockito.MockitoAnnotations import org.mockito.kotlin.any +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyZeroInteractions import org.mockito.kotlin.whenever @@ -56,6 +61,8 @@ class GradientColorWallpaperTest : SysuiTestCase() { @Mock private lateinit var mockContext: Context + @Mock private lateinit var mockResources: Resources + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -64,6 +71,13 @@ class GradientColorWallpaperTest : SysuiTestCase() { whenever(surfaceHolder.surfaceFrame).thenReturn(surfaceFrame) whenever(surface.lockHardwareCanvas()).thenReturn(canvas) whenever(mockContext.getColor(anyInt())).thenReturn(1) + whenever(mockContext.resources).thenReturn(mockResources) + whenever( + mockResources.getDimensionPixelOffset( + eq(R.dimen.gradient_color_wallpaper_center_offset) + ) + ) + .thenReturn(OFFSET_PX) } private fun createGradientColorWallpaperEngine(): Engine { @@ -93,9 +107,11 @@ class GradientColorWallpaperTest : SysuiTestCase() { engine.onSurfaceRedrawNeeded(surfaceHolder) verify(canvas).drawRect(any<RectF>(), any<Paint>()) + verify(canvas, times(2)).drawCircle(anyFloat(), anyFloat(), anyFloat(), any<Paint>()) } private companion object { val surfaceFrame = Rect(0, 0, 100, 100) + const val OFFSET_PX = 100 } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt index 115edd0d3bb0..2b16c00107c3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt @@ -25,16 +25,15 @@ import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R -import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.res.R as SysUIR import com.android.systemui.shared.Flags as SharedFlags import com.android.systemui.user.data.model.SelectedUserModel import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.wallpapers.data.repository.WallpaperRepositoryImpl.Companion.MAGIC_PORTRAIT_CLASSNAME import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -72,9 +71,12 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { ) } + lateinit var focalAreaTarget: String + @Before fun setUp() { whenever(wallpaperManager.isWallpaperSupported).thenReturn(true) + focalAreaTarget = context.resources.getString(SysUIR.string.focal_area_target) } @Test @@ -248,17 +250,17 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_MAGIC_PORTRAIT_WALLPAPERS) - fun shouldSendNotificationLayout_setMagicPortraitWallpaper_launchSendLayoutJob() = + @EnableFlags(SharedFlags.FLAG_EXTENDED_WALLPAPER_EFFECTS) + fun shouldSendNotificationLayout_setExtendedEffectsWallpaper_launchSendLayoutJob() = testScope.runTest { val latest by collectLastValue(underTest.shouldSendFocalArea) - val magicPortraitWallpaper = + val extedendEffectsWallpaper = mock<WallpaperInfo>().apply { - whenever(this.component) - .thenReturn(ComponentName(context, MAGIC_PORTRAIT_CLASSNAME)) + whenever(this.component).thenReturn(ComponentName(context, focalAreaTarget)) } + whenever(wallpaperManager.getWallpaperInfoForUser(any())) - .thenReturn(magicPortraitWallpaper) + .thenReturn(extedendEffectsWallpaper) fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(Intent.ACTION_WALLPAPER_CHANGED), @@ -269,13 +271,16 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_MAGIC_PORTRAIT_WALLPAPERS) - fun shouldSendNotificationLayout_setNotMagicPortraitWallpaper_cancelSendLayoutJob() = + @EnableFlags(SharedFlags.FLAG_EXTENDED_WALLPAPER_EFFECTS) + fun shouldSendNotificationLayout_setNotExtendedEffectsWallpaper_cancelSendLayoutJob() = testScope.runTest { val latest by collectLastValue(underTest.shouldSendFocalArea) - val magicPortraitWallpaper = MAGIC_PORTRAIT_WP + val extendedEffectsWallpaper = + mock<WallpaperInfo>().apply { + whenever(this.component).thenReturn(ComponentName("", focalAreaTarget)) + } whenever(wallpaperManager.getWallpaperInfoForUser(any())) - .thenReturn(magicPortraitWallpaper) + .thenReturn(extendedEffectsWallpaper) fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(Intent.ACTION_WALLPAPER_CHANGED), @@ -284,9 +289,7 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { assertThat(underTest.sendLockscreenLayoutJob).isNotNull() assertThat(underTest.sendLockscreenLayoutJob!!.isActive).isEqualTo(true) - val nonMagicPortraitWallpaper = UNSUPPORTED_WP - whenever(wallpaperManager.getWallpaperInfoForUser(any())) - .thenReturn(nonMagicPortraitWallpaper) + whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(UNSUPPORTED_WP) fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(Intent.ACTION_WALLPAPER_CHANGED), @@ -303,10 +306,5 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { val USER_WITH_SUPPORTED_WP = UserInfo(/* id= */ 4, /* name= */ "user4", /* flags= */ 0) val SUPPORTED_WP = mock<WallpaperInfo>().apply { whenever(this.supportsAmbientMode()).thenReturn(true) } - - val MAGIC_PORTRAIT_WP = - mock<WallpaperInfo>().apply { - whenever(this.component).thenReturn(ComponentName("", MAGIC_PORTRAIT_CLASSNAME)) - } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java index 75c174229564..ab6fbc3f47a2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java @@ -30,7 +30,7 @@ import com.android.wm.shell.bubbles.BubbleData; import com.android.wm.shell.bubbles.BubbleDataRepository; import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubblePositioner; -import com.android.wm.shell.bubbles.properties.BubbleProperties; +import com.android.wm.shell.bubbles.ResizabilityChecker; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; @@ -82,14 +82,14 @@ public class TestableBubbleController extends BubbleController { Transitions transitions, SyncTransactionQueue syncQueue, IWindowManager wmService, - BubbleProperties bubbleProperties) { + ResizabilityChecker resizabilityChecker) { super(context, shellInit, shellCommandHandler, shellController, data, Runnable::run, floatingContentCoordinator, dataRepository, statusBarService, windowManager, displayInsetsController, displayImeController, userManager, launcherApps, bubbleLogger, taskStackListener, shellTaskOrganizer, positioner, displayController, oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler, new SyncExecutor(), taskViewRepository, taskViewTransitions, transitions, - syncQueue, wmService, bubbleProperties); + syncQueue, wmService, resizabilityChecker); setInflateSynchronously(true); onInit(); } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt index eab7d7913129..be0362fd7481 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt @@ -16,17 +16,10 @@ package com.android.systemui.plugins.qs -import androidx.compose.runtime.Composable - /** * The base view model class for rendering the Tile's TileDetailsView. */ abstract class TileDetailsViewModel { - - // The view content of this tile details view. - @Composable - abstract fun GetContentView() - // The callback when the settings button is clicked. Currently this is the same as the on tile // long press callback abstract fun clickOnSettingsButton() diff --git a/packages/SystemUI/res/drawable/media_output_item_expand_group.xml b/packages/SystemUI/res/drawable/media_output_item_expand_group.xml new file mode 100644 index 000000000000..833843d9633a --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_item_expand_group.xml @@ -0,0 +1,26 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M12,15.4 L6,9.4l1.4,-1.4 4.6,4.6 4.6,-4.6 1.4,1.4 -6,6Z" /> +</vector> diff --git a/packages/SystemUI/res/drawable/notif_footer_btn_settings.xml b/packages/SystemUI/res/drawable/notif_footer_btn_settings.xml index 800060db7757..6533460a9c99 100644 --- a/packages/SystemUI/res/drawable/notif_footer_btn_settings.xml +++ b/packages/SystemUI/res/drawable/notif_footer_btn_settings.xml @@ -6,5 +6,5 @@ android:tint="?attr/colorControlNormal"> <path android:fillColor="@android:color/white" - android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/> + android:pathData="M480,864q-30,0 -51,-21t-21,-51h144q0,30 -21,51t-51,21ZM192,744v-72h48v-240q0,-88 55.5,-155T439,196q-5,17 -7,34t-2,34q0,59 23.5,109.5T517,461q40,37 92,56t111,14v141h48v72L192,744ZM664,456 L652,400q-14,-5 -26.5,-11.5T602,372l-55,17 -32,-55 41,-40q-3,-14 -3,-29t3,-29l-41,-39 32,-56 54,16q11,-11 24,-18t27,-11l13,-56h64l13,56q14,5 27.5,11.5T793,157l54,-15 32,55 -40,38q3,14 2.5,29.5T838,294l41,39 -32,55 -55,-16q-11,10 -23.5,16.5T742,400l-14,56h-64ZM697,336q30,0 51,-21t21,-51q0,-30 -21,-51t-51,-21q-30,0 -51,21t-21,51q0,30 21,51t51,21Z"/> </vector> diff --git a/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml b/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml index e47fc62c6e16..8944efb99f69 100644 --- a/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml +++ b/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml @@ -42,7 +42,6 @@ android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:textColor="@androidprv:color/materialColorOnSecondaryFixed" - android:textSize="14sp" android:maxLines="1" android:ellipsize="end" /> diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid.xml b/packages/SystemUI/res/layout/notification_2025_hybrid.xml index 8c34cd4165e0..8fd10fb3ddb8 100644 --- a/packages/SystemUI/res/layout/notification_2025_hybrid.xml +++ b/packages/SystemUI/res/layout/notification_2025_hybrid.xml @@ -29,6 +29,7 @@ android:layout_height="wrap_content" android:singleLine="true" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" + android:textSize="@*android:dimen/notification_2025_title_text_size" android:paddingEnd="4dp" /> <TextView diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml index a338e4c70cfa..35f2ef901bdd 100644 --- a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml +++ b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml @@ -54,6 +54,7 @@ android:singleLine="true" android:paddingEnd="4dp" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" + android:textSize="@*android:dimen/notification_2025_title_text_size" /> <TextView diff --git a/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml b/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml index 3ca4b94d3003..aefe92798c99 100644 --- a/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml +++ b/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml @@ -77,6 +77,7 @@ </LinearLayout> <LinearLayout android:id="@+id/privacy_dialog_item_header_expanded_layout" + android:importantForAccessibility="no" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/packages/SystemUI/res/values-xlarge-land/config.xml b/packages/SystemUI/res/values-xlarge-land/config.xml index 6d8b64ade259..4c77f30f2e69 100644 --- a/packages/SystemUI/res/values-xlarge-land/config.xml +++ b/packages/SystemUI/res/values-xlarge-land/config.xml @@ -16,5 +16,5 @@ <resources> <item name="shortcut_helper_screen_width_fraction" format="float" type="dimen">0.8</item> - <bool name="center_align_magic_portrait_shape">true</bool> + <bool name="center_align_focal_area_shape">true</bool> </resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 68e33f27aefa..9b8926e921c9 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -1107,7 +1107,7 @@ <!-- The dream component used when the device is low light environment. --> <string translatable="false" name="config_lowLightDreamComponent"/> - <!--Whether we should position magic portrait shape effects in the center of lockscreen - it's false by default, and only be true in tablet landscape --> - <bool name="center_align_magic_portrait_shape">false</bool> + <!-- Configuration for wallpaper focal area --> + <bool name="center_align_focal_area_shape">false</bool> + <string name="focal_area_target" translatable="false" /> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index c7f037f3d619..42d66e23feb9 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -2150,4 +2150,8 @@ <dimen name="volume_panel_slice_vertical_padding">8dp</dimen> <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen> <!-- Volume end --> + + <!-- Gradient color wallpaper start --> + <dimen name="gradient_color_wallpaper_center_offset">128dp</dimen> + <!-- Gradient color wallpaper end --> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 3b89e9c42c93..7b2e8126b0c2 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -588,6 +588,12 @@ <!-- Content description of the cast label showing what we are connected to. [CHAR LIMIT=NONE] --> <string name="accessibility_cast_name">Connected to <xliff:g id="cast" example="TV">%s</xliff:g>.</string> + <!-- Content description of the button to expand the group of devices. [CHAR LIMIT=NONE] --> + <string name="accessibility_expand_group">Expand group.</string> + + <!-- Content description of the button to open the application . [CHAR LIMIT=NONE] --> + <string name="accessibility_open_application">Open application.</string> + <!-- Content description of an item with no signal and no connection for accessibility (not shown on the screen) [CHAR LIMIT=NONE] --> <string name="accessibility_not_connected">Not connected.</string> <!-- Content description of the roaming data connection type. [CHAR LIMIT=NONE] --> @@ -1359,6 +1365,8 @@ <string name="hub_onboarding_bottom_sheet_text">Access your favorite widgets and screen savers while charging.</string> <!-- Hub onboarding bottom sheet action button title. [CHAR LIMIT=NONE] --> <string name="hub_onboarding_bottom_sheet_action_button">Let\u2019s go</string> + <!-- Text for a tooltip that appears over the "show screensaver" button on glanceable hub. [CHAR LIMIT=NONE] --> + <string name="glanceable_hub_to_dream_button_tooltip">Show your favorite screensavers while charging</string> <!-- Related to user switcher --><skip/> @@ -3393,6 +3401,8 @@ <!-- Content description for a chip in the status bar showing that the user is currently on a call. [CHAR LIMIT=NONE] --> <string name="ongoing_call_content_description">Ongoing call</string> + <!-- Content description for a chip in the status bar showing that the user currently has an ongoing activity. [CHAR LIMIT=NONE]--> + <string name="ongoing_notification_extra_content_description">Ongoing</string> <!-- Provider Model: Default title of the mobile network in the mobile layout. [CHAR LIMIT=50] --> <string name="mobile_data_settings_title">Mobile data</string> diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt index 870e6e6bf803..5b99a3f16fc2 100644 --- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt +++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt @@ -17,13 +17,6 @@ package com.android.systemui.biometrics import android.Manifest import android.app.ActivityTaskManager -import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC -import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC -import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX -import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED -import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC -import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX -import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING import android.content.Context import android.content.pm.PackageManager import android.graphics.Bitmap @@ -44,6 +37,9 @@ import android.view.WindowMetrics import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityManager import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD +import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN +import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN import com.android.systemui.biometrics.shared.model.PromptKind object Utils { @@ -80,7 +76,7 @@ object Utils { view.notifySubtreeAccessibilityStateChanged( view, view, - AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE + AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE, ) } @@ -94,14 +90,10 @@ object Utils { @JvmStatic fun getCredentialType(utils: LockPatternUtils, userId: Int): PromptKind = - when (utils.getKeyguardStoredPasswordQuality(userId)) { - PASSWORD_QUALITY_SOMETHING -> PromptKind.Pattern - PASSWORD_QUALITY_NUMERIC, - PASSWORD_QUALITY_NUMERIC_COMPLEX -> PromptKind.Pin - PASSWORD_QUALITY_ALPHABETIC, - PASSWORD_QUALITY_ALPHANUMERIC, - PASSWORD_QUALITY_COMPLEX, - PASSWORD_QUALITY_MANAGED -> PromptKind.Password + when (utils.getCredentialTypeForUser(userId)) { + CREDENTIAL_TYPE_PATTERN -> PromptKind.Pattern + CREDENTIAL_TYPE_PIN -> PromptKind.Pin + CREDENTIAL_TYPE_PASSWORD -> PromptKind.Password else -> PromptKind.Password } @@ -112,7 +104,7 @@ object Utils { @JvmStatic fun <T : SensorPropertiesInternal> findFirstSensorProperties( properties: List<T>?, - sensorIds: IntArray + sensorIds: IntArray, ): T? = properties?.firstOrNull { sensorIds.contains(it.sensorId) } @JvmStatic diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ILauncherProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ILauncherProxy.aidl index b43ffc530289..10b930381c44 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ILauncherProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ILauncherProxy.aidl @@ -146,9 +146,9 @@ oneway interface ILauncherProxy { void onUnbind(IRemoteCallback reply) = 35; /** - * Sent when {@link TaskbarDelegate#onDisplayReady} is called. + * Sent when {@link TaskbarDelegate#onDisplayAddSystemDecorations} is called. */ - void onDisplayReady(int displayId) = 36; + void onDisplayAddSystemDecorations(int displayId) = 36; /** * Sent when {@link TaskbarDelegate#onDisplayRemoved} is called. diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index e76f38c8c75c..9507b0483a06 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -62,15 +62,14 @@ public abstract class ClockRegistryModule { scope, mainDispatcher, bgDispatcher, - com.android.systemui.Flags.lockscreenCustomClocks() + com.android.systemui.shared.Flags.lockscreenCustomClocks() || featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS), /* handleAllUsers= */ true, new DefaultClockProvider( context, layoutInflater, resources, - - com.android.systemui.Flags.clockReactiveVariants() + com.android.systemui.shared.Flags.clockReactiveVariants() ), context.getString(R.string.lockscreen_clock_id_fallback), clockBuffers, diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index 1c994731c393..126471234fa1 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -32,6 +32,9 @@ import com.android.systemui.authentication.shared.model.AuthenticationWipeModel. import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.scene.domain.SceneFrameworkTableLog import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.time.SystemClock import javax.inject.Inject @@ -66,6 +69,7 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, private val repository: AuthenticationRepository, private val selectedUserInteractor: SelectedUserInteractor, + @SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer, ) { /** * The currently-configured authentication method. This determines how the authentication @@ -85,7 +89,11 @@ constructor( * `true` even when the lockscreen is showing and still needs to be dismissed by the user to * proceed. */ - val authenticationMethod: Flow<AuthenticationMethodModel> = repository.authenticationMethod + val authenticationMethod: Flow<AuthenticationMethodModel> = + repository.authenticationMethod.logDiffsForTable( + tableLogBuffer = tableLogBuffer, + initialValue = AuthenticationMethodModel.None, + ) /** * Whether the auto confirm feature is enabled for the currently-selected user. diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt index 4e45fcc25fb8..744fd7e94ab4 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt @@ -16,6 +16,9 @@ package com.android.systemui.authentication.shared.model +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger + /** Enumerates all known authentication methods. */ sealed class AuthenticationMethodModel( /** @@ -24,8 +27,8 @@ sealed class AuthenticationMethodModel( * "Secure" authentication methods require authentication to unlock the device. Non-secure auth * methods simply require user dismissal. */ - open val isSecure: Boolean, -) { + open val isSecure: Boolean +) : Diffable<AuthenticationMethodModel> { /** * Device doesn't use a secure authentication method. Either there is no lockscreen or the lock * screen can be swiped away when displayed. @@ -39,4 +42,8 @@ sealed class AuthenticationMethodModel( data object Pattern : AuthenticationMethodModel(isSecure = true) data object Sim : AuthenticationMethodModel(isSecure = true) + + override fun logDiffs(prevVal: AuthenticationMethodModel, row: TableRowLogger) { + row.logChange(columnName = "authenticationMethod", value = toString()) + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt index 08b3e99fadd0..84d3d7f81c25 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt @@ -72,10 +72,9 @@ constructor( // Request LockSettingsService to return the Gatekeeper Password in the // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the // Gatekeeper Password and operationId. - var effectiveUserId = request.userInfo.deviceCredentialOwnerId + val effectiveUserId = request.userInfo.deviceCredentialOwnerId val response = if (Flags.privateSpaceBp() && effectiveUserId != request.userInfo.userId) { - effectiveUserId = request.userInfo.userId lockPatternUtils.verifyTiedProfileChallenge( credential, request.userInfo.userId, @@ -101,7 +100,7 @@ constructor( lockPatternUtils.verifyGatekeeperPasswordHandle( pwHandle, request.operationInfo.gatekeeperChallenge, - effectiveUserId, + request.userInfo.userId, ) val hat = gkResponse.gatekeeperHAT lockPatternUtils.removeGatekeeperPasswordHandle(pwHandle) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt index 008fb26424e2..a164ff483e47 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt @@ -126,7 +126,12 @@ constructor( is PromptKind.Biometric -> BiometricPromptRequest.Biometric( info = promptInfo, - userInfo = BiometricUserInfo(userId = userId), + userInfo = + BiometricUserInfo( + userId = userId, + deviceCredentialOwnerId = + credentialInteractor.getCredentialOwnerOrSelfId(userId), + ), operationInfo = BiometricOperationInfo(gatekeeperChallenge = challenge), modalities = kind.activeModalities, opPackageName = opPackageName, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt index e7a68ac2cfb9..8d5ea3c21e29 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt @@ -12,6 +12,7 @@ import android.window.OnBackInvokedDispatcher import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.biometrics.ui.CredentialPasswordView import com.android.systemui.biometrics.ui.CredentialView import com.android.systemui.biometrics.ui.IPinPad @@ -21,7 +22,6 @@ import com.android.systemui.res.R import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull -import com.android.app.tracing.coroutines.launchTraced as launch /** Sub-binder for the [CredentialPasswordView]. */ object CredentialPasswordViewBinder { @@ -42,7 +42,7 @@ object CredentialPasswordViewBinder { view.repeatWhenAttached { // the header info never changes - do it early val header = viewModel.header.first() - passwordField.setTextOperationUser(UserHandle.of(header.user.userIdForPasswordEntry)) + passwordField.setTextOperationUser(UserHandle.of(header.user.deviceCredentialOwnerId)) viewModel.inputFlags.firstOrNull()?.let { flags -> passwordField.inputType = flags } if (requestFocusForInput) { passwordField.requestFocus() @@ -65,7 +65,7 @@ object CredentialPasswordViewBinder { if (attestation != null) { imeManager.hideSoftInputFromWindow( view.windowToken, - 0 // flag + 0, // flag ) host.onCredentialMatched(attestation) } else { @@ -79,7 +79,7 @@ object CredentialPasswordViewBinder { launch { onBackInvokedDispatcher.registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_DEFAULT, - onBackInvokedCallback + onBackInvokedCallback, ) awaitCancellation() } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt index d82311f6ca7c..832afb1799b1 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt @@ -22,20 +22,25 @@ import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.onBroadcastMetadataChanged +import com.android.settingslib.bluetooth.onBroadcastStartedOrStopped import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.withContext /** Holds business logic for the audio sharing state. */ @@ -71,6 +76,7 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, ) : AudioSharingInteractor { + private val audioSharingStartedEvents = Channel<Unit>(Channel.BUFFERED) private var previewEnabled: Boolean? = null override val isAudioSharingOn: Flow<Boolean> = @@ -99,12 +105,18 @@ constructor( withContext(backgroundDispatcher) { if (audioSharingAvailable()) { audioSharingRepository.leAudioBroadcastProfile?.let { profile -> - isAudioSharingOn - // Skip the default value, we only care about adding source for newly - // started audio sharing session - .drop(1) - .mapNotNull { audioSharingOn -> - if (audioSharingOn) { + merge( + // Register and start listen to onBroadcastMetadataChanged (means ready + // to add source) + audioSharingStartedEvents.receiveAsFlow().map { true }, + // When session is off or failed to start, stop listening to + // onBroadcastMetadataChanged as we won't be adding source + profile.onBroadcastStartedOrStopped + .filterNot { profile.isEnabled(null) } + .map { false }, + ) + .mapNotNull { shouldListenToMetadata -> + if (shouldListenToMetadata) { // onBroadcastMetadataChanged could emit multiple times during one // audio sharing session, we only perform add source on the first // time @@ -146,6 +158,7 @@ constructor( if (!audioSharingAvailable()) { return } + audioSharingStartedEvents.trySend(Unit) audioSharingRepository.startAudioSharing() } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContent.kt new file mode 100644 index 000000000000..8bc929985052 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContent.kt @@ -0,0 +1,36 @@ +/* + * 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.bluetooth.qsdialog + +import android.view.LayoutInflater +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.res.R + +@Composable +fun BluetoothDetailsContent() { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + // Inflate with the existing dialog xml layout + LayoutInflater.from(context).inflate(R.layout.bluetooth_tile_dialog, null) + // TODO: b/378513956 - Implement the bluetooth details view + }, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt index 9dd3b6de423e..ac4d82a95834 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt @@ -16,30 +16,11 @@ package com.android.systemui.bluetooth.qsdialog -import android.view.LayoutInflater -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.plugins.qs.TileDetailsViewModel -import com.android.systemui.res.R class BluetoothDetailsViewModel(onLongClick: () -> Unit) : TileDetailsViewModel() { private val _onLongClick = onLongClick - @Composable - override fun GetContentView() { - AndroidView( - modifier = Modifier.fillMaxWidth().fillMaxHeight(), - factory = { context -> - // Inflate with the existing dialog xml layout - LayoutInflater.from(context).inflate(R.layout.bluetooth_tile_dialog, null) - // TODO: b/378513956 - Implement the bluetooth details view - }, - ) - } - override fun clickOnSettingsButton() { _onLongClick() } diff --git a/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt index 19238804fb12..79e66a89cd6b 100644 --- a/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt @@ -16,6 +16,8 @@ package com.android.systemui.common.data +import com.android.systemui.common.data.repository.BatteryRepository +import com.android.systemui.common.data.repository.BatteryRepositoryImpl import com.android.systemui.common.data.repository.PackageChangeRepository import com.android.systemui.common.data.repository.PackageChangeRepositoryImpl import dagger.Binds @@ -27,4 +29,6 @@ abstract class CommonDataLayerModule { abstract fun bindPackageChangeRepository( impl: PackageChangeRepositoryImpl ): PackageChangeRepository + + @Binds abstract fun bindBatteryRepository(impl: BatteryRepositoryImpl): BatteryRepository } diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/BatteryRepository.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/BatteryRepository.kt new file mode 100644 index 000000000000..63b051339d4b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/BatteryRepository.kt @@ -0,0 +1,44 @@ +/* + * 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.common.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.util.kotlin.isDevicePluggedIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn + +interface BatteryRepository { + val isDevicePluggedIn: Flow<Boolean> +} + +@SysUISingleton +class BatteryRepositoryImpl +@Inject +constructor(@Background bgScope: CoroutineScope, batteryController: BatteryController) : + BatteryRepository { + + /** Returns {@code true} if the device is currently plugged in or wireless charging. */ + override val isDevicePluggedIn: Flow<Boolean> = + batteryController + .isDevicePluggedIn() + .stateIn(bgScope, SharingStarted.WhileSubscribed(), batteryController.isPluggedIn) +} diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/IExplicitHealthCheckService.aidl b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/BatteryInteractor.kt index 90965092ac2b..987776d14b2b 100644 --- a/packages/CrashRecovery/framework/java/android/service/watchdog/IExplicitHealthCheckService.aidl +++ b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/BatteryInteractor.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 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,19 +14,13 @@ * limitations under the License. */ -package android.service.watchdog; +package com.android.systemui.common.domain.interactor -import android.os.RemoteCallback; +import com.android.systemui.common.data.repository.BatteryRepository +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject -/** - * @hide - */ -@PermissionManuallyEnforced -oneway interface IExplicitHealthCheckService -{ - void setCallback(in @nullable RemoteCallback callback); - void request(String packageName); - void cancel(String packageName); - void getSupportedPackages(in RemoteCallback callback); - void getRequestedPackages(in RemoteCallback callback); +@SysUISingleton +class BatteryInteractor @Inject constructor(batteryRepository: BatteryRepository) { + val isDevicePluggedIn = batteryRepository.isDevicePluggedIn } diff --git a/packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt b/packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt index 47040fa4a572..af8a5fa23ccb 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt @@ -58,7 +58,6 @@ constructor( .distinctUntilChanged() .logDiffsForTable( tableLogBuffer = tableLogBuffer, - columnPrefix = "", columnName = "postured", initialValue = false, ) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt index 882991aacdbb..b89d32244e17 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt @@ -52,7 +52,6 @@ constructor( override val mediaModel: Flow<CommunalMediaModel> = _mediaModel.logDiffsForTable( tableLogBuffer = tableLogBuffer, - columnPrefix = "", initialValue = CommunalMediaModel.INACTIVE, ) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt index 090264678f35..b7476900d784 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt @@ -56,6 +56,12 @@ interface CommunalPrefsRepository { /** Save the hub onboarding dismissed state for the current user. */ suspend fun setHubOnboardingDismissed(user: UserInfo) + + /** Whether dream button tooltip has been dismissed. */ + fun isDreamButtonTooltipDismissed(user: UserInfo): Flow<Boolean> + + /** Save the dream button tooltip dismissed state for the current user. */ + suspend fun setDreamButtonTooltipDismissed(user: UserInfo) } @SysUISingleton @@ -87,27 +93,34 @@ constructor( readKeyForUser(user, CTA_DISMISSED_STATE) override suspend fun setCtaDismissed(user: UserInfo) = - withContext(bgDispatcher) { - getSharedPrefsForUser(user).edit().putBoolean(CTA_DISMISSED_STATE, true).apply() - logger.i("Dismissed CTA tile") - } + setBooleanKeyValueForUser(user, CTA_DISMISSED_STATE, "Dismissed CTA tile") override fun isHubOnboardingDismissed(user: UserInfo): Flow<Boolean> = readKeyForUser(user, HUB_ONBOARDING_DISMISSED_STATE) override suspend fun setHubOnboardingDismissed(user: UserInfo) = - withContext(bgDispatcher) { - getSharedPrefsForUser(user) - .edit() - .putBoolean(HUB_ONBOARDING_DISMISSED_STATE, true) - .apply() - logger.i("Dismissed hub onboarding") - } + setBooleanKeyValueForUser(user, HUB_ONBOARDING_DISMISSED_STATE, "Dismissed hub onboarding") + + override fun isDreamButtonTooltipDismissed(user: UserInfo): Flow<Boolean> = + readKeyForUser(user, DREAM_BUTTON_TOOLTIP_DISMISSED_STATE) + + override suspend fun setDreamButtonTooltipDismissed(user: UserInfo) = + setBooleanKeyValueForUser( + user, + DREAM_BUTTON_TOOLTIP_DISMISSED_STATE, + "Dismissed dream button tooltip", + ) private fun getSharedPrefsForUser(user: UserInfo): SharedPreferences { return userFileManager.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, user.id) } + private suspend fun setBooleanKeyValueForUser(user: UserInfo, key: String, logMsg: String) = + withContext(bgDispatcher) { + getSharedPrefsForUser(user).edit().putBoolean(key, true).apply() + logger.i(logMsg) + } + private fun readKeyForUser(user: UserInfo, key: String): Flow<Boolean> { return backupRestorationEvents .flatMapLatest { @@ -122,5 +135,6 @@ constructor( const val FILE_NAME = "communal_hub_prefs" const val CTA_DISMISSED_STATE = "cta_dismissed" const val HUB_ONBOARDING_DISMISSED_STATE = "hub_onboarding_dismissed" + const val DREAM_BUTTON_TOOLTIP_DISMISSED_STATE = "dream_button_tooltip_dismissed_state" } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt index 53122c56ed2c..abd101693b43 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt @@ -34,6 +34,7 @@ import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_I import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_USER_SETTING import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryModule.Companion.DEFAULT_BACKGROUND_TYPE import com.android.systemui.communal.shared.model.CommunalBackgroundType +import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -60,6 +61,12 @@ interface CommunalSettingsRepository { fun getScreensaverEnabledState(user: UserInfo): Flow<Boolean> /** + * Returns a [WhenToDream] for the specified user, indicating what state the device should be in + * to trigger dreams. + */ + fun getWhenToDreamState(user: UserInfo): Flow<WhenToDream> + + /** * Returns true if any glanceable hub functionality should be enabled via configs and flags. * * This should be used for preventing basic glanceable hub functionality from running on devices @@ -157,6 +164,49 @@ constructor( } .flowOn(bgDispatcher) + override fun getWhenToDreamState(user: UserInfo): Flow<WhenToDream> = + secureSettings + .observerFlow( + userId = user.id, + names = + arrayOf( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + ), + ) + .emitOnStart() + .map { + if ( + secureSettings.getIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + 0, + user.id, + ) == 1 + ) { + WhenToDream.WHILE_CHARGING + } else if ( + secureSettings.getIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, + 0, + user.id, + ) == 1 + ) { + WhenToDream.WHILE_DOCKED + } else if ( + secureSettings.getIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + 0, + user.id, + ) == 1 + ) { + WhenToDream.WHILE_POSTURED + } else { + WhenToDream.NEVER + } + } + .flowOn(bgDispatcher) + override fun getAllowedByDevicePolicy(user: UserInfo): Flow<Boolean> = broadcastDispatcher .broadcastFlow( diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt index 19666e4df1cf..1b0a6a06caa3 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt @@ -98,7 +98,6 @@ constructor( .filterNotNull() .logDiffsForTable( tableLogBuffer = tableLogBuffer, - columnPrefix = "", columnName = "tutorialSettingState", initialValue = HUB_MODE_TUTORIAL_NOT_STARTED, ) diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 74c335e79d4e..de55c92e84f9 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -30,11 +30,13 @@ import com.android.compose.animation.scene.TransitionKey import com.android.systemui.Flags.communalResponsiveGrid import com.android.systemui.Flags.glanceableHubBlurredBackground import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.domain.interactor.BatteryInteractor import com.android.systemui.communal.data.repository.CommunalMediaRepository import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository import com.android.systemui.communal.data.repository.CommunalWidgetRepository import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.domain.model.CommunalContentModel.WidgetContent +import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.shared.model.CommunalContentSize.FixedSize.FULL @@ -43,11 +45,14 @@ import com.android.systemui.communal.shared.model.CommunalContentSize.FixedSize. import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.shared.model.EditModeState +import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dock.DockManager +import com.android.systemui.dock.retrieveIsDocked import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -67,6 +72,7 @@ import com.android.systemui.statusbar.phone.ManagedProfileController import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.emitOnStart +import com.android.systemui.util.kotlin.isDevicePluggedIn import javax.inject.Inject import kotlin.time.Duration.Companion.minutes import kotlinx.coroutines.CoroutineDispatcher @@ -86,6 +92,7 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -117,6 +124,9 @@ constructor( @CommunalLog logBuffer: LogBuffer, @CommunalTableLog tableLogBuffer: TableLogBuffer, private val managedProfileController: ManagedProfileController, + private val batteryInteractor: BatteryInteractor, + private val dockManager: DockManager, + private val posturingInteractor: PosturingInteractor, ) { private val logger = Logger(logBuffer, "CommunalInteractor") @@ -163,7 +173,6 @@ constructor( } .logDiffsForTable( tableLogBuffer = tableLogBuffer, - columnPrefix = "", columnName = "isCommunalAvailable", initialValue = false, ) @@ -173,6 +182,37 @@ constructor( replay = 1, ) + /** + * Whether communal hub should be shown automatically, depending on the user's [WhenToDream] + * state. + */ + val shouldShowCommunal: StateFlow<Boolean> = + allOf( + isCommunalAvailable, + communalSettingsInteractor.whenToDream + .flatMapLatest { whenToDream -> + when (whenToDream) { + WhenToDream.NEVER -> flowOf(false) + + WhenToDream.WHILE_CHARGING -> batteryInteractor.isDevicePluggedIn + + WhenToDream.WHILE_DOCKED -> + allOf( + batteryInteractor.isDevicePluggedIn, + dockManager.retrieveIsDocked(), + ) + + WhenToDream.WHILE_POSTURED -> + allOf( + batteryInteractor.isDevicePluggedIn, + posturingInteractor.postured, + ) + } + } + .flowOn(bgDispatcher), + ) + .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false) + private val _isDisclaimerDismissed = MutableStateFlow(false) val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow() @@ -300,7 +340,6 @@ constructor( } .logDiffsForTable( tableLogBuffer = tableLogBuffer, - columnPrefix = "", columnName = "isCommunalShowing", initialValue = false, ) @@ -506,10 +545,16 @@ constructor( } /** CTA tile to be displayed in the glanceable hub (view mode). */ - val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> = - communalPrefsInteractor.isCtaDismissed.map { isDismissed -> - if (isDismissed) emptyList() else listOf(CommunalContentModel.CtaTileInViewMode()) + val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> by lazy { + if (communalSettingsInteractor.isV2FlagEnabled()) { + flowOf(listOf<CommunalContentModel.CtaTileInViewMode>()) + } else { + communalPrefsInteractor.isCtaDismissed.map { isDismissed -> + if (isDismissed) listOf<CommunalContentModel.CtaTileInViewMode>() + else listOf(CommunalContentModel.CtaTileInViewMode()) + } } + } /** A list of tutorial content to be displayed in the communal hub in tutorial mode. */ val tutorialContent: List<CommunalContentModel.Tutorial> = diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt index ec45d6c8d545..cdf17033cc01 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt @@ -49,7 +49,6 @@ constructor( .flatMapLatest { user -> repository.isCtaDismissed(user) } .logDiffsForTable( tableLogBuffer = tableLogBuffer, - columnPrefix = "", columnName = "isCtaDismissed", initialValue = false, ) @@ -67,7 +66,6 @@ constructor( .flatMapLatest { user -> repository.isHubOnboardingDismissed(user) } .logDiffsForTable( tableLogBuffer = tableLogBuffer, - columnPrefix = "", columnName = "isHubOnboardingDismissed", initialValue = false, ) @@ -80,6 +78,24 @@ constructor( fun setHubOnboardingDismissed(user: UserInfo = userTracker.userInfo) = bgScope.launch { repository.setHubOnboardingDismissed(user) } + val isDreamButtonTooltipDismissed: Flow<Boolean> = + userInteractor.selectedUserInfo + .flatMapLatest { user -> repository.isDreamButtonTooltipDismissed(user) } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "", + columnName = "isDreamButtonTooltipDismissed", + initialValue = false, + ) + .stateIn( + scope = bgScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + + fun setDreamButtonTooltipDismissed(user: UserInfo = userTracker.userInfo) = + bgScope.launch { repository.setDreamButtonTooltipDismissed(user) } + private companion object { const val TAG = "CommunalPrefsInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt index 1738f37b7f0c..a0b1261df346 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.communal.data.model.CommunalEnabledState import com.android.systemui.communal.data.repository.CommunalSettingsRepository import com.android.systemui.communal.shared.model.CommunalBackgroundType +import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.dagger.CommunalTableLog @@ -73,6 +74,12 @@ constructor( repository.getScreensaverEnabledState(user) } + /** When to dream for the currently selected user. */ + val whenToDream: Flow<WhenToDream> = + userInteractor.selectedUserInfo.flatMapLatest { user -> + repository.getWhenToDreamState(user) + } + /** * Returns true if any glanceable hub functionality should be enabled via configs and flags. * diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt index 4e4ecc949d09..8f55d96e947a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt @@ -65,7 +65,6 @@ constructor( } .logDiffsForTable( tableLogBuffer = tableLogBuffer, - columnPrefix = "", columnName = "isTutorialAvailable", initialValue = false, ) diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/PackageConfig.aidl b/packages/SystemUI/src/com/android/systemui/communal/shared/model/WhenToDream.kt index 013158676f79..0d4eb60c5240 100644 --- a/packages/CrashRecovery/framework/java/android/service/watchdog/PackageConfig.aidl +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/WhenToDream.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 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,9 +14,11 @@ * limitations under the License. */ -package android.service.watchdog; +package com.android.systemui.communal.shared.model -/** - * @hide - */ -parcelable PackageConfig; +enum class WhenToDream { + NEVER, + WHILE_CHARGING, + WHILE_DOCKED, + WHILE_POSTURED, +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalLockIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalLockIconViewBinder.kt new file mode 100644 index 000000000000..b1407da78816 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalLockIconViewBinder.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.communal.ui.binder + +import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.util.Log +import android.util.StateSet +import android.view.HapticFeedbackConstants +import android.view.View +import androidx.core.view.isInvisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.common.ui.view.LongPressHandlingView +import com.android.systemui.communal.ui.viewmodel.CommunalLockIconViewModel +import com.android.systemui.keyguard.ui.view.DeviceEntryIconView +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.res.R +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.util.kotlin.DisposableHandles +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DisposableHandle + +object CommunalLockIconViewBinder { + private const val TAG = "CommunalLockIconViewBinder" + + /** + * Updates UI for: + * - device entry containing view (parent view for the below views) + * - long-press handling view (transparent, no UI) + * - foreground icon view (lock/unlock) + */ + @SuppressLint("ClickableViewAccessibility") + @JvmStatic + fun bind( + applicationScope: CoroutineScope, + view: DeviceEntryIconView, + viewModel: CommunalLockIconViewModel, + falsingManager: FalsingManager, + vibratorHelper: VibratorHelper, + ): DisposableHandle { + val disposables = DisposableHandles() + val longPressHandlingView = view.longPressHandlingView + val fgIconView = view.iconView + val bgView = view.bgView + longPressHandlingView.listener = + object : LongPressHandlingView.Listener { + override fun onLongPressDetected( + view: View, + x: Int, + y: Int, + isA11yAction: Boolean, + ) { + if ( + !isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY) + ) { + Log.d( + TAG, + "Long press rejected because it is not a11yAction " + + "and it is a falseLongTap", + ) + return + } + vibratorHelper.performHapticFeedback(view, HapticFeedbackConstants.CONFIRM) + applicationScope.launch { + view.clearFocus() + view.clearAccessibilityFocus() + viewModel.onUserInteraction() + } + } + } + + longPressHandlingView.isInvisible = false + view.isClickable = true + longPressHandlingView.longPressDuration = { + view.resources.getInteger(R.integer.config_lockIconLongPress).toLong() + } + bgView.visibility = View.GONE + + disposables += + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch("$TAG#viewModel.isLongPressEnabled") { + viewModel.isLongPressEnabled.collect { isEnabled -> + longPressHandlingView.setLongPressHandlingEnabled(isEnabled) + } + } + launch("$TAG#viewModel.accessibilityDelegateHint") { + viewModel.accessibilityDelegateHint.collect { hint -> + view.accessibilityHintType = hint + if (hint != DeviceEntryIconView.AccessibilityHintType.NONE) { + view.setOnClickListener { + vibratorHelper.performHapticFeedback( + view, + HapticFeedbackConstants.CONFIRM, + ) + applicationScope.launch { + view.clearFocus() + view.clearAccessibilityFocus() + viewModel.onUserInteraction() + } + } + } else { + view.setOnClickListener(null) + } + } + } + } + } + + disposables += + fgIconView.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + // Start with an empty state + fgIconView.setImageState(StateSet.NOTHING, /* merge */ false) + launch("$TAG#fpIconView.viewModel") { + viewModel.viewAttributes.collect { attributes -> + if (attributes.type.contentDescriptionResId != -1) { + fgIconView.contentDescription = + fgIconView.resources.getString( + attributes.type.contentDescriptionResId + ) + } + fgIconView.imageTintList = ColorStateList.valueOf(attributes.tint) + fgIconView.setPadding( + attributes.padding, + attributes.padding, + attributes.padding, + attributes.padding, + ) + // Set image state at the end after updating other view state. This + // method forces the ImageView to recompute the bounds of the drawable. + fgIconView.setImageState( + view.getIconState(attributes.type, false), + /* merge */ false, + ) + // Invalidate, just in case the padding changes just after icon changes + fgIconView.invalidate() + } + } + } + } + return disposables + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt new file mode 100644 index 000000000000..19eeabd98c88 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt @@ -0,0 +1,148 @@ +/* + * 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.communal.ui.viewmodel + +import android.content.Context +import com.android.keyguard.KeyguardViewController +import com.android.settingslib.Utils +import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntrySourceInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.ui.view.DeviceEntryIconView +import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel +import com.android.systemui.keyguard.ui.viewmodel.toAccessibilityHintType +import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.util.kotlin.emitOnStart +import dagger.Lazy +import javax.inject.Inject +import kotlin.math.roundToInt +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +/** + * Simpler implementation of [DeviceEntryIconViewModel] for use in glanceable hub, where fingerprint + * is not supported. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class CommunalLockIconViewModel +@Inject +constructor( + @ShadeDisplayAware val context: Context, + @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, + private val deviceEntryInteractor: DeviceEntryInteractor, + keyguardInteractor: KeyguardInteractor, + private val keyguardViewController: Lazy<KeyguardViewController>, + private val deviceEntrySourceInteractor: DeviceEntrySourceInteractor, + accessibilityInteractor: AccessibilityInteractor, +) { + + private val isUnlocked: Flow<Boolean> = + if (SceneContainerFlag.isEnabled) { + deviceEntryInteractor.isUnlocked + } else { + keyguardInteractor.isKeyguardDismissible + } + .flatMapLatest { isUnlocked -> + if (!isUnlocked) { + flowOf(false) + } else { + flow { + // delay in case device ends up transitioning away from the lock screen; + // we don't want to animate to the unlocked icon and just let the + // icon fade with the transition to GONE + delay(DeviceEntryIconViewModel.UNLOCKED_DELAY_MS) + emit(true) + } + } + } + + private val iconType: Flow<DeviceEntryIconView.IconType> = + isUnlocked.map { unlocked -> + if (unlocked) { + DeviceEntryIconView.IconType.UNLOCK + } else { + DeviceEntryIconView.IconType.LOCK + } + } + + val isLongPressEnabled: Flow<Boolean> = + iconType.map { deviceEntryStatus -> + when (deviceEntryStatus) { + DeviceEntryIconView.IconType.UNLOCK -> true + DeviceEntryIconView.IconType.LOCK, + DeviceEntryIconView.IconType.FINGERPRINT, + DeviceEntryIconView.IconType.NONE -> false + } + } + + val accessibilityDelegateHint: Flow<DeviceEntryIconView.AccessibilityHintType> = + accessibilityInteractor.isEnabled.flatMapLatest { touchExplorationEnabled -> + if (touchExplorationEnabled) { + iconType.map { it.toAccessibilityHintType() } + } else { + flowOf(DeviceEntryIconView.AccessibilityHintType.NONE) + } + } + + private val padding: Flow<Int> = + configurationInteractor.scaleForResolution.map { scale -> + (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale) + .roundToInt() + } + + private fun getColor() = + Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent) + + private val color: Flow<Int> = + configurationInteractor.onAnyConfigurationChange + .emitOnStart() + .map { getColor() } + .distinctUntilChanged() + + suspend fun onUserInteraction() { + if (SceneContainerFlag.isEnabled) { + deviceEntryInteractor.attemptDeviceEntry() + } else { + keyguardViewController.get().showPrimaryBouncer(/* scrim */ true) + } + deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() + } + + val viewAttributes: Flow<CommunalLockIconAttributes> = + combine(iconType, color, padding) { iconType, color, padding -> + CommunalLockIconAttributes(type = iconType, tint = color, padding = padding) + } +} + +data class CommunalLockIconAttributes( + val type: DeviceEntryIconView.IconType, + val tint: Int, + val padding: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt index bbb168680221..7e683c45e525 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt @@ -20,11 +20,14 @@ import android.annotation.SuppressLint import android.app.DreamManager import android.content.Intent import android.provider.Settings +import androidx.compose.runtime.getValue import com.android.internal.logging.UiEventLogger +import com.android.systemui.communal.domain.interactor.CommunalPrefsInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.util.kotlin.isDevicePluggedIn @@ -37,7 +40,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -47,18 +50,36 @@ class CommunalToDreamButtonViewModel constructor( @Background private val backgroundContext: CoroutineContext, batteryController: BatteryController, + private val prefsInteractor: CommunalPrefsInteractor, private val settingsInteractor: CommunalSettingsInteractor, private val activityStarter: ActivityStarter, private val dreamManager: DreamManager, private val uiEventLogger: UiEventLogger, ) : ExclusiveActivatable() { + private val hydrator = Hydrator("CommunalToDreamButtonViewModel.hydrator") private val _requests = Channel<Unit>(Channel.BUFFERED) /** Whether we should show a button on hub to switch to dream. */ - @SuppressLint("MissingPermission") - val shouldShowDreamButtonOnHub = - batteryController.isDevicePluggedIn().distinctUntilChanged().flowOn(backgroundContext) + val shouldShowDreamButtonOnHub: Boolean by + hydrator.hydratedStateOf( + traceName = "shouldShowDreamButtonOnHub", + initialValue = false, + source = batteryController.isDevicePluggedIn().distinctUntilChanged(), + ) + + /** Return whether the dream button tooltip has been dismissed. */ + val shouldShowTooltip: Boolean by + hydrator.hydratedStateOf( + traceName = "shouldShowTooltip", + initialValue = false, + source = prefsInteractor.isDreamButtonTooltipDismissed.map { !it }, + ) + + /** Set the dream button tooltip to be dismissed. */ + fun setDreamButtonTooltipDismissed() { + prefsInteractor.setDreamButtonTooltipDismissed() + } /** Handle a tap on the "show dream" button. */ fun onShowDreamButtonTap() { @@ -86,6 +107,8 @@ constructor( } } + launch { hydrator.activate() } + awaitCancellation() } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt index 1e7bec257432..69da67e055fe 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt @@ -68,4 +68,10 @@ constructor( emptyFlow() } } + + /** Triggered if a face failure occurs regardless of the mode. */ + val faceFailure: Flow<FailedFaceAuthenticationStatus> = + deviceEntryFaceAuthInteractor.authenticationStatus.filterIsInstance< + FailedFaceAuthenticationStatus + >() } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt index cdd2b054711e..079d624e6fe0 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.CoreStartable import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus +import com.android.systemui.log.table.TableLogBuffer import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -81,6 +82,8 @@ interface DeviceEntryFaceAuthInteractor : CoreStartable { /** Whether face auth is considered class 3 */ fun isFaceAuthStrong(): Boolean + + suspend fun hydrateTableLogBuffer(tableLogBuffer: TableLogBuffer) } /** @@ -93,17 +96,17 @@ interface DeviceEntryFaceAuthInteractor : CoreStartable { */ interface FaceAuthenticationListener { /** Receive face isAuthenticated updates */ - fun onAuthenticatedChanged(isAuthenticated: Boolean) + fun onAuthenticatedChanged(isAuthenticated: Boolean) = Unit /** Receive face authentication status updates */ - fun onAuthenticationStatusChanged(status: FaceAuthenticationStatus) + fun onAuthenticationStatusChanged(status: FaceAuthenticationStatus) = Unit /** Receive status updates whenever face detection runs */ - fun onDetectionStatusChanged(status: FaceDetectionStatus) + fun onDetectionStatusChanged(status: FaceDetectionStatus) = Unit - fun onLockoutStateChanged(isLockedOut: Boolean) + fun onLockoutStateChanged(isLockedOut: Boolean) = Unit - fun onRunningStateChanged(isRunning: Boolean) + fun onRunningStateChanged(isRunning: Boolean) = Unit - fun onAuthEnrollmentStateChanged(enrolled: Boolean) + fun onAuthEnrollmentStateChanged(enrolled: Boolean) = Unit } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt index cd456a618c48..38e0503440f9 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -123,7 +123,7 @@ constructor( private val playErrorHapticForBiometricFailure: Flow<Unit> = merge( deviceEntryFingerprintAuthInteractor.fingerprintFailure, - deviceEntryBiometricAuthInteractor.faceOnlyFaceFailure, + deviceEntryBiometricAuthInteractor.faceFailure, ) // map to Unit .map {} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index 4ddc98cd434f..5fc924b14814 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -16,7 +16,6 @@ package com.android.systemui.deviceentry.domain.interactor -import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.policy.IKeyguardDismissCallback import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel @@ -25,7 +24,10 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository import com.android.systemui.keyguard.DismissCallbackRegistry +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.scene.data.model.asIterable +import com.android.systemui.scene.domain.SceneFrameworkTableLog import com.android.systemui.scene.domain.interactor.SceneBackInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes @@ -43,6 +45,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** * Hosts application business logic related to device entry. @@ -62,6 +65,7 @@ constructor( private val alternateBouncerInteractor: AlternateBouncerInteractor, private val dismissCallbackRegistry: DismissCallbackRegistry, sceneBackInteractor: SceneBackInteractor, + @SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer, ) { /** * Whether the device is unlocked. @@ -147,6 +151,11 @@ constructor( ) { enteredDirectly, enteredOnBackStack -> enteredOnBackStack || enteredDirectly } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnName = "isDeviceEntered", + initialValue = false, + ) .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, @@ -184,6 +193,11 @@ constructor( deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen == false)) && !isDeviceEntered } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnName = "canSwipeToEnter", + initialValue = false, + ) .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, @@ -268,7 +282,7 @@ constructor( } /** Locks the device instantly. */ - fun lockNow() { - deviceUnlockedInteractor.lockNow() + fun lockNow(debuggingReason: String) { + deviceUnlockedInteractor.lockNow(debuggingReason) } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt index 68aef521be7b..c6ae35317c72 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt @@ -33,8 +33,11 @@ import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.TrustInteractor import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.scene.domain.SceneFrameworkTableLog import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated @@ -48,6 +51,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -55,6 +59,7 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.receiveAsFlow @@ -66,7 +71,7 @@ class DeviceUnlockedInteractor constructor( private val authenticationInteractor: AuthenticationInteractor, private val repository: DeviceEntryRepository, - trustInteractor: TrustInteractor, + private val trustInteractor: TrustInteractor, faceAuthInteractor: DeviceEntryFaceAuthInteractor, fingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, private val powerInteractor: PowerInteractor, @@ -74,6 +79,7 @@ constructor( private val systemPropertiesHelper: SystemPropertiesHelper, private val userAwareSecureSettingsRepository: UserAwareSecureSettingsRepository, private val keyguardInteractor: KeyguardInteractor, + @SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer, ) : ExclusiveActivatable() { private val deviceUnlockSource = @@ -176,20 +182,37 @@ constructor( val deviceUnlockStatus: StateFlow<DeviceUnlockStatus> = repository.deviceUnlockStatus.asStateFlow() - private val lockNowRequests = Channel<Unit>() + /** A [Channel] of "lock now" requests where the values are the debugging reasons. */ + private val lockNowRequests = Channel<String>() override suspend fun onActivated(): Nothing { - authenticationInteractor.authenticationMethod.collectLatest { authMethod -> - if (!authMethod.isSecure) { - // Device remains unlocked as long as the authentication method is not secure. - Log.d(TAG, "remaining unlocked because auth method not secure") - repository.deviceUnlockStatus.value = DeviceUnlockStatus(true, null) - } else if (authMethod == AuthenticationMethodModel.Sim) { - // Device remains locked while SIM is locked. - Log.d(TAG, "remaining locked because SIM locked") - repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null) - } else { - handleLockAndUnlockEvents() + coroutineScope { + launch { + authenticationInteractor.authenticationMethod.collectLatest { authMethod -> + if (!authMethod.isSecure) { + // Device remains unlocked as long as the authentication method is not + // secure. + Log.d(TAG, "remaining unlocked because auth method not secure") + repository.deviceUnlockStatus.value = DeviceUnlockStatus(true, null) + } else if (authMethod == AuthenticationMethodModel.Sim) { + // Device remains locked while SIM is locked. + Log.d(TAG, "remaining locked because SIM locked") + repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null) + } else { + handleLockAndUnlockEvents() + } + } + } + + launch { + deviceUnlockStatus + .map { it.isUnlocked } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnName = "isUnlocked", + initialValue = deviceUnlockStatus.value.isUnlocked, + ) + .collect() } } @@ -197,8 +220,8 @@ constructor( } /** Locks the device instantly. */ - fun lockNow() { - lockNowRequests.trySend(Unit) + fun lockNow(debuggingReason: String) { + lockNowRequests.trySend(debuggingReason) } private suspend fun handleLockAndUnlockEvents() { @@ -223,47 +246,70 @@ constructor( private suspend fun handleLockEvents() { merge( - // Device wakefulness events. - powerInteractor.detailedWakefulness - .map { Pair(it.isAsleep(), it.lastSleepReason) } - .distinctUntilChangedBy { it.first } - .map { (isAsleep, lastSleepReason) -> - if (isAsleep) { - if ( - (lastSleepReason == WakeSleepReason.POWER_BUTTON) && - authenticationInteractor.getPowerButtonInstantlyLocks() - ) { - LockImmediately("locked instantly from power button") - } else if (lastSleepReason == WakeSleepReason.SLEEP_BUTTON) { - LockImmediately("locked instantly from sleep button") - } else { - LockWithDelay("entering sleep") - } - } else { - CancelDelayedLock("waking up") - } - }, + trustInteractor.isTrusted.flatMapLatestConflated { isTrusted -> + if (isTrusted) { + // When entering a trusted environment, power-related lock events are + // ignored. + Log.d(TAG, "In trusted environment, ignoring power-related lock events") + flowOf(CancelDelayedLock("in trusted environment")) + } else { + // When not in a trusted environment, power-related lock events are treated + // as normal. + Log.d( + TAG, + "Not in trusted environment, power-related lock events treated as" + + " normal", + ) + merge( + // Device wakefulness events. + powerInteractor.detailedWakefulness + .map { Pair(it.isAsleep(), it.lastSleepReason) } + .distinctUntilChangedBy { it.first } + .map { (isAsleep, lastSleepReason) -> + if (isAsleep) { + if ( + (lastSleepReason == WakeSleepReason.POWER_BUTTON) && + authenticationInteractor + .getPowerButtonInstantlyLocks() + ) { + LockImmediately("locked instantly from power button") + } else if ( + lastSleepReason == WakeSleepReason.SLEEP_BUTTON + ) { + LockImmediately("locked instantly from sleep button") + } else { + LockWithDelay("entering sleep") + } + } else { + CancelDelayedLock("waking up") + } + }, + // Started dreaming + powerInteractor.isInteractive.flatMapLatestConflated { isInteractive -> + // Only respond to dream state changes while the device is + // interactive. + if (isInteractive) { + keyguardInteractor.isDreamingAny.distinctUntilChanged().map { + isDreaming -> + if (isDreaming) { + LockWithDelay("started dreaming") + } else { + CancelDelayedLock("stopped dreaming") + } + } + } else { + emptyFlow() + } + }, + ) + } + }, // Device enters lockdown. isInLockdown .distinctUntilChanged() .filter { it } .map { LockImmediately("lockdown") }, - // Started dreaming - powerInteractor.isInteractive.flatMapLatestConflated { isInteractive -> - // Only respond to dream state changes while the device is interactive. - if (isInteractive) { - keyguardInteractor.isDreamingAny.distinctUntilChanged().map { isDreaming -> - if (isDreaming) { - LockWithDelay("started dreaming") - } else { - CancelDelayedLock("stopped dreaming") - } - } - } else { - emptyFlow() - } - }, - lockNowRequests.receiveAsFlow().map { LockImmediately("lockNow") }, + lockNowRequests.receiveAsFlow().map { reason -> LockImmediately(reason) }, ) .collectLatest(::onLockEvent) } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt index 9b8c2b1acc33..ecc4dbc2326a 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus +import com.android.systemui.log.table.TableLogBuffer import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -73,4 +74,6 @@ class NoopDeviceEntryFaceAuthInteractor @Inject constructor() : DeviceEntryFaceA override fun onWalletLaunched() = Unit override fun onDeviceUnfolded() {} + + override suspend fun hydrateTableLogBuffer(tableLogBuffer: TableLogBuffer) {} } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index b19b2d9ece02..4b90e1d52ea0 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -44,6 +44,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.log.FaceAuthenticationLogger +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneInteractor @@ -53,13 +55,16 @@ import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.sample +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import dagger.Lazy 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.StateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull @@ -379,6 +384,27 @@ constructor( .launchIn(applicationScope) } + override suspend fun hydrateTableLogBuffer(tableLogBuffer: TableLogBuffer) { + conflatedCallbackFlow { + val listener = + object : FaceAuthenticationListener { + override fun onAuthEnrollmentStateChanged(enrolled: Boolean) { + trySend(isFaceAuthEnabledAndEnrolled()) + } + } + + registerListener(listener) + + awaitClose { unregisterListener(listener) } + } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnName = "isFaceAuthEnabledAndEnrolled", + initialValue = isFaceAuthEnabledAndEnrolled(), + ) + .collect() + } + companion object { const val TAG = "DeviceEntryFaceAuthInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt index 02e1824dc7dd..11b7e9dfe319 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt @@ -20,6 +20,7 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor @@ -51,6 +52,7 @@ constructor( private val toLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, private val fromDreamingTransitionInteractor: FromDreamingTransitionInteractor, private val communalInteractor: CommunalInteractor, + private val communalSettingsInteractor: CommunalSettingsInteractor, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val userTracker: UserTracker, dumpManager: DumpManager, @@ -58,8 +60,12 @@ constructor( fun startTransitionFromDream() { val showGlanceableHub = - communalInteractor.isCommunalEnabled.value && - !keyguardUpdateMonitor.isEncryptedOrLockdown(userTracker.userId) + if (communalSettingsInteractor.isV2FlagEnabled()) { + communalInteractor.shouldShowCommunal.value + } else { + communalInteractor.isCommunalEnabled.value && + !keyguardUpdateMonitor.isEncryptedOrLockdown(userTracker.userId) + } fromDreamingTransitionInteractor.startToLockscreenOrGlanceableHubTransition( showGlanceableHub && !glanceableHubAllowKeyguardWhenDreaming() ) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt index 0054dd772659..6395bb736a34 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -1 +1,1101 @@ -/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.keyboard.shortcut.ui.composable
import android.graphics.drawable.Icon
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.FlowRowScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.DeleteOutline
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationDrawerItemColors
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.Hyphens
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import com.android.compose.modifiers.thenIf
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.keyboard.shortcut.ui.model.IconSource
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCategoryUi
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
import com.android.systemui.res.R
import kotlinx.coroutines.delay
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut as ShortcutModel
@Composable
fun ShortcutHelper(
onSearchQueryChanged: (String) -> Unit,
onKeyboardSettingsClicked: () -> Unit,
modifier: Modifier = Modifier,
shortcutsUiState: ShortcutsUiState,
useSinglePane: @Composable () -> Boolean = { shouldUseSinglePane() },
onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
when (shortcutsUiState) {
is ShortcutsUiState.Active -> {
ActiveShortcutHelper(
shortcutsUiState,
useSinglePane,
onSearchQueryChanged,
modifier,
onKeyboardSettingsClicked,
onCustomizationRequested,
)
}
else -> {
// No-op for now.
}
}
}
@Composable
private fun ActiveShortcutHelper(
shortcutsUiState: ShortcutsUiState.Active,
useSinglePane: @Composable () -> Boolean,
onSearchQueryChanged: (String) -> Unit,
modifier: Modifier,
onKeyboardSettingsClicked: () -> Unit,
onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
var selectedCategoryType by
remember(shortcutsUiState.defaultSelectedCategory) {
mutableStateOf(shortcutsUiState.defaultSelectedCategory)
}
if (useSinglePane()) {
ShortcutHelperSinglePane(
shortcutsUiState.searchQuery,
onSearchQueryChanged,
shortcutsUiState.shortcutCategories,
selectedCategoryType,
onCategorySelected = { selectedCategoryType = it },
onKeyboardSettingsClicked,
modifier,
)
} else {
ShortcutHelperTwoPane(
shortcutsUiState.searchQuery,
onSearchQueryChanged,
modifier,
shortcutsUiState.shortcutCategories,
selectedCategoryType,
onCategorySelected = { selectedCategoryType = it },
onKeyboardSettingsClicked,
shortcutsUiState.isShortcutCustomizerFlagEnabled,
onCustomizationRequested,
shortcutsUiState.shouldShowResetButton,
)
}
}
@Composable private fun shouldUseSinglePane() = hasCompactWindowSize()
@Composable
private fun ShortcutHelperSinglePane(
searchQuery: String,
onSearchQueryChanged: (String) -> Unit,
categories: List<ShortcutCategoryUi>,
selectedCategoryType: ShortcutCategoryType?,
onCategorySelected: (ShortcutCategoryType?) -> Unit,
onKeyboardSettingsClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier =
modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, top = 26.dp)
) {
TitleBar()
Spacer(modifier = Modifier.height(6.dp))
ShortcutsSearchBar(onSearchQueryChanged)
Spacer(modifier = Modifier.height(16.dp))
if (categories.isEmpty()) {
Box(modifier = Modifier.weight(1f)) {
NoSearchResultsText(horizontalPadding = 16.dp, fillHeight = true)
}
} else {
CategoriesPanelSinglePane(
searchQuery,
categories,
selectedCategoryType,
onCategorySelected,
)
Spacer(modifier = Modifier.weight(1f))
}
KeyboardSettings(
horizontalPadding = 16.dp,
verticalPadding = 32.dp,
onClick = onKeyboardSettingsClicked,
)
}
}
@Composable
private fun CategoriesPanelSinglePane(
searchQuery: String,
categories: List<ShortcutCategoryUi>,
selectedCategoryType: ShortcutCategoryType?,
onCategorySelected: (ShortcutCategoryType?) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
categories.fastForEachIndexed { index, category ->
val isExpanded = selectedCategoryType == category.type
val itemShape =
if (categories.size == 1) {
ShortcutHelper.Shapes.singlePaneSingleCategory
} else if (index == 0) {
ShortcutHelper.Shapes.singlePaneFirstCategory
} else if (index == categories.lastIndex) {
ShortcutHelper.Shapes.singlePaneLastCategory
} else {
ShortcutHelper.Shapes.singlePaneCategory
}
CategoryItemSinglePane(
searchQuery = searchQuery,
category = category,
isExpanded = isExpanded,
onClick = {
onCategorySelected(
if (isExpanded) {
null
} else {
category.type
}
)
},
shape = itemShape,
)
}
}
}
@Composable
private fun CategoryItemSinglePane(
searchQuery: String,
category: ShortcutCategoryUi,
isExpanded: Boolean,
onClick: () -> Unit,
shape: Shape,
) {
Surface(color = MaterialTheme.colorScheme.surfaceBright, shape = shape, onClick = onClick) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp),
) {
ShortcutCategoryIcon(modifier = Modifier.size(24.dp), source = category.iconSource)
Spacer(modifier = Modifier.width(16.dp))
Text(category.label)
Spacer(modifier = Modifier.weight(1f))
RotatingExpandCollapseIcon(isExpanded)
}
AnimatedVisibility(visible = isExpanded) {
ShortcutCategoryDetailsSinglePane(searchQuery, category)
}
}
}
}
@Composable
fun ShortcutCategoryIcon(
source: IconSource,
modifier: Modifier = Modifier,
contentDescription: String? = null,
tint: Color = LocalContentColor.current,
) {
if (source.imageVector != null) {
Icon(source.imageVector, contentDescription, modifier, tint)
} else if (source.painter != null) {
Image(source.painter, contentDescription, modifier)
}
}
@Composable
private fun RotatingExpandCollapseIcon(isExpanded: Boolean) {
val expandIconRotationDegrees by
animateFloatAsState(
targetValue =
if (isExpanded) {
180f
} else {
0f
},
label = "Expand icon rotation animation",
)
Icon(
modifier =
Modifier.background(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = CircleShape,
)
.graphicsLayer { rotationZ = expandIconRotationDegrees },
imageVector = Icons.Default.ExpandMore,
contentDescription =
if (isExpanded) {
stringResource(R.string.shortcut_helper_content_description_collapse_icon)
} else {
stringResource(R.string.shortcut_helper_content_description_expand_icon)
},
tint = MaterialTheme.colorScheme.onSurface,
)
}
@Composable
private fun ShortcutCategoryDetailsSinglePane(searchQuery: String, category: ShortcutCategoryUi) {
Column(Modifier.padding(horizontal = 16.dp)) {
category.subCategories.fastForEach { subCategory ->
ShortcutSubCategorySinglePane(searchQuery, subCategory)
}
}
}
@Composable
private fun ShortcutSubCategorySinglePane(searchQuery: String, subCategory: ShortcutSubCategory) {
// This @Composable is expected to be in a Column.
SubCategoryTitle(subCategory.label)
subCategory.shortcuts.fastForEachIndexed { index, shortcut ->
if (index > 0) {
HorizontalDivider(color = MaterialTheme.colorScheme.surfaceContainerHigh)
}
Shortcut(Modifier.padding(vertical = 24.dp), searchQuery, shortcut)
}
}
@Composable
private fun ShortcutHelperTwoPane(
searchQuery: String,
onSearchQueryChanged: (String) -> Unit,
modifier: Modifier = Modifier,
categories: List<ShortcutCategoryUi>,
selectedCategoryType: ShortcutCategoryType?,
onCategorySelected: (ShortcutCategoryType?) -> Unit,
onKeyboardSettingsClicked: () -> Unit,
isShortcutCustomizerFlagEnabled: Boolean,
onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
shouldShowResetButton: Boolean,
) {
val selectedCategory = categories.fastFirstOrNull { it.type == selectedCategoryType }
var isCustomizing by remember { mutableStateOf(false) }
Column(modifier = modifier.fillMaxSize().padding(horizontal = 24.dp)) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
// Keep title centered whether customize button is visible or not.
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
TitleBar(isCustomizing)
}
if (isShortcutCustomizerFlagEnabled) {
CustomizationButtonsContainer(
isCustomizing = isCustomizing,
onToggleCustomizationMode = { isCustomizing = !isCustomizing },
onReset = {
onCustomizationRequested(ShortcutCustomizationRequestInfo.Reset)
},
shouldShowResetButton = shouldShowResetButton,
)
} else {
Spacer(modifier = Modifier.width(if (isCustomizing) 69.dp else 133.dp))
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Row(Modifier.fillMaxWidth()) {
StartSidePanel(
onSearchQueryChanged = onSearchQueryChanged,
modifier = Modifier.width(240.dp).semantics { isTraversalGroup = true },
categories = categories,
onKeyboardSettingsClicked = onKeyboardSettingsClicked,
selectedCategory = selectedCategoryType,
onCategoryClicked = { onCategorySelected(it.type) },
)
Spacer(modifier = Modifier.width(24.dp))
EndSidePanel(
searchQuery,
Modifier.fillMaxSize().padding(top = 8.dp).semantics { isTraversalGroup = true },
selectedCategory,
isCustomizing = isCustomizing,
onCustomizationRequested = onCustomizationRequested,
)
}
}
}
@Composable
private fun CustomizationButtonsContainer(
isCustomizing: Boolean,
shouldShowResetButton: Boolean,
onToggleCustomizationMode: () -> Unit,
onReset: () -> Unit,
) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (isCustomizing) {
if (shouldShowResetButton) {
ResetButton(onClick = onReset)
}
DoneButton(onClick = onToggleCustomizationMode)
} else {
CustomizeButton(onClick = onToggleCustomizationMode)
}
}
}
@Composable
private fun ResetButton(onClick: () -> Unit) {
ShortcutHelperButton(
modifier = Modifier.heightIn(40.dp),
onClick = onClick,
color = Color.Transparent,
iconSource = IconSource(imageVector = Icons.Default.Refresh),
text = stringResource(id = R.string.shortcut_helper_reset_button_text),
contentColor = MaterialTheme.colorScheme.primary,
border = BorderStroke(color = MaterialTheme.colorScheme.outlineVariant, width = 1.dp),
)
}
@Composable
private fun CustomizeButton(onClick: () -> Unit) {
ShortcutHelperButton(
modifier = Modifier.heightIn(40.dp),
onClick = onClick,
color = MaterialTheme.colorScheme.secondaryContainer,
iconSource = IconSource(imageVector = Icons.Default.Tune),
text = stringResource(id = R.string.shortcut_helper_customize_button_text),
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
@Composable
private fun DoneButton(onClick: () -> Unit) {
ShortcutHelperButton(
modifier = Modifier.heightIn(40.dp),
onClick = onClick,
color = MaterialTheme.colorScheme.primary,
text = stringResource(R.string.shortcut_helper_done_button_text),
contentColor = MaterialTheme.colorScheme.onPrimary,
)
}
@Composable
private fun EndSidePanel(
searchQuery: String,
modifier: Modifier,
category: ShortcutCategoryUi?,
isCustomizing: Boolean,
onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
val listState = rememberLazyListState()
LaunchedEffect(key1 = category) { if (category != null) listState.animateScrollToItem(0) }
if (category == null) {
NoSearchResultsText(horizontalPadding = 24.dp, fillHeight = false)
return
}
LazyColumn(modifier = modifier, state = listState) {
items(category.subCategories) { subcategory ->
SubCategoryContainerDualPane(
searchQuery = searchQuery,
subCategory = subcategory,
isCustomizing = isCustomizing and category.type.includeInCustomization,
onCustomizationRequested = { requestInfo ->
when (requestInfo) {
is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add ->
onCustomizationRequested(requestInfo.copy(categoryType = category.type))
is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete ->
onCustomizationRequested(requestInfo.copy(categoryType = category.type))
ShortcutCustomizationRequestInfo.Reset ->
onCustomizationRequested(requestInfo)
}
},
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
@Composable
private fun NoSearchResultsText(horizontalPadding: Dp, fillHeight: Boolean) {
var modifier = Modifier.fillMaxWidth()
if (fillHeight) {
modifier = modifier.fillMaxHeight()
}
Text(
stringResource(R.string.shortcut_helper_no_search_results),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier =
modifier
.padding(vertical = 8.dp)
.background(MaterialTheme.colorScheme.surfaceBright, RoundedCornerShape(28.dp))
.padding(horizontal = horizontalPadding, vertical = 24.dp),
)
}
@Composable
private fun SubCategoryContainerDualPane(
searchQuery: String,
subCategory: ShortcutSubCategory,
isCustomizing: Boolean,
onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit,
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(28.dp),
color = MaterialTheme.colorScheme.surfaceBright,
) {
Column(Modifier.padding(16.dp)) {
SubCategoryTitle(subCategory.label)
Spacer(Modifier.height(8.dp))
subCategory.shortcuts.fastForEachIndexed { index, shortcut ->
if (index > 0) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 8.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
)
}
Shortcut(
modifier = Modifier.padding(vertical = 8.dp),
searchQuery = searchQuery,
shortcut = shortcut,
isCustomizing = isCustomizing && shortcut.isCustomizable,
onCustomizationRequested = { requestInfo ->
when (requestInfo) {
is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add ->
onCustomizationRequested(
requestInfo.copy(subCategoryLabel = subCategory.label)
)
is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete ->
onCustomizationRequested(
requestInfo.copy(subCategoryLabel = subCategory.label)
)
ShortcutCustomizationRequestInfo.Reset ->
onCustomizationRequested(requestInfo)
}
},
)
}
}
}
}
@Composable
private fun SubCategoryTitle(title: String) {
Text(
title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
)
}
@Composable
private fun Shortcut(
modifier: Modifier,
searchQuery: String,
shortcut: ShortcutModel,
isCustomizing: Boolean = false,
onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
val focusColor = MaterialTheme.colorScheme.secondary
Row(
modifier
.thenIf(isFocused) {
Modifier.border(width = 3.dp, color = focusColor, shape = RoundedCornerShape(16.dp))
}
.focusable(interactionSource = interactionSource)
.padding(8.dp)
.semantics(mergeDescendants = true) { contentDescription = shortcut.contentDescription }
) {
Row(
modifier =
Modifier.width(128.dp).align(Alignment.CenterVertically).weight(0.333f).semantics {
hideFromAccessibility()
},
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (shortcut.icon != null) {
ShortcutIcon(
shortcut.icon,
modifier = Modifier.size(24.dp).semantics { hideFromAccessibility() },
)
}
ShortcutDescriptionText(
searchQuery = searchQuery,
shortcut = shortcut,
modifier = Modifier.semantics { hideFromAccessibility() },
)
}
Spacer(modifier = Modifier.width(24.dp).semantics { hideFromAccessibility() })
ShortcutKeyCombinations(
modifier = Modifier.weight(.666f).semantics { hideFromAccessibility() },
shortcut = shortcut,
isCustomizing = isCustomizing,
onAddShortcutRequested = {
onCustomizationRequested(
ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add(
label = shortcut.label,
shortcutCommand = shortcut.commands.first(),
)
)
},
onDeleteShortcutRequested = {
onCustomizationRequested(
ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete(
label = shortcut.label,
shortcutCommand = shortcut.commands.first(),
)
)
},
)
}
}
@Composable
fun ShortcutIcon(
icon: ShortcutIcon,
modifier: Modifier = Modifier,
contentDescription: String? = null,
) {
val context = LocalContext.current
val drawable =
remember(icon.packageName, icon.resourceId) {
Icon.createWithResource(icon.packageName, icon.resourceId).loadDrawable(context)
} ?: return
Image(
painter = rememberDrawablePainter(drawable),
contentDescription = contentDescription,
modifier = modifier,
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun ShortcutKeyCombinations(
modifier: Modifier = Modifier,
shortcut: ShortcutModel,
isCustomizing: Boolean = false,
onAddShortcutRequested: () -> Unit = {},
onDeleteShortcutRequested: () -> Unit = {},
) {
FlowRow(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
itemVerticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End,
) {
shortcut.commands.forEachIndexed { index, command ->
if (index > 0) {
ShortcutOrSeparator(spacing = 16.dp)
}
ShortcutCommandContainer(showBackground = command.isCustom) { ShortcutCommand(command) }
}
if (isCustomizing) {
Spacer(modifier = Modifier.width(16.dp))
if (shortcut.containsCustomShortcutCommands) {
DeleteShortcutButton(onDeleteShortcutRequested)
} else {
AddShortcutButton(onAddShortcutRequested)
}
}
}
}
@Composable
private fun AddShortcutButton(onClick: () -> Unit) {
ShortcutHelperButton(
modifier = Modifier.size(32.dp),
onClick = onClick,
color = Color.Transparent,
iconSource = IconSource(imageVector = Icons.Default.Add),
contentColor = MaterialTheme.colorScheme.primary,
contentPaddingVertical = 0.dp,
contentPaddingHorizontal = 0.dp,
contentDescription = stringResource(R.string.shortcut_helper_add_shortcut_button_label),
shape = CircleShape,
border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.outline)
)
}
@Composable
private fun DeleteShortcutButton(onClick: () -> Unit) {
ShortcutHelperButton(
modifier = Modifier.size(32.dp),
onClick = onClick,
color = Color.Transparent,
iconSource = IconSource(imageVector = Icons.Default.DeleteOutline),
contentColor = MaterialTheme.colorScheme.primary,
contentPaddingVertical = 0.dp,
contentPaddingHorizontal = 0.dp,
contentDescription = stringResource(R.string.shortcut_helper_delete_shortcut_button_label),
shape = CircleShape,
border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.outline)
)
}
@Composable
private fun ShortcutCommandContainer(showBackground: Boolean, content: @Composable () -> Unit) {
if (showBackground) {
Box(
modifier =
Modifier.wrapContentSize()
.background(
color = MaterialTheme.colorScheme.outlineVariant,
shape = RoundedCornerShape(16.dp),
)
.padding(4.dp)
) {
content()
}
} else {
content()
}
}
@Composable
private fun ShortcutCommand(command: ShortcutCommand) {
Row {
command.keys.forEachIndexed { keyIndex, key ->
if (keyIndex > 0) {
Spacer(Modifier.width(4.dp))
}
ShortcutKeyContainer {
if (key is ShortcutKey.Text) {
ShortcutTextKey(key)
} else if (key is ShortcutKey.Icon) {
ShortcutIconKey(key)
}
}
}
}
}
@Composable
private fun ShortcutKeyContainer(shortcutKeyContent: @Composable BoxScope.() -> Unit) {
Box(
modifier =
Modifier.height(36.dp)
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(12.dp),
)
) {
shortcutKeyContent()
}
}
@Composable
private fun BoxScope.ShortcutTextKey(key: ShortcutKey.Text) {
Text(
text = key.value,
modifier =
Modifier.align(Alignment.Center).padding(horizontal = 12.dp).semantics {
hideFromAccessibility()
},
style = MaterialTheme.typography.titleSmall,
)
}
@Composable
private fun BoxScope.ShortcutIconKey(key: ShortcutKey.Icon) {
Icon(
painter =
when (key) {
is ShortcutKey.Icon.ResIdIcon -> painterResource(key.drawableResId)
is ShortcutKey.Icon.DrawableIcon -> rememberDrawablePainter(drawable = key.drawable)
},
contentDescription = null,
modifier = Modifier.align(Alignment.Center).padding(6.dp),
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun FlowRowScope.ShortcutOrSeparator(spacing: Dp) {
Spacer(Modifier.width(spacing))
Text(
text = stringResource(R.string.shortcut_helper_key_combinations_or_separator),
modifier = Modifier.align(Alignment.CenterVertically).semantics { hideFromAccessibility() },
style = MaterialTheme.typography.titleSmall,
)
Spacer(Modifier.width(spacing))
}
@Composable
private fun ShortcutDescriptionText(
searchQuery: String,
shortcut: ShortcutModel,
modifier: Modifier = Modifier,
) {
Text(
modifier = modifier,
text = textWithHighlightedSearchQuery(shortcut.label, searchQuery),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
@Composable
private fun textWithHighlightedSearchQuery(text: String, searchValue: String) =
buildAnnotatedString {
val searchIndex = text.lowercase().indexOf(searchValue.trim().lowercase())
val postSearchIndex = searchIndex + searchValue.trim().length
if (searchIndex > 0) {
val preSearchText = text.substring(0, searchIndex)
append(preSearchText)
}
if (searchIndex >= 0) {
val searchText = text.substring(searchIndex, postSearchIndex)
withStyle(style = SpanStyle(background = MaterialTheme.colorScheme.primaryContainer)) {
append(searchText)
}
if (postSearchIndex < text.length) {
val postSearchText = text.substring(postSearchIndex)
append(postSearchText)
}
} else {
append(text)
}
}
@Composable
private fun StartSidePanel(
onSearchQueryChanged: (String) -> Unit,
modifier: Modifier,
categories: List<ShortcutCategoryUi>,
onKeyboardSettingsClicked: () -> Unit,
selectedCategory: ShortcutCategoryType?,
onCategoryClicked: (ShortcutCategoryUi) -> Unit,
) {
CompositionLocalProvider(
// Restrict system font scale increases up to a max so categories display correctly.
LocalDensity provides
Density(
density = LocalDensity.current.density,
fontScale = LocalDensity.current.fontScale.coerceIn(1f, 1.5f),
)
) {
Column(modifier) {
ShortcutsSearchBar(onSearchQueryChanged)
Spacer(modifier = Modifier.heightIn(8.dp))
CategoriesPanelTwoPane(categories, selectedCategory, onCategoryClicked)
Spacer(modifier = Modifier.weight(1f))
KeyboardSettings(
horizontalPadding = 24.dp,
verticalPadding = 24.dp,
onKeyboardSettingsClicked,
)
}
}
}
@Composable
private fun CategoriesPanelTwoPane(
categories: List<ShortcutCategoryUi>,
selectedCategory: ShortcutCategoryType?,
onCategoryClicked: (ShortcutCategoryUi) -> Unit,
) {
Column {
categories.fastForEach {
CategoryItemTwoPane(
label = it.label,
iconSource = it.iconSource,
selected = selectedCategory == it.type,
onClick = { onCategoryClicked(it) },
)
}
}
}
@Composable
private fun CategoryItemTwoPane(
label: String,
iconSource: IconSource,
selected: Boolean,
onClick: () -> Unit,
colors: NavigationDrawerItemColors =
NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent),
) {
SelectableShortcutSurface(
selected = selected,
onClick = onClick,
modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 64.dp).fillMaxWidth(),
shape = RoundedCornerShape(28.dp),
color = colors.containerColor(selected).value,
interactionsConfig =
InteractionsConfig(
hoverOverlayColor = MaterialTheme.colorScheme.onSurface,
hoverOverlayAlpha = 0.11f,
pressedOverlayColor = MaterialTheme.colorScheme.onSurface,
pressedOverlayAlpha = 0.15f,
focusOutlineColor = MaterialTheme.colorScheme.secondary,
focusOutlineStrokeWidth = 3.dp,
focusOutlinePadding = 2.dp,
surfaceCornerRadius = 28.dp,
focusOutlineCornerRadius = 33.dp,
),
) {
Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) {
ShortcutCategoryIcon(
modifier = Modifier.size(24.dp),
source = iconSource,
contentDescription = null,
tint = colors.iconColor(selected).value,
)
Spacer(Modifier.width(12.dp))
Box(Modifier.weight(1f)) {
Text(
fontSize = 18.sp,
color = colors.textColor(selected).value,
style = MaterialTheme.typography.titleSmall.copy(hyphens = Hyphens.Auto),
text = label,
)
}
}
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun TitleBar(isCustomizing: Boolean = false) {
val text =
if (isCustomizing) {
stringResource(R.string.shortcut_helper_customize_mode_title)
} else {
stringResource(R.string.shortcut_helper_title)
}
CenterAlignedTopAppBar(
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color.Transparent),
title = {
Text(
text = text,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineSmall,
)
},
windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp),
expandedHeight = 64.dp,
)
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun ShortcutsSearchBar(onQueryChange: (String) -> Unit) {
// Using an "internal query" to make sure the SearchBar is immediately updated, otherwise
// the cursor moves to the wrong position sometimes, when waiting for the query to come back
// from the ViewModel.
var queryInternal by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
// TODO(b/272065229): Added minor delay so TalkBack can take focus of search box by default,
// remove when default a11y focus is fixed.
delay(50)
focusRequester.requestFocus()
}
SearchBar(
modifier =
Modifier.fillMaxWidth().focusRequester(focusRequester).onKeyEvent {
if (it.key == Key.DirectionDown) {
focusManager.moveFocus(FocusDirection.Down)
return@onKeyEvent true
} else {
return@onKeyEvent false
}
},
colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceBright),
query = queryInternal,
active = false,
onActiveChange = {},
onQueryChange = {
queryInternal = it
onQueryChange(it)
},
onSearch = {},
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
placeholder = { Text(text = stringResource(R.string.shortcut_helper_search_placeholder)) },
windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp),
content = {},
)
}
@Composable
private fun KeyboardSettings(horizontalPadding: Dp, verticalPadding: Dp, onClick: () -> Unit) {
ClickableShortcutSurface(
onClick = onClick,
shape = RoundedCornerShape(24.dp),
color = Color.Transparent,
modifier =
Modifier.semantics { role = Role.Button }.fillMaxWidth().padding(horizontal = 12.dp),
interactionsConfig =
InteractionsConfig(
hoverOverlayColor = MaterialTheme.colorScheme.onSurface,
hoverOverlayAlpha = 0.11f,
pressedOverlayColor = MaterialTheme.colorScheme.onSurface,
pressedOverlayAlpha = 0.15f,
focusOutlineColor = MaterialTheme.colorScheme.secondary,
focusOutlinePadding = 8.dp,
focusOutlineStrokeWidth = 3.dp,
surfaceCornerRadius = 24.dp,
focusOutlineCornerRadius = 28.dp,
hoverPadding = 8.dp,
),
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text =
stringResource(id = R.string.shortcut_helper_keyboard_settings_buttons_label),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 16.sp,
style = MaterialTheme.typography.titleSmall,
)
Spacer(modifier = Modifier.weight(1f))
Icon(
imageVector = Icons.AutoMirrored.Default.OpenInNew,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp),
)
}
}
}
object ShortcutHelper {
object Shapes {
val singlePaneFirstCategory =
RoundedCornerShape(
topStart = Dimensions.SinglePaneCategoryCornerRadius,
topEnd = Dimensions.SinglePaneCategoryCornerRadius,
)
val singlePaneLastCategory =
RoundedCornerShape(
bottomStart = Dimensions.SinglePaneCategoryCornerRadius,
bottomEnd = Dimensions.SinglePaneCategoryCornerRadius,
)
val singlePaneSingleCategory =
RoundedCornerShape(size = Dimensions.SinglePaneCategoryCornerRadius)
val singlePaneCategory = RectangleShape
}
object Dimensions {
val SinglePaneCategoryCornerRadius = 28.dp
}
internal const val TAG = "ShortcutHelperUI"
}
\ No newline at end of file +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.ui.composable + +import android.graphics.drawable.Icon +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.FlowRowScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.DeleteOutline +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationDrawerItemColors +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.Hyphens +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.fastFirstOrNull +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed +import com.android.compose.modifiers.thenIf +import com.android.compose.ui.graphics.painter.rememberDrawablePainter +import com.android.systemui.keyboard.shortcut.shared.model.Shortcut as ShortcutModel +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory +import com.android.systemui.keyboard.shortcut.ui.model.IconSource +import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCategoryUi +import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState +import com.android.systemui.res.R +import kotlinx.coroutines.delay + +@Composable +fun ShortcutHelper( + onSearchQueryChanged: (String) -> Unit, + onKeyboardSettingsClicked: () -> Unit, + modifier: Modifier = Modifier, + shortcutsUiState: ShortcutsUiState, + useSinglePane: @Composable () -> Boolean = { shouldUseSinglePane() }, + onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, +) { + when (shortcutsUiState) { + is ShortcutsUiState.Active -> { + ActiveShortcutHelper( + shortcutsUiState, + useSinglePane, + onSearchQueryChanged, + modifier, + onKeyboardSettingsClicked, + onCustomizationRequested, + ) + } + + else -> { + // No-op for now. + } + } +} + +@Composable +private fun ActiveShortcutHelper( + shortcutsUiState: ShortcutsUiState.Active, + useSinglePane: @Composable () -> Boolean, + onSearchQueryChanged: (String) -> Unit, + modifier: Modifier, + onKeyboardSettingsClicked: () -> Unit, + onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, +) { + var selectedCategoryType by + remember(shortcutsUiState.defaultSelectedCategory) { + mutableStateOf(shortcutsUiState.defaultSelectedCategory) + } + if (useSinglePane()) { + ShortcutHelperSinglePane( + shortcutsUiState.searchQuery, + onSearchQueryChanged, + shortcutsUiState.shortcutCategories, + selectedCategoryType, + onCategorySelected = { selectedCategoryType = it }, + onKeyboardSettingsClicked, + modifier, + ) + } else { + ShortcutHelperTwoPane( + shortcutsUiState.searchQuery, + onSearchQueryChanged, + modifier, + shortcutsUiState.shortcutCategories, + selectedCategoryType, + onCategorySelected = { selectedCategoryType = it }, + onKeyboardSettingsClicked, + shortcutsUiState.isShortcutCustomizerFlagEnabled, + onCustomizationRequested, + shortcutsUiState.shouldShowResetButton, + ) + } +} + +@Composable private fun shouldUseSinglePane() = hasCompactWindowSize() + +@Composable +private fun ShortcutHelperSinglePane( + searchQuery: String, + onSearchQueryChanged: (String) -> Unit, + categories: List<ShortcutCategoryUi>, + selectedCategoryType: ShortcutCategoryType?, + onCategorySelected: (ShortcutCategoryType?) -> Unit, + onKeyboardSettingsClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(start = 16.dp, end = 16.dp, top = 26.dp) + ) { + TitleBar() + Spacer(modifier = Modifier.height(6.dp)) + ShortcutsSearchBar(onSearchQueryChanged) + Spacer(modifier = Modifier.height(16.dp)) + if (categories.isEmpty()) { + Box(modifier = Modifier.weight(1f)) { + NoSearchResultsText(horizontalPadding = 16.dp, fillHeight = true) + } + } else { + CategoriesPanelSinglePane( + searchQuery, + categories, + selectedCategoryType, + onCategorySelected, + ) + Spacer(modifier = Modifier.weight(1f)) + } + KeyboardSettings( + horizontalPadding = 16.dp, + verticalPadding = 32.dp, + onClick = onKeyboardSettingsClicked, + ) + } +} + +@Composable +private fun CategoriesPanelSinglePane( + searchQuery: String, + categories: List<ShortcutCategoryUi>, + selectedCategoryType: ShortcutCategoryType?, + onCategorySelected: (ShortcutCategoryType?) -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + categories.fastForEachIndexed { index, category -> + val isExpanded = selectedCategoryType == category.type + val itemShape = + if (categories.size == 1) { + ShortcutHelper.Shapes.singlePaneSingleCategory + } else if (index == 0) { + ShortcutHelper.Shapes.singlePaneFirstCategory + } else if (index == categories.lastIndex) { + ShortcutHelper.Shapes.singlePaneLastCategory + } else { + ShortcutHelper.Shapes.singlePaneCategory + } + CategoryItemSinglePane( + searchQuery = searchQuery, + category = category, + isExpanded = isExpanded, + onClick = { + onCategorySelected( + if (isExpanded) { + null + } else { + category.type + } + ) + }, + shape = itemShape, + ) + } + } +} + +@Composable +private fun CategoryItemSinglePane( + searchQuery: String, + category: ShortcutCategoryUi, + isExpanded: Boolean, + onClick: () -> Unit, + shape: Shape, +) { + Surface(color = MaterialTheme.colorScheme.surfaceBright, shape = shape, onClick = onClick) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp), + ) { + ShortcutCategoryIcon(modifier = Modifier.size(24.dp), source = category.iconSource) + Spacer(modifier = Modifier.width(16.dp)) + Text(category.label) + Spacer(modifier = Modifier.weight(1f)) + RotatingExpandCollapseIcon(isExpanded) + } + AnimatedVisibility(visible = isExpanded) { + ShortcutCategoryDetailsSinglePane(searchQuery, category) + } + } + } +} + +@Composable +fun ShortcutCategoryIcon( + source: IconSource, + modifier: Modifier = Modifier, + contentDescription: String? = null, + tint: Color = LocalContentColor.current, +) { + if (source.imageVector != null) { + Icon(source.imageVector, contentDescription, modifier, tint) + } else if (source.painter != null) { + Image(source.painter, contentDescription, modifier) + } +} + +@Composable +private fun RotatingExpandCollapseIcon(isExpanded: Boolean) { + val expandIconRotationDegrees by + animateFloatAsState( + targetValue = + if (isExpanded) { + 180f + } else { + 0f + }, + label = "Expand icon rotation animation", + ) + Icon( + modifier = + Modifier.background( + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = CircleShape, + ) + .graphicsLayer { rotationZ = expandIconRotationDegrees }, + imageVector = Icons.Default.ExpandMore, + contentDescription = + if (isExpanded) { + stringResource(R.string.shortcut_helper_content_description_collapse_icon) + } else { + stringResource(R.string.shortcut_helper_content_description_expand_icon) + }, + tint = MaterialTheme.colorScheme.onSurface, + ) +} + +@Composable +private fun ShortcutCategoryDetailsSinglePane(searchQuery: String, category: ShortcutCategoryUi) { + Column(Modifier.padding(horizontal = 16.dp)) { + category.subCategories.fastForEach { subCategory -> + ShortcutSubCategorySinglePane(searchQuery, subCategory) + } + } +} + +@Composable +private fun ShortcutSubCategorySinglePane(searchQuery: String, subCategory: ShortcutSubCategory) { + // This @Composable is expected to be in a Column. + SubCategoryTitle(subCategory.label) + subCategory.shortcuts.fastForEachIndexed { index, shortcut -> + if (index > 0) { + HorizontalDivider(color = MaterialTheme.colorScheme.surfaceContainerHigh) + } + Shortcut(Modifier.padding(vertical = 24.dp), searchQuery, shortcut) + } +} + +@Composable +private fun ShortcutHelperTwoPane( + searchQuery: String, + onSearchQueryChanged: (String) -> Unit, + modifier: Modifier = Modifier, + categories: List<ShortcutCategoryUi>, + selectedCategoryType: ShortcutCategoryType?, + onCategorySelected: (ShortcutCategoryType?) -> Unit, + onKeyboardSettingsClicked: () -> Unit, + isShortcutCustomizerFlagEnabled: Boolean, + onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, + shouldShowResetButton: Boolean, +) { + val selectedCategory = categories.fastFirstOrNull { it.type == selectedCategoryType } + var isCustomizing by remember { mutableStateOf(false) } + + Column(modifier = modifier.fillMaxSize().padding(horizontal = 24.dp)) { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + // Keep title centered whether customize button is visible or not. + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + TitleBar(isCustomizing) + } + if (isShortcutCustomizerFlagEnabled) { + CustomizationButtonsContainer( + isCustomizing = isCustomizing, + onToggleCustomizationMode = { isCustomizing = !isCustomizing }, + onReset = { + onCustomizationRequested(ShortcutCustomizationRequestInfo.Reset) + }, + shouldShowResetButton = shouldShowResetButton, + ) + } else { + Spacer(modifier = Modifier.width(if (isCustomizing) 69.dp else 133.dp)) + } + } + } + Spacer(modifier = Modifier.height(12.dp)) + Row(Modifier.fillMaxWidth()) { + StartSidePanel( + onSearchQueryChanged = onSearchQueryChanged, + modifier = Modifier.width(240.dp).semantics { isTraversalGroup = true }, + categories = categories, + onKeyboardSettingsClicked = onKeyboardSettingsClicked, + selectedCategory = selectedCategoryType, + onCategoryClicked = { onCategorySelected(it.type) }, + ) + Spacer(modifier = Modifier.width(24.dp)) + EndSidePanel( + searchQuery, + Modifier.fillMaxSize().padding(top = 8.dp).semantics { isTraversalGroup = true }, + selectedCategory, + isCustomizing = isCustomizing, + onCustomizationRequested = onCustomizationRequested, + ) + } + } +} + +@Composable +private fun CustomizationButtonsContainer( + isCustomizing: Boolean, + shouldShowResetButton: Boolean, + onToggleCustomizationMode: () -> Unit, + onReset: () -> Unit, +) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if (isCustomizing) { + if (shouldShowResetButton) { + ResetButton(onClick = onReset) + } + DoneButton(onClick = onToggleCustomizationMode) + } else { + CustomizeButton(onClick = onToggleCustomizationMode) + } + } +} + +@Composable +private fun ResetButton(onClick: () -> Unit) { + ShortcutHelperButton( + modifier = Modifier.heightIn(40.dp), + onClick = onClick, + color = Color.Transparent, + iconSource = IconSource(imageVector = Icons.Default.Refresh), + text = stringResource(id = R.string.shortcut_helper_reset_button_text), + contentColor = MaterialTheme.colorScheme.primary, + border = BorderStroke(color = MaterialTheme.colorScheme.outlineVariant, width = 1.dp), + ) +} + +@Composable +private fun CustomizeButton(onClick: () -> Unit) { + ShortcutHelperButton( + modifier = Modifier.heightIn(40.dp), + onClick = onClick, + color = MaterialTheme.colorScheme.secondaryContainer, + iconSource = IconSource(imageVector = Icons.Default.Tune), + text = stringResource(id = R.string.shortcut_helper_customize_button_text), + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ) +} + +@Composable +private fun DoneButton(onClick: () -> Unit) { + ShortcutHelperButton( + modifier = Modifier.heightIn(40.dp), + onClick = onClick, + color = MaterialTheme.colorScheme.primary, + text = stringResource(R.string.shortcut_helper_done_button_text), + contentColor = MaterialTheme.colorScheme.onPrimary, + ) +} + +@Composable +private fun EndSidePanel( + searchQuery: String, + modifier: Modifier, + category: ShortcutCategoryUi?, + isCustomizing: Boolean, + onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, +) { + val listState = rememberLazyListState() + LaunchedEffect(key1 = category) { if (category != null) listState.animateScrollToItem(0) } + if (category == null) { + NoSearchResultsText(horizontalPadding = 24.dp, fillHeight = false) + return + } + LazyColumn(modifier = modifier, state = listState) { + items(category.subCategories) { subcategory -> + SubCategoryContainerDualPane( + searchQuery = searchQuery, + subCategory = subcategory, + isCustomizing = isCustomizing and category.type.includeInCustomization, + onCustomizationRequested = { requestInfo -> + when (requestInfo) { + is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add -> + onCustomizationRequested(requestInfo.copy(categoryType = category.type)) + + is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete -> + onCustomizationRequested(requestInfo.copy(categoryType = category.type)) + + ShortcutCustomizationRequestInfo.Reset -> + onCustomizationRequested(requestInfo) + } + }, + ) + Spacer(modifier = Modifier.height(8.dp)) + } + } +} + +@Composable +private fun NoSearchResultsText(horizontalPadding: Dp, fillHeight: Boolean) { + var modifier = Modifier.fillMaxWidth() + if (fillHeight) { + modifier = modifier.fillMaxHeight() + } + Text( + stringResource(R.string.shortcut_helper_no_search_results), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = + modifier + .padding(vertical = 8.dp) + .background(MaterialTheme.colorScheme.surfaceBright, RoundedCornerShape(28.dp)) + .padding(horizontal = horizontalPadding, vertical = 24.dp), + ) +} + +@Composable +private fun SubCategoryContainerDualPane( + searchQuery: String, + subCategory: ShortcutSubCategory, + isCustomizing: Boolean, + onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceBright, + ) { + Column(Modifier.padding(16.dp)) { + SubCategoryTitle(subCategory.label) + Spacer(Modifier.height(8.dp)) + subCategory.shortcuts.fastForEachIndexed { index, shortcut -> + if (index > 0) { + HorizontalDivider( + modifier = Modifier.padding(horizontal = 8.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) + } + Shortcut( + modifier = Modifier.padding(vertical = 8.dp), + searchQuery = searchQuery, + shortcut = shortcut, + isCustomizing = isCustomizing && shortcut.isCustomizable, + onCustomizationRequested = { requestInfo -> + when (requestInfo) { + is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add -> + onCustomizationRequested( + requestInfo.copy(subCategoryLabel = subCategory.label) + ) + + is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete -> + onCustomizationRequested( + requestInfo.copy(subCategoryLabel = subCategory.label) + ) + + ShortcutCustomizationRequestInfo.Reset -> + onCustomizationRequested(requestInfo) + } + }, + ) + } + } + } +} + +@Composable +private fun SubCategoryTitle(title: String) { + Text( + title, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary, + ) +} + +@Composable +private fun Shortcut( + modifier: Modifier, + searchQuery: String, + shortcut: ShortcutModel, + isCustomizing: Boolean = false, + onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, +) { + val interactionSource = remember { MutableInteractionSource() } + val isFocused by interactionSource.collectIsFocusedAsState() + val focusColor = MaterialTheme.colorScheme.secondary + Row( + modifier + .thenIf(isFocused) { + Modifier.border(width = 3.dp, color = focusColor, shape = RoundedCornerShape(16.dp)) + } + .focusable(interactionSource = interactionSource) + .padding(8.dp) + .semantics(mergeDescendants = true) { contentDescription = shortcut.contentDescription } + ) { + Row( + modifier = + Modifier.width(128.dp).align(Alignment.CenterVertically).weight(0.333f).semantics { + hideFromAccessibility() + }, + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (shortcut.icon != null) { + ShortcutIcon( + shortcut.icon, + modifier = Modifier.size(24.dp).semantics { hideFromAccessibility() }, + ) + } + ShortcutDescriptionText( + searchQuery = searchQuery, + shortcut = shortcut, + modifier = Modifier.semantics { hideFromAccessibility() }, + ) + } + Spacer(modifier = Modifier.width(24.dp).semantics { hideFromAccessibility() }) + ShortcutKeyCombinations( + modifier = Modifier.weight(.666f).semantics { hideFromAccessibility() }, + shortcut = shortcut, + isCustomizing = isCustomizing, + onAddShortcutRequested = { + onCustomizationRequested( + ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add( + label = shortcut.label, + shortcutCommand = shortcut.commands.first(), + ) + ) + }, + onDeleteShortcutRequested = { + onCustomizationRequested( + ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete( + label = shortcut.label, + shortcutCommand = shortcut.commands.first(), + ) + ) + }, + ) + } +} + +@Composable +fun ShortcutIcon( + icon: ShortcutIcon, + modifier: Modifier = Modifier, + contentDescription: String? = null, +) { + val context = LocalContext.current + val drawable = + remember(icon.packageName, icon.resourceId) { + Icon.createWithResource(icon.packageName, icon.resourceId).loadDrawable(context) + } ?: return + Image( + painter = rememberDrawablePainter(drawable), + contentDescription = contentDescription, + modifier = modifier, + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ShortcutKeyCombinations( + modifier: Modifier = Modifier, + shortcut: ShortcutModel, + isCustomizing: Boolean = false, + onAddShortcutRequested: () -> Unit = {}, + onDeleteShortcutRequested: () -> Unit = {}, +) { + FlowRow( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + itemVerticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + ) { + shortcut.commands.forEachIndexed { index, command -> + if (index > 0) { + ShortcutOrSeparator(spacing = 16.dp) + } + ShortcutCommandContainer(showBackground = command.isCustom) { ShortcutCommand(command) } + } + + if (isCustomizing) Spacer(modifier = Modifier.width(16.dp)) + + AnimatedVisibility(visible = isCustomizing) { + if (shortcut.containsCustomShortcutCommands) { + DeleteShortcutButton(onDeleteShortcutRequested) + } else { + AddShortcutButton(onAddShortcutRequested) + } + } + } +} + +@Composable +private fun AddShortcutButton(onClick: () -> Unit) { + ShortcutHelperButton( + modifier = Modifier.size(32.dp), + onClick = onClick, + color = Color.Transparent, + iconSource = IconSource(imageVector = Icons.Default.Add), + contentColor = MaterialTheme.colorScheme.primary, + contentPaddingVertical = 0.dp, + contentPaddingHorizontal = 0.dp, + contentDescription = stringResource(R.string.shortcut_helper_add_shortcut_button_label), + shape = CircleShape, + border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.outline), + ) +} + +@Composable +private fun DeleteShortcutButton(onClick: () -> Unit) { + ShortcutHelperButton( + modifier = Modifier.size(32.dp), + onClick = onClick, + color = Color.Transparent, + iconSource = IconSource(imageVector = Icons.Default.DeleteOutline), + contentColor = MaterialTheme.colorScheme.primary, + contentPaddingVertical = 0.dp, + contentPaddingHorizontal = 0.dp, + contentDescription = stringResource(R.string.shortcut_helper_delete_shortcut_button_label), + shape = CircleShape, + border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.outline), + ) +} + +@Composable +private fun ShortcutCommandContainer(showBackground: Boolean, content: @Composable () -> Unit) { + if (showBackground) { + Box( + modifier = + Modifier.wrapContentSize() + .background( + color = MaterialTheme.colorScheme.outlineVariant, + shape = RoundedCornerShape(16.dp), + ) + .padding(4.dp) + ) { + content() + } + } else { + content() + } +} + +@Composable +private fun ShortcutCommand(command: ShortcutCommand) { + Row { + command.keys.forEachIndexed { keyIndex, key -> + if (keyIndex > 0) { + Spacer(Modifier.width(4.dp)) + } + ShortcutKeyContainer { + if (key is ShortcutKey.Text) { + ShortcutTextKey(key) + } else if (key is ShortcutKey.Icon) { + ShortcutIconKey(key) + } + } + } + } +} + +@Composable +private fun ShortcutKeyContainer(shortcutKeyContent: @Composable BoxScope.() -> Unit) { + Box( + modifier = + Modifier.height(36.dp) + .background( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(12.dp), + ) + ) { + shortcutKeyContent() + } +} + +@Composable +private fun BoxScope.ShortcutTextKey(key: ShortcutKey.Text) { + Text( + text = key.value, + modifier = + Modifier.align(Alignment.Center).padding(horizontal = 12.dp).semantics { + hideFromAccessibility() + }, + style = MaterialTheme.typography.titleSmall, + ) +} + +@Composable +private fun BoxScope.ShortcutIconKey(key: ShortcutKey.Icon) { + Icon( + painter = + when (key) { + is ShortcutKey.Icon.ResIdIcon -> painterResource(key.drawableResId) + is ShortcutKey.Icon.DrawableIcon -> rememberDrawablePainter(drawable = key.drawable) + }, + contentDescription = null, + modifier = Modifier.align(Alignment.Center).padding(6.dp), + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun FlowRowScope.ShortcutOrSeparator(spacing: Dp) { + Spacer(Modifier.width(spacing)) + Text( + text = stringResource(R.string.shortcut_helper_key_combinations_or_separator), + modifier = Modifier.align(Alignment.CenterVertically).semantics { hideFromAccessibility() }, + style = MaterialTheme.typography.titleSmall, + ) + Spacer(Modifier.width(spacing)) +} + +@Composable +private fun ShortcutDescriptionText( + searchQuery: String, + shortcut: ShortcutModel, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier, + text = textWithHighlightedSearchQuery(shortcut.label, searchQuery), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface, + ) +} + +@Composable +private fun textWithHighlightedSearchQuery(text: String, searchValue: String) = + buildAnnotatedString { + val searchIndex = text.lowercase().indexOf(searchValue.trim().lowercase()) + val postSearchIndex = searchIndex + searchValue.trim().length + + if (searchIndex > 0) { + val preSearchText = text.substring(0, searchIndex) + append(preSearchText) + } + if (searchIndex >= 0) { + val searchText = text.substring(searchIndex, postSearchIndex) + withStyle(style = SpanStyle(background = MaterialTheme.colorScheme.primaryContainer)) { + append(searchText) + } + if (postSearchIndex < text.length) { + val postSearchText = text.substring(postSearchIndex) + append(postSearchText) + } + } else { + append(text) + } + } + +@Composable +private fun StartSidePanel( + onSearchQueryChanged: (String) -> Unit, + modifier: Modifier, + categories: List<ShortcutCategoryUi>, + onKeyboardSettingsClicked: () -> Unit, + selectedCategory: ShortcutCategoryType?, + onCategoryClicked: (ShortcutCategoryUi) -> Unit, +) { + CompositionLocalProvider( + // Restrict system font scale increases up to a max so categories display correctly. + LocalDensity provides + Density( + density = LocalDensity.current.density, + fontScale = LocalDensity.current.fontScale.coerceIn(1f, 1.5f), + ) + ) { + Column(modifier) { + ShortcutsSearchBar(onSearchQueryChanged) + Spacer(modifier = Modifier.heightIn(8.dp)) + CategoriesPanelTwoPane(categories, selectedCategory, onCategoryClicked) + Spacer(modifier = Modifier.weight(1f)) + KeyboardSettings( + horizontalPadding = 24.dp, + verticalPadding = 24.dp, + onKeyboardSettingsClicked, + ) + } + } +} + +@Composable +private fun CategoriesPanelTwoPane( + categories: List<ShortcutCategoryUi>, + selectedCategory: ShortcutCategoryType?, + onCategoryClicked: (ShortcutCategoryUi) -> Unit, +) { + Column { + categories.fastForEach { + CategoryItemTwoPane( + label = it.label, + iconSource = it.iconSource, + selected = selectedCategory == it.type, + onClick = { onCategoryClicked(it) }, + ) + } + } +} + +@Composable +private fun CategoryItemTwoPane( + label: String, + iconSource: IconSource, + selected: Boolean, + onClick: () -> Unit, + colors: NavigationDrawerItemColors = + NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent), +) { + SelectableShortcutSurface( + selected = selected, + onClick = onClick, + modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 64.dp).fillMaxWidth(), + shape = RoundedCornerShape(28.dp), + color = colors.containerColor(selected).value, + interactionsConfig = + InteractionsConfig( + hoverOverlayColor = MaterialTheme.colorScheme.onSurface, + hoverOverlayAlpha = 0.11f, + pressedOverlayColor = MaterialTheme.colorScheme.onSurface, + pressedOverlayAlpha = 0.15f, + focusOutlineColor = MaterialTheme.colorScheme.secondary, + focusOutlineStrokeWidth = 3.dp, + focusOutlinePadding = 2.dp, + surfaceCornerRadius = 28.dp, + focusOutlineCornerRadius = 33.dp, + ), + ) { + Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) { + ShortcutCategoryIcon( + modifier = Modifier.size(24.dp), + source = iconSource, + contentDescription = null, + tint = colors.iconColor(selected).value, + ) + Spacer(Modifier.width(12.dp)) + Box(Modifier.weight(1f)) { + Text( + fontSize = 18.sp, + color = colors.textColor(selected).value, + style = MaterialTheme.typography.titleSmall.copy(hyphens = Hyphens.Auto), + text = label, + ) + } + } + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun TitleBar(isCustomizing: Boolean = false) { + val text = + if (isCustomizing) { + stringResource(R.string.shortcut_helper_customize_mode_title) + } else { + stringResource(R.string.shortcut_helper_title) + } + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color.Transparent), + title = { + Text( + text = text, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.headlineSmall, + ) + }, + windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp), + expandedHeight = 64.dp, + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun ShortcutsSearchBar(onQueryChange: (String) -> Unit) { + // Using an "internal query" to make sure the SearchBar is immediately updated, otherwise + // the cursor moves to the wrong position sometimes, when waiting for the query to come back + // from the ViewModel. + var queryInternal by remember { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + LaunchedEffect(Unit) { + // TODO(b/272065229): Added minor delay so TalkBack can take focus of search box by default, + // remove when default a11y focus is fixed. + delay(50) + focusRequester.requestFocus() + } + SearchBar( + modifier = + Modifier.fillMaxWidth().focusRequester(focusRequester).onKeyEvent { + if (it.key == Key.DirectionDown) { + focusManager.moveFocus(FocusDirection.Down) + return@onKeyEvent true + } else { + return@onKeyEvent false + } + }, + colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceBright), + query = queryInternal, + active = false, + onActiveChange = {}, + onQueryChange = { + queryInternal = it + onQueryChange(it) + }, + onSearch = {}, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, + placeholder = { Text(text = stringResource(R.string.shortcut_helper_search_placeholder)) }, + windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp), + content = {}, + ) +} + +@Composable +private fun KeyboardSettings(horizontalPadding: Dp, verticalPadding: Dp, onClick: () -> Unit) { + ClickableShortcutSurface( + onClick = onClick, + shape = RoundedCornerShape(24.dp), + color = Color.Transparent, + modifier = + Modifier.semantics { role = Role.Button }.fillMaxWidth().padding(horizontal = 12.dp), + interactionsConfig = + InteractionsConfig( + hoverOverlayColor = MaterialTheme.colorScheme.onSurface, + hoverOverlayAlpha = 0.11f, + pressedOverlayColor = MaterialTheme.colorScheme.onSurface, + pressedOverlayAlpha = 0.15f, + focusOutlineColor = MaterialTheme.colorScheme.secondary, + focusOutlinePadding = 8.dp, + focusOutlineStrokeWidth = 3.dp, + surfaceCornerRadius = 24.dp, + focusOutlineCornerRadius = 28.dp, + hoverPadding = 8.dp, + ), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = + stringResource(id = R.string.shortcut_helper_keyboard_settings_buttons_label), + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 16.sp, + style = MaterialTheme.typography.titleSmall, + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = Icons.AutoMirrored.Default.OpenInNew, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp), + ) + } + } +} + +object ShortcutHelper { + + object Shapes { + val singlePaneFirstCategory = + RoundedCornerShape( + topStart = Dimensions.SinglePaneCategoryCornerRadius, + topEnd = Dimensions.SinglePaneCategoryCornerRadius, + ) + val singlePaneLastCategory = + RoundedCornerShape( + bottomStart = Dimensions.SinglePaneCategoryCornerRadius, + bottomEnd = Dimensions.SinglePaneCategoryCornerRadius, + ) + val singlePaneSingleCategory = + RoundedCornerShape(size = Dimensions.SinglePaneCategoryCornerRadius) + val singlePaneCategory = RectangleShape + } + + object Dimensions { + val SinglePaneCategoryCornerRadius = 28.dp + } + + internal const val TAG = "ShortcutHelperUI" +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 1d36076347bf..c1a59f180225 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -671,7 +671,7 @@ public class KeyguardService extends Service { checkPermission(); if (SceneContainerFlag.isEnabled()) { - mDeviceEntryInteractorLazy.get().lockNow(); + mDeviceEntryInteractorLazy.get().lockNow("doKeyguardTimeout"); } else if (KeyguardWmStateRefactor.isEnabled()) { mKeyguardServiceLockNowInteractor.onKeyguardServiceDoKeyguardTimeout(options); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 5baef915ea01..7fed7d253efe 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -16,6 +16,7 @@ package com.android.systemui.keyguard; +import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK; import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.provider.Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT; import static android.provider.Settings.System.LOCKSCREEN_SOUNDS_ENABLED; @@ -75,6 +76,7 @@ import android.os.Bundle; import android.os.DeadObjectException; import android.os.Handler; import android.os.IBinder; +import android.os.IRemoteCallback; import android.os.Looper; import android.os.Message; import android.os.PowerManager; @@ -190,6 +192,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; +import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -271,6 +275,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private static final int SYSTEM_READY = 18; private static final int CANCEL_KEYGUARD_EXIT_ANIM = 19; private static final int BOOT_INTERACTOR = 20; + private static final int BEFORE_USER_SWITCHING = 21; + private static final int USER_SWITCHING = 22; + private static final int USER_SWITCH_COMPLETE = 23; /** Enum for reasons behind updating wakeAndUnlock state. */ @Retention(RetentionPolicy.SOURCE) @@ -288,6 +295,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, int WAKE_AND_UNLOCK = 3; } + private final List<LockNowCallback> mLockNowCallbacks = new ArrayList<>(); + /** * The default amount of time we stay awake (used for all key input) */ @@ -345,6 +354,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final ScreenOffAnimationController mScreenOffAnimationController; private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController; private final Lazy<ShadeController> mShadeController; + /* + * Records the user id on request to go away, for validation when WM calls back to start the + * exit animation. + */ + private int mGoingAwayRequestedForUserId = -1; private boolean mSystemReady; private boolean mBootCompleted; @@ -352,7 +366,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private boolean mShuttingDown; private boolean mDozing; private boolean mAnimatingScreenOff; - private boolean mIgnoreDismiss; private final Context mContext; private final FalsingCollector mFalsingCollector; @@ -615,41 +628,92 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }; - KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { + @VisibleForTesting + protected UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { @Override - public void onKeyguardVisibilityChanged(boolean visible) { - synchronized (KeyguardViewMediator.this) { - if (!visible && mPendingPinLock) { - Log.i(TAG, "PIN lock requested, starting keyguard"); + public void onBeforeUserSwitching(int newUser, @NonNull Runnable resultCallback) { + mHandler.sendMessage(mHandler.obtainMessage(BEFORE_USER_SWITCHING, + newUser, 0, resultCallback)); + } - // Bring the keyguard back in order to show the PIN lock - mPendingPinLock = false; - doKeyguardLocked(null); - } - } + @Override + public void onUserChanging(int newUser, @NonNull Context userContext, + @NonNull Runnable resultCallback) { + mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCHING, + newUser, 0, resultCallback)); } @Override - public void onUserSwitching(int userId) { - Log.d(TAG, String.format("onUserSwitching %d", userId)); - synchronized (KeyguardViewMediator.this) { - mIgnoreDismiss = true; - notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId)); - resetKeyguardDonePendingLocked(); + public void onUserChanged(int newUser, Context userContext) { + mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCH_COMPLETE, + newUser, 0)); + } + }; + + /** + * Handle {@link #BEFORE_USER_SWITCHING} + */ + @VisibleForTesting + void handleBeforeUserSwitching(int userId, Runnable resultCallback) { + Log.d(TAG, String.format("onBeforeUserSwitching %d", userId)); + synchronized (KeyguardViewMediator.this) { + mHandler.removeMessages(DISMISS); + notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId)); + resetKeyguardDonePendingLocked(); + adjustStatusBarLocked(); + mKeyguardStateController.notifyKeyguardGoingAway(false); + if (mLockPatternUtils.isSecure(userId) && !mShowing) { + doKeyguardLocked(null); + } else { resetStateLocked(); - adjustStatusBarLocked(); } + resultCallback.run(); } + } - @Override - public void onUserSwitchComplete(int userId) { - mIgnoreDismiss = false; - Log.d(TAG, String.format("onUserSwitchComplete %d", userId)); + /** + * Handle {@link #USER_SWITCHING} + */ + @VisibleForTesting + void handleUserSwitching(int userId, Runnable resultCallback) { + Log.d(TAG, String.format("onUserSwitching %d", userId)); + synchronized (KeyguardViewMediator.this) { + if (!mLockPatternUtils.isSecure(userId)) { + dismiss(null, null); + } + resultCallback.run(); + } + } + + /** + * Handle {@link #USER_SWITCH_COMPLETE} + */ + @VisibleForTesting + void handleUserSwitchComplete(int userId) { + Log.d(TAG, String.format("onUserSwitchComplete %d", userId)); + // Calling dismiss on a secure user will show the bouncer + if (mLockPatternUtils.isSecure(userId)) { // We are calling dismiss with a delay as there are race conditions in some scenarios // caused by async layout listeners mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500); } + } + + KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { + + @Override + public void onKeyguardVisibilityChanged(boolean visible) { + synchronized (KeyguardViewMediator.this) { + if (!visible && mPendingPinLock) { + Log.i(TAG, "PIN lock requested, starting keyguard"); + + // Bring the keyguard back in order to show the PIN lock + mPendingPinLock = false; + doKeyguardLocked(null); + } + } + } @Override public void onDeviceProvisioned() { @@ -1658,7 +1722,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, com.android.internal.R.anim.lock_screen_behind_enter); mWorkLockController = new WorkLockActivityController(mContext, mUserTracker); - + mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); + // start() can be invoked in the middle of user switching, so check for this state and issue + // the call manually as that important event was missed. + if (mUserTracker.isUserSwitching()) { + handleBeforeUserSwitching(mUserTracker.getUserId(), () -> {}); + handleUserSwitching(mUserTracker.getUserId(), () -> {}); + } mJavaAdapter.alwaysCollectFlow( mWallpaperRepository.getWallpaperSupportsAmbientMode(), this::setWallpaperSupportsAmbientMode); @@ -1707,7 +1777,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // System ready can be invoked in the middle of user switching, so check for this state // and issue the call manually as that important event was missed. if (mUserTracker.isUserSwitching()) { - mUpdateCallback.onUserSwitching(mUserTracker.getUserId()); + mUserChangedCallback.onUserChanging(mUserTracker.getUserId(), mContext, () -> {}); } } // Most services aren't available until the system reaches the ready state, so we @@ -2341,12 +2411,23 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, * Enable the keyguard if the settings are appropriate. */ private void doKeyguardLocked(Bundle options) { + int currentUserId = mSelectedUserInteractor.getSelectedUserId(); + if (options != null && options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK) != null) { + LockNowCallback callback = new LockNowCallback(currentUserId, + IRemoteCallback.Stub.asInterface( + options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK))); + synchronized (mLockNowCallbacks) { + mLockNowCallbacks.add(callback); + } + Log.d(TAG, "LockNowCallback required for user: " + callback.mUserId); + } + // if another app is disabling us, don't show if (!mExternallyEnabled && !mLockPatternUtils.isUserInLockdown( mSelectedUserInteractor.getSelectedUserId())) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled"); - + notifyLockNowCallback(); mNeedToReshowWhenReenabled = true; return; } @@ -2364,6 +2445,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // We're removing "reset" in the refactor - "resetting" the views will happen // as a reaction to the root cause of the "reset" signal. if (KeyguardWmStateRefactor.isEnabled()) { + notifyLockNowCallback(); return; } @@ -2376,6 +2458,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, + "previously hiding. It should be safe to short-circuit " + "here."); resetStateLocked(/* hideBouncer= */ false); + notifyLockNowCallback(); return; } } else { @@ -2402,6 +2485,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Log.d(TAG, "doKeyguard: not showing because device isn't provisioned and the sim is" + " not locked or missing"); } + notifyLockNowCallback(); return; } @@ -2409,6 +2493,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (mLockPatternUtils.isLockScreenDisabled(mSelectedUserInteractor.getSelectedUserId()) && !lockedOrMissing && !forceShow) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because lockscreen is off"); + notifyLockNowCallback(); return; } @@ -2456,11 +2541,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } public void dismiss(IKeyguardDismissCallback callback, CharSequence message) { - if (mIgnoreDismiss) { - android.util.Log.i(TAG, "Ignoring request to dismiss (user switch in progress?)"); - return; - } - if (mKeyguardStateController.isKeyguardGoingAway()) { Log.i(TAG, "Ignoring dismiss because we're already going away."); return; @@ -2478,7 +2558,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } private void resetStateLocked(boolean hideBouncer) { - if (DEBUG) Log.e(TAG, "resetStateLocked"); + if (DEBUG) Log.d(TAG, "resetStateLocked"); Message msg = mHandler.obtainMessage(RESET, hideBouncer ? 1 : 0, 0); mHandler.sendMessage(msg); } @@ -2726,6 +2806,18 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, message = "BOOT_INTERACTOR"; handleBootInteractor(); break; + case BEFORE_USER_SWITCHING: + message = "BEFORE_USER_SWITCHING"; + handleBeforeUserSwitching(msg.arg1, (Runnable) msg.obj); + break; + case USER_SWITCHING: + message = "USER_SWITCHING"; + handleUserSwitching(msg.arg1, (Runnable) msg.obj); + break; + case USER_SWITCH_COMPLETE: + message = "USER_SWITCH_COMPLETE"; + handleUserSwitchComplete(msg.arg1); + break; } Log.d(TAG, "KeyguardViewMediator queue processing message: " + message); } @@ -2867,6 +2959,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mUiBgExecutor.execute(() -> { Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ", " + reason + ")"); + if (showing) { + notifyLockNowCallback(); + } if (KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager if flag is enabled. @@ -2911,6 +3006,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, synchronized (KeyguardViewMediator.this) { if (!mSystemReady) { if (DEBUG) Log.d(TAG, "ignoring handleShow because system is not ready."); + notifyLockNowCallback(); return; } if (DEBUG) Log.d(TAG, "handleShow"); @@ -2969,12 +3065,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } } - private final Runnable mKeyguardGoingAwayRunnable = new Runnable() { + final Runnable mKeyguardGoingAwayRunnable = new Runnable() { @SuppressLint("MissingPermission") @Override public void run() { Trace.beginSection("KeyguardViewMediator.mKeyGuardGoingAwayRunnable"); - Log.d(TAG, "keyguardGoingAwayRunnable"); mKeyguardViewControllerLazy.get().keyguardGoingAway(); int flags = 0; @@ -3011,6 +3106,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // Handled in WmLockscreenVisibilityManager if flag is enabled. if (!KeyguardWmStateRefactor.isEnabled()) { + mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId(); + Log.d(TAG, "keyguardGoingAway requested for userId: " + + mGoingAwayRequestedForUserId); + // Don't actually hide the Keyguard at the moment, wait for window manager // until it tells us it's safe to do so with startKeyguardExitAnimation. // Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager @@ -3149,6 +3248,30 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) { Log.d(TAG, "handleStartKeyguardExitAnimation startTime=" + startTime + " fadeoutDuration=" + fadeoutDuration); + int currentUserId = mSelectedUserInteractor.getSelectedUserId(); + if (mGoingAwayRequestedForUserId != currentUserId) { + Log.e(TAG, "Not executing handleStartKeyguardExitAnimationInner() due to userId " + + "mismatch. Requested: " + mGoingAwayRequestedForUserId + ", current: " + + currentUserId); + if (finishedCallback != null) { + // There will not execute animation, send a finish callback to ensure the remote + // animation won't hang there. + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onAnimationFinished", e); + } + } + mHiding = false; + if (mLockPatternUtils.isSecure(currentUserId)) { + doKeyguardLocked(null); + } else { + resetStateLocked(); + dismiss(null, null); + } + return; + } + synchronized (KeyguardViewMediator.this) { mIsKeyguardExitAnimationCanceled = false; // Tell ActivityManager that we canceled the keyguard animation if @@ -3393,6 +3516,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, * app transition before finishing the current RemoteAnimation, or the keyguard being re-shown). */ private void handleCancelKeyguardExitAnimation() { + if (mGoingAwayRequestedForUserId != mSelectedUserInteractor.getSelectedUserId()) { + Log.e(TAG, "Setting pendingLock = true due to userId mismatch. Requested: " + + mGoingAwayRequestedForUserId + ", current: " + + mSelectedUserInteractor.getSelectedUserId()); + setPendingLock(true); + } if (mPendingLock) { Log.d(TAG, "#handleCancelKeyguardExitAnimation: keyguard exit animation cancelled. " + "There's a pending lock, so we were cancelled because the device was locked " @@ -3493,6 +3622,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mSurfaceBehindRemoteAnimationRequested = true; if (ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS && !KeyguardWmStateRefactor.isEnabled()) { + mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId(); startKeyguardTransition(false /* keyguardShowing */, false /* aodShowing */); return; } @@ -3513,6 +3643,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (!KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager. + mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId(); + Log.d(TAG, "keyguardGoingAway requested for userId: " + + mGoingAwayRequestedForUserId); mActivityTaskManagerService.keyguardGoingAway(flags); } } catch (RemoteException e) { @@ -3966,6 +4099,29 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mUiBgExecutor.execute(mTrustManager::reportKeyguardShowingChanged); } + private void notifyLockNowCallback() { + List<LockNowCallback> callbacks; + synchronized (mLockNowCallbacks) { + callbacks = new ArrayList<LockNowCallback>(mLockNowCallbacks); + mLockNowCallbacks.clear(); + } + Iterator<LockNowCallback> iter = callbacks.listIterator(); + while (iter.hasNext()) { + LockNowCallback callback = iter.next(); + iter.remove(); + if (callback.mUserId != mSelectedUserInteractor.getSelectedUserId()) { + Log.i(TAG, "Not notifying lockNowCallback due to user mismatch"); + continue; + } + Log.i(TAG, "Notifying lockNowCallback"); + try { + callback.mRemoteCallback.sendResult(null); + } catch (RemoteException e) { + Log.e(TAG, "Could not issue LockNowCallback sendResult", e); + } + } + } + private void notifyTrustedChangedLocked(boolean trusted) { int size = mKeyguardStateCallbacks.size(); for (int i = size - 1; i >= 0; i--) { @@ -4130,4 +4286,14 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }; } + + private class LockNowCallback { + final int mUserId; + final IRemoteCallback mRemoteCallback; + + LockNowCallback(int userId, IRemoteCallback remoteCallback) { + mUserId = userId; + mRemoteCallback = remoteCallback; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 621cc4666d31..aaad10140a92 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -326,10 +326,7 @@ interface KeyguardRepository { fun setShortcutAbsoluteTop(top: Float) - /** - * Set bottom of notifications from notification stack, and Magic Portrait will layout base on - * this value - */ + /** Set bottom of notifications from notification stack */ fun setNotificationStackAbsoluteBottom(bottom: Float) fun setWallpaperFocalAreaBounds(bounds: RectF) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 8c6037107c5a..cf712f111034 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -19,8 +19,12 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import android.util.MathUtils import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags.communalSceneKtfRefactor +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -39,6 +43,10 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.sample +import java.util.UUID +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -47,11 +55,6 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds -import java.util.UUID -import javax.inject.Inject -import com.android.app.tracing.coroutines.launchTraced as launch @SysUISingleton class FromLockscreenTransitionInteractor @@ -68,6 +71,8 @@ constructor( powerInteractor: PowerInteractor, private val glanceableHubTransitions: GlanceableHubTransitions, private val communalSettingsInteractor: CommunalSettingsInteractor, + private val communalInteractor: CommunalInteractor, + private val communalSceneInteractor: CommunalSceneInteractor, private val swipeToDismissInteractor: SwipeToDismissInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : @@ -94,6 +99,9 @@ constructor( if (!communalSceneKtfRefactor()) { listenForLockscreenToGlanceableHub() } + if (communalSettingsInteractor.isV2FlagEnabled()) { + listenForLockscreenToGlanceableHubV2() + } } /** @@ -268,9 +276,7 @@ constructor( it.transitionState == TransitionState.CANCELED && it.to == KeyguardState.PRIMARY_BOUNCER } - .collect { - transitionId = null - } + .collect { transitionId = null } } } @@ -370,6 +376,19 @@ constructor( } } + private fun listenForLockscreenToGlanceableHubV2() { + scope.launch { + communalInteractor.shouldShowCommunal + .filterRelevantKeyguardStateAnd { shouldShow -> shouldShow } + .collect { + communalSceneInteractor.changeScene( + newScene = CommunalScenes.Communal, + loggingReason = "lockscreen to communal", + ) + } + } + } + override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator { return ValueAnimator().apply { interpolator = Interpolators.LINEAR diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt index 42cbd7d39248..a1f288edcdd3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt @@ -24,6 +24,8 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.kotlin.sample @@ -32,6 +34,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -166,4 +169,14 @@ constructor( isKeyguardEnabled.value && lockPatternUtils.isLockScreenDisabled(userId) } } + + suspend fun hydrateTableLogBuffer(tableLogBuffer: TableLogBuffer) { + isKeyguardEnabled + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnName = "isKeyguardEnabled", + initialValue = isKeyguardEnabled.value, + ) + .collect() + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 75178f0ffef0..3739d17da6c4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -42,6 +42,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -60,6 +62,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.debounce @@ -533,6 +536,16 @@ constructor( repository.setNotificationStackAbsoluteBottom(bottom) } + suspend fun hydrateTableLogBuffer(tableLogBuffer: TableLogBuffer) { + isDozing + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnName = "isDozing", + initialValue = isDozing.value, + ) + .collect() + } + companion object { private const val TAG = "KeyguardInteractor" /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index de5088c3521c..898b68d0f4b4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -475,7 +475,7 @@ constructor( KeyguardPickerFlag( name = Contract.FlagsTable.FLAG_NAME_CUSTOM_CLOCKS_ENABLED, value = - com.android.systemui.Flags.lockscreenCustomClocks() || + com.android.systemui.shared.Flags.lockscreenCustomClocks() || featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS), ), KeyguardPickerFlag( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 89bc4bc9a1e0..58fb4230ccf5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -125,7 +125,7 @@ constructor( repository.transitions .pairwise() .filter { it.newValue.transitionState == TransitionState.STARTED } - .shareIn(scope, SharingStarted.Eagerly) + .shareIn(scope, SharingStarted.Eagerly, replay = 1) init { // Collect non-canceled steps and emit transition values. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt index 8429c23d2018..0b116ded42da 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt @@ -15,9 +15,11 @@ */ package com.android.systemui.keyguard.domain.interactor +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.keyguard.logging.ScrimLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.DEFAULT_REVEAL_DURATION import com.android.systemui.keyguard.data.repository.LightRevealScrimRepository import com.android.systemui.keyguard.shared.model.Edge @@ -31,12 +33,13 @@ import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.sample import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.flow.flowOn @SysUISingleton class LightRevealScrimInteractor @@ -47,6 +50,7 @@ constructor( @Application private val scope: CoroutineScope, private val scrimLogger: ScrimLogger, private val powerInteractor: Lazy<PowerInteractor>, + @Background backgroundDispatcher: CoroutineDispatcher, ) { init { listenForStartedKeyguardTransitionStep() @@ -113,6 +117,7 @@ constructor( repository.maxAlpha } } + .flowOn(backgroundDispatcher) val revealAmount = repository.revealAmount.filter { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractor.kt index 934afe248a36..9c744d63a093 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractor.kt @@ -50,8 +50,7 @@ constructor( keyguardClockRepository: KeyguardClockRepository, wallpaperRepository: WallpaperRepository, ) { - // When there's notifications in splitshade, magic portrait shape effects should be left - // aligned in foldable + // When there's notifications in splitshade, the focal area shape effect should be left aligned private val notificationInShadeWideLayout: Flow<Boolean> = combine( shadeRepository.isShadeLayoutWide, @@ -104,7 +103,7 @@ constructor( ) val (left, right) = // tablet landscape - if (context.resources.getBoolean(R.bool.center_align_magic_portrait_shape)) { + if (context.resources.getBoolean(R.bool.center_align_focal_area_shape)) { Pair( scaledBounds.centerX() - maxFocalAreaWidth / 2F, scaledBounds.centerX() + maxFocalAreaWidth / 2F, @@ -129,7 +128,7 @@ constructor( wallpaperZoomedInScale val top = // tablet landscape - if (context.resources.getBoolean(R.bool.center_align_magic_portrait_shape)) { + if (context.resources.getBoolean(R.bool.center_align_focal_area_shape)) { // no strict constraints for top, use bottom margin to make it symmetric // vertically scaledBounds.top + scaledBottomMargin @@ -169,8 +168,8 @@ constructor( ) } - // A max width for magic portrait shape effects bounds, to avoid it going too large - // in large screen portrait mode + // A max width for focal area shape effects bounds, to avoid + // it becoming too large in large screen portrait mode const val FOCAL_AREA_MAX_WIDTH_DP = 500 } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt index 360b8a3eeaf5..61cf2cdab92d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt @@ -32,7 +32,6 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor import com.android.systemui.util.kotlin.Utils.Companion.toQuad -import com.android.systemui.util.kotlin.sample import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import dagger.Lazy import javax.inject.Inject @@ -232,12 +231,12 @@ constructor( private val lockscreenVisibilityLegacy = combine( transitionInteractor.currentKeyguardState, + transitionInteractor.startedStepWithPrecedingStep, wakeToGoneInteractor.canWakeDirectlyToGone, surfaceBehindVisibility, - ::Triple, + ::toQuad, ) - .sample(transitionInteractor.startedStepWithPrecedingStep, ::toQuad) - .map { (currentState, canWakeDirectlyToGone, surfaceBehindVis, startedWithPrev) -> + .map { (currentState, startedWithPrev, canWakeDirectlyToGone, surfaceBehindVis) -> val startedFromStep = startedWithPrev.previousValue val startedStep = startedWithPrev.newValue val returningToGoneAfterCancellation = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 70a52afeb8c2..017fe169ca88 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -56,6 +56,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotificationSection import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel @@ -184,6 +185,7 @@ object KeyguardRootViewBinder { viewModel.translationY.collect { y -> childViews[burnInLayerId]?.translationY = y childViews[largeClockId]?.translationY = y + childViews[aodPromotedNotificationId]?.translationY = y childViews[aodNotificationIconContainerId]?.translationY = y } } @@ -195,6 +197,7 @@ object KeyguardRootViewBinder { state.isToOrFrom(KeyguardState.AOD) -> { // Large Clock is not translated in the x direction childViews[burnInLayerId]?.translationX = px + childViews[aodPromotedNotificationId]?.translationX = px childViews[aodNotificationIconContainerId]?.translationX = px } state.isToOrFrom(KeyguardState.GLANCEABLE_HUB) -> { @@ -291,11 +294,17 @@ object KeyguardRootViewBinder { blueprintViewModel.refreshBlueprint() } childViews[aodNotificationIconContainerId] - ?.setAodNotifIconContainerIsVisible( - isVisible, - iconsAppearTranslationPx.value, - screenOffAnimationController, - ) + ?.setAodNotifIconContainerIsVisible(isVisible) + } + } + + launch { + viewModel.isNotifIconContainerVisible.collect { isVisible -> + if (isVisible.value) { + blueprintViewModel.refreshBlueprint() + } + childViews[aodPromotedNotificationId] + ?.setAodNotifIconContainerIsVisible(isVisible) } } @@ -524,11 +533,7 @@ object KeyguardRootViewBinder { } } - private fun View.setAodNotifIconContainerIsVisible( - isVisible: AnimatedValue<Boolean>, - iconsAppearTranslationPx: Int, - screenOffAnimationController: ScreenOffAnimationController, - ) { + private fun View.setAodNotifIconContainerIsVisible(isVisible: AnimatedValue<Boolean>) { animate().cancel() val animatorListener = object : AnimatorListenerAdapter() { @@ -563,6 +568,7 @@ object KeyguardRootViewBinder { } private val burnInLayerId = R.id.burn_in_layer + private val aodPromotedNotificationId = AodPromotedNotificationSection.viewId private val aodNotificationIconContainerId = R.id.aod_notification_icon_container private val largeClockId = customR.id.lockscreen_clock_view_large private val smallClockId = customR.id.lockscreen_clock_view diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt index 13c2ffb70220..220846d08de7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.binder import android.graphics.Rect +import android.util.TypedValue import android.view.View import android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED import android.widget.TextView @@ -101,6 +102,13 @@ object KeyguardSettingsViewBinder { } } } + + launch("$TAG#viewModel.textSize") { + viewModel.textSize.collect { textSize -> + val textView: TextView = view.requireViewById(R.id.text) + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize.toFloat()) + } + } } } return disposableHandle diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/InWindowLauncherUnlockAnimationManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/InWindowLauncherUnlockAnimationManager.kt index 454ba9af5745..d2808627163e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/InWindowLauncherUnlockAnimationManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/InWindowLauncherUnlockAnimationManager.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.view import android.graphics.Rect +import android.os.DeadObjectException import android.util.Log import android.view.View import com.android.systemui.dagger.SysUISingleton @@ -192,7 +193,12 @@ constructor( launcherAnimationController?.let { manualUnlockAmount = amount - it.setUnlockAmount(amount, forceIfAnimating) + + try { + it.setUnlockAmount(amount, forceIfAnimating) + } catch (e: DeadObjectException) { + Log.e(TAG, "DeadObjectException in setUnlockAmount($amount, $forceIfAnimating)", e) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt index 856e1d6ffdb7..8b213be02969 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt @@ -23,6 +23,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.view.layout.sections.AccessibilityActionsSection import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection +import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotificationSection import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection @@ -58,6 +59,7 @@ constructor( defaultStatusBarSection: DefaultStatusBarSection, splitShadeNotificationStackScrollLayoutSection: SplitShadeNotificationStackScrollLayoutSection, splitShadeGuidelines: SplitShadeGuidelines, + aodPromotedNotificationSection: AodPromotedNotificationSection, aodNotificationIconsSection: AodNotificationIconsSection, aodBurnInSection: AodBurnInSection, clockSection: ClockSection, @@ -76,6 +78,7 @@ constructor( defaultStatusBarSection, splitShadeNotificationStackScrollLayoutSection, splitShadeGuidelines, + aodPromotedNotificationSection, aodNotificationIconsSection, smartspaceSection, aodBurnInSection, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt index ed1bdb0e2922..ea4acce037b8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt @@ -26,6 +26,7 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.promoted.AODPromotedNotification import com.android.systemui.statusbar.notification.promoted.PromotedNotificationLogger import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod @@ -36,6 +37,7 @@ class AodPromotedNotificationSection @Inject constructor( private val viewModelFactory: AODPromotedNotificationViewModel.Factory, + private val shadeInteractor: ShadeInteractor, private val logger: PromotedNotificationLogger, ) : KeyguardSection() { var view: ComposeView? = null @@ -77,9 +79,12 @@ constructor( checkNotNull(view) constraintSet.apply { + val isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value + val endGuidelineId = if (isShadeLayoutWide) R.id.split_shade_guideline else PARENT_ID + connect(viewId, TOP, R.id.smart_space_barrier_bottom, BOTTOM, 0) connect(viewId, START, PARENT_ID, START, 0) - connect(viewId, END, PARENT_ID, END, 0) + connect(viewId, END, endGuidelineId, END, 0) constrainWidth(viewId, ConstraintSet.MATCH_CONSTRAINT) constrainHeight(viewId, ConstraintSet.WRAP_CONTENT) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt index 84fdc6e3a433..13cd5839e1c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt @@ -262,16 +262,6 @@ constructor( deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() } - private fun DeviceEntryIconView.IconType.toAccessibilityHintType(): - DeviceEntryIconView.AccessibilityHintType { - return when (this) { - DeviceEntryIconView.IconType.FINGERPRINT, - DeviceEntryIconView.IconType.LOCK -> DeviceEntryIconView.AccessibilityHintType.BOUNCER - DeviceEntryIconView.IconType.UNLOCK -> DeviceEntryIconView.AccessibilityHintType.ENTER - DeviceEntryIconView.IconType.NONE -> DeviceEntryIconView.AccessibilityHintType.NONE - } - } - companion object { const val UNLOCKED_DELAY_MS = 50L } @@ -282,3 +272,13 @@ data class BurnInOffsets( val y: Int, // current y burn in offset based on the aodTransitionAmount val progress: Float, // current progress based on the aodTransitionAmount ) + +fun DeviceEntryIconView.IconType.toAccessibilityHintType(): + DeviceEntryIconView.AccessibilityHintType { + return when (this) { + DeviceEntryIconView.IconType.FINGERPRINT, + DeviceEntryIconView.IconType.LOCK -> DeviceEntryIconView.AccessibilityHintType.BOUNCER + DeviceEntryIconView.IconType.UNLOCK -> DeviceEntryIconView.AccessibilityHintType.ENTER + DeviceEntryIconView.IconType.NONE -> DeviceEntryIconView.AccessibilityHintType.NONE + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index f0c924f99033..11a509a4fa61 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -307,6 +307,16 @@ constructor( BurnInScaleViewModel(scale = it.scale, scaleClockOnly = it.scaleClockOnly) } + val isAodPromotedNotifVisible: StateFlow<Boolean> = + keyguardTransitionInteractor + .transitionValue(AOD) + .map { it == 1f } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + /** Is the notification icon container visible? */ val isNotifIconContainerVisible: StateFlow<AnimatedValue<Boolean>> = combine( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt index 36a342b13df7..4584ea24b0f2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTouchHandlingInteractor import com.android.systemui.res.R import javax.inject.Inject @@ -29,19 +30,18 @@ class KeyguardSettingsMenuViewModel @Inject constructor( private val interactor: KeyguardTouchHandlingInteractor, + configurationInteractor: ConfigurationInteractor, ) { val isVisible: Flow<Boolean> = interactor.isMenuVisible val shouldOpenSettings: Flow<Boolean> = interactor.shouldOpenSettings - val icon: Icon = - Icon.Resource( - res = R.drawable.ic_palette, - contentDescription = null, - ) + val icon: Icon = Icon.Resource(res = R.drawable.ic_palette, contentDescription = null) + + val text: Text = Text.Resource(res = R.string.lock_screen_settings) - val text: Text = - Text.Resource( - res = R.string.lock_screen_settings, + val textSize = + configurationInteractor.dimensionPixelSize( + com.android.internal.R.dimen.text_size_small_material ) fun onTouchGestureStarted() { @@ -49,9 +49,7 @@ constructor( } fun onTouchGestureEnded(isClick: Boolean) { - interactor.onMenuTouchGestureEnded( - isClick = isClick, - ) + interactor.onMenuTouchGestureEnded(isClick = isClick) } fun onSettingsShown() { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java index 4496b258bde4..7b1c62e2a0e5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java @@ -36,6 +36,7 @@ public class MediaItem { private final String mTitle; @MediaItemType private final int mMediaItemType; + private final boolean mIsFirstDeviceInGroup; @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -54,7 +55,18 @@ public class MediaItem { * name. */ public static MediaItem createDeviceMediaItem(@NonNull MediaDevice device) { - return new MediaItem(device, device.getName(), MediaItemType.TYPE_DEVICE); + return new MediaItem(device, device.getName(), MediaItemType.TYPE_DEVICE, false); + } + + /** + * Returns a new {@link MediaItemType#TYPE_DEVICE} {@link MediaItem} with its {@link + * #getMediaDevice() media device} set to {@code device} and its title set to {@code device}'s + * name. + */ + public static MediaItem createDeviceMediaItem( + @NonNull MediaDevice device, boolean isFirstDeviceInGroup) { + return new MediaItem( + device, device.getName(), MediaItemType.TYPE_DEVICE, isFirstDeviceInGroup); } /** @@ -63,7 +75,10 @@ public class MediaItem { */ public static MediaItem createPairNewDeviceMediaItem() { return new MediaItem( - /* device */ null, /* title */ null, MediaItemType.TYPE_PAIR_NEW_DEVICE); + /* device */ null, + /* title */ null, + MediaItemType.TYPE_PAIR_NEW_DEVICE, + /* mIsFirstDeviceInGroup */ false); } /** @@ -71,14 +86,22 @@ public class MediaItem { * title and a {@code null} {@link #getMediaDevice() media device}. */ public static MediaItem createGroupDividerMediaItem(@Nullable String title) { - return new MediaItem(/* device */ null, title, MediaItemType.TYPE_GROUP_DIVIDER); + return new MediaItem( + /* device */ null, + title, + MediaItemType.TYPE_GROUP_DIVIDER, + /* misFirstDeviceInGroup */ false); } private MediaItem( - @Nullable MediaDevice device, @Nullable String title, @MediaItemType int type) { + @Nullable MediaDevice device, + @Nullable String title, + @MediaItemType int type, + boolean isFirstDeviceInGroup) { this.mMediaDeviceOptional = Optional.ofNullable(device); this.mTitle = title; this.mMediaItemType = type; + this.mIsFirstDeviceInGroup = isFirstDeviceInGroup; } public Optional<MediaDevice> getMediaDevice() { @@ -106,4 +129,8 @@ public class MediaItem { public int getMediaItemType() { return mMediaItemType; } + + public boolean isFirstDeviceInGroup() { + return mIsFirstDeviceInGroup; + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index 53f3b3a7a59d..52b3c3ecacc6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -21,6 +21,7 @@ import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECT import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER; import android.annotation.DrawableRes; +import android.annotation.StringRes; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.drawable.AnimatedVectorDrawable; @@ -38,6 +39,7 @@ import androidx.core.widget.CompoundButtonCompat; import androidx.recyclerview.widget.RecyclerView; import com.android.internal.annotations.VisibleForTesting; +import com.android.media.flags.Flags; import com.android.settingslib.media.LocalMediaManager.MediaDeviceState; import com.android.settingslib.media.MediaDevice; import com.android.systemui.res.R; @@ -55,6 +57,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { private static final float DEVICE_DISCONNECTED_ALPHA = 0.5f; private static final float DEVICE_CONNECTED_ALPHA = 1f; protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>(); + private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherSessionGrouping(); public MediaOutputAdapter(MediaSwitchingController controller) { super(controller); @@ -65,6 +68,12 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { public void updateItems() { mMediaItemList.clear(); mMediaItemList.addAll(mController.getMediaItemList()); + if (mShouldGroupSelectedMediaItems) { + if (mController.getSelectedMediaDevice().size() == 1) { + // Don't group devices if initially there isn't more than one selected. + mShouldGroupSelectedMediaItems = false; + } + } notifyDataSetChanged(); } @@ -101,7 +110,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { break; case MediaItem.MediaItemType.TYPE_DEVICE: ((MediaDeviceViewHolder) viewHolder).onBind( - currentMediaItem.getMediaDevice().get(), + currentMediaItem, position); break; default: @@ -141,8 +150,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { super(view); } - @Override - void onBind(MediaDevice device, int position) { + void onBind(MediaItem mediaItem, int position) { + MediaDevice device = mediaItem.getMediaDevice().get(); super.onBind(device, position); boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice(); final boolean currentlyConnected = isCurrentlyConnected(device); @@ -150,6 +159,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { if (mCurrentActivePosition == position) { mCurrentActivePosition = -1; } + mItemLayout.setVisibility(View.VISIBLE); mStatusIcon.setVisibility(View.GONE); enableFocusPropertyForView(mContainerLayout); @@ -174,6 +184,30 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { updateFullItemClickListener(v -> onItemClick(v, device)); setSingleLineLayout(device.getName()); initFakeActiveDevice(device); + } else if (mShouldGroupSelectedMediaItems + && mController.getSelectedMediaDevice().size() > 1 + && isDeviceIncluded(mController.getSelectedMediaDevice(), device)) { + if (!mediaItem.isFirstDeviceInGroup()) { + mItemLayout.setVisibility(View.GONE); + mEndTouchArea.setVisibility(View.GONE); + } else { + String sessionName = mController.getSessionName().toString(); + updateUnmutedVolumeIcon(null); + updateEndClickAreaWithIcon( + v -> { + mShouldGroupSelectedMediaItems = false; + notifyDataSetChanged(); + }, + R.drawable.media_output_item_expand_group, + R.string.accessibility_expand_group); + disableFocusPropertyForView(mContainerLayout); + setUpContentDescriptionForView(mSeekBar, mContext.getString( + R.string.accessibility_cast_name, sessionName)); + setSingleLineLayout(sessionName, true /* showSeekBar */, + false /* showProgressBar */, false /* showCheckBox */, + true /* showEndTouchArea */); + initGroupSeekbar(isCurrentSeekbarInvisible); + } } else if (device.hasSubtext()) { boolean isActiveWithOngoingSession = (device.hasOngoingSession() && (currentlyConnected || isDeviceIncluded( @@ -237,6 +271,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { // selected device in group boolean isDeviceDeselectable = isDeviceIncluded( mController.getDeselectableMediaDevice(), device); + boolean showEndArea = !Flags.enableOutputSwitcherSessionGrouping() + || isDeviceDeselectable; updateUnmutedVolumeIcon(device); updateGroupableCheckBox(true, isDeviceDeselectable, device); updateEndClickArea(device, isDeviceDeselectable); @@ -244,7 +280,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { setUpContentDescriptionForView(mSeekBar, device); setSingleLineLayout(device.getName(), true /* showSeekBar */, false /* showProgressBar */, true /* showCheckBox */, - true /* showEndTouchArea */); + showEndArea /* showEndTouchArea */); initSeekbar(device, isCurrentSeekbarInvisible); } else if (!mController.hasAdjustVolumeUserRestriction() && currentlyConnected) { @@ -335,19 +371,29 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } private void updateEndClickAreaAsSessionEditing(MediaDevice device, @DrawableRes int id) { - mEndClickIcon.setOnClickListener(null); - mEndTouchArea.setOnClickListener(null); + updateEndClickAreaWithIcon( + v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v), + id, + R.string.accessibility_open_application); + } + + private void updateEndClickAreaWithIcon(View.OnClickListener clickListener, + @DrawableRes int iconDrawableId, + @StringRes int accessibilityStringId) { updateEndClickAreaColor(mController.getColorSeekbarProgress()); mEndClickIcon.setImageTintList( ColorStateList.valueOf(mController.getColorItemContent())); - mEndClickIcon.setOnClickListener( - v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v)); + mEndClickIcon.setOnClickListener(clickListener); mEndTouchArea.setOnClickListener(v -> mEndClickIcon.performClick()); - Drawable drawable = mContext.getDrawable(id); + Drawable drawable = mContext.getDrawable(iconDrawableId); mEndClickIcon.setImageDrawable(drawable); if (drawable instanceof AnimatedVectorDrawable) { ((AnimatedVectorDrawable) drawable).start(); } + if (Flags.enableOutputSwitcherSessionGrouping()) { + setUpContentDescriptionForView( + mEndClickIcon, mContext.getString(accessibilityStringId)); + } } public void updateEndClickAreaColor(int color) { @@ -479,12 +525,17 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } private void setUpContentDescriptionForView(View view, MediaDevice device) { - view.setContentDescription( + setUpContentDescriptionForView( + view, mContext.getString(device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE ? R.string.accessibility_bluetooth_name : R.string.accessibility_cast_name, device.getName())); } + + protected void setUpContentDescriptionForView(View view, String description) { + view.setContentDescription(description); + } } class MediaGroupDividerViewHolder extends RecyclerView.ViewHolder { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index 0c8196693bc4..ee2d8aa46264 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -16,8 +16,6 @@ package com.android.systemui.media.dialog; -import static com.android.systemui.media.dialog.MediaOutputSeekbar.VOLUME_PERCENTAGE_SCALE_SIZE; - import android.animation.Animator; import android.animation.ValueAnimator; import android.app.WallpaperColors; @@ -44,6 +42,7 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.RecyclerView; +import com.android.media.flags.Flags; import com.android.settingslib.media.InputMediaDevice; import com.android.settingslib.media.MediaDevice; import com.android.settingslib.utils.ThreadUtils; @@ -213,6 +212,10 @@ public abstract class MediaOutputBaseAdapter extends mTitleText.setText(title); mCheckBox.setVisibility(showCheckBox ? View.VISIBLE : View.GONE); mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE); + if (Flags.enableOutputSwitcherSessionGrouping()) { + mEndClickIcon.setVisibility( + !showCheckBox && showEndTouchArea ? View.VISIBLE : View.GONE); + } ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams(); params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable() @@ -267,14 +270,8 @@ public abstract class MediaOutputBaseAdapter extends mController.getActiveRadius(), 0, 0}); } - void initSeekbar(MediaDevice device, boolean isCurrentSeekbarInvisible) { - if (!mController.isVolumeControlEnabled(device)) { - disableSeekBar(); - } else { - enableSeekBar(device); - } - mSeekBar.setMaxVolume(device.getMaxVolume()); - final int currentVolume = device.getCurrentVolume(); + private void initializeSeekbarVolume( + MediaDevice device, int currentVolume, boolean isCurrentSeekbarInvisible) { if (!mIsDragging) { if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1 || currentVolume == mLatestUpdateVolume)) { @@ -289,10 +286,7 @@ public abstract class MediaOutputBaseAdapter extends } } else { if (!mVolumeAnimator.isStarted()) { - int percentage = - (int) ((double) currentVolume * VOLUME_PERCENTAGE_SCALE_SIZE - / (double) mSeekBar.getMax()); - if (percentage == 0) { + if (currentVolume == 0) { updateMutedVolumeIcon(device); } else { updateUnmutedVolumeIcon(device); @@ -312,54 +306,75 @@ public abstract class MediaOutputBaseAdapter extends if (mIsInitVolumeFirstTime) { mIsInitVolumeFirstTime = false; } - mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - boolean mStartFromMute = false; + } + + void initSeekbar(MediaDevice device, boolean isCurrentSeekbarInvisible) { + SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() { @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (device == null || !fromUser) { - return; - } - int progressToVolume = MediaOutputSeekbar.scaleProgressToVolume(progress); - int deviceVolume = device.getCurrentVolume(); - int percentage = - (int) ((double) progressToVolume * VOLUME_PERCENTAGE_SCALE_SIZE - / (double) seekBar.getMax()); - mVolumeValueText.setText(mContext.getResources().getString( - R.string.media_output_dialog_volume_percentage, percentage)); - if (mStartFromMute) { - updateUnmutedVolumeIcon(device); - mStartFromMute = false; - } - if (progressToVolume != deviceVolume) { - mLatestUpdateVolume = progressToVolume; - mController.adjustVolume(device, progressToVolume); - } + public int getVolume() { + return device.getCurrentVolume(); + } + @Override + public void setVolume(int volume) { + mController.adjustVolume(device, volume); } @Override - public void onStartTrackingTouch(SeekBar seekBar) { - mTitleIcon.setVisibility(View.INVISIBLE); - mVolumeValueText.setVisibility(View.VISIBLE); - int currentVolume = MediaOutputSeekbar.scaleProgressToVolume( - seekBar.getProgress()); - mStartFromMute = (currentVolume == 0); - mIsDragging = true; + public void onMute() { + mController.logInteractionUnmuteDevice(device); } + }; + if (!mController.isVolumeControlEnabled(device)) { + disableSeekBar(); + } else { + enableSeekBar(volumeControl); + } + mSeekBar.setMaxVolume(device.getMaxVolume()); + final int currentVolume = device.getCurrentVolume(); + initializeSeekbarVolume(device, currentVolume, isCurrentSeekbarInvisible); + + mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener( + device, volumeControl) { @Override - public void onStopTrackingTouch(SeekBar seekBar) { - int currentVolume = MediaOutputSeekbar.scaleProgressToVolume( - seekBar.getProgress()); - if (currentVolume == 0) { - seekBar.setProgress(0); - updateMutedVolumeIcon(device); - } else { - updateUnmutedVolumeIcon(device); - } - mTitleIcon.setVisibility(View.VISIBLE); - mVolumeValueText.setVisibility(View.GONE); + public void onStopTrackingTouch(SeekBar seekbar) { + super.onStopTrackingTouch(seekbar); mController.logInteractionAdjustVolume(device); - mIsDragging = false; + } + }); + } + + // Initializes the seekbar for a group of devices. + void initGroupSeekbar(boolean isCurrentSeekbarInvisible) { + SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() { + @Override + public int getVolume() { + return mController.getSessionVolume(); + } + + @Override + public void setVolume(int volume) { + mController.adjustSessionVolume(volume); + } + + @Override + public void onMute() {} + }; + + if (!mController.isVolumeControlEnabledForSession()) { + disableSeekBar(); + } else { + enableSeekBar(volumeControl); + } + mSeekBar.setMaxVolume(mController.getSessionVolumeMax()); + + final int currentVolume = mController.getSessionVolume(); + initializeSeekbarVolume(null, currentVolume, isCurrentSeekbarInvisible); + mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener( + null, volumeControl) { + @Override + protected boolean shouldHandleProgressChanged() { + return true; } }); } @@ -390,7 +405,7 @@ public abstract class MediaOutputBaseAdapter extends int getDrawableId(boolean isInputDevice, boolean isMutedVolumeIcon) { // Returns the microphone icon when the flag is enabled and the device is an input // device. - if (com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl() + if (Flags.enableAudioInputDeviceRoutingAndVolumeControl() && isInputDevice) { return isMutedVolumeIcon ? R.drawable.ic_mic_off : R.drawable.ic_mic_26dp; } @@ -457,27 +472,28 @@ public abstract class MediaOutputBaseAdapter extends updateIconAreaClickListener(null); } - private void enableSeekBar(MediaDevice device) { + private void enableSeekBar(SeekBarVolumeControl volumeControl) { mSeekBar.setEnabled(true); + mSeekBar.setOnTouchListener((v, event) -> false); updateIconAreaClickListener((v) -> { - if (device.getCurrentVolume() == 0) { - mController.logInteractionUnmuteDevice(device); + if (volumeControl.getVolume() == 0) { mSeekBar.setVolume(UNMUTE_DEFAULT_VOLUME); - mController.adjustVolume(device, UNMUTE_DEFAULT_VOLUME); - updateUnmutedVolumeIcon(device); + volumeControl.setVolume(UNMUTE_DEFAULT_VOLUME); + updateUnmutedVolumeIcon(null); mIconAreaLayout.setOnTouchListener(((iconV, event) -> false)); } else { - mController.logInteractionMuteDevice(device); + volumeControl.onMute(); mSeekBar.resetVolume(); - mController.adjustVolume(device, 0); - updateMutedVolumeIcon(device); + volumeControl.setVolume(0); + updateMutedVolumeIcon(null); mIconAreaLayout.setOnTouchListener(((iconV, event) -> { mSeekBar.dispatchTouchEvent(event); return false; })); } }); + } protected void setUpDeviceIcon(MediaDevice device) { @@ -493,5 +509,74 @@ public abstract class MediaOutputBaseAdapter extends }); }); } + + interface SeekBarVolumeControl { + int getVolume(); + void setVolume(int volume); + void onMute(); + } + + private abstract class MediaSeekBarChangedListener + implements SeekBar.OnSeekBarChangeListener { + boolean mStartFromMute = false; + private MediaDevice mMediaDevice; + private SeekBarVolumeControl mVolumeControl; + + MediaSeekBarChangedListener(MediaDevice device, SeekBarVolumeControl volumeControl) { + mMediaDevice = device; + mVolumeControl = volumeControl; + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (!shouldHandleProgressChanged() || !fromUser) { + return; + } + + final String percentageString = mContext.getResources().getString( + R.string.media_output_dialog_volume_percentage, + mSeekBar.getPercentage()); + mVolumeValueText.setText(percentageString); + + if (mStartFromMute) { + updateUnmutedVolumeIcon(mMediaDevice); + mStartFromMute = false; + } + + int seekBarVolume = MediaOutputSeekbar.scaleProgressToVolume(progress); + if (seekBarVolume != mVolumeControl.getVolume()) { + mLatestUpdateVolume = seekBarVolume; + mVolumeControl.setVolume(seekBarVolume); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mTitleIcon.setVisibility(View.INVISIBLE); + mVolumeValueText.setVisibility(View.VISIBLE); + int currentVolume = MediaOutputSeekbar.scaleProgressToVolume( + seekBar.getProgress()); + mStartFromMute = (currentVolume == 0); + mIsDragging = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + int currentVolume = MediaOutputSeekbar.scaleProgressToVolume( + seekBar.getProgress()); + if (currentVolume == 0) { + seekBar.setProgress(0); + updateMutedVolumeIcon(mMediaDevice); + } else { + updateUnmutedVolumeIcon(mMediaDevice); + } + mTitleIcon.setVisibility(View.VISIBLE); + mVolumeValueText.setVisibility(View.GONE); + mIsDragging = false; + } + protected boolean shouldHandleProgressChanged() { + return mMediaDevice != null; + } + }; } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java index be5d60799f79..b7381dafcf12 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java @@ -16,22 +16,62 @@ package com.android.systemui.media.dialog; +import android.annotation.Nullable; import android.content.Context; import android.util.AttributeSet; import android.widget.SeekBar; +import com.android.systemui.res.R; + /** * Customized SeekBar for MediaOutputDialog, apply scale between device volume and progress, to make * adjustment smoother. */ public class MediaOutputSeekbar extends SeekBar { + // The scale is added to make slider value change smooth. private static final int SCALE_SIZE = 1000; - private static final int INITIAL_PROGRESS = 500; - public static final int VOLUME_PERCENTAGE_SCALE_SIZE = 100000; + + @Nullable + private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener = null; public MediaOutputSeekbar(Context context, AttributeSet attrs) { super(context, attrs); setMin(0); + super.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + final String percentageString = context.getResources().getString( + R.string.media_output_dialog_volume_percentage, + getPercentage()); + // Override the default TTS for the seekbar. The percentage should correspond to + // the volume value, not the progress value. I.e. for the volume range 0 - 25, the + // percentage should be 0%, 4%, 8%, etc. It should never be 6% since 6% doesn't map + // to an integer volume value. + setStateDescription(percentageString); + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onStartTrackingTouch(seekBar); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onStopTrackingTouch(seekBar); + } + } + }); + } + + @Override + public void setOnSeekBarChangeListener(@Nullable SeekBar.OnSeekBarChangeListener listener) { + mOnSeekBarChangeListener = listener; } static int scaleProgressToVolume(int progress) { @@ -39,11 +79,11 @@ public class MediaOutputSeekbar extends SeekBar { } static int scaleVolumeToProgress(int volume) { - return volume == 0 ? 0 : INITIAL_PROGRESS + volume * SCALE_SIZE; + return volume * SCALE_SIZE; } int getVolume() { - return getProgress() / SCALE_SIZE; + return scaleProgressToVolume(getProgress()); } void setVolume(int volume) { @@ -51,10 +91,18 @@ public class MediaOutputSeekbar extends SeekBar { } void setMaxVolume(int maxVolume) { - setMax(maxVolume * SCALE_SIZE); + setMax(scaleVolumeToProgress(maxVolume)); } void resetVolume() { setProgress(getMin()); } + + int getPercentage() { + // The progress -> volume -> progress conversion is necessary to ensure that progress + // strictly corresponds to an integer volume value. + // Example: 10424 (progress) -> 10 (volume) -> 10000 (progress). + int normalizedProgress = scaleVolumeToProgress(scaleProgressToVolume(getProgress())); + return (int) ((double) normalizedProgress * 100 / getMax()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index 15afd22a27d8..35c872f8a203 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -760,14 +760,26 @@ public class MediaSwitchingController if (connectedMediaDevice != null) { selectedDevicesIds.add(connectedMediaDevice.getId()); } + boolean groupSelectedDevices = + com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping(); + int nextSelectedItemIndex = 0; boolean suggestedDeviceAdded = false; boolean displayGroupAdded = false; + boolean selectedDeviceAdded = false; for (MediaDevice device : devices) { if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) { finalMediaItems.add(0, MediaItem.createDeviceMediaItem(device)); + nextSelectedItemIndex++; } else if (!needToHandleMutingExpectedDevice && selectedDevicesIds.contains( device.getId())) { - finalMediaItems.add(0, MediaItem.createDeviceMediaItem(device)); + if (groupSelectedDevices) { + finalMediaItems.add( + nextSelectedItemIndex++, + MediaItem.createDeviceMediaItem(device, !selectedDeviceAdded)); + selectedDeviceAdded = true; + } else { + finalMediaItems.add(0, MediaItem.createDeviceMediaItem(device)); + } } else { if (device.isSuggestedDevice() && !suggestedDeviceAdded) { addSuggestedDeviceGroupDivider(finalMediaItems); @@ -1331,6 +1343,10 @@ public class MediaSwitchingController return !device.isVolumeFixed(); } + boolean isVolumeControlEnabledForSession() { + return mLocalMediaManager.isMediaSessionAvailableForVolumeControl(); + } + private void startActivity(Intent intent, ActivityTransitionAnimator.Controller controller) { // Media Output dialog can be shown from the volume panel. This makes sure the panel is // closed when navigating to another activity, so it doesn't stays on top of it diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt index ea0f63ca9721..d503fb7f94f7 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.mediaprojection.data.repository import android.app.ActivityManager.RunningTaskInfo import android.hardware.display.DisplayManager +import android.media.projection.MediaProjectionEvent import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager import android.media.projection.StopReason @@ -43,6 +44,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -83,48 +87,59 @@ constructor( } } - override val mediaProjectionState: Flow<MediaProjectionState> = - conflatedCallbackFlow { - val callback = - object : MediaProjectionManager.Callback() { - override fun onStart(info: MediaProjectionInfo?) { - logger.log( - TAG, - LogLevel.DEBUG, - {}, - { "MediaProjectionManager.Callback#onStart" }, - ) - trySendWithFailureLogging(CallbackEvent.OnStart(info), TAG) - } + private val callbackEventsFlow = conflatedCallbackFlow { + val callback = + object : MediaProjectionManager.Callback() { + override fun onStart(info: MediaProjectionInfo?) { + logger.log(TAG, LogLevel.DEBUG, {}, { "Callback#onStart" }) + trySendWithFailureLogging(CallbackEvent.OnStart(info), TAG) + } - override fun onStop(info: MediaProjectionInfo?) { - logger.log( - TAG, - LogLevel.DEBUG, - {}, - { "MediaProjectionManager.Callback#onStop" }, - ) - trySendWithFailureLogging(CallbackEvent.OnStop, TAG) - } + override fun onStop(info: MediaProjectionInfo?) { + logger.log(TAG, LogLevel.DEBUG, {}, { "Callback#onStop" }) + trySendWithFailureLogging(CallbackEvent.OnStop, TAG) + } - override fun onRecordingSessionSet( - info: MediaProjectionInfo, - session: ContentRecordingSession?, - ) { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = session.toString() }, - { "MediaProjectionManager.Callback#onSessionStarted: $str1" }, - ) - trySendWithFailureLogging( - CallbackEvent.OnRecordingSessionSet(info, session), - TAG, - ) - } + override fun onRecordingSessionSet( + info: MediaProjectionInfo, + session: ContentRecordingSession?, + ) { + logger.log( + TAG, + LogLevel.DEBUG, + { str1 = session.toString() }, + { "Callback#onSessionSet: $str1" }, + ) + trySendWithFailureLogging( + CallbackEvent.OnRecordingSessionSet(info, session), + TAG, + ) + } + + override fun onMediaProjectionEvent( + event: MediaProjectionEvent, + info: MediaProjectionInfo?, + session: ContentRecordingSession?, + ) { + if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) { + logger.log( + TAG, + LogLevel.DEBUG, + { str1 = event.toString() }, + { "Callback#onMediaProjectionEvent : $str1" }, + ) + trySendWithFailureLogging(CallbackEvent.OnMediaProjectionEvent(event), TAG) } - mediaProjectionManager.addCallback(callback, handler) - awaitClose { mediaProjectionManager.removeCallback(callback) } + } + } + mediaProjectionManager.addCallback(callback, handler) + awaitClose { mediaProjectionManager.removeCallback(callback) } + } + + override val mediaProjectionState: Flow<MediaProjectionState> = + callbackEventsFlow + .filterNot { + it is CallbackEvent.OnMediaProjectionEvent // Exclude OnMediaProjectionEvent } // When we get an #onRecordingSessionSet event, we need to do some work in the // background before emitting the right state value. But when we get an #onStop @@ -159,6 +174,11 @@ constructor( } is CallbackEvent.OnStop -> MediaProjectionState.NotProjecting is CallbackEvent.OnRecordingSessionSet -> stateForSession(it.info, it.session) + is CallbackEvent.OnMediaProjectionEvent -> + throw IllegalStateException( + "Unexpected OnMediaProjectionEvent in mediaProjectionState flow. It " + + "should have been filtered out." + ) } } .stateIn( @@ -167,6 +187,16 @@ constructor( initialValue = MediaProjectionState.NotProjecting, ) + override val projectionStartedDuringCallAndActivePostCallEvent: Flow<Unit> = + callbackEventsFlow + .filter { + com.android.media.projection.flags.Flags.showStopDialogPostCallEnd() && + it is CallbackEvent.OnMediaProjectionEvent && + it.event.eventType == + MediaProjectionEvent.PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL + } + .map {} + private suspend fun stateForSession( info: MediaProjectionInfo, session: ContentRecordingSession?, @@ -206,6 +236,8 @@ constructor( val info: MediaProjectionInfo, val session: ContentRecordingSession?, ) : CallbackEvent + + data class OnMediaProjectionEvent(val event: MediaProjectionEvent) : CallbackEvent } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt index a01d8c2c98de..826ee589e8ff 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt @@ -32,4 +32,10 @@ interface MediaProjectionRepository { /** Represents the current [MediaProjectionState]. */ val mediaProjectionState: Flow<MediaProjectionState> + + /** + * Emits each time a call ends but media projection is still active and media projection was + * starting during the call. + */ + val projectionStartedDuringCallAndActivePostCallEvent: Flow<Unit> } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java index ebda3765cf90..babb64050ed5 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java @@ -284,7 +284,10 @@ public class NavigationBarControllerImpl implements } @Override - public void onDisplayReady(int displayId) { + public void onDisplayAddSystemDecorations(int displayId) { + if (enableDisplayContentModeManagement()) { + mHasNavBar.put(displayId, true); + } Display display = mDisplayManager.getDisplay(displayId); mIsLargeScreen = isLargeScreen(mContext); createNavigationBar(display, null /* savedState */, null /* result */); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index 9d8943052b38..c4d847f18269 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java @@ -238,16 +238,16 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, } @Override - public void onDisplayReady(int displayId) { - CommandQueue.Callbacks.super.onDisplayReady(displayId); + public void onDisplayAddSystemDecorations(int displayId) { + CommandQueue.Callbacks.super.onDisplayAddSystemDecorations(displayId); if (mLauncherProxyService.getProxy() == null) { return; } try { - mLauncherProxyService.getProxy().onDisplayReady(displayId); + mLauncherProxyService.getProxy().onDisplayAddSystemDecorations(displayId); } catch (RemoteException e) { - Log.e(TAG, "onDisplayReady() failed", e); + Log.e(TAG, "onDisplayAddSystemDecorations() failed", e); } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl index 7803f229dda7..d803ebed94e3 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl +++ b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl @@ -26,6 +26,6 @@ interface INoteTaskBubblesService { boolean areBubblesAvailable(); - void showOrHideAppBubble(in Intent intent, in UserHandle userHandle, in Icon icon, + void showOrHideNoteBubble(in Intent intent, in UserHandle userHandle, in Icon icon, in NoteTaskBubbleExpandBehavior bubbleExpandBehavior); } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt index 169285f6742e..e20ccfa13e17 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt @@ -76,8 +76,8 @@ constructor( } } - /** Calls the [Bubbles.showOrHideAppBubble] API as [UserHandle.USER_SYSTEM]. */ - open suspend fun showOrHideAppBubble( + /** Calls the [Bubbles.showOrHideNoteBubble] API as [UserHandle.USER_SYSTEM]. */ + open suspend fun showOrHideNoteBubble( intent: Intent, userHandle: UserHandle, icon: Icon, @@ -85,7 +85,7 @@ constructor( ) { withContext(bgDispatcher) { serviceConnector - .post { it.showOrHideAppBubble(intent, userHandle, icon, bubbleExpandBehavior) } + .post { it.showOrHideNoteBubble(intent, userHandle, icon, bubbleExpandBehavior) } .whenComplete { _, error -> if (error != null) { debugLog(error = error) { @@ -119,7 +119,7 @@ constructor( return object : INoteTaskBubblesService.Stub() { override fun areBubblesAvailable() = mOptionalBubbles.isPresent - override fun showOrHideAppBubble( + override fun showOrHideNoteBubble( intent: Intent, userHandle: UserHandle, icon: Icon, @@ -131,12 +131,12 @@ constructor( bubbleExpandBehavior == NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED && bubbles.isBubbleExpanded( - Bubble.getAppBubbleKeyForApp(intent.`package`, userHandle) + Bubble.getNoteBubbleKeyForApp(intent.`package`, userHandle) ) ) { return@ifPresentOrElse } - bubbles.showOrHideAppBubble(intent, userHandle, icon) + bubbles.showOrHideNoteBubble(intent, userHandle, icon) }, { debugLog { diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index ad1f37070c9d..7e0128ab8f13 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -96,7 +96,7 @@ constructor( val info = infoReference.getAndSet(null) ?: return - if (key != Bubble.getAppBubbleKeyForApp(info.packageName, info.user)) return + if (key != Bubble.getNoteBubbleKeyForApp(info.packageName, info.user)) return // Safe guard mechanism, this callback should only be called for app bubbles. if (info.launchMode !is NoteTaskLaunchMode.AppBubble) return @@ -219,7 +219,7 @@ constructor( val intent = createNoteTaskIntent(info, useStylusMode) val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) - noteTaskBubblesController.showOrHideAppBubble( + noteTaskBubblesController.showOrHideNoteBubble( intent, user, icon, diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt index f15a7b30dce7..f8d442de0f55 100644 --- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt @@ -22,6 +22,8 @@ import com.android.systemui.camera.CameraGestureHelper import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorActual import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.PowerRepository import com.android.systemui.power.shared.model.DozeScreenStateModel @@ -35,6 +37,7 @@ import javax.inject.Provider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -228,6 +231,15 @@ constructor( repository.updateWakefulness(powerButtonLaunchGestureTriggered = true) } + suspend fun hydrateTableLogBuffer(tableLogBuffer: TableLogBuffer) { + detailedWakefulness + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + initialValue = detailedWakefulness.value, + ) + .collect() + } + companion object { private const val FSI_WAKE_WHY = "full_screen_intent" diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt index 0f49c94c3195..297c6af5a4a7 100644 --- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt +++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt @@ -1,6 +1,8 @@ package com.android.systemui.power.shared.model import com.android.systemui.keyguard.KeyguardService +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger /** * Models whether the device is awake or asleep, along with information about why we're in that @@ -35,7 +37,7 @@ data class WakefulnessModel( * to a subsequent power gesture. */ val powerButtonLaunchGestureTriggered: Boolean = false, -) { +) : Diffable<WakefulnessModel> { fun isAwake() = internalWakefulnessState == WakefulnessState.AWAKE || internalWakefulnessState == WakefulnessState.STARTING_TO_WAKE @@ -58,4 +60,8 @@ data class WakefulnessModel( return isAwake() && (lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE) } + + override fun logDiffs(prevVal: WakefulnessModel, row: TableRowLogger) { + row.logChange(columnName = "wakefulness", value = toString()) + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index 07de4662e82f..85b677b65aeb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -32,11 +32,7 @@ import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.annotation.VisibleForTesting -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box @@ -120,6 +116,7 @@ import com.android.systemui.qs.composefragment.ui.GridAnchor import com.android.systemui.qs.composefragment.ui.NotificationScrimClipParams import com.android.systemui.qs.composefragment.ui.notificationScrimClip import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings +import com.android.systemui.qs.composefragment.ui.toEditMode import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.qs.footer.ui.compose.FooterActions @@ -144,6 +141,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -273,36 +271,7 @@ constructor( // by the composables. .gesturesDisabled(viewModel.showingMirror) ) { - val isEditing by - viewModel.containerViewModel.editModeViewModel.isEditing - .collectAsStateWithLifecycle() - val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS) - AnimatedContent( - targetState = isEditing, - transitionSpec = { - fadeIn(animationSpecEditMode) togetherWith - fadeOut(animationSpecEditMode) - }, - label = "EditModeAnimatedContent", - ) { editing -> - if (editing) { - val qqsPadding = viewModel.qqsHeaderHeight - EditMode( - viewModel = viewModel.containerViewModel.editModeViewModel, - modifier = - Modifier.fillMaxWidth() - .padding(top = { qqsPadding }) - .padding( - horizontal = { - QuickSettingsShade.Dimensions.Padding - .roundToPx() - } - ), - ) - } else { - CollapsableQuickSettingsSTL() - } - } + CollapsableQuickSettingsSTL() } } } @@ -324,12 +293,17 @@ constructor( from(QuickQuickSettings, QuickSettings) { quickQuickSettingsToQuickSettings(viewModel::animateTilesExpansion::get) } + to(SceneKeys.EditMode) { + spec = tween(durationMillis = EDIT_MODE_TIME_MILLIS) + toEditMode() + } }, ) LaunchedEffect(Unit) { synchronizeQsState( sceneState, + viewModel.containerViewModel.editModeViewModel.isEditing, snapshotFlow { viewModel.expansionState }.map { it.progress }, ) } @@ -337,12 +311,20 @@ constructor( SceneTransitionLayout(state = sceneState, modifier = Modifier.fillMaxSize()) { scene(QuickSettings) { LaunchedEffect(Unit) { viewModel.onQSOpen() } - QuickSettingsElement() + QuickSettingsElement(Modifier.element(QuickSettings.rootElementKey)) } scene(QuickQuickSettings) { LaunchedEffect(Unit) { viewModel.onQQSOpen() } - QuickQuickSettingsElement() + // Cannot pass the element modifier in because the top element has a `testTag` + // and this would overwrite it. + Box(Modifier.element(QuickQuickSettings.rootElementKey)) { + QuickQuickSettingsElement() + } + } + + scene(SceneKeys.EditMode) { + EditModeElement(Modifier.element(SceneKeys.EditMode.rootElementKey)) } } } @@ -582,7 +564,7 @@ constructor( } @Composable - private fun ContentScope.QuickQuickSettingsElement() { + private fun ContentScope.QuickQuickSettingsElement(modifier: Modifier = Modifier) { val qqsPadding = viewModel.qqsHeaderHeight val bottomPadding = viewModel.qqsBottomPadding DisposableEffect(Unit) { @@ -595,7 +577,7 @@ constructor( .squishiness .collectAsStateWithLifecycle() - Column(modifier = Modifier.sysuiResTag(ResIdTags.quickQsPanel)) { + Column(modifier = modifier.sysuiResTag(ResIdTags.quickQsPanel)) { Box( modifier = Modifier.fillMaxWidth() @@ -666,12 +648,12 @@ constructor( } @Composable - private fun ContentScope.QuickSettingsElement() { + private fun ContentScope.QuickSettingsElement(modifier: Modifier = Modifier) { val qqsPadding = viewModel.qqsHeaderHeight val qsExtraPadding = dimensionResource(R.dimen.qs_panel_padding_top) Column( modifier = - Modifier.collapseExpandSemanticAction( + modifier.collapseExpandSemanticAction( stringResource(id = R.string.accessibility_quick_settings_collapse) ) ) { @@ -776,6 +758,18 @@ constructor( } } + @Composable + private fun EditModeElement(modifier: Modifier = Modifier) { + // No need for top padding, the Scaffold inside takes care of the correct insets + EditMode( + viewModel = viewModel.containerViewModel.editModeViewModel, + modifier = + modifier + .fillMaxWidth() + .padding(horizontal = { QuickSettingsShade.Dimensions.Padding.roundToPx() }), + ) + } + private fun Modifier.collapseExpandSemanticAction(label: String): Modifier { return viewModel.collapseExpandAccessibilityAction?.let { semantics { @@ -863,6 +857,7 @@ private val instanceProvider = object SceneKeys { val QuickQuickSettings = SceneKey("QuickQuickSettingsScene") val QuickSettings = SceneKey("QuickSettingsScene") + val EditMode = SceneKey("EditModeScene") fun QSFragmentComposeViewModel.QSExpansionState.toIdleSceneKey(): SceneKey { return when { @@ -880,7 +875,11 @@ object SceneKeys { } } -suspend fun synchronizeQsState(state: MutableSceneTransitionLayoutState, expansion: Flow<Float>) { +private suspend fun synchronizeQsState( + state: MutableSceneTransitionLayoutState, + editMode: Flow<Boolean>, + expansion: Flow<Float>, +) { coroutineScope { val animationScope = this @@ -891,23 +890,30 @@ suspend fun synchronizeQsState(state: MutableSceneTransitionLayoutState, expansi currentTransition = null } - expansion.collectLatest { progress -> - when (progress) { - 0f -> snapTo(QuickQuickSettings) - 1f -> snapTo(QuickSettings) - else -> { - val transition = currentTransition - if (transition != null) { - transition.progress = progress - return@collectLatest - } + editMode.combine(expansion, ::Pair).collectLatest { (editMode, progress) -> + if (editMode && state.currentScene != SceneKeys.EditMode) { + state.setTargetScene(SceneKeys.EditMode, animationScope)?.second?.join() + } else if (!editMode && state.currentScene == SceneKeys.EditMode) { + state.setTargetScene(SceneKeys.QuickSettings, animationScope)?.second?.join() + } + if (!editMode) { + when (progress) { + 0f -> snapTo(QuickQuickSettings) + 1f -> snapTo(QuickSettings) + else -> { + val transition = currentTransition + if (transition != null) { + transition.progress = progress + return@collectLatest + } - val newTransition = - ExpansionTransition(progress).also { currentTransition = it } - state.startTransitionImmediately( - animationScope = animationScope, - transition = newTransition, - ) + val newTransition = + ExpansionTransition(progress).also { currentTransition = it } + state.startTransitionImmediately( + animationScope = animationScope, + transition = newTransition, + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/ToEditMode.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/ToEditMode.kt new file mode 100644 index 000000000000..0c6f3ee88312 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/ToEditMode.kt @@ -0,0 +1,28 @@ +/* + * 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.qs.composefragment.ui + +import com.android.compose.animation.scene.TransitionBuilder +import com.android.systemui.qs.composefragment.SceneKeys + +fun TransitionBuilder.toEditMode() { + fractionRange(start = 0.5f) { fade(SceneKeys.EditMode.rootElementKey) } + fractionRange(end = 0.5f) { + fade(SceneKeys.QuickQuickSettings.rootElementKey) + fade(SceneKeys.QuickSettings.rootElementKey) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt index 30fb50db82a2..1233a2f285d5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt @@ -38,8 +38,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContent +import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsViewModel +import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel +import com.android.systemui.qs.tiles.dialog.InternetDetailsContent +import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel +import com.android.systemui.qs.tiles.dialog.ModesDetailsContent +import com.android.systemui.qs.tiles.dialog.ModesDetailsViewModel +import com.android.systemui.qs.tiles.dialog.ScreenRecordDetailsContent +import com.android.systemui.qs.tiles.dialog.ScreenRecordDetailsViewModel @Composable fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewModel) { @@ -107,7 +116,17 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode style = MaterialTheme.typography.titleSmall, ) } - tileDetailedViewModel.GetContentView() + MapTileDetailsContent(tileDetailedViewModel) + } +} + +@Composable +private fun MapTileDetailsContent(tileDetailsViewModel: TileDetailsViewModel) { + when (tileDetailsViewModel) { + is InternetDetailsViewModel -> InternetDetailsContent(tileDetailsViewModel) + is ScreenRecordDetailsViewModel -> ScreenRecordDetailsContent(tileDetailsViewModel) + is BluetoothDetailsViewModel -> BluetoothDetailsContent() + is ModesDetailsViewModel -> ModesDetailsContent(tileDetailsViewModel) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt new file mode 100644 index 000000000000..7d396c58630e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt @@ -0,0 +1,56 @@ +/* + * 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.qs.tiles.dialog + +import android.view.LayoutInflater +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.res.R + +@Composable +fun InternetDetailsContent(viewModel: InternetDetailsViewModel) { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + + val internetDetailsContentManager = remember { + viewModel.contentManagerFactory.create( + canConfigMobileData = viewModel.getCanConfigMobileData(), + canConfigWifi = viewModel.getCanConfigWifi(), + coroutineScope = coroutineScope, + context = context, + ) + } + + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + // Inflate with the existing dialog xml layout and bind it with the manager + val view = + LayoutInflater.from(context).inflate(R.layout.internet_connectivity_dialog, null) + internetDetailsContentManager.bind(view) + + view + // TODO: b/377388104 - Polish the internet details view UI + }, + onRelease = { internetDetailsContentManager.unBind() }, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt index df4dddbca9e6..0ed56f62ee6c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt @@ -16,18 +16,7 @@ package com.android.systemui.qs.tiles.dialog -import android.util.Log -import android.view.LayoutInflater -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.plugins.qs.TileDetailsViewModel -import com.android.systemui.res.R import com.android.systemui.statusbar.connectivity.AccessPointController import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -37,45 +26,9 @@ class InternetDetailsViewModel @AssistedInject constructor( private val accessPointController: AccessPointController, - private val contentManagerFactory: InternetDetailsContentManager.Factory, + val contentManagerFactory: InternetDetailsContentManager.Factory, @Assisted private val onLongClick: () -> Unit, ) : TileDetailsViewModel() { - private lateinit var internetDetailsContentManager: InternetDetailsContentManager - - @Composable - override fun GetContentView() { - val coroutineScope = rememberCoroutineScope() - val context = LocalContext.current - - internetDetailsContentManager = remember { - contentManagerFactory.create( - canConfigMobileData = accessPointController.canConfigMobileData(), - canConfigWifi = accessPointController.canConfigWifi(), - coroutineScope = coroutineScope, - context = context, - ) - } - AndroidView( - modifier = Modifier.fillMaxWidth().fillMaxHeight(), - factory = { context -> - // Inflate with the existing dialog xml layout and bind it with the manager - val view = - LayoutInflater.from(context) - .inflate(R.layout.internet_connectivity_dialog, null) - internetDetailsContentManager.bind(view) - - view - // TODO: b/377388104 - Polish the internet details view UI - }, - onRelease = { - internetDetailsContentManager.unBind() - if (DEBUG) { - Log.d(TAG, "onRelease") - } - }, - ) - } - override fun clickOnSettingsButton() { onLongClick() } @@ -96,13 +49,16 @@ constructor( return "Tab a network to connect" } + fun getCanConfigMobileData(): Boolean { + return accessPointController.canConfigMobileData() + } + + fun getCanConfigWifi(): Boolean { + return accessPointController.canConfigWifi() + } + @AssistedFactory interface Factory { fun create(onLongClick: () -> Unit): InternetDetailsViewModel } - - companion object { - private const val TAG = "InternetDetailsVModel" - private val DEBUG: Boolean = Log.isLoggable(TAG, Log.DEBUG) - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsContent.kt new file mode 100644 index 000000000000..c5ecaffdf188 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsContent.kt @@ -0,0 +1,26 @@ +/* + * 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.qs.tiles.dialog + +import androidx.compose.runtime.Composable +import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid + +@Composable +fun ModesDetailsContent(viewModel: ModesDetailsViewModel) { + // TODO(b/378513940): Finish implementing this function. + ModeTileGrid(viewModel = viewModel.viewModel) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt index 511597d05d37..9a39c3c095ef 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt @@ -16,22 +16,14 @@ package com.android.systemui.qs.tiles.dialog -import androidx.compose.runtime.Composable import com.android.systemui.plugins.qs.TileDetailsViewModel -import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel /** The view model used for the modes details view in the Quick Settings */ class ModesDetailsViewModel( private val onSettingsClick: () -> Unit, - private val viewModel: ModesDialogViewModel, + val viewModel: ModesDialogViewModel, ) : TileDetailsViewModel() { - @Composable - override fun GetContentView() { - // TODO(b/378513940): Finish implementing this function. - ModeTileGrid(viewModel = viewModel) - } - override fun clickOnSettingsButton() { onSettingsClick() } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsContent.kt new file mode 100644 index 000000000000..bf1a51d8cd59 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsContent.kt @@ -0,0 +1,56 @@ +/* + * 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.qs.tiles.dialog + +import android.view.LayoutInflater +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.res.R +import com.android.systemui.screenrecord.ScreenRecordPermissionViewBinder + +@Composable +fun ScreenRecordDetailsContent(viewModel: ScreenRecordDetailsViewModel) { + // TODO(b/378514312): Finish implementing this function. + + if (viewModel.recordingController.isScreenCaptureDisabled) { + // TODO(b/388345506): Show disabled page here. + return + } + + val viewBinder: ScreenRecordPermissionViewBinder = remember { + viewModel.recordingController.createScreenRecordPermissionViewBinder( + viewModel.onStartRecordingClicked + ) + } + + AndroidView( + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + factory = { context -> + // Inflate with the existing dialog xml layout + val view = LayoutInflater.from(context).inflate(R.layout.screen_share_dialog, null) + viewBinder.bind(view) + + view + // TODO(b/378514473): Revamp the details view according to the spec. + }, + onRelease = { viewBinder.unbind() }, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt index 54e4a521c239..c84ddb6fdb36 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt @@ -16,49 +16,15 @@ package com.android.systemui.qs.tiles.dialog -import android.view.LayoutInflater -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.plugins.qs.TileDetailsViewModel -import com.android.systemui.res.R import com.android.systemui.screenrecord.RecordingController -import com.android.systemui.screenrecord.ScreenRecordPermissionViewBinder /** The view model used for the screen record details view in the Quick Settings */ class ScreenRecordDetailsViewModel( - private val recordingController: RecordingController, - private val onStartRecordingClicked: Runnable, + val recordingController: RecordingController, + val onStartRecordingClicked: Runnable, ) : TileDetailsViewModel() { - private var viewBinder: ScreenRecordPermissionViewBinder = - recordingController.createScreenRecordPermissionViewBinder(onStartRecordingClicked) - - @Composable - override fun GetContentView() { - // TODO(b/378514312): Finish implementing this function. - - if (recordingController.isScreenCaptureDisabled) { - // TODO(b/388345506): Show disabled page here. - return - } - - AndroidView( - modifier = Modifier.fillMaxWidth().fillMaxHeight(), - factory = { context -> - // Inflate with the existing dialog xml layout - val view = LayoutInflater.from(context).inflate(R.layout.screen_share_dialog, null) - viewBinder.bind(view) - - view - // TODO(b/378514473): Revamp the details view according to the spec. - }, - onRelease = { viewBinder.unbind() }, - ) - } - override fun clickOnSettingsButton() { // No settings button in this tile. } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt index f6f7a3b5a385..0b0f2feaa909 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt @@ -22,6 +22,7 @@ import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.base.logging.QSTileLogger import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository @@ -56,6 +57,7 @@ constructor( private val packageUpdatesRepository: CustomTilePackageUpdatesRepository, userRepository: UserRepository, @QSTileScope private val tileScope: CoroutineScope, + qsTileLogger: QSTileLogger, ) : QSTileDataInteractor<CustomTileDataModel> { private val mutableUserFlow = MutableStateFlow(userRepository.getSelectedUserInfo().userHandle) @@ -69,6 +71,7 @@ constructor( // binding the service might access it customTileInteractor.initForUser(user) // Bind the TileService for not active tile + qsTileLogger.logInfo(tileSpec, "onBindingFlow for user:$user") serviceInteractor.bindOnStart() packageUpdatesRepository diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt index ad22b3ef07ee..c0fc93fc914b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt @@ -25,6 +25,7 @@ import android.os.UserHandle import android.service.quicksettings.IQSTileService import android.service.quicksettings.Tile import android.service.quicksettings.TileService +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.external.CustomTileInterface import com.android.systemui.qs.external.TileServiceManager @@ -42,7 +43,6 @@ import kotlinx.coroutines.channels.produce import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import com.android.app.tracing.coroutines.launchTraced as launch /** * Communicates with [TileService] via [TileServiceManager] and [IQSTileService]. This interactor is @@ -72,6 +72,7 @@ constructor( val callingAppIds: Flow<Int> get() = tileReceivingInterface.mutableCallingAppIds + val refreshEvents: Flow<Unit> get() = tileReceivingInterface.mutableRefreshEvents @@ -144,6 +145,7 @@ constructor( private fun getTileServiceManager(): TileServiceManager = synchronized(tileServices) { + qsTileLogger.logInfo(tileSpec, "getTileServiceManager called") if (tileServiceManager == null) { tileServices .getTileWrapper(tileReceivingInterface) @@ -173,8 +175,10 @@ constructor( override val user: Int get() = currentUser.identifier + override val qsTile: Tile get() = customTileInteractor.getTile(currentUser) + override val component: ComponentName = tileSpec.componentName val mutableCallingAppIds = MutableStateFlow(Process.INVALID_UID) diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/SceneDomainModule.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/SceneDomainModule.kt index be792df340c9..f2f237ac987e 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/SceneDomainModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/SceneDomainModule.kt @@ -16,13 +16,27 @@ package com.android.systemui.scene.domain +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.scene.domain.resolver.SceneResolverModule import dagger.Module +import dagger.Provides +import javax.inject.Qualifier -@Module( - includes = - [ - SceneResolverModule::class, - ] -) -object SceneDomainModule +@Module(includes = [SceneResolverModule::class]) +object SceneDomainModule { + + @JvmStatic + @Provides + @SysUISingleton + @SceneFrameworkTableLog + fun provideSceneFrameworkTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer { + return factory.create("SceneFrameworkTableLog", 100) + } +} + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class SceneFrameworkTableLog diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt index bebd398ac972..c9d8e0244d20 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt @@ -18,11 +18,16 @@ package com.android.systemui.scene.domain.interactor import com.android.compose.animation.scene.SceneKey import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableRowLogger import com.android.systemui.scene.data.model.SceneStack +import com.android.systemui.scene.data.model.asIterable import com.android.systemui.scene.data.model.peek import com.android.systemui.scene.data.model.pop import com.android.systemui.scene.data.model.push import com.android.systemui.scene.data.model.sceneStackOf +import com.android.systemui.scene.domain.SceneFrameworkTableLog import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.scene.shared.model.SceneContainerConfig import javax.inject.Inject @@ -39,6 +44,7 @@ class SceneBackInteractor constructor( private val logger: SceneLogger, private val sceneContainerConfig: SceneContainerConfig, + @SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer, ) { private val _backStack = MutableStateFlow(sceneStackOf()) val backStack: StateFlow<SceneStack> = _backStack.asStateFlow() @@ -58,6 +64,7 @@ constructor( fun onSceneChange(from: SceneKey, to: SceneKey) { check(from != to) { "from == to, from=${from.debugName}, to=${to.debugName}" } + val prevVal = backStack.value _backStack.update { stack -> when (stackOperation(from, to, stack)) { null -> stack @@ -68,12 +75,21 @@ constructor( } } logger.logSceneBackStack(backStack.value) + tableLogBuffer.logDiffs( + prevVal = DiffableSceneStack(prevVal), + newVal = DiffableSceneStack(backStack.value), + ) } /** Applies the given [transform] to the back stack. */ fun updateBackStack(transform: (SceneStack) -> SceneStack) { + val prevVal = backStack.value _backStack.update { stack -> transform(stack) } logger.logSceneBackStack(backStack.value) + tableLogBuffer.logDiffs( + prevVal = DiffableSceneStack(prevVal), + newVal = DiffableSceneStack(backStack.value), + ) } private fun stackOperation(from: SceneKey, to: SceneKey, stack: SceneStack): StackOperation? { @@ -106,4 +122,15 @@ constructor( private data object Push : StackOperation private data object Pop : StackOperation + + private class DiffableSceneStack(private val sceneStack: SceneStack) : + Diffable<DiffableSceneStack> { + + override fun logDiffs(prevVal: DiffableSceneStack, row: TableRowLogger) { + row.logChange( + columnName = "backStack", + value = sceneStack.asIterable().joinToString { it.debugName }, + ) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 8bc9d96c064a..9c04323f2a0e 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -27,14 +27,19 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableRowLogger import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.domain.resolver.SceneResolver import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.util.kotlin.pairwise import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -47,6 +52,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch /** * Generic business logic and app state accessors for the scene framework. @@ -562,6 +568,28 @@ constructor( decrementActiveTransitionAnimationCount() } + suspend fun hydrateTableLogBuffer(tableLogBuffer: TableLogBuffer) { + coroutineScope { + launch { + currentScene + .map { sceneKey -> DiffableSceneKey(key = sceneKey) } + .pairwise() + .collect { (prev, current) -> + tableLogBuffer.logDiffs(prevVal = prev, newVal = current) + } + } + + launch { + currentOverlays + .map { overlayKeys -> DiffableOverlayKeys(keys = overlayKeys) } + .pairwise() + .collect { (prev, current) -> + tableLogBuffer.logDiffs(prevVal = prev, newVal = current) + } + } + } + } + private fun decrementActiveTransitionAnimationCount() { repository.activeTransitionAnimationCount.update { current -> (current - 1).also { @@ -573,4 +601,20 @@ constructor( } } } + + private class DiffableSceneKey(private val key: SceneKey) : Diffable<DiffableSceneKey> { + override fun logDiffs(prevVal: DiffableSceneKey, row: TableRowLogger) { + row.logChange(columnName = "currentScene", value = key.debugName) + } + } + + private class DiffableOverlayKeys(private val keys: Set<OverlayKey>) : + Diffable<DiffableOverlayKeys> { + override fun logDiffs(prevVal: DiffableOverlayKeys, row: TableRowLogger) { + row.logChange( + columnName = "currentOverlays", + value = keys.joinToString { key -> key.debugName }, + ) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 8602884ec4ee..94e32fcb9ac6 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -44,7 +44,9 @@ import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.TrustInteractor import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor.Companion.keyguardScenes +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.model.SceneContainerPlugin import com.android.systemui.model.SysUiState import com.android.systemui.model.updateFlags @@ -54,6 +56,7 @@ import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.scene.data.model.asIterable import com.android.systemui.scene.data.model.sceneStackOf +import com.android.systemui.scene.domain.SceneFrameworkTableLog import com.android.systemui.scene.domain.interactor.DisabledContentInteractor import com.android.systemui.scene.domain.interactor.SceneBackInteractor import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor @@ -145,6 +148,8 @@ constructor( private val disabledContentInteractor: DisabledContentInteractor, private val activityTransitionAnimator: ActivityTransitionAnimator, private val shadeModeInteractor: ShadeModeInteractor, + @SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer, + private val trustInteractor: TrustInteractor, ) : CoreStartable { private val centralSurfaces: CentralSurfaces? get() = centralSurfacesOptLazy.get().getOrNull() @@ -154,6 +159,7 @@ constructor( override fun start() { if (SceneContainerFlag.isEnabled) { sceneLogger.logFrameworkEnabled(isEnabled = true) + applicationScope.launch { hydrateTableLogBuffer() } hydrateVisibility() automaticallySwitchScenes() hydrateSystemUiState() @@ -169,6 +175,7 @@ constructor( notifyKeyguardDismissCancelledCallbacks() refreshLockscreenEnabled() hydrateActivityTransitionAnimationState() + lockWhenDeviceBecomesUntrusted() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -224,6 +231,16 @@ constructor( } } + private suspend fun hydrateTableLogBuffer() { + coroutineScope { + launch { sceneInteractor.hydrateTableLogBuffer(tableLogBuffer) } + launch { keyguardEnabledInteractor.hydrateTableLogBuffer(tableLogBuffer) } + launch { faceUnlockInteractor.hydrateTableLogBuffer(tableLogBuffer) } + launch { powerInteractor.hydrateTableLogBuffer(tableLogBuffer) } + launch { keyguardInteractor.hydrateTableLogBuffer(tableLogBuffer) } + } + } + private fun resetShadeSessions() { applicationScope.launch { combine( @@ -984,6 +1001,18 @@ constructor( ) } + private fun lockWhenDeviceBecomesUntrusted() { + applicationScope.launch { + trustInteractor.isTrusted.pairwise().collect { (wasTrusted, isTrusted) -> + if (wasTrusted && !isTrusted && !deviceEntryInteractor.isDeviceEntered.value) { + deviceEntryInteractor.lockNow( + "Exited trusted environment while not device not entered" + ) + } + } + } + } + companion object { private const val TAG = "SceneContainerStartable" } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java index 3bca4e421cbd..1a91afcf6cac 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java @@ -152,8 +152,8 @@ public class AppClipsService extends Service { return CAPTURE_CONTENT_FOR_NOTE_FAILED; } - if (!mOptionalBubbles.get().isAppBubbleTaskId(taskId)) { - Log.d(TAG, String.format("Taskid %d is not app bubble task", taskId)); + if (!mOptionalBubbles.get().isNoteBubbleTaskId(taskId)) { + Log.d(TAG, String.format("Taskid %d is not note bubble task", taskId)); return CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java index 6844f053cd21..eae0ba66925d 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java @@ -16,6 +16,8 @@ package com.android.systemui.settings.brightness; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.Intent.EXTRA_BRIGHTNESS_DIALOG_IS_FULL_WIDTH; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; @@ -157,6 +159,7 @@ public class BrightnessDialog extends Activity { } void setBrightnessDialogViewAttributes(View container) { + Configuration configuration = getResources().getConfiguration(); // The brightness mirror container is INVISIBLE by default. container.setVisibility(View.VISIBLE); ViewGroup.MarginLayoutParams lp = @@ -171,9 +174,16 @@ public class BrightnessDialog extends Activity { R.dimen.notification_guts_option_vertical_padding); lp.topMargin = verticalMargin; + // If in multi-window or freeform, increase the top margin so the brightness dialog + // doesn't get cut off. + final int windowingMode = configuration.windowConfiguration.getWindowingMode(); + if (windowingMode == WINDOWING_MODE_MULTI_WINDOW + || windowingMode == WINDOWING_MODE_FREEFORM) { + lp.topMargin += 50; + } + lp.bottomMargin = verticalMargin; - Configuration configuration = getResources().getConfiguration(); int orientation = configuration.orientation; int windowWidth = getWindowAvailableWidth(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 255494f014e3..10a9fd20ee5a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -45,6 +45,7 @@ import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags; import com.android.systemui.bouncer.ui.binder.BouncerViewBinder; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlagsClassic; @@ -87,6 +88,8 @@ import com.android.systemui.util.time.SystemClock; import com.android.systemui.window.ui.WindowRootViewBinder; import com.android.systemui.window.ui.viewmodel.WindowRootViewModel; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.ExperimentalCoroutinesApi; import kotlinx.coroutines.flow.Flow; import java.io.PrintWriter; @@ -119,6 +122,7 @@ public class NotificationShadeWindowViewController implements Dumpable { private final PrimaryBouncerInteractor mPrimaryBouncerInteractor; private final AlternateBouncerInteractor mAlternateBouncerInteractor; private final QuickSettingsController mQuickSettingsController; + private final CoroutineDispatcher mMainDispatcher; private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; private final GlanceableHubContainerController mGlanceableHubContainerController; @@ -204,7 +208,8 @@ public class NotificationShadeWindowViewController implements Dumpable { AlternateBouncerInteractor alternateBouncerInteractor, BouncerViewBinder bouncerViewBinder, @ShadeDisplayAware Provider<ConfigurationForwarder> configurationForwarder, - BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor) { + BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor, + @Main CoroutineDispatcher mainDispatcher) { mLockscreenShadeTransitionController = transitionController; mFalsingCollector = falsingCollector; mStatusBarStateController = statusBarStateController; @@ -232,6 +237,7 @@ public class NotificationShadeWindowViewController implements Dumpable { mPrimaryBouncerInteractor = primaryBouncerInteractor; mAlternateBouncerInteractor = alternateBouncerInteractor; mQuickSettingsController = quickSettingsController; + mMainDispatcher = mainDispatcher; // This view is not part of the newly inflated expanded status bar. mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container); @@ -286,7 +292,7 @@ public class NotificationShadeWindowViewController implements Dumpable { if (SceneContainerFlag.isEnabled()) return; WindowRootViewBinder.INSTANCE.bind(mView, windowRootViewModelFactory, blurUtils, - choreographer); + choreographer, mMainDispatcher); } private void bindBouncer(BouncerViewBinder bouncerViewBinder) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt index 59d812403777..01451502b859 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt @@ -19,6 +19,9 @@ package com.android.systemui.shade.domain.interactor import android.provider.Settings import androidx.annotation.FloatRange import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.scene.domain.SceneFrameworkTableLog import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shade.shared.model.ShadeMode @@ -81,8 +84,9 @@ class ShadeModeInteractorImpl @Inject constructor( @Application applicationScope: CoroutineScope, - repository: ShadeRepository, + private val repository: ShadeRepository, secureSettingsRepository: SecureSettingsRepository, + @SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer, ) : ShadeModeInteractor { private val isDualShadeEnabled: Flow<Boolean> = @@ -93,17 +97,17 @@ constructor( override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide + private val shadeModeInitialValue: ShadeMode + get() = + determineShadeMode( + isDualShadeEnabled = DUAL_SHADE_ENABLED_DEFAULT, + isShadeLayoutWide = repository.isShadeLayoutWide.value, + ) + override val shadeMode: StateFlow<ShadeMode> = combine(isDualShadeEnabled, repository.isShadeLayoutWide, ::determineShadeMode) - .stateIn( - applicationScope, - SharingStarted.Eagerly, - initialValue = - determineShadeMode( - isDualShadeEnabled = DUAL_SHADE_ENABLED_DEFAULT, - isShadeLayoutWide = repository.isShadeLayoutWide.value, - ), - ) + .logDiffsForTable(tableLogBuffer = tableLogBuffer, initialValue = shadeModeInitialValue) + .stateIn(applicationScope, SharingStarted.Eagerly, initialValue = shadeModeInitialValue) @FloatRange(from = 0.0, to = 1.0) override fun getTopEdgeSplitFraction(): Float = 0.5f diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt index a8199a402ef1..8b3ce0f69742 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt @@ -16,15 +16,18 @@ package com.android.systemui.shade.shared.model +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger + /** Enumerates all known modes of operation of the shade. */ -sealed interface ShadeMode { +sealed class ShadeMode : Diffable<ShadeMode> { /** * The single or "accordion" shade where the QS and notification parts are in two vertically * stacked panels and the user can swipe up and down to expand or collapse between the two * parts. */ - data object Single : ShadeMode + data object Single : ShadeMode() /** * The split shade where, on large screens and unfolded foldables, the QS and notification parts @@ -32,14 +35,18 @@ sealed interface ShadeMode { * * Note: This isn't the only mode where the shade is wide. */ - data object Split : ShadeMode + data object Split : ShadeMode() /** * The dual shade where the QS and notification parts each have their own independently * expandable/collapsible panel on either side of the large screen / unfolded device or sharing * a space on a small screen or folded device. */ - data object Dual : ShadeMode + data object Dual : ShadeMode() + + override fun logDiffs(prevVal: ShadeMode, row: TableRowLogger) { + row.logChange("shadeMode", toString()) + } companion object { @JvmStatic fun dual(): Dual = Dual diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index dcea8d85e10d..1720898229a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -111,7 +111,7 @@ public class CommandQueue extends IStatusBar.Stub implements private static final int MSG_COLLAPSE_PANELS = 4 << MSG_SHIFT; private static final int MSG_EXPAND_SETTINGS = 5 << MSG_SHIFT; private static final int MSG_SYSTEM_BAR_CHANGED = 6 << MSG_SHIFT; - private static final int MSG_DISPLAY_READY = 7 << MSG_SHIFT; + private static final int MSG_DISPLAY_ADD_SYSTEM_DECORATIONS = 7 << MSG_SHIFT; private static final int MSG_SHOW_IME_BUTTON = 8 << MSG_SHIFT; private static final int MSG_TOGGLE_RECENT_APPS = 9 << MSG_SHIFT; private static final int MSG_PRELOAD_RECENT_APPS = 10 << MSG_SHIFT; @@ -415,9 +415,9 @@ public class CommandQueue extends IStatusBar.Stub implements } /** - * @see IStatusBar#onDisplayReady(int) + * @see IStatusBar#onDisplayAddSystemDecorations(int) */ - default void onDisplayReady(int displayId) { + default void onDisplayAddSystemDecorations(int displayId) { } /** @@ -1205,9 +1205,9 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override - public void onDisplayReady(int displayId) { + public void onDisplayAddSystemDecorations(int displayId) { synchronized (mLock) { - mHandler.obtainMessage(MSG_DISPLAY_READY, displayId, 0).sendToTarget(); + mHandler.obtainMessage(MSG_DISPLAY_ADD_SYSTEM_DECORATIONS, displayId, 0).sendToTarget(); } } @@ -1851,9 +1851,9 @@ public class CommandQueue extends IStatusBar.Stub implements mCallbacks.get(i).showPinningEscapeToast(); } break; - case MSG_DISPLAY_READY: + case MSG_DISPLAY_ADD_SYSTEM_DECORATIONS: for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).onDisplayReady(msg.arg1); + mCallbacks.get(i).onDisplayAddSystemDecorations(msg.arg1); } break; case MSG_DISPLAY_REMOVE_SYSTEM_DECORATIONS: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index 2544323d83d5..79a872edd2c5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -573,13 +573,13 @@ public final class KeyboardShortcutListSearch { Pair.create(KeyEvent.KEYCODE_ESCAPE, KeyEvent.META_META_ON), Pair.create(KeyEvent.KEYCODE_DEL, KeyEvent.META_META_ON), Pair.create(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_META_ON))), - /* Take a full screenshot: Meta + Ctrl + S */ + /* Take a full screenshot: Meta + S */ new ShortcutKeyGroupMultiMappingInfo( context.getString(R.string.group_system_full_screenshot), Arrays.asList( Pair.create( KeyEvent.KEYCODE_S, - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON))), + KeyEvent.META_META_ON))), /* Access list of system / apps shortcuts: Meta + / */ new ShortcutKeyGroupMultiMappingInfo( context.getString(R.string.group_system_access_system_app_shortcuts), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index 541a07c47df2..98b75216bbe9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel +import android.content.Context import android.view.View import com.android.internal.jank.Cuj import com.android.systemui.animation.ActivityTransitionAnimator @@ -23,6 +24,7 @@ import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.plugins.ActivityStarter @@ -52,6 +54,7 @@ import kotlinx.coroutines.flow.stateIn open class CallChipViewModel @Inject constructor( + @Main private val context: Context, @Application private val scope: CoroutineScope, interactor: CallChipInteractor, systemClock: SystemClock, @@ -65,15 +68,18 @@ constructor( is OngoingCallModel.NoCall, is OngoingCallModel.InCallWithVisibleApp -> OngoingActivityChipModel.Hidden() is OngoingCallModel.InCall -> { + val contentDescription = getContentDescription(state.appName) val icon = if (state.notificationIconView != null) { StatusBarConnectedDisplays.assertInLegacyMode() OngoingActivityChipModel.ChipIcon.StatusBarView( - state.notificationIconView + state.notificationIconView, + contentDescription, ) } else if (StatusBarConnectedDisplays.isEnabled) { OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon( - state.notificationKey + state.notificationKey, + contentDescription, ) } else { OngoingActivityChipModel.ChipIcon.SingleColorIcon(phoneIcon) @@ -155,6 +161,17 @@ constructor( ) } + private fun getContentDescription(appName: String): ContentDescription { + val ongoingCallDescription = context.getString(R.string.ongoing_call_content_description) + return ContentDescription.Loaded( + context.getString( + R.string.accessibility_desc_notification_icon, + appName, + ongoingCallDescription, + ) + ) + } + companion object { private val phoneIcon = Icon.Resource( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt index 49c44798d2ea..49d69f26b538 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map @@ -53,6 +54,9 @@ constructor( private val packageManager: PackageManager, @StatusBarChipsLog private val logger: LogBuffer, ) { + val projectionStartedDuringCallAndActivePostCallEvent: Flow<Unit> = + mediaProjectionRepository.projectionStartedDuringCallAndActivePostCallEvent + val projection: StateFlow<ProjectionChipModel> = mediaProjectionRepository.mediaProjectionState .map { state -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt new file mode 100644 index 000000000000..b37c76232f01 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt @@ -0,0 +1,42 @@ +/* + * 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.statusbar.chips.mediaprojection.domain.model + +import com.android.systemui.statusbar.phone.SystemUIDialog + +/** Represents the visibility state of a media projection stop dialog. */ +sealed interface MediaProjectionStopDialogModel { + /** The dialog is hidden and not visible to the user. */ + data object Hidden : MediaProjectionStopDialogModel + + /** The dialog is shown to the user. */ + data class Shown( + val dialogDelegate: SystemUIDialog.Delegate, + private val onDismissAction: () -> Unit, + ) : MediaProjectionStopDialogModel { + /** + * Creates and shows the dialog. Ensures that onDismissAction callback is invoked when the + * dialog is canceled or dismissed. + */ + fun createAndShowDialog() { + val dialog = dialogDelegate.createDialog() + dialog.setOnCancelListener { onDismissAction.invoke() } + dialog.setOnDismissListener { onDismissAction.invoke() } + dialog.show() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt index cece52110567..a9338885d4c2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt @@ -138,7 +138,7 @@ constructor( } } - return NotificationChipModel(key, statusBarChipIconView, promotedContent) + return NotificationChipModel(key, appName, statusBarChipIconView, promotedContent) } @AssistedFactory diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt index c6759da304bb..e7a90804a768 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt @@ -22,6 +22,8 @@ import com.android.systemui.statusbar.notification.promoted.shared.model.Promote /** Modeling all the data needed to render a status bar notification chip. */ data class NotificationChipModel( val key: String, + /** The user-readable name of the app that posted the call notification. */ + val appName: String, val statusBarChipIconView: StatusBarIconView?, val promotedContent: PromotedNotificationContentModel, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 46456b841e3f..b0da6428f579 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -16,10 +16,14 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel +import android.content.Context import android.view.View import com.android.systemui.Flags +import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips @@ -43,6 +47,7 @@ import kotlinx.coroutines.launch class NotifChipsViewModel @Inject constructor( + @Main private val context: Context, @Application private val applicationScope: CoroutineScope, private val notifChipsInteractor: StatusBarNotificationChipsInteractor, headsUpNotificationInteractor: HeadsUpNotificationInteractor, @@ -65,13 +70,20 @@ constructor( headsUpState: TopPinnedState ): OngoingActivityChipModel.Shown { StatusBarNotifChips.assertInNewMode() + val contentDescription = getContentDescription(this.appName) val icon = if (this.statusBarChipIconView != null) { StatusBarConnectedDisplays.assertInLegacyMode() - OngoingActivityChipModel.ChipIcon.StatusBarView(this.statusBarChipIconView) + OngoingActivityChipModel.ChipIcon.StatusBarView( + this.statusBarChipIconView, + contentDescription, + ) } else { StatusBarConnectedDisplays.assertInNewMode() - OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(this.key) + OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon( + this.key, + contentDescription, + ) } val colors = this.promotedContent.toCustomColorsModel() @@ -79,6 +91,7 @@ constructor( // The notification pipeline needs everything to run on the main thread, so keep // this event on the main thread. applicationScope.launch { + // TODO(b/364653005): Move accessibility focus to the HUN when chip is tapped. notifChipsInteractor.onPromotedNotificationChipTapped(this@toActivityChipModel.key) } } @@ -173,4 +186,16 @@ constructor( } } } + + private fun getContentDescription(appName: String): ContentDescription { + val ongoingDescription = + context.getString(R.string.ongoing_notification_extra_content_description) + return ContentDescription.Loaded( + context.getString( + R.string.accessibility_desc_notification_icon, + appName, + ongoingDescription, + ) + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt index 6654d4a8f104..7a46fff157cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel import android.content.Context import androidx.annotation.DrawableRes import com.android.internal.jank.Cuj +import com.android.systemui.CoreStartable import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription @@ -31,6 +32,7 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor +import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndGenericShareToAppDialogDelegate @@ -41,13 +43,18 @@ import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickCallback import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener +import com.android.systemui.util.kotlin.sample import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** * View model for the share-to-app chip, shown when sharing your phone screen content to another app @@ -64,7 +71,59 @@ constructor( private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, private val dialogTransitionAnimator: DialogTransitionAnimator, @StatusBarChipsLog private val logger: LogBuffer, -) : OngoingActivityChipViewModel { +) : OngoingActivityChipViewModel, CoreStartable { + + private val _stopDialogToShow: MutableStateFlow<MediaProjectionStopDialogModel> = + MutableStateFlow(MediaProjectionStopDialogModel.Hidden) + + /** + * Represents the current state of the media projection stop dialog. Emits + * [MediaProjectionStopDialogModel.Shown] when the dialog should be displayed, and + * [MediaProjectionStopDialogModel.Hidden] when it is dismissed. + */ + val stopDialogToShow: StateFlow<MediaProjectionStopDialogModel> = + _stopDialogToShow.asStateFlow() + + /** + * Emits a [MediaProjectionStopDialogModel] based on the current projection state when a + * projectionStartedDuringCallAndActivePostCallEvent event is emitted. If projecting, determines + * the appropriate dialog type to show. Otherwise, emits a hidden dialog state. + */ + private val stopDialogDueToCallEndedState: StateFlow<MediaProjectionStopDialogModel> = + mediaProjectionChipInteractor.projectionStartedDuringCallAndActivePostCallEvent + .sample(mediaProjectionChipInteractor.projection) { _, currentProjection -> + when (currentProjection) { + is ProjectionChipModel.NotProjecting -> MediaProjectionStopDialogModel.Hidden + is ProjectionChipModel.Projecting -> { + when (currentProjection.receiver) { + ProjectionChipModel.Receiver.ShareToApp -> { + when (currentProjection.contentType) { + ProjectionChipModel.ContentType.Screen -> + createShareScreenToAppStopDialog(currentProjection) + ProjectionChipModel.ContentType.Audio -> + createGenericShareScreenToAppStopDialog() + } + } + ProjectionChipModel.Receiver.CastToOtherDevice -> + MediaProjectionStopDialogModel.Hidden + } + } + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), MediaProjectionStopDialogModel.Hidden) + + /** + * Initializes background flow collector during SysUI startup for events determining the + * visibility of media projection stop dialogs. + */ + override fun start() { + if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) { + scope.launch { + stopDialogDueToCallEndedState.collect { event -> _stopDialogToShow.value = event } + } + } + } + private val internalChip = mediaProjectionChipInteractor.projection .map { projectionModel -> @@ -92,7 +151,25 @@ constructor( private val chipTransitionHelper = ChipTransitionHelper(scope) override val chip: StateFlow<OngoingActivityChipModel> = - chipTransitionHelper.createChipFlow(internalChip) + combine(chipTransitionHelper.createChipFlow(internalChip), stopDialogToShow) { + currentChip, + stopDialog -> + if ( + com.android.media.projection.flags.Flags.showStopDialogPostCallEnd() && + stopDialog is MediaProjectionStopDialogModel.Shown + ) { + logger.log( + TAG, + LogLevel.INFO, + {}, + { "Hiding the chip as stop dialog is being shown" }, + ) + OngoingActivityChipModel.Hidden() + } else { + currentChip + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden()) /** * Notifies this class that the user just stopped a screen recording from the dialog that's @@ -108,6 +185,12 @@ constructor( chipTransitionHelper.onActivityStoppedFromDialog() } + /** Called when the stop dialog is dismissed or cancelled. */ + private fun onStopDialogDismissed() { + logger.log(TAG, LogLevel.INFO, {}, { "The media projection stop dialog was dismissed" }) + _stopDialogToShow.value = MediaProjectionStopDialogModel.Hidden + } + /** Stops the currently active projection. */ private fun stopProjectingFromDialog() { logger.log(TAG, LogLevel.INFO, {}, { "Stop sharing requested from dialog" }) @@ -115,6 +198,24 @@ constructor( mediaProjectionChipInteractor.stopProjecting() } + private fun createShareScreenToAppStopDialog( + projectionModel: ProjectionChipModel.Projecting + ): MediaProjectionStopDialogModel { + val dialogDelegate = createShareScreenToAppDialogDelegate(projectionModel) + return MediaProjectionStopDialogModel.Shown( + dialogDelegate, + onDismissAction = ::onStopDialogDismissed, + ) + } + + private fun createGenericShareScreenToAppStopDialog(): MediaProjectionStopDialogModel { + val dialogDelegate = createGenericShareToAppDialogDelegate() + return MediaProjectionStopDialogModel.Shown( + dialogDelegate, + onDismissAction = ::onStopDialogDismissed, + ) + } + private fun createShareScreenToAppChip( state: ProjectionChipModel.Projecting ): OngoingActivityChipModel.Shown { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt index d46638fac46c..de9d4974c0c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt @@ -26,6 +26,8 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import androidx.annotation.UiThread +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView @@ -187,7 +189,13 @@ object OngoingActivityChipBinder { } is OngoingActivityChipModel.ChipIcon.StatusBarView -> { StatusBarConnectedDisplays.assertInLegacyMode() - setStatusBarIconView(defaultIconView, icon.impl, iconTint, backgroundView) + setStatusBarIconView( + defaultIconView, + icon.impl, + icon.contentDescription, + iconTint, + backgroundView, + ) } is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> { StatusBarConnectedDisplays.assertInNewMode() @@ -196,7 +204,13 @@ object OngoingActivityChipBinder { // This means that the notification key doesn't exist anymore. return } - setStatusBarIconView(defaultIconView, iconView, iconTint, backgroundView) + setStatusBarIconView( + defaultIconView, + iconView, + icon.contentDescription, + iconTint, + backgroundView, + ) } } } @@ -215,6 +229,7 @@ object OngoingActivityChipBinder { private fun setStatusBarIconView( defaultIconView: ImageView, iconView: StatusBarIconView, + iconContentDescription: ContentDescription, iconTint: Int, backgroundView: ChipBackgroundContainer, ) { @@ -224,9 +239,12 @@ object OngoingActivityChipBinder { // 1. Set up the right visual params. with(iconView) { id = CUSTOM_ICON_VIEW_ID - // TODO(b/354930838): For RON chips, use the app name for the content description. - contentDescription = - context.resources.getString(R.string.ongoing_call_content_description) + if (StatusBarNotifChips.isEnabled) { + ContentDescriptionViewBinder.bind(iconContentDescription, this) + } else { + contentDescription = + context.resources.getString(R.string.ongoing_call_content_description) + } tintView(iconTint) } @@ -418,6 +436,7 @@ object OngoingActivityChipBinder { } is OngoingActivityChipModel.Shown.Timer, is OngoingActivityChipModel.Shown.Text, + is OngoingActivityChipModel.Shown.ShortTimeDelta, is OngoingActivityChipModel.Shown.IconOnly -> { chipView.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_NONE } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt index 13f4e51f2ba2..375e02989a3d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt @@ -95,6 +95,10 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = is OngoingActivityChipModel.Shown.ShortTimeDelta -> { // TODO(b/372657935): Implement ShortTimeDelta content in compose. } + + is OngoingActivityChipModel.Shown.IconOnly -> { + throw IllegalStateException("ChipContent should only be used if the chip shows text") + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt index 647f3bd469f1..816f291b9273 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -37,11 +37,14 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.Expandable import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.common.ui.compose.load import com.android.systemui.res.R import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel @@ -82,11 +85,25 @@ private fun ChipBody( val isClickable = onClick != {} val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView + val contentDescription = + when (val icon = model.icon) { + is OngoingActivityChipModel.ChipIcon.StatusBarView -> icon.contentDescription.load() + is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> + icon.contentDescription.load() + is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> null + null -> null + } + // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible // height of the chip is determined by the height of the background of the Row below. Box( contentAlignment = Alignment.Center, - modifier = modifier.fillMaxHeight().clickable(enabled = isClickable, onClick = onClick), + modifier = + modifier.fillMaxHeight().clickable(enabled = isClickable, onClick = onClick).semantics { + if (contentDescription != null) { + this.contentDescription = contentDescription + } + }, ) { Row( horizontalArrangement = Arrangement.Center, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index c6d6da2ad9aa..d44646cb6144 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.ui.model import android.view.View import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips @@ -40,7 +41,7 @@ sealed class OngoingActivityChipModel { } /** This chip should be shown with the given information. */ - abstract class Shown( + sealed class Shown( /** The icon to show on the chip. If null, no icon will be shown. */ open val icon: ChipIcon?, /** What colors to use for the chip. */ @@ -140,7 +141,10 @@ sealed class OngoingActivityChipModel { * The icon is a custom icon, which is set on [impl]. The icon was likely created by an * external app. */ - data class StatusBarView(val impl: StatusBarIconView) : ChipIcon { + data class StatusBarView( + val impl: StatusBarIconView, + val contentDescription: ContentDescription, + ) : ChipIcon { init { StatusBarConnectedDisplays.assertInLegacyMode() } @@ -150,7 +154,10 @@ sealed class OngoingActivityChipModel { * The icon is a custom icon, which is set on a notification, and can be looked up using the * provided [notificationKey]. The icon was likely created by an external app. */ - data class StatusBarNotificationIcon(val notificationKey: String) : ChipIcon { + data class StatusBarNotificationIcon( + val notificationKey: String, + val contentDescription: ContentDescription, + ) : ChipIcon { init { StatusBarConnectedDisplays.assertInNewMode() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarter.kt index b057fb0433fe..eeb7a4066eca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarter.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.core import android.view.Display +import android.view.IWindowManager import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton @@ -54,6 +55,7 @@ constructor( private val autoHideControllerStore: AutoHideControllerStore, private val privacyDotWindowControllerStore: PrivacyDotWindowControllerStore, private val lightBarControllerStore: LightBarControllerStore, + private val windowManager: IWindowManager, ) : CoreStartable { init { @@ -68,7 +70,13 @@ constructor( } .onStart { emit(displayRepository.displays.value) } .collect { newDisplays -> - newDisplays.forEach { createAndStartComponentsForDisplay(it) } + newDisplays.forEach { + // TODO(b/393191204): Split navbar, status bar, etc. functionality + // from WindowManager#shouldShowSystemDecors. + if (windowManager.shouldShowSystemDecors(it.displayId)) { + createAndStartComponentsForDisplay(it) + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt index 351cdc8e7f36..b5a781ecdfb8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt @@ -24,6 +24,7 @@ import com.android.systemui.SysUICutoutProviderImpl import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory +import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.data.StatusBarDataLayerModule import com.android.systemui.statusbar.data.repository.LightBarControllerStore @@ -140,6 +141,20 @@ interface StatusBarModule { @Provides @SysUISingleton @IntoMap + @ClassKey(ShareToAppChipViewModel::class) + fun providesShareToAppChipViewModel( + shareToAppChipViewModelLazy: Lazy<ShareToAppChipViewModel> + ): CoreStartable { + return if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) { + shareToAppChipViewModelLazy.get() + } else { + CoreStartable.NOP + } + } + + @Provides + @SysUISingleton + @IntoMap @ClassKey(MultiDisplayStatusBarWindowControllerStore::class) fun multiDisplayControllerStoreAsCoreStartable( storeLazy: Lazy<MultiDisplayStatusBarWindowControllerStore> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ViewConfigCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ViewConfigCoordinator.kt index 2d1eccdf1abd..a0a86710b4ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ViewConfigCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ViewConfigCoordinator.kt @@ -22,6 +22,7 @@ import com.android.internal.widget.MessagingGroup import com.android.internal.widget.MessagingMessage import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.Flags import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener @@ -144,7 +145,12 @@ internal constructor( ) log { "ViewConfigCoordinator.updateNotificationsOnUiModeChanged()" } traceSection("updateNotifOnUiModeChanged") { - mPipeline?.allNotifs?.forEach { entry -> entry.row?.onUiModeChanged() } + mPipeline?.allNotifs?.forEach { entry -> + entry.row?.onUiModeChanged() + if (Flags.notificationUndoGutsOnConfigChanged()) { + mGutsManager.closeAndUndoGuts() + } + } } } @@ -152,9 +158,15 @@ internal constructor( colorUpdateLogger.logEvent("VCC.updateNotificationsOnDensityOrFontScaleChanged()") mPipeline?.allNotifs?.forEach { entry -> entry.onDensityOrFontScaleChanged() - val exposedGuts = entry.areGutsExposed() - if (exposedGuts) { - mGutsManager.onDensityOrFontScaleChanged(entry) + if (Flags.notificationUndoGutsOnConfigChanged()) { + mGutsManager.closeAndUndoGuts() + } else { + // This property actually gets reset when the guts are re-inflated, so we're never + // actually calling onDensityOrFontScaleChanged below. + val exposedGuts = entry.areGutsExposed() + if (exposedGuts) { + mGutsManager.onDensityOrFontScaleChanged(entry) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt index fd5973e0ab3b..bde3c4d8c632 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt @@ -21,10 +21,12 @@ import android.app.Notification.CallStyle.CALL_TYPE_SCREENING import android.app.Notification.CallStyle.CALL_TYPE_UNKNOWN import android.app.Notification.EXTRA_CALL_TYPE import android.app.PendingIntent +import android.content.Context import android.graphics.drawable.Icon import android.service.notification.StatusBarNotification import android.util.ArrayMap import com.android.app.tracing.traceSection +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry @@ -50,6 +52,7 @@ class RenderNotificationListInteractor constructor( private val repository: ActiveNotificationListRepository, private val sectionStyleProvider: SectionStyleProvider, + @Main private val context: Context, ) { /** * Sets the current list of rendered notification entries as displayed in the notification list. @@ -57,7 +60,7 @@ constructor( fun setRenderedList(entries: List<ListEntry>) { traceSection("RenderNotificationListInteractor.setRenderedList") { repository.activeNotifications.update { existingModels -> - buildActiveNotificationsStore(existingModels, sectionStyleProvider) { + buildActiveNotificationsStore(existingModels, sectionStyleProvider, context) { entries.forEach(::addListEntry) setRankingsMap(entries) } @@ -69,13 +72,17 @@ constructor( private fun buildActiveNotificationsStore( existingModels: ActiveNotificationsStore, sectionStyleProvider: SectionStyleProvider, + context: Context, block: ActiveNotificationsStoreBuilder.() -> Unit, ): ActiveNotificationsStore = - ActiveNotificationsStoreBuilder(existingModels, sectionStyleProvider).apply(block).build() + ActiveNotificationsStoreBuilder(existingModels, sectionStyleProvider, context) + .apply(block) + .build() private class ActiveNotificationsStoreBuilder( private val existingModels: ActiveNotificationsStore, private val sectionStyleProvider: SectionStyleProvider, + private val context: Context, ) { private val builder = ActiveNotificationsStore.Builder() @@ -154,6 +161,7 @@ private class ActiveNotificationsStoreBuilder( statusBarChipIconView = icons.statusBarChipIcon, uid = sbn.uid, packageName = sbn.packageName, + appName = sbn.notification.loadHeaderAppName(context), contentIntent = sbn.notification.contentIntent, instanceId = sbn.instanceId?.id, isGroupSummary = sbn.notification.isGroupSummary, @@ -180,6 +188,7 @@ private fun ActiveNotificationsStore.createOrReuse( statusBarChipIconView: StatusBarIconView?, uid: Int, packageName: String, + appName: String, contentIntent: PendingIntent?, instanceId: Int?, isGroupSummary: Boolean, @@ -206,6 +215,7 @@ private fun ActiveNotificationsStore.createOrReuse( instanceId = instanceId, isGroupSummary = isGroupSummary, packageName = packageName, + appName = appName, contentIntent = contentIntent, bucket = bucket, callType = callType, @@ -230,6 +240,7 @@ private fun ActiveNotificationsStore.createOrReuse( instanceId = instanceId, isGroupSummary = isGroupSummary, packageName = packageName, + appName = appName, contentIntent = contentIntent, bucket = bucket, callType = callType, @@ -253,6 +264,7 @@ private fun ActiveNotificationModel.isCurrent( statusBarChipIconView: StatusBarIconView?, uid: Int, packageName: String, + appName: String, contentIntent: PendingIntent?, instanceId: Int?, isGroupSummary: Boolean, @@ -278,6 +290,7 @@ private fun ActiveNotificationModel.isCurrent( instanceId != this.instanceId -> false isGroupSummary != this.isGroupSummary -> false packageName != this.packageName -> false + appName != this.appName -> false contentIntent != this.contentIntent -> false bucket != this.bucket -> false callType != this.callType -> false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt index 7e2361f24da9..fa0cea15c43f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel import android.content.Context import android.icu.text.MessageFormat +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager import com.android.systemui.modes.shared.ModesUi @@ -36,9 +37,11 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart /** * ViewModel for the empty shade (aka the "No notifications" text shown when there are no @@ -51,6 +54,7 @@ constructor( zenModeInteractor: ZenModeInteractor, seenNotificationsInteractor: SeenNotificationsInteractor, notificationSettingsInteractor: NotificationSettingsInteractor, + configurationInteractor: ConfigurationInteractor, @Background bgDispatcher: CoroutineDispatcher, dumpManager: DumpManager, ) : FlowDumperImpl(dumpManager) { @@ -71,6 +75,13 @@ constructor( "hasFilteredOutSeenNotifications" ) + private val primaryLocale by lazy { + configurationInteractor.configurationValues + .map { it.locales.get(0) ?: Locale.getDefault() } + .onStart { emit(Locale.getDefault()) } + .distinctUntilChanged() + } + val text: Flow<String> by lazy { if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) { flowOf(context.getString(R.string.empty_shade_text)) @@ -79,14 +90,16 @@ constructor( // recommended architecture, and making it so it reacts to changes for the new Modes. // The former does not depend on the modes flags being on, but the latter does. if (ModesUi.isEnabled) { - zenModeInteractor.modesHidingNotifications.map { modes -> + combine(zenModeInteractor.modesHidingNotifications, primaryLocale) { + modes, + locale -> // Create a string that is either "No notifications" if no modes are - // filtering - // them out, or something like "Notifications paused by SomeMode" otherwise. + // filtering them out, or something like "Notifications paused by SomeMode" + // otherwise. val msgFormat = MessageFormat( context.getString(R.string.modes_suppressing_shade_text), - Locale.getDefault(), + locale, ) val count = modes.count() val args: MutableMap<String, Any> = HashMap() 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 cb9bd4a3fd35..33c71d4a9c5a 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 @@ -27,8 +27,19 @@ import android.widget.DateTimeView import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.isVisible import com.android.app.tracing.traceSection @@ -41,6 +52,8 @@ import com.android.internal.widget.NotificationProgressBar import com.android.internal.widget.NotificationRowIconView import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R as systemuiR +import com.android.systemui.statusbar.notification.promoted.AodPromotedNotificationColor.PrimaryText +import com.android.systemui.statusbar.notification.promoted.AodPromotedNotificationColor.SecondaryText import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When @@ -59,29 +72,53 @@ fun AODPromotedNotification(viewModelFactory: AODPromotedNotificationViewModel.F key(content.identity) { val layoutResource = content.layoutResource ?: return - AndroidView( - factory = { context -> + val topPadding = dimensionResource(systemuiR.dimen.below_clock_padding_start_icons) + val sidePaddings = dimensionResource(systemuiR.dimen.notification_side_paddings) + val paddingValues = + PaddingValues(top = topPadding, start = sidePaddings, end = sidePaddings, bottom = 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(paddingValues)) { + AODPromotedNotificationView( + layoutResource = layoutResource, + content = content, + modifier = Modifier.border(borderStroke, borderShape), + ) + } + } +} + +@Composable +fun AODPromotedNotificationView( + layoutResource: Int, + content: PromotedNotificationContentModel, + modifier: Modifier = Modifier, +) { + AndroidView( + factory = { context -> + val view = traceSection("$TAG.inflate") { - LayoutInflater.from(context).inflate(layoutResource, /* root= */ null) - } - .apply { - setTag( - viewUpdaterTagId, - traceSection("$TAG.findViews") { - AODPromotedNotificationViewUpdater(this) - }, - ) - } - }, - update = { view -> - traceSection("$TAG.update") { - (view.getTag(viewUpdaterTagId) as AODPromotedNotificationViewUpdater).update( - content - ) + 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) } + }, + modifier = modifier, + ) } private val PromotedNotificationContentModel.layoutResource: Int? @@ -262,12 +299,12 @@ private class AODPromotedNotificationViewUpdater(root: View) { } private fun updateTitle(titleView: TextView?, content: PromotedNotificationContentModel) { - updateTextView(titleView, content.title, color = Color.PrimaryText) + updateTextView(titleView, content.title, color = PrimaryText) } private fun updateTimeAndChronometer(content: PromotedNotificationContentModel) { - setTextViewColor(time, Color.SecondaryText) - setTextViewColor(chronometer, Color.SecondaryText) + setTextViewColor(time, SecondaryText) + setTextViewColor(chronometer, SecondaryText) val timeValue = content.time @@ -309,7 +346,7 @@ private class AODPromotedNotificationViewUpdater(root: View) { private fun updateTextView( view: TextView?, text: CharSequence?, - color: Color = Color.SecondaryText, + color: AodPromotedNotificationColor = SecondaryText, ) { setTextViewColor(view, color) @@ -322,15 +359,19 @@ private class AODPromotedNotificationViewUpdater(root: View) { } } - private fun setTextViewColor(view: TextView?, color: Color) { - view?.setTextColor(color.color.toInt()) + private fun setTextViewColor(view: TextView?, color: AodPromotedNotificationColor) { + view?.setTextColor(color.colorInt) } +} - private enum class Color(val color: UInt) { - Background(0x00000000u), - PrimaryText(0xFFFFFFFFu), - SecondaryText(0xFFCCCCCCu), - } +private enum class AodPromotedNotificationColor(colorUInt: UInt) { + Background(0x00000000u), + PrimaryText(0xFFFFFFFFu), + SecondaryText(0xFFCCCCCCu); + + val colorInt = colorUInt.toInt() + val color = Color(colorInt) + val brush = SolidColor(color) } private val viewUpdaterTagId = systemuiR.id.aod_promoted_notification_view_updater_tag 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 bd13dcd4e10b..a6dde103e6ff 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 @@ -1247,15 +1247,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** - * Prepares expansion changed. - */ - public void prepareExpansionChanged() { - if (mIsSummaryWithChildren) { - mChildrenContainer.prepareExpansionChanged(); - } - } - - /** * Starts child animations. */ public void startChildAnimation(AnimationProperties properties) { @@ -1569,7 +1560,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView // Let's update our childrencontainer. This is intentionally not guarded with // mIsSummaryWithChildren since we might have had children but not anymore. if (mChildrenContainer != null) { - mChildrenContainer.reInflateViews(mExpandClickListener, mEntry.getSbn()); + mChildrenContainer.reInflateViews(mExpandClickListener); } if (mGuts != null) { NotificationGuts oldGuts = mGuts; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java index b86d1d934269..75d1c7c3d51e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java @@ -287,7 +287,7 @@ public class NotificationGuts extends FrameLayout { * @param save whether the state should be saved * @param force whether the guts should be force-closed regardless of state. */ - private void closeControls(int x, int y, boolean save, boolean force) { + public void closeControls(int x, int y, boolean save, boolean force) { // First try to dismiss any blocking helper. if (getWindowToken() == null) { if (mClosedListener != null) { 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 b1e5b22f9b1a..445cd010cd86 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 @@ -48,6 +48,7 @@ import com.android.internal.logging.nano.MetricsProto; import com.android.internal.statusbar.IStatusBarService; import com.android.settingslib.notification.ConversationIconFactory; import com.android.systemui.CoreStartable; +import com.android.systemui.Flags; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -223,6 +224,10 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta } public void onDensityOrFontScaleChanged(NotificationEntry entry) { + if (!Flags.notificationUndoGutsOnConfigChanged()) { + Log.wtf(TAG, "onDensityOrFontScaleChanged should not be called if" + + " notificationUndoGutsOnConfigChanged is off"); + } setExposedGuts(entry.getGuts()); bindGuts(entry.getRow()); } @@ -590,7 +595,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta } /** - * Closes guts or notification menus that might be visible and saves any changes. + * Closes guts or notification menus that might be visible and saves any changes if applicable + * (see {@link NotificationGuts.GutsContent#shouldBeSavedOnClose}). * * @param removeLeavebehinds true if leavebehinds (e.g. snooze) should be closed. * @param force true if guts should be closed regardless of state (used for snooze only). @@ -611,6 +617,20 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta } /** + * Closes all guts that might be visible without saving changes. + */ + public void closeAndUndoGuts() { + if (mNotificationGutsExposed != null) { + mNotificationGutsExposed.removeCallbacks(mOpenRunnable); + mNotificationGutsExposed.closeControls( + /* x = */ -1, + /* y = */ -1, + /* save = */ false, + /* force = */ false); + } + } + + /** * Returns the exposed NotificationGuts or null if none are exposed. */ public NotificationGuts getExposedGuts() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java index 99a6f6a59bd0..83897f5bc3a7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java @@ -51,6 +51,7 @@ import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Flags; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption; import com.android.systemui.res.R; @@ -86,18 +87,26 @@ public class NotificationSnooze extends LinearLayout private NotificationSwipeActionHelper mSnoozeListener; private StatusBarNotification mSbn; - private View mSnoozeView; - private TextView mSelectedOptionText; + @VisibleForTesting + public View mSnoozeView; + @VisibleForTesting + public TextView mSelectedOptionText; private TextView mUndoButton; - private ImageView mExpandButton; - private View mDivider; - private ViewGroup mSnoozeOptionContainer; - private List<SnoozeOption> mSnoozeOptions; + @VisibleForTesting + public ImageView mExpandButton; + @VisibleForTesting + public View mDivider; + @VisibleForTesting + public ViewGroup mSnoozeOptionContainer; + @VisibleForTesting + public List<SnoozeOption> mSnoozeOptions; private int mCollapsedHeight; private SnoozeOption mDefaultOption; - private SnoozeOption mSelectedOption; + @VisibleForTesting + public SnoozeOption mSelectedOption; private boolean mSnoozing; - private boolean mExpanded; + @VisibleForTesting + public boolean mExpanded; private AnimatorSet mExpandAnimation; private KeyValueListParser mParser; @@ -334,7 +343,8 @@ public class NotificationSnooze extends LinearLayout } } - private void showSnoozeOptions(boolean show) { + @VisibleForTesting + public void showSnoozeOptions(boolean show) { int drawableId = show ? com.android.internal.R.drawable.ic_collapse_notification : com.android.internal.R.drawable.ic_expand_notification; mExpandButton.setImageResource(drawableId); @@ -381,7 +391,8 @@ public class NotificationSnooze extends LinearLayout mExpandAnimation.start(); } - private void setSelected(SnoozeOption option, boolean userAction) { + @VisibleForTesting + public void setSelected(SnoozeOption option, boolean userAction) { if (option != mSelectedOption) { mSelectedOption = option; mSelectedOptionText.setText(option.getConfirmation()); @@ -466,7 +477,12 @@ public class NotificationSnooze extends LinearLayout @Override public boolean handleCloseControls(boolean save, boolean force) { - if (mExpanded && !force) { + if (Flags.notificationUndoGutsOnConfigChanged() && !save) { + // Undo changes and let the guts handle closing the view + mSelectedOption = null; + showSnoozeOptions(false); + return false; + } else if (mExpanded && !force) { // Collapse expanded state on outside touch showSnoozeOptions(false); return true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt index ab8be306ab5e..f00c3ae20e30 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt @@ -68,6 +68,8 @@ data class ActiveNotificationModel( val uid: Int, /** The notifying app's packageName. */ val packageName: String, + /** The notifying app's display name. */ + val appName: String, /** The intent to execute if UI related to this notification is clicked. */ val contentIntent: PendingIntent?, /** A small per-notification ID, used for statsd logging. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 8e48065d9d1d..ea397b61fe84 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -77,7 +77,7 @@ public class NotificationChildrenContainer extends ViewGroup static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5; public static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8; private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() { - private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); + private final AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); @Override public AnimationFilter getAnimationFilter() { @@ -123,6 +123,8 @@ public class NotificationChildrenContainer extends ViewGroup private NotificationHeaderViewWrapper mMinimizedGroupHeaderWrapper; private NotificationGroupingUtil mGroupingUtil; private ViewState mHeaderViewState; + private ViewState mTopLineViewState; + private ViewState mExpandButtonViewState; private int mClipBottomAmount; private boolean mIsMinimized; private OnClickListener mHeaderClickListener; @@ -138,7 +140,7 @@ public class NotificationChildrenContainer extends ViewGroup private float mHeaderVisibleAmount = 1.0f; private int mUntruncatedChildCount; private boolean mContainingNotificationIsFaded = false; - private RoundableState mRoundableState; + private final RoundableState mRoundableState; private int mMinSingleLineHeight; private NotificationChildrenContainerLogger mLogger; @@ -446,7 +448,7 @@ public class NotificationChildrenContainer extends ViewGroup } mGroupHeaderWrapper.setExpanded(mChildrenExpanded); mGroupHeaderWrapper.onContentUpdated(mContainingNotification); - recreateLowPriorityHeader(builder, isConversation); + recreateLowPriorityHeader(builder); updateHeaderVisibility(false /* animate */); updateChildrenAppearance(); Trace.endSection(); @@ -559,7 +561,7 @@ public class NotificationChildrenContainer extends ViewGroup * @param builder a builder to reuse. Otherwise the builder will be recovered. */ @VisibleForTesting - void recreateLowPriorityHeader(Notification.Builder builder, boolean isConversation) { + void recreateLowPriorityHeader(Notification.Builder builder) { AsyncGroupHeaderViewInflation.assertInLegacyMode(); RemoteViews header; StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); @@ -866,10 +868,7 @@ public class NotificationChildrenContainer extends ViewGroup } } if (mGroupHeader != null) { - if (mHeaderViewState == null) { - mHeaderViewState = new ViewState(); - } - mHeaderViewState.initFrom(mGroupHeader); + mHeaderViewState = initStateForGroupHeader(mHeaderViewState); if (mContainingNotification.hasExpandingChild()) { // Not modifying translationZ during expand animation. @@ -881,38 +880,33 @@ public class NotificationChildrenContainer extends ViewGroup } mHeaderViewState.setYTranslation(mCurrentHeaderTranslation); mHeaderViewState.setAlpha(mHeaderVisibleAmount); - // The hiding is done automatically by the alpha, otherwise we'll pick it up again - // in the next frame with the initFrom call above and have an invisible header - mHeaderViewState.hidden = false; + + if (notificationsRedesignTemplates()) { + mTopLineViewState = initStateForGroupHeader(mTopLineViewState); + mTopLineViewState.setYTranslation( + mGroupHeader.getTopLineTranslation() * expandFactor); + + mExpandButtonViewState = initStateForGroupHeader(mExpandButtonViewState); + mExpandButtonViewState.setYTranslation( + mGroupHeader.getExpandButtonTranslation() * expandFactor); + } } } /** - * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its - * height, children in the group after this are gone. - * - * @param child the child who's height to adjust. - * @param parentHeight the height of the parent. - * @param childState the state to update. - * @param yPosition the yPosition of the view. - * @return true if children after this one should be hidden. + * Initialise a new ViewState for the group header or its children, or update and return + * {@code existingState} if not null. */ - private boolean updateChildStateForExpandedGroup( - ExpandableNotificationRow child, - int parentHeight, - ExpandableViewState childState, - int yPosition) { - final int top = yPosition + child.getClipTopAmount(); - final int intrinsicHeight = child.getIntrinsicHeight(); - final int bottom = top + intrinsicHeight; - int newHeight = intrinsicHeight; - if (bottom >= parentHeight) { - // Child is either clipped or gone - newHeight = Math.max((parentHeight - top), 0); - } - childState.hidden = newHeight == 0; - childState.height = newHeight; - return childState.height != intrinsicHeight && !childState.hidden; + private ViewState initStateForGroupHeader(ViewState existingState) { + ViewState viewState = existingState; + if (viewState == null) { + viewState = new ViewState(); + } + viewState.initFrom(mGroupHeader); + // The hiding is done automatically by the alpha, otherwise we'll pick it up again + // in the next frame with the initFrom call above and have an invisible header + viewState.hidden = false; + return viewState; } @VisibleForTesting @@ -976,6 +970,14 @@ public class NotificationChildrenContainer extends ViewGroup if (mHeaderViewState != null) { mHeaderViewState.applyToView(mGroupHeader); } + if (notificationsRedesignTemplates()) { + if (mTopLineViewState != null) { + mTopLineViewState.applyToView(mGroupHeader.getTopLineView()); + } + if (mExpandButtonViewState != null) { + mExpandButtonViewState.applyToView(mGroupHeader.getExpandButton()); + } + } updateChildrenClipping(); } @@ -1010,7 +1012,7 @@ public class NotificationChildrenContainer extends ViewGroup } @Override - protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + protected boolean drawChild(@NonNull Canvas canvas, View child, long drawingTime) { boolean isCanvasChanged = false; Path clipPath = mChildClipPath; @@ -1062,16 +1064,6 @@ public class NotificationChildrenContainer extends ViewGroup } } - - /** - * This is called when the children expansion has changed and positions the children properly - * for an appear animation. - */ - public void prepareExpansionChanged() { - // TODO: do something that makes sense, like placing the invisible views correctly - return; - } - /** * Animate to a given state. */ @@ -1478,7 +1470,7 @@ public class NotificationChildrenContainer extends ViewGroup return mIsMinimized && !mContainingNotification.isExpanded(); } - public void reInflateViews(OnClickListener listener, StatusBarNotification notification) { + public void reInflateViews(OnClickListener listener) { if (!AsyncGroupHeaderViewInflation.isEnabled()) { // When Async header inflation is enabled, we do not reinflate headers because they are // inflated from the background thread @@ -1567,7 +1559,7 @@ public class NotificationChildrenContainer extends ViewGroup mIsMinimized = isMinimized; if (mContainingNotification != null) { /* we're not yet set up yet otherwise */ if (!AsyncGroupHeaderViewInflation.isEnabled()) { - recreateLowPriorityHeader(null /* existingBuilder */, mIsConversation); + recreateLowPriorityHeader(null /* existingBuilder */); } updateHeaderVisibility(false /* animate */); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 04862c91e7ec..876090101f6e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -6683,10 +6683,23 @@ public class NotificationStackScrollLayout } NotificationHeaderView header = childrenContainer.getGroupHeader(); if (header != null) { + resetYTranslation(header.getTopLineView()); + resetYTranslation(header.getExpandButton()); header.centerTopLine(expanded); } } + /** + * Reset the y translation of the {@code view} via the {@link ViewState}, to ensure that the + * animation state is updated correctly. + */ + private static void resetYTranslation(View view) { + ViewState viewState = new ViewState(); + viewState.initFrom(view); + viewState.setYTranslation(0); + viewState.applyToView(view); + } + private final ExpandHelper.Callback mExpandHelperCallback = new ExpandHelper.Callback() { @Override public ExpandableView getChildAtPosition(float touchX, float touchY) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index 4686bef9ca5a..c783250f2e0a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -468,10 +468,6 @@ public class StackStateAnimator { if (isFullySwipedOut) { changingView.removeFromTransientContainer(); } - } else if (event.animationType == NotificationStackScrollLayout - .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) { - ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView; - row.prepareExpansionChanged(); } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_CYCLING_IN) { mHeadsUpAppearChildren.add(changingView); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index f7401440cfcb..ece1803e14c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -28,6 +28,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shared.Flags.extendedWallpaperEffects import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer @@ -143,7 +144,7 @@ constructor( } if (!SceneContainerFlag.isEnabled) { - if (Flags.magicPortraitWallpapers()) { + if (extendedWallpaperEffects()) { launch { combine( viewModel.getNotificationStackAbsoluteBottom( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index 09531c3b18bd..01f2e9b8371d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -230,7 +230,11 @@ public final class DozeServiceHost implements DozeHost { mDozingRequested = true; updateDozing(); mDozeLog.traceDozing(mStatusBarStateController.isDozing()); - mCentralSurfaces.updateIsKeyguard(); + // This is initialized in a CoreStartable, but binder calls from DreamManagerService can + // arrive earlier + if (mCentralSurfaces != null) { + mCentralSurfaces.updateIsKeyguard(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index a29934fa3a16..949cb0a718d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -160,6 +160,7 @@ constructor( notificationIconView = currentInfo.notificationIconView, intent = currentInfo.intent, notificationKey = currentInfo.key, + appName = currentInfo.appName, promotedContent = currentInfo.promotedContent, ) } else { @@ -217,6 +218,7 @@ constructor( notifModel.statusBarChipIconView, notifModel.contentIntent, notifModel.uid, + notifModel.appName, notifModel.promotedContent, isOngoing = true, statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false, @@ -337,6 +339,7 @@ constructor( val notificationIconView: StatusBarIconView?, val intent: PendingIntent?, val uid: Int, + val appName: String, /** * If the call notification also meets promoted notification criteria, this field is filled * in with the content related to promotion. Otherwise null. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt index ba7628fb3c07..2fd7d82043a0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt @@ -163,6 +163,7 @@ constructor( notificationIconView = model.statusBarChipIconView, intent = model.contentIntent, notificationKey = model.key, + appName = model.appName, promotedContent = model.promotedContent, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt index 7d00e9d58e5b..6507b727eb48 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt @@ -42,6 +42,7 @@ sealed interface OngoingCallModel { * @property notificationIconView the [android.app.Notification.getSmallIcon] that's set on the * call notification. We may use this icon in the chip instead of the default phone icon. * @property intent the intent associated with the call notification. + * @property appName the user-readable name of the app that posted the call notification. * @property promotedContent if the call notification also meets promoted notification criteria, * this field is filled in with the content related to promotion. Otherwise null. */ @@ -50,6 +51,7 @@ sealed interface OngoingCallModel { val notificationIconView: StatusBarIconView?, val intent: PendingIntent?, val notificationKey: String, + val appName: String, val promotedContent: PromotedNotificationContentModel?, ) : OngoingCallModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 7e76d77abe61..bd6906066ac8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -28,6 +28,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipBinder import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel @@ -115,6 +116,17 @@ constructor( } } + if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) { + launch { + viewModel.mediaProjectionStopDialogDueToCallEndedState.collect { stopDialog + -> + if (stopDialog is MediaProjectionStopDialogModel.Shown) { + stopDialog.createAndShowDialog() + } + } + } + } + if (!StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled) { val primaryChipViewBinding = OngoingActivityChipBinder.createBinding( 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 a59d95f27c38..b116b47929d5 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 @@ -36,7 +36,9 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel @@ -96,6 +98,12 @@ interface HomeStatusBarViewModel { val transitionFromLockscreenToDreamStartedEvent: Flow<Unit> /** + * The current media projection stop dialog to be shown, or + * `MediaProjectionStopDialogModel.Hidden` if no dialog is visible. + */ + val mediaProjectionStopDialogDueToCallEndedState: StateFlow<MediaProjectionStopDialogModel> + + /** * The ongoing activity chip that should be primarily shown on the left-hand side of the status * bar. If there are multiple ongoing activity chips, this one should take priority. */ @@ -180,6 +188,7 @@ constructor( sceneInteractor: SceneInteractor, sceneContainerOcclusionInteractor: SceneContainerOcclusionInteractor, shadeInteractor: ShadeInteractor, + shareToAppChipViewModel: ShareToAppChipViewModel, ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel, animations: SystemStatusEventAnimationInteractor, @@ -206,6 +215,9 @@ constructor( .filter { it.transitionState == TransitionState.STARTED } .map {} + override val mediaProjectionStopDialogDueToCallEndedState = + shareToAppChipViewModel.stopDialogToShow + override val primaryOngoingActivityChip = ongoingActivityChipsViewModel.primaryChip override val ongoingActivityChips = ongoingActivityChipsViewModel.chips diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt index 9795cda97f37..eecea9228ea3 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -82,18 +83,29 @@ fun TutorialSelectionScreen( } ), ) { - val padding = if (hasCompactWindowSize()) 24.dp else 60.dp + val isCompactWindow = hasCompactWindowSize() + val padding = if (isCompactWindow) 24.dp else 60.dp val configuration = LocalConfiguration.current when (configuration.orientation) { Configuration.ORIENTATION_LANDSCAPE -> { - HorizontalSelectionButtons( - onBackTutorialClicked = onBackTutorialClicked, - onHomeTutorialClicked = onHomeTutorialClicked, - onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, - onSwitchAppsTutorialClicked = onSwitchAppsTutorialClicked, - modifier = Modifier.weight(1f).padding(padding), - lastSelectedScreen, - ) + if (isCompactWindow) + HorizontalCompactSelectionButtons( + onBackTutorialClicked = onBackTutorialClicked, + onHomeTutorialClicked = onHomeTutorialClicked, + onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, + onSwitchAppsTutorialClicked = onSwitchAppsTutorialClicked, + lastSelectedScreen, + modifier = Modifier.weight(1f).padding(padding), + ) + else + HorizontalSelectionButtons( + onBackTutorialClicked = onBackTutorialClicked, + onHomeTutorialClicked = onHomeTutorialClicked, + onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, + onSwitchAppsTutorialClicked = onSwitchAppsTutorialClicked, + lastSelectedScreen, + modifier = Modifier.weight(1f).padding(padding), + ) } else -> { VerticalSelectionButtons( @@ -101,8 +113,8 @@ fun TutorialSelectionScreen( onHomeTutorialClicked = onHomeTutorialClicked, onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, onSwitchAppsTutorialClicked = onSwitchAppsTutorialClicked, - modifier = Modifier.weight(1f).padding(padding), lastSelectedScreen, + modifier = Modifier.weight(1f).padding(padding), ) } } @@ -120,11 +132,99 @@ private fun HorizontalSelectionButtons( onHomeTutorialClicked: () -> Unit, onRecentAppsTutorialClicked: () -> Unit, onSwitchAppsTutorialClicked: () -> Unit, + lastSelectedScreen: Screen, modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + TwoByTwoTutorialButtons( + onBackTutorialClicked, + onHomeTutorialClicked, + onRecentAppsTutorialClicked, + onSwitchAppsTutorialClicked, + lastSelectedScreen, + modifier = Modifier.weight(1f).fillMaxSize(), + ) + } +} + +@Composable +private fun TwoByTwoTutorialButtons( + onBackTutorialClicked: () -> Unit, + onHomeTutorialClicked: () -> Unit, + onRecentAppsTutorialClicked: () -> Unit, + onSwitchAppsTutorialClicked: () -> Unit, lastSelectedScreen: Screen, + modifier: Modifier = Modifier, +) { + val homeFocusRequester = remember { FocusRequester() } + val backFocusRequester = remember { FocusRequester() } + val recentAppsFocusRequester = remember { FocusRequester() } + val switchAppsFocusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + when (lastSelectedScreen) { + Screen.HOME_GESTURE -> homeFocusRequester.requestFocus() + Screen.BACK_GESTURE -> backFocusRequester.requestFocus() + Screen.RECENT_APPS_GESTURE -> recentAppsFocusRequester.requestFocus() + Screen.SWITCH_APPS_GESTURE -> switchAppsFocusRequester.requestFocus() + else -> {} // No-Op. + } + } + Column { + Row(Modifier.weight(1f)) { + TutorialButton( + text = stringResource(R.string.touchpad_tutorial_home_gesture_button), + icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon), + iconColor = MaterialTheme.colorScheme.onPrimary, + onClick = onHomeTutorialClicked, + backgroundColor = MaterialTheme.colorScheme.primary, + modifier = modifier.focusRequester(homeFocusRequester).focusable().fillMaxSize(), + ) + Spacer(modifier = Modifier.size(16.dp)) + TutorialButton( + text = stringResource(R.string.touchpad_tutorial_back_gesture_button), + icon = Icons.AutoMirrored.Outlined.ArrowBack, + iconColor = MaterialTheme.colorScheme.onTertiary, + onClick = onBackTutorialClicked, + backgroundColor = MaterialTheme.colorScheme.tertiary, + modifier = modifier.focusRequester(backFocusRequester).focusable().fillMaxSize(), + ) + } + Spacer(modifier = Modifier.size(16.dp)) + Row(Modifier.weight(1f)) { + TutorialButton( + text = stringResource(R.string.touchpad_tutorial_recent_apps_gesture_button), + icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_recents_icon), + iconColor = MaterialTheme.colorScheme.onSecondary, + onClick = onRecentAppsTutorialClicked, + backgroundColor = MaterialTheme.colorScheme.secondary, + modifier = + modifier.focusRequester(recentAppsFocusRequester).focusable().fillMaxSize(), + ) + Spacer(modifier = Modifier.size(16.dp)) + TutorialButton( + text = stringResource(R.string.touchpad_tutorial_switch_apps_gesture_button), + icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_apps_icon), + iconColor = MaterialTheme.colorScheme.primary, + onClick = onSwitchAppsTutorialClicked, + backgroundColor = MaterialTheme.colorScheme.onPrimary, + modifier = + modifier.focusRequester(switchAppsFocusRequester).focusable().fillMaxSize(), + ) + } + } +} + +@Composable +private fun HorizontalCompactSelectionButtons( + onBackTutorialClicked: () -> Unit, + onHomeTutorialClicked: () -> Unit, + onRecentAppsTutorialClicked: () -> Unit, + onSwitchAppsTutorialClicked: () -> Unit, + lastSelectedScreen: Screen, + modifier: Modifier = Modifier, ) { Row( - horizontalArrangement = Arrangement.spacedBy(20.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = modifier, ) { @@ -133,8 +233,8 @@ private fun HorizontalSelectionButtons( onHomeTutorialClicked, onRecentAppsTutorialClicked, onSwitchAppsTutorialClicked, - modifier = Modifier.weight(1f).fillMaxSize(), lastSelectedScreen, + modifier = Modifier.weight(1f).fillMaxSize(), ) } } @@ -145,8 +245,8 @@ private fun VerticalSelectionButtons( onHomeTutorialClicked: () -> Unit, onRecentAppsTutorialClicked: () -> Unit, onSwitchAppsTutorialClicked: () -> Unit, - modifier: Modifier = Modifier, lastSelectedScreen: Screen, + modifier: Modifier = Modifier, ) { Column( verticalArrangement = Arrangement.spacedBy(16.dp), @@ -158,8 +258,8 @@ private fun VerticalSelectionButtons( onHomeTutorialClicked, onRecentAppsTutorialClicked, onSwitchAppsTutorialClicked, - modifier = Modifier.weight(1f).fillMaxSize(), lastSelectedScreen, + modifier = Modifier.weight(1f).fillMaxSize(), ) } } @@ -170,8 +270,8 @@ private fun FourTutorialButtons( onHomeTutorialClicked: () -> Unit, onRecentAppsTutorialClicked: () -> Unit, onSwitchAppsTutorialClicked: () -> Unit, - modifier: Modifier = Modifier, lastSelectedScreen: Screen, + modifier: Modifier = Modifier, ) { val homeFocusRequester = remember { FocusRequester() } val backFocusRequester = remember { FocusRequester() } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt index 760e94c72f19..33e1929ebf8b 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt @@ -19,10 +19,14 @@ package com.android.systemui.wallpapers import android.app.Flags import android.graphics.Canvas import android.graphics.Paint +import android.graphics.RadialGradient +import android.graphics.Shader import android.service.wallpaper.WallpaperService import android.util.Log import android.view.SurfaceHolder +import androidx.core.graphics.ColorUtils import androidx.core.graphics.toRectF +import com.android.systemui.res.R /** A wallpaper that shows a static gradient color image wallpaper. */ class GradientColorWallpaper : WallpaperService() { @@ -54,9 +58,60 @@ class GradientColorWallpaper : WallpaperService() { canvas = surface.lockHardwareCanvas() val destRectF = surfaceHolder.surfaceFrame.toRectF() val toColor = context.getColor(com.android.internal.R.color.materialColorPrimary) + val fromColor = + ColorUtils.setAlphaComponent( + context.getColor( + com.android.internal.R.color.materialColorPrimaryContainer + ), + /* alpha= */ 153, // 0.6f * 255 + ) - // TODO(b/384519696): Draw the actual gradient color wallpaper instead. canvas.drawRect(destRectF, Paint().apply { color = toColor }) + + val offsetPx: Float = + context.resources + .getDimensionPixelSize(R.dimen.gradient_color_wallpaper_center_offset) + .toFloat() + val totalHeight = destRectF.height() + (offsetPx * 2) + val leftCenterX = -offsetPx + val leftCenterY = -offsetPx + val rightCenterX = offsetPx + destRectF.width() + val rightCenterY = totalHeight - offsetPx + val radius = (destRectF.width() / 2) + offsetPx + + canvas.drawCircle( + leftCenterX, + leftCenterY, + radius, + Paint().apply { + shader = + RadialGradient( + /* centerX= */ leftCenterX, + /* centerY= */ leftCenterY, + /* radius= */ radius, + /* centerColor= */ fromColor, + /* edgeColor= */ toColor, + /* tileMode= */ Shader.TileMode.CLAMP, + ) + }, + ) + + canvas.drawCircle( + rightCenterX, + rightCenterY, + radius, + Paint().apply { + shader = + RadialGradient( + /* centerX= */ rightCenterX, + /* centerY= */ rightCenterY, + /* radius= */ radius, + /* centerColor= */ fromColor, + /* edgeColor= */ toColor, + /* tileMode= */ Shader.TileMode.CLAMP, + ) + }, + ) } catch (exception: IllegalStateException) { Log.d(TAG, "Fail to draw in the canvas", exception) } finally { diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt index 9794c619041e..79a9630e6887 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt @@ -27,12 +27,13 @@ import android.view.View import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.R -import com.android.systemui.Flags import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.res.R as SysUIR import com.android.systemui.shared.Flags.ambientAod +import com.android.systemui.shared.Flags.extendedWallpaperEffects import com.android.systemui.user.data.model.SelectedUserModel import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.UserRepository @@ -66,7 +67,7 @@ interface WallpaperRepository { /** Set rootView to get its windowToken afterwards */ var rootView: View? - /** when we use magic portrait wallpapers, we should always get its bounds from keyguard */ + /** some wallpapers require bounds to be sent from keyguard */ val shouldSendFocalArea: StateFlow<Boolean> } @@ -80,7 +81,7 @@ constructor( userRepository: UserRepository, keyguardRepository: KeyguardRepository, private val wallpaperManager: WallpaperManager, - context: Context, + private val context: Context, ) : WallpaperRepository { private val wallpaperChanged: Flow<Unit> = broadcastDispatcher @@ -125,8 +126,8 @@ constructor( override val shouldSendFocalArea = wallpaperInfo .map { - val shouldSendNotificationLayout = - it?.component?.className == MAGIC_PORTRAIT_CLASSNAME + val focalAreaTarget = context.resources.getString(SysUIR.string.focal_area_target) + val shouldSendNotificationLayout = it?.component?.className == focalAreaTarget if (shouldSendNotificationLayout) { sendLockscreenLayoutJob = scope.launch { @@ -167,9 +168,8 @@ constructor( } .stateIn( scope, - // Always be listening for wallpaper changes when magic portrait flag is on - if (Flags.magicPortraitWallpapers()) SharingStarted.Eagerly else WhileSubscribed(), - initialValue = Flags.magicPortraitWallpapers(), + if (extendedWallpaperEffects()) SharingStarted.Eagerly else WhileSubscribed(), + initialValue = extendedWallpaperEffects(), ) private suspend fun getWallpaper(selectedUser: SelectedUserModel): WallpaperInfo? { @@ -177,9 +177,4 @@ constructor( wallpaperManager.getWallpaperInfoForUser(selectedUser.userInfo.id) } } - - companion object { - const val MAGIC_PORTRAIT_CLASSNAME = - "com.google.android.apps.magicportrait.service.MagicPortraitWallpaperService" - } } diff --git a/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt index 7f4bfb094a7b..e09a74cd0ad3 100644 --- a/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt @@ -26,6 +26,7 @@ import com.android.systemui.lifecycle.viewModel import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.statusbar.BlurUtils import com.android.systemui.window.ui.viewmodel.WindowRootViewModel +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -42,11 +43,12 @@ object WindowRootViewBinder { viewModelFactory: WindowRootViewModel.Factory, blurUtils: BlurUtils?, choreographer: Choreographer?, + mainDispatcher: CoroutineDispatcher, ) { if (!Flags.bouncerUiRevamp() && !Flags.glanceableHubBlurredBackground()) return if (blurUtils == null || choreographer == null) return - view.repeatWhenAttached { + view.repeatWhenAttached(mainDispatcher) { Log.d(TAG, "Binding root view") var frameCallbackPendingExecution: FrameCallback? = null view.viewModel( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 38acd23d282c..0c9213c3a722 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -206,6 +206,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock ShadeInteractor mShadeInteractor; private @Mock ShadeWindowLogger mShadeWindowLogger; private @Mock SelectedUserInteractor mSelectedUserInteractor; + private @Mock UserTracker.Callback mUserTrackerCallback; private @Mock KeyguardInteractor mKeyguardInteractor; private @Mock KeyguardTransitionBootInteractor mKeyguardTransitionBootInteractor; private @Captor ArgumentCaptor<KeyguardStateController.Callback> @@ -280,7 +281,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { () -> mShadeInteractor, mShadeWindowLogger, () -> mSelectedUserInteractor, - mUserTracker, + mock(UserTracker.class), mKosmos.getNotificationShadeWindowModel(), mSecureSettings, mKosmos::getCommunalInteractor, @@ -318,7 +319,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { } catch (Exception e) { // Just so we don't have to add the exception signature to every test. - fail(); + fail(e.getMessage()); } } @@ -330,18 +331,156 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { /* First test the default behavior: handleUserSwitching() is not invoked */ when(mUserTracker.isUserSwitching()).thenReturn(false); - mViewMediator.mUpdateCallback = mock(KeyguardUpdateMonitorCallback.class); mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - verify(mViewMediator.mUpdateCallback, never()).onUserSwitching(userId); + verify(mUserTrackerCallback, never()).onUserChanging(eq(userId), eq(mContext), + any(Runnable.class)); /* Next test user switching is already in progress when started */ when(mUserTracker.isUserSwitching()).thenReturn(true); mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - verify(mViewMediator.mUpdateCallback).onUserSwitching(userId); + verify(mUserTrackerCallback).onUserChanging(eq(userId), eq(mContext), + any(Runnable.class)); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testGoingAwayFollowedByBeforeUserSwitchDoesNotHideKeyguard() { + setCurrentUser(/* userId= */1099, /* isSecure= */false); + + // Setup keyguard + mViewMediator.onSystemReady(); + processAllMessagesAndBgExecutorMessages(); + mViewMediator.setShowingLocked(true, ""); + + // Request keyguard going away + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); + mViewMediator.mKeyguardGoingAwayRunnable.run(); + + // After the request, begin a switch to a new secure user + int nextUserId = 500; + setCurrentUser(nextUserId, /* isSecure= */true); + Runnable result = mock(Runnable.class); + mViewMediator.handleBeforeUserSwitching(nextUserId, result); + processAllMessagesAndBgExecutorMessages(); + verify(result).run(); + + // After that request has begun, have WM tell us to exit keyguard + RemoteAnimationTarget[] apps = new RemoteAnimationTarget[]{ + mock(RemoteAnimationTarget.class) + }; + RemoteAnimationTarget[] wallpapers = new RemoteAnimationTarget[]{ + mock(RemoteAnimationTarget.class) + }; + IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class); + mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers, + null, callback); + processAllMessagesAndBgExecutorMessages(); + + // The call to exit should be rejected, and keyguard should still be visible + verify(mKeyguardUnlockAnimationController, never()).notifyStartSurfaceBehindRemoteAnimation( + any(), any(), any(), anyLong(), anyBoolean()); + try { + assertATMSLockScreenShowing(true); + } catch (Exception e) { + fail(e.getMessage()); + } + assertTrue(mViewMediator.isShowingAndNotOccluded()); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testUserSwitchToSecureUserShowsBouncer() { + setCurrentUser(/* userId= */1099, /* isSecure= */true); + + // Setup keyguard + mViewMediator.onSystemReady(); + processAllMessagesAndBgExecutorMessages(); + mViewMediator.setShowingLocked(true, ""); + + // After the request, begin a switch to a new secure user + int nextUserId = 500; + setCurrentUser(nextUserId, /* isSecure= */true); + + Runnable beforeResult = mock(Runnable.class); + mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult); + processAllMessagesAndBgExecutorMessages(); + verify(beforeResult).run(); + + // Dismiss should not be called while user switch is in progress + Runnable onSwitchResult = mock(Runnable.class); + mViewMediator.handleUserSwitching(nextUserId, onSwitchResult); + processAllMessagesAndBgExecutorMessages(); + verify(onSwitchResult).run(); + verify(mStatusBarKeyguardViewManager, never()).dismissAndCollapse(); + + // The attempt to dismiss only comes on user switch complete, which will trigger a call to + // show the bouncer in StatusBarKeyguardViewManager + mViewMediator.handleUserSwitchComplete(nextUserId); + TestableLooper.get(this).moveTimeForward(600); + processAllMessagesAndBgExecutorMessages(); + + verify(mStatusBarKeyguardViewManager).dismissAndCollapse(); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testUserSwitchToInsecureUserDismissesKeyguard() { + int userId = 1099; + when(mUserTracker.getUserId()).thenReturn(userId); + + // Setup keyguard + mViewMediator.onSystemReady(); + processAllMessagesAndBgExecutorMessages(); + mViewMediator.setShowingLocked(true, ""); + + // After the request, begin a switch to an insecure user + int nextUserId = 500; + when(mLockPatternUtils.isSecure(nextUserId)).thenReturn(false); + + Runnable beforeResult = mock(Runnable.class); + mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult); + processAllMessagesAndBgExecutorMessages(); + verify(beforeResult).run(); + + // The call to dismiss comes during the user switch + Runnable onSwitchResult = mock(Runnable.class); + mViewMediator.handleUserSwitching(nextUserId, onSwitchResult); + processAllMessagesAndBgExecutorMessages(); + verify(onSwitchResult).run(); + + verify(mStatusBarKeyguardViewManager).dismissAndCollapse(); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testUserSwitchToSecureUserWhileKeyguardNotVisibleShowsKeyguard() { + setCurrentUser(/* userId= */1099, /* isSecure= */true); + + // Setup keyguard as not visible + mViewMediator.onSystemReady(); + processAllMessagesAndBgExecutorMessages(); + mViewMediator.setShowingLocked(false, ""); + processAllMessagesAndBgExecutorMessages(); + + // Begin a switch to a new secure user + int nextUserId = 500; + setCurrentUser(nextUserId, /* isSecure= */true); + + Runnable beforeResult = mock(Runnable.class); + mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult); + processAllMessagesAndBgExecutorMessages(); + verify(beforeResult).run(); + + try { + assertATMSLockScreenShowing(true); + } catch (Exception e) { + fail(); + } + assertTrue(mViewMediator.isShowingAndNotOccluded()); } @Test @@ -1105,7 +1244,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { processAllMessagesAndBgExecutorMessages(); verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean()); - assertATMSAndKeyguardViewMediatorStatesMatch(); + } @Test @@ -1149,6 +1288,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class); when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); + mViewMediator.mKeyguardGoingAwayRunnable.run(); mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers, null, callback); processAllMessagesAndBgExecutorMessages(); @@ -1203,13 +1343,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { // The captor will have the most recent setLockScreenShown call's value. assertEquals(showing, showingCaptor.getValue()); - - // We're now just after the last setLockScreenShown call. If we expect the lockscreen to be - // showing, ensure that we didn't subsequently ask for it to go away. - if (showing) { - orderedSetLockScreenShownCalls.verify(mActivityTaskManagerService, never()) - .keyguardGoingAway(anyInt()); - } } /** @@ -1370,6 +1503,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mKeyguardInteractor, mKeyguardTransitionBootInteractor, mock(WindowManagerOcclusionManager.class)); + mViewMediator.mUserChangedCallback = mUserTrackerCallback; mViewMediator.start(); mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null); @@ -1383,4 +1517,10 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private void captureKeyguardUpdateMonitorCallback() { verify(mUpdateMonitor).registerCallback(mKeyguardUpdateMonitorCallbackCaptor.capture()); } + + private void setCurrentUser(int userId, boolean isSecure) { + when(mUserTracker.getUserId()).thenReturn(userId); + when(mSelectedUserInteractor.getSelectedUserId()).thenReturn(userId); + when(mLockPatternUtils.isSecure(userId)).thenReturn(isSecure); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt index b5eb90402f43..676d8fa06d82 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt @@ -235,6 +235,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa modifyNotification(context).also { it.setSmallIcon(android.R.drawable.ic_media_pause) it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setContentIntent(getNewPendingIntent()) } build() } @@ -2156,6 +2157,28 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY)) } + @Test + @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) + fun postDifferentIntentNotifications_CallsListeners() { + addNotificationAndLoad() + reset(listener) + mediaNotification = + mediaNotification.also { it.notification.contentIntent = getNewPendingIntent() } + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + testScope.assertRunAllReady(foreground = 1, background = 1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false), + ) + verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY)) + } + private fun TestScope.assertRunAllReady(foreground: Int = 0, background: Int = 0) { runCurrent() if (Flags.mediaLoadMetadataViaMediaDataLoader()) { @@ -2235,4 +2258,14 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa backgroundExecutor.runAllReady() foregroundExecutor.runAllReady() } + + private fun getNewPendingIntent(): PendingIntent { + val intent = Intent().setAction(null) + return PendingIntent.getBroadcast( + mContext, + 1, + intent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index 042d30ee23a2..496b31990b9d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -251,6 +251,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor) verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor) session = MediaSession(context, "MediaDataProcessorTestSession") + mediaNotification = SbnBuilder().run { setUser(UserHandle(USER_ID)) @@ -258,6 +259,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { modifyNotification(context).also { it.setSmallIcon(android.R.drawable.ic_media_pause) it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setContentIntent(getNewPendingIntent()) } build() } @@ -2250,6 +2252,33 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY)) } + @Test + @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) + fun postDifferentIntentNotifications_CallsListeners() { + whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) + whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) + + mediaDataProcessor.addInternalListener(mediaDataFilter) + mediaDataFilter.mediaDataProcessor = mediaDataProcessor + addNotificationAndLoad() + reset(listener) + mediaNotification = + mediaNotification.also { it.notification.contentIntent = getNewPendingIntent() } + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + testScope.assertRunAllReady(foreground = 1, background = 1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false), + ) + verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY)) + } + private fun TestScope.assertRunAllReady(foreground: Int = 0, background: Int = 0) { runCurrent() if (Flags.mediaLoadMetadataViaMediaDataLoader()) { @@ -2329,4 +2358,14 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { runCurrent() advanceUntilIdle() } + + private fun getNewPendingIntent(): PendingIntent { + val intent = Intent().setAction(null) + return PendingIntent.getBroadcast( + mContext, + 1, + intent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java index 86063acbf2e1..2715cb31ca8b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java @@ -1512,6 +1512,60 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1); } + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void selectedDevicesAddedInSameOrder() { + when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); + doReturn(mMediaDevices) + .when(mLocalMediaManager) + .getSelectedMediaDevice(); + mMediaSwitchingController.start(mCb); + reset(mCb); + mMediaSwitchingController.getMediaItemList().clear(); + + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); + + List<MediaItem> items = mMediaSwitchingController.getMediaItemList(); + assertThat(items.get(0).getMediaDevice().get()).isEqualTo(mMediaDevice1); + assertThat(items.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice2); + } + + @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void selectedDevicesAddedInReverseOrder() { + when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); + doReturn(mMediaDevices) + .when(mLocalMediaManager) + .getSelectedMediaDevice(); + mMediaSwitchingController.start(mCb); + reset(mCb); + mMediaSwitchingController.getMediaItemList().clear(); + + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); + + List<MediaItem> items = mMediaSwitchingController.getMediaItemList(); + assertThat(items.get(0).getMediaDevice().get()).isEqualTo(mMediaDevice2); + assertThat(items.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice1); + } + + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @Test + public void firstSelectedDeviceIsFirstDeviceInGroupIsTrue() { + when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); + doReturn(mMediaDevices) + .when(mLocalMediaManager) + .getSelectedMediaDevice(); + mMediaSwitchingController.start(mCb); + reset(mCb); + mMediaSwitchingController.getMediaItemList().clear(); + + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); + + List<MediaItem> items = mMediaSwitchingController.getMediaItemList(); + assertThat(items.get(0).isFirstDeviceInGroup()).isTrue(); + assertThat(items.get(1).isFirstDeviceInGroup()).isFalse(); + } + private int getNumberOfConnectDeviceButtons() { int numberOfConnectDeviceButtons = 0; for (MediaItem item : mMediaSwitchingController.getMediaItemList()) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index 7849ea5ab7ed..69539743f96f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -179,7 +179,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { .apply { infoReference.set(expectedInfo) } .onBubbleExpandChanged( isExpanding = true, - key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), + key = Bubble.getNoteBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), ) verify(eventLogger).logNoteTaskOpened(expectedInfo) @@ -194,7 +194,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { .apply { infoReference.set(expectedInfo) } .onBubbleExpandChanged( isExpanding = false, - key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), + key = Bubble.getNoteBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), ) verify(eventLogger).logNoteTaskClosed(expectedInfo) @@ -209,7 +209,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { .apply { infoReference.set(expectedInfo) } .onBubbleExpandChanged( isExpanding = true, - key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), + key = Bubble.getNoteBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), ) verifyNoMoreInteractions(bubbles, keyguardManager, userManager, eventLogger) @@ -223,14 +223,14 @@ internal class NoteTaskControllerTest : SysuiTestCase() { .apply { infoReference.set(expectedInfo) } .onBubbleExpandChanged( isExpanding = false, - key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), + key = Bubble.getNoteBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), ) verifyNoMoreInteractions(bubbles, keyguardManager, userManager, eventLogger) } @Test - fun onBubbleExpandChanged_notKeyAppBubble_shouldDoNothing() { + fun onBubbleExpandChanged_notKeyNoteBubble_shouldDoNothing() { createNoteTaskController().onBubbleExpandChanged(isExpanding = true, key = "any other key") verifyNoMoreInteractions(bubbles, keyguardManager, userManager, eventLogger) @@ -241,7 +241,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController(isEnabled = false) .onBubbleExpandChanged( isExpanding = true, - key = Bubble.getAppBubbleKeyForApp(NOTE_TASK_INFO.packageName, NOTE_TASK_INFO.user), + key = Bubble.getNoteBubbleKeyForApp(NOTE_TASK_INFO.packageName, NOTE_TASK_INFO.user), ) verifyNoMoreInteractions(bubbles, keyguardManager, userManager, eventLogger) @@ -740,7 +740,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() val iconCaptor = argumentCaptor<Icon>() verify(bubbles) - .showOrHideAppBubble(capture(intentCaptor), eq(userHandle), capture(iconCaptor)) + .showOrHideNoteBubble(capture(intentCaptor), eq(userHandle), capture(iconCaptor)) assertThat(intentCaptor.value).run { hasAction(ACTION_CREATE_NOTE) hasPackage(NOTE_TASK_PACKAGE_NAME) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 70450d29c74e..49d6909c1f93 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -254,6 +254,7 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : mock(BouncerViewBinder::class.java), { mock(ConfigurationForwarder::class.java) }, brightnessMirrorShowingInteractor, + kosmos.testDispatcher, ) underTest.setupExpandedStatusBar() underTest.setDragDownHelper(dragDownHelper) 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 493468e8f675..77b116e2e465 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 @@ -370,7 +370,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { row.onDensityOrFontScaleChanged(); - verify(mockContainer).reInflateViews(any(), any()); + verify(mockContainer).reInflateViews(any()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 5d88f72b805b..097f3929db42 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -26,8 +26,6 @@ import static android.service.notification.NotificationListenerService.REASON_AP import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED; -import static androidx.test.ext.truth.content.IntentSubject.assertThat; - import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING; import static com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR; @@ -175,6 +173,7 @@ import com.android.wm.shell.bubbles.BubbleEducationController; import com.android.wm.shell.bubbles.BubbleEntry; import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubbleOverflow; +import com.android.wm.shell.bubbles.BubbleResizabilityChecker; import com.android.wm.shell.bubbles.BubbleStackView; import com.android.wm.shell.bubbles.BubbleTaskView; import com.android.wm.shell.bubbles.BubbleViewInfoTask; @@ -182,7 +181,6 @@ import com.android.wm.shell.bubbles.BubbleViewProvider; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.bubbles.StackEducationView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; -import com.android.wm.shell.bubbles.properties.BubbleProperties; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; @@ -297,7 +295,7 @@ public class BubblesTest extends SysuiTestCase { private BubbleEntry mBubbleEntryUser11; private BubbleEntry mBubbleEntry2User11; - private Intent mAppBubbleIntent; + private Intent mNotesBubbleIntent; @Mock private ShellInit mShellInit; @@ -354,7 +352,7 @@ public class BubblesTest extends SysuiTestCase { @Mock private NotifPipelineFlags mNotifPipelineFlags; @Mock - private Icon mAppBubbleIcon; + private Icon mNotesBubbleIcon; @Mock private Display mDefaultDisplay; @Mock @@ -380,8 +378,6 @@ public class BubblesTest extends SysuiTestCase { private UserHandle mUser0; - private FakeBubbleProperties mBubbleProperties; - @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag(); @@ -457,8 +453,8 @@ public class BubblesTest extends SysuiTestCase { mNotificationShadeWindowController.fetchWindowRootView(); mNotificationShadeWindowController.attach(); - mAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class); - mAppBubbleIntent.setPackage(mContext.getPackageName()); + mNotesBubbleIntent = new Intent(mContext, BubblesTestActivity.class); + mNotesBubbleIntent.setPackage(mContext.getPackageName()); mZenModeConfig.suppressedVisualEffects = 0; when(mZenModeController.getConfig()).thenReturn(mZenModeConfig); @@ -522,7 +518,6 @@ public class BubblesTest extends SysuiTestCase { mTaskViewRepository = new TaskViewRepository(); mTaskViewTransitions = new TaskViewTransitions(mTransitions, mTaskViewRepository, mShellTaskOrganizer, mSyncQueue); - mBubbleProperties = new FakeBubbleProperties(); mBubbleController = new TestableBubbleController( mContext, mShellInit, @@ -551,7 +546,7 @@ public class BubblesTest extends SysuiTestCase { mTransitions, mock(SyncTransactionQueue.class), mock(IWindowManager.class), - mBubbleProperties); + new BubbleResizabilityChecker()); mBubbleController.setExpandListener(mBubbleExpandListener); spyOn(mBubbleController); @@ -1478,8 +1473,8 @@ public class BubblesTest extends SysuiTestCase { } @Test - public void testShowManageMenuChangesSysuiState_appBubble() { - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + public void testShowManageMenuChangesSysuiState_notesBubble() { + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); assertTrue(mBubbleController.hasBubbles()); // Expand the stack @@ -1979,79 +1974,80 @@ public class BubblesTest extends SysuiTestCase { } @Test - public void testShowOrHideAppBubble_addsAndExpand() { + public void testShowOrHideNotesBubble_addsAndExpand() { assertThat(mBubbleController.isStackExpanded()).isFalse(); - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); verify(mBubbleController).inflateAndAdd(any(Bubble.class), /* suppressFlyout= */ eq(true), /* showInShade= */ eq(false)); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo( - Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0)); + Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0)); assertThat(mBubbleController.isStackExpanded()).isTrue(); } @Test - public void testShowOrHideAppBubble_expandIfCollapsed() { - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + public void testShowOrHideNotesBubble_expandIfCollapsed() { + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); mBubbleController.updateBubble(mBubbleEntry); mBubbleController.collapseStack(); assertThat(mBubbleController.isStackExpanded()).isFalse(); // Calling this while collapsed will expand the app bubble - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo( - Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0)); + Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0)); assertThat(mBubbleController.isStackExpanded()).isTrue(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); } @Test - public void testShowOrHideAppBubble_collapseIfSelected() { - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + public void testShowOrHideNotesBubble_collapseIfSelected() { + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo( - Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0)); + Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0)); assertThat(mBubbleController.isStackExpanded()).isTrue(); // Calling this while the app bubble is expanded should collapse the stack - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo( - Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0)); + Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0)); assertThat(mBubbleController.isStackExpanded()).isFalse(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(mUser0); } @Test - public void testShowOrHideAppBubbleWithNonPrimaryUser_bubbleCollapsedWithExpectedUser() { + public void testShowOrHideNotesBubbleWithNonPrimaryUser_bubbleCollapsedWithExpectedUser() { UserHandle user10 = createUserHandle(/* userId = */ 10); - String appBubbleKey = Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), user10); - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, user10, mAppBubbleIcon); - assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey); + String notesKey = Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), user10); + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, user10, mNotesBubbleIcon); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(notesKey); assertThat(mBubbleController.isStackExpanded()).isTrue(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(user10); // Calling this while the app bubble is expanded should collapse the stack - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, user10, mAppBubbleIcon); + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, user10, mNotesBubbleIcon); - assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(notesKey); assertThat(mBubbleController.isStackExpanded()).isFalse(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(user10); } @Test - public void testShowOrHideAppBubbleOnUser10AndThenUser0_user0BubbleExpanded() { + public void testShowOrHideNotesBubbleOnUser10AndThenUser0_user0BubbleExpanded() { UserHandle user10 = createUserHandle(/* userId = */ 10); - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, user10, mAppBubbleIcon); + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, user10, mNotesBubbleIcon); - String appBubbleUser0Key = Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0); - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + String notesBubbleUser0Key = Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), + mUser0); + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); - assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleUser0Key); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(notesBubbleUser0Key); assertThat(mBubbleController.isStackExpanded()).isTrue(); assertThat(mBubbleData.getBubbles()).hasSize(2); assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(mUser0); @@ -2059,63 +2055,64 @@ public class BubblesTest extends SysuiTestCase { } @Test - public void testShowOrHideAppBubble_selectIfNotSelected() { - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + public void testShowOrHideNotesBubble_selectIfNotSelected() { + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); mBubbleController.updateBubble(mBubbleEntry); mBubbleController.expandStackAndSelectBubble(mBubbleEntry); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(mBubbleEntry.getKey()); assertThat(mBubbleController.isStackExpanded()).isTrue(); - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo( - Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0)); + Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0)); assertThat(mBubbleController.isStackExpanded()).isTrue(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); } @Test - public void testShowOrHideAppBubble_addsFromOverflow() { - String appBubbleKey = Bubble.getAppBubbleKeyForApp(mAppBubbleIntent.getPackage(), mUser0); - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); - + public void testShowOrHideNotesBubble_addsFromOverflow() { + String noteBubbleKey = Bubble.getNoteBubbleKeyForApp(mNotesBubbleIntent.getPackage(), + mUser0); + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); // Collapse the stack so we don't need to wait for the dismiss animation in the test mBubbleController.collapseStack(); // Dismiss the app bubble so it's in the overflow - mBubbleController.dismissBubble(appBubbleKey, Bubbles.DISMISS_USER_GESTURE); - assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNotNull(); + mBubbleController.dismissBubble(noteBubbleKey, Bubbles.DISMISS_USER_GESTURE); + assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNotNull(); // Calling this while collapsed will re-add and expand the app bubble - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); - assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey); + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(noteBubbleKey); assertThat(mBubbleController.isStackExpanded()).isTrue(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); - assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNull(); + assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNull(); } @Test - public void testShowOrHideAppBubble_updateExistedBubbleInOverflow_updateIntentInBubble() { - String appBubbleKey = Bubble.getAppBubbleKeyForApp(mAppBubbleIntent.getPackage(), mUser0); - mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); + public void testShowOrHideNotesBubble_updateExistedBubbleInOverflow_updateIntentInBubble() { + String noteBubbleKey = Bubble.getNoteBubbleKeyForApp(mNotesBubbleIntent.getPackage(), + mUser0); + mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon); // Collapse the stack so we don't need to wait for the dismiss animation in the test mBubbleController.collapseStack(); // Dismiss the app bubble so it's in the overflow - mBubbleController.dismissBubble(appBubbleKey, Bubbles.DISMISS_USER_GESTURE); - assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNotNull(); + mBubbleController.dismissBubble(noteBubbleKey, Bubbles.DISMISS_USER_GESTURE); + assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNotNull(); // Modify the intent to include new extras. - Intent newAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class) + Intent newIntent = new Intent(mContext, BubblesTestActivity.class) .setPackage(mContext.getPackageName()) .putExtra("hello", "world"); // Calling this while collapsed will re-add and expand the app bubble - mBubbleController.showOrHideAppBubble(newAppBubbleIntent, mUser0, mAppBubbleIcon); - assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey); + mBubbleController.showOrHideNotesBubble(newIntent, mUser0, mNotesBubbleIcon); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(noteBubbleKey); assertThat(mBubbleController.isStackExpanded()).isTrue(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); - assertThat(mBubbleData.getBubbles().get(0).getAppBubbleIntent()).extras().string( - "hello").isEqualTo("world"); - assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNull(); + assertThat(mBubbleData.getBubbles().get(0).getAppBubbleIntent() + .getStringExtra("hello")).isEqualTo("world"); + assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNull(); } @Test @@ -2143,9 +2140,9 @@ public class BubblesTest extends SysuiTestCase { assertFalse("FLAG_NO_DISMISS Notifs should be non-dismissable", bubble.isDismissable()); } + @DisableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void registerBubbleBarListener_barDisabled_largeScreen_shouldBeIgnored() { - mBubbleProperties.mIsBubbleBarEnabled = false; mPositioner.setIsLargeScreen(true); mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); @@ -2161,9 +2158,9 @@ public class BubblesTest extends SysuiTestCase { assertThat(mBubbleController.getStackView().getBubbleCount()).isEqualTo(1); } + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void registerBubbleBarListener_barEnabled_smallScreen_shouldBeIgnored() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(false); mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); @@ -2179,9 +2176,9 @@ public class BubblesTest extends SysuiTestCase { assertThat(mBubbleController.getStackView().getBubbleCount()).isEqualTo(1); } + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void registerBubbleBarListener_switchToBarAndBackToStack() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); @@ -2211,9 +2208,9 @@ public class BubblesTest extends SysuiTestCase { assertBubbleIsInflatedForStack(mBubbleData.getOverflow()); } + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void registerBubbleBarListener_switchToBarWhileExpanded() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); mEntryListener.onEntryAdded(mRow); @@ -2238,9 +2235,9 @@ public class BubblesTest extends SysuiTestCase { assertThat(layerView.isExpanded()).isTrue(); } + @DisableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void switchBetweenBarAndStack_noBubbles_shouldBeIgnored() { - mBubbleProperties.mIsBubbleBarEnabled = false; mPositioner.setIsLargeScreen(true); assertFalse(mBubbleController.hasBubbles()); @@ -2256,9 +2253,9 @@ public class BubblesTest extends SysuiTestCase { assertNoBubbleContainerViews(); } + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void bubbleBarBubbleExpandedAndCollapsed() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); @@ -2277,7 +2274,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void dragBubbleBarBubble_selectedBubble_expandedViewCollapsesDuringDrag() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2306,7 +2302,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void dragBubbleBarBubble_unselectedBubble_expandedViewCollapsesDuringDrag() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2335,7 +2330,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void dismissBubbleBarBubble_selected_selectsAndExpandsNext() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2359,7 +2353,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void dismissBubbleBarBubble_unselected_selectionDoesNotChange() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2407,9 +2400,9 @@ public class BubblesTest extends SysuiTestCase { verify(mBubbleController).onSensitiveNotificationProtectionStateChanged(false); } + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void setBubbleBarLocation_listenerNotified() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); @@ -2421,9 +2414,9 @@ public class BubblesTest extends SysuiTestCase { BubbleBarLocation.LEFT); } + @DisableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void setBubbleBarLocation_barDisabled_shouldBeIgnored() { - mBubbleProperties.mIsBubbleBarEnabled = false; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); @@ -2496,7 +2489,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void testEventLogging_bubbleBar_addBubble() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2510,7 +2502,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void testEventLogging_bubbleBar_updateBubble() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2527,7 +2518,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void testEventLogging_bubbleBar_dragSelectedBubbleToDismiss() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2554,7 +2544,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void testEventLogging_bubbleBar_dragOtherBubbleToDismiss() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2579,7 +2568,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void testEventLogging_bubbleBar_dragBarToDismiss() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); // Not a user gesture, should not log an event @@ -2594,7 +2582,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void testEventLogging_bubbleBar_expandAndCollapse() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2614,7 +2601,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void testEventLogging_bubbleBar_autoExpandingBubble() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2630,7 +2616,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void testEventLogging_bubbleBar_switchBubble() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2656,7 +2641,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void testEventLogging_bubbleBar_openOverflow() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2672,7 +2656,6 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test public void testEventLogging_bubbleBar_fromOverflowToBar() { - mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); @@ -2902,16 +2885,4 @@ public class BubblesTest extends SysuiTestCase { @Override public void onItemDraggedOutsideBubbleBarDropZone() {} } - - private static class FakeBubbleProperties implements BubbleProperties { - boolean mIsBubbleBarEnabled = false; - - @Override - public boolean isBubbleBarEnabled() { - return mIsBubbleBarEnabled; - } - - @Override - public void refresh() {} - } } diff --git a/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt index 025f556991f2..80254d58781a 100644 --- a/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt @@ -24,4 +24,6 @@ val Kosmos.fakeWindowManager by Kosmos.Fixture { FakeWindowManager(applicationCo val Kosmos.mockWindowManager: WindowManager by Kosmos.Fixture { mock(WindowManager::class.java) } +val Kosmos.mockIWindowManager: IWindowManager by Kosmos.Fixture { mock(IWindowManager::class.java) } + var Kosmos.windowManager: WindowManager by Kosmos.Fixture { mockWindowManager } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt index 2bd104dd375d..48b801cb06be 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt @@ -20,6 +20,7 @@ import com.android.systemui.authentication.data.repository.authenticationReposit import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.log.table.logcatTableLogBuffer import com.android.systemui.user.domain.interactor.selectedUserInteractor val Kosmos.authenticationInteractor by @@ -29,5 +30,6 @@ val Kosmos.authenticationInteractor by backgroundDispatcher = testDispatcher, repository = authenticationRepository, selectedUserInteractor = selectedUserInteractor, + tableLogBuffer = logcatTableLogBuffer(this, "sceneFrameworkTableLogBuffer"), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/BatteryRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/BatteryRepositoryKosmos.kt new file mode 100644 index 000000000000..edfe8ecd0775 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/BatteryRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * 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.common.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.batteryRepository: BatteryRepository by Kosmos.Fixture { FakeBatteryRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakeBatteryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakeBatteryRepository.kt new file mode 100644 index 000000000000..ac94335b42c3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakeBatteryRepository.kt @@ -0,0 +1,34 @@ +/* + * 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.common.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeBatteryRepository : BatteryRepository { + private val _isDevicePluggedIn = MutableStateFlow(false) + + override val isDevicePluggedIn: Flow<Boolean> = _isDevicePluggedIn.asStateFlow() + + fun setDevicePluggedIn(isPluggedIn: Boolean) { + _isDevicePluggedIn.value = isPluggedIn + } +} + +val BatteryRepository.fake + get() = this as FakeBatteryRepository diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/BatteryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/BatteryInteractorKosmos.kt new file mode 100644 index 000000000000..2153955f3cc1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/BatteryInteractorKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.common.domain.interactor + +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.kosmos.Kosmos + +var Kosmos.batteryInteractor by Kosmos.Fixture { BatteryInteractor(batteryRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt index 163625747d85..603160dea715 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt @@ -25,7 +25,8 @@ import kotlinx.coroutines.flow.map /** Fake implementation of [CommunalPrefsRepository] */ class FakeCommunalPrefsRepository : CommunalPrefsRepository { private val _isCtaDismissed = MutableStateFlow<Set<UserInfo>>(emptySet()) - private val _isHubOnboardingismissed = MutableStateFlow<Set<UserInfo>>(emptySet()) + private val _isHubOnboardingDismissed = MutableStateFlow<Set<UserInfo>>(emptySet()) + private val _isDreamButtonTooltipDismissed = MutableStateFlow<Set<UserInfo>>(emptySet()) override fun isCtaDismissed(user: UserInfo): Flow<Boolean> = _isCtaDismissed.map { it.contains(user) } @@ -35,10 +36,18 @@ class FakeCommunalPrefsRepository : CommunalPrefsRepository { } override fun isHubOnboardingDismissed(user: UserInfo): Flow<Boolean> = - _isHubOnboardingismissed.map { it.contains(user) } + _isHubOnboardingDismissed.map { it.contains(user) } override suspend fun setHubOnboardingDismissed(user: UserInfo) { - _isHubOnboardingismissed.value = - _isHubOnboardingismissed.value.toMutableSet().apply { add(user) } + _isHubOnboardingDismissed.value = + _isHubOnboardingDismissed.value.toMutableSet().apply { add(user) } + } + + override fun isDreamButtonTooltipDismissed(user: UserInfo): Flow<Boolean> = + _isDreamButtonTooltipDismissed.map { it.contains(user) } + + override suspend fun setDreamButtonTooltipDismissed(user: UserInfo) { + _isDreamButtonTooltipDismissed.value = + _isDreamButtonTooltipDismissed.value.toMutableSet().apply { add(user) } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index 89aad4be7cc0..b0a6de1f931a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -19,10 +19,13 @@ package com.android.systemui.communal.domain.interactor import android.content.testableContext import android.os.userManager import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.common.domain.interactor.batteryInteractor import com.android.systemui.communal.data.repository.communalMediaRepository import com.android.systemui.communal.data.repository.communalSmartspaceRepository import com.android.systemui.communal.data.repository.communalWidgetRepository +import com.android.systemui.communal.posturing.domain.interactor.posturingInteractor import com.android.systemui.communal.widgets.EditWidgetsActivityStarter +import com.android.systemui.dock.dockManager import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -64,6 +67,9 @@ val Kosmos.communalInteractor by Fixture { logBuffer = logcatLogBuffer("CommunalInteractor"), tableLogBuffer = mock(), managedProfileController = fakeManagedProfileController, + batteryInteractor = batteryInteractor, + dockManager = dockManager, + posturingInteractor = posturingInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt index c2d2392186b7..43d3eb7b857a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.communal.ui.viewmodel import android.service.dream.dreamManager import com.android.internal.logging.uiEventLogger +import com.android.systemui.communal.domain.interactor.communalPrefsInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher @@ -29,6 +30,7 @@ val Kosmos.communalToDreamButtonViewModel by CommunalToDreamButtonViewModel( backgroundContext = testDispatcher, batteryController = batteryController, + prefsInteractor = communalPrefsInteractor, settingsInteractor = communalSettingsInteractor, activityStarter = activityStarter, dreamManager = dreamManager, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt index 1d3fd300da06..c927b5563bba 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.deviceentry.data.repository.deviceEntryRepository import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.log.table.logcatTableLogBuffer import com.android.systemui.scene.domain.interactor.sceneBackInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -36,5 +37,6 @@ val Kosmos.deviceEntryInteractor by alternateBouncerInteractor = alternateBouncerInteractor, dismissCallbackRegistry = dismissCallbackRegistry, sceneBackInteractor = sceneBackInteractor, + tableLogBuffer = logcatTableLogBuffer(this, "sceneFrameworkTableLogBuffer"), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt index e4c7df64fdc6..9e36428d119d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt @@ -25,6 +25,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn +import com.android.systemui.log.table.logcatTableLogBuffer import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository @@ -40,6 +41,7 @@ val Kosmos.deviceUnlockedInteractor by Fixture { systemPropertiesHelper = fakeSystemPropertiesHelper, userAwareSecureSettingsRepository = userAwareSecureSettingsRepository, keyguardInteractor = keyguardInteractor, + tableLogBuffer = logcatTableLogBuffer(this, "sceneFrameworkTableLogBuffer"), ) .apply { activateIn(testScope) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt index ef9bd8282090..5793695a7f01 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt @@ -62,6 +62,7 @@ val Kosmos.defaultKeyguardBlueprint by defaultSettingsPopupMenuSection = mock(), defaultStatusBarSection = mock(), defaultNotificationStackScrollLayoutSection = mock(), + aodPromotedNotificationSection = mock(), aodNotificationIconsSection = mock(), aodBurnInSection = mock(), clockSection = keyguardClockSection, @@ -69,7 +70,6 @@ val Kosmos.defaultKeyguardBlueprint by keyguardSliceViewSection = mock(), udfpsAccessibilityOverlaySection = mock(), accessibilityActionsSection = mock(), - aodPromotedNotificationSection = mock(), ) } @@ -84,6 +84,7 @@ val Kosmos.splitShadeBlueprint by defaultStatusBarSection = mock(), splitShadeNotificationStackScrollLayoutSection = mock(), splitShadeGuidelines = mock(), + aodPromotedNotificationSection = mock(), aodNotificationIconsSection = mock(), aodBurnInSection = mock(), clockSection = keyguardClockSection, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt index b07de16be567..ff7a06c5087e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt @@ -16,6 +16,8 @@ package com.android.systemui.keyguard.domain.interactor +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos @@ -41,5 +43,7 @@ var Kosmos.fromLockscreenTransitionInteractor by communalSettingsInteractor = communalSettingsInteractor, swipeToDismissInteractor = swipeToDismissInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + communalInteractor = communalInteractor, + communalSceneInteractor = communalSceneInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorKosmos.kt index d5bdbdbaa90d..1b1fe593a2e4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorKosmos.kt @@ -20,6 +20,7 @@ import com.android.keyguard.logging.scrimLogger import com.android.systemui.keyguard.data.lightRevealScrimRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.power.domain.interactor.powerInteractor val Kosmos.lightRevealScrimInteractor by @@ -30,5 +31,6 @@ val Kosmos.lightRevealScrimInteractor by applicationCoroutineScope, scrimLogger, { powerInteractor }, + backgroundDispatcher = testDispatcher, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt index e6256a58a365..6c52d54fdd06 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt @@ -19,6 +19,8 @@ package com.android.systemui.mediaprojection.data.repository import android.app.ActivityManager import android.media.projection.StopReason import com.android.systemui.mediaprojection.data.model.MediaProjectionState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow class FakeMediaProjectionRepository : MediaProjectionRepository { @@ -27,9 +29,18 @@ class FakeMediaProjectionRepository : MediaProjectionRepository { override val mediaProjectionState: MutableStateFlow<MediaProjectionState> = MutableStateFlow(MediaProjectionState.NotProjecting) + private val _projectionStartedDuringCallAndActivePostCallEvent = MutableSharedFlow<Unit>() + + override val projectionStartedDuringCallAndActivePostCallEvent: Flow<Unit> = + _projectionStartedDuringCallAndActivePostCallEvent + var stopProjectingInvoked = false override suspend fun stopProjecting(@StopReason stopReason: Int) { stopProjectingInvoked = true } + + suspend fun emitProjectionStartedDuringCallAndActivePostCallEvent() { + _projectionStartedDuringCallAndActivePostCallEvent.emit(Unit) + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.kt index 2b6032ccafe5..9b600513b8aa 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.kt @@ -16,6 +16,7 @@ package com.android.systemui.mediaprojection.taskswitcher +import android.media.projection.MediaProjectionEvent import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager import android.os.Binder @@ -61,14 +62,22 @@ class FakeMediaProjectionManager { fun dispatchOnSessionSet( info: MediaProjectionInfo = DEFAULT_INFO, - session: ContentRecordingSession? + session: ContentRecordingSession?, ) { callbacks.forEach { it.onRecordingSessionSet(info, session) } } + fun dispatchEvent( + event: MediaProjectionEvent, + info: MediaProjectionInfo? = DEFAULT_INFO, + session: ContentRecordingSession? = null, + ) { + callbacks.forEach { it.onMediaProjectionEvent(event, info, session) } + } + companion object { fun createDisplaySession(): ContentRecordingSession = - ContentRecordingSession.createDisplaySession(/* displayToMirror = */ 123) + ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123) fun createSingleTaskSession(token: IBinder = Binder()): ContentRecordingSession = ContentRecordingSession.createTaskSession(token) @@ -76,10 +85,6 @@ class FakeMediaProjectionManager { private const val DEFAULT_PACKAGE_NAME = "com.media.projection.test" private val DEFAULT_USER_HANDLE = UserHandle.getUserHandleForUid(UserHandle.myUserId()) private val DEFAULT_INFO = - MediaProjectionInfo( - DEFAULT_PACKAGE_NAME, - DEFAULT_USER_HANDLE, - /* launchCookie = */ null - ) + MediaProjectionInfo(DEFAULT_PACKAGE_NAME, DEFAULT_USER_HANDLE, /* launchCookie= */ null) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsContent.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsContent.kt new file mode 100644 index 000000000000..84a736439e08 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsContent.kt @@ -0,0 +1,31 @@ +/* + * 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.qs + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign + +@Composable +fun FakeTileDetailsContent() { + Text( + text = "Fake details content", + textAlign = TextAlign.Center, + fontWeight = FontWeight.ExtraBold, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt index 555f019822c2..4f8d5a14e390 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt @@ -16,24 +16,11 @@ package com.android.systemui.qs -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import com.android.systemui.plugins.qs.TileDetailsViewModel class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel() { private var _clickOnSettingsButton = 0 - @Composable - override fun GetContentView() { - Text( - text = "Fake details content", - textAlign = TextAlign.Center, - fontWeight = FontWeight.ExtraBold, - ) - } - override fun clickOnSettingsButton() { _clickOnSettingsButton++ } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorKosmos.kt index e46ede65bfb6..e9ba42547883 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.scene.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.log.table.logcatTableLogBuffer import com.android.systemui.scene.sceneContainerConfig import com.android.systemui.scene.shared.logger.sceneLogger @@ -25,5 +26,6 @@ val Kosmos.sceneBackInteractor by Fixture { SceneBackInteractor( logger = sceneLogger, sceneContainerConfig = sceneContainerConfig, + tableLogBuffer = logcatTableLogBuffer(this, "sceneFrameworkTableLogBuffer"), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index d105326ec3d0..7a9b052481cb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -33,9 +33,11 @@ import com.android.systemui.haptics.vibratorHelper import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.domain.interactor.trustInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testScope +import com.android.systemui.log.table.logcatTableLogBuffer import com.android.systemui.model.sysUiState import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.disabledContentInteractor @@ -89,5 +91,7 @@ val Kosmos.sceneContainerStartable by Fixture { disabledContentInteractor = disabledContentInteractor, activityTransitionAnimator = activityTransitionAnimator, shadeModeInteractor = shadeModeInteractor, + tableLogBuffer = logcatTableLogBuffer(this, "sceneFrameworkTableLogBuffer"), + trustInteractor = trustInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt index a4631f17cb37..2ba9c8094aac 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt @@ -21,6 +21,7 @@ import android.provider.Settings import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.log.table.logcatTableLogBuffer import com.android.systemui.res.R import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.data.repository.shadeRepository @@ -31,6 +32,7 @@ val Kosmos.shadeModeInteractor by Fixture { applicationScope = applicationCoroutineScope, repository = shadeRepository, secureSettingsRepository = fakeSecureSettingsRepository, + tableLogBuffer = logcatTableLogBuffer(this, "sceneFrameworkTableLogBuffer"), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelKosmos.kt index 1e304d979e03..ab61a3ef4c3f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel +import android.content.applicationContext import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.activityStarter @@ -26,6 +27,7 @@ import com.android.systemui.util.time.fakeSystemClock val Kosmos.callChipViewModel: CallChipViewModel by Kosmos.Fixture { CallChipViewModel( + applicationContext, scope = applicationCoroutineScope, interactor = callChipInteractor, systemClock = fakeSystemClock, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt index d0c80c7332b3..878c2deb43b2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel +import android.content.applicationContext import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor @@ -24,6 +25,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.heads val Kosmos.notifChipsViewModel: NotifChipsViewModel by Kosmos.Fixture { NotifChipsViewModel( + applicationContext, applicationCoroutineScope, statusBarNotificationChipsInteractor, headsUpNotificationInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt index 8c37bd739bc5..bdcab5fd2eca 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.core import android.content.testableContext +import android.view.mockIWindowManager import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.display.data.repository.displayScopeRepository @@ -84,5 +85,6 @@ val Kosmos.multiDisplayStatusBarStarter by multiDisplayAutoHideControllerStore, privacyDotWindowControllerStore, lightBarControllerStore, + mockIWindowManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt index c6ae15df6859..63085e178e7d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt @@ -43,6 +43,7 @@ fun activeNotificationModel( instanceId: Int? = null, isGroupSummary: Boolean = false, packageName: String = "pkg", + appName: String = "appName", contentIntent: PendingIntent? = null, bucket: Int = BUCKET_UNKNOWN, callType: CallType = CallType.None, @@ -64,6 +65,7 @@ fun activeNotificationModel( statusBarChipIconView = statusBarChipIcon, uid = uid, packageName = packageName, + appName = appName, contentIntent = contentIntent, instanceId = instanceId, isGroupSummary = isGroupSummary, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractorKosmos.kt index f7acae9846df..0acf98fec054 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractorKosmos.kt @@ -16,11 +16,16 @@ package com.android.systemui.statusbar.notification.domain.interactor +import android.content.applicationContext import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.notification.collection.provider.sectionStyleProvider import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository val Kosmos.renderNotificationListInteractor by Kosmos.Fixture { - RenderNotificationListInteractor(activeNotificationListRepository, sectionStyleProvider) + RenderNotificationListInteractor( + activeNotificationListRepository, + sectionStyleProvider, + applicationContext, + ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt index ca33a8663a51..f679fa4fafb6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel import android.content.applicationContext +import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher @@ -31,6 +32,7 @@ val Kosmos.emptyShadeViewModel by zenModeInteractor, seenNotificationsInteractor, notificationSettingsInteractor, + configurationInteractor, testDispatcher, dumpManager, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt index f4e74fe0e6bb..923b36d4f2cf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt @@ -26,5 +26,14 @@ fun inCallModel( notificationIcon: StatusBarIconView? = null, intent: PendingIntent? = null, notificationKey: String = "test", + appName: String = "", promotedContent: PromotedNotificationContentModel? = null, -) = OngoingCallModel.InCall(startTimeMs, notificationIcon, intent, notificationKey, promotedContent) +) = + OngoingCallModel.InCall( + startTimeMs, + notificationIcon, + intent, + notificationKey, + appName, + promotedContent, + ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt index f8bf3c3fbbd9..1626904a9c19 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt @@ -25,6 +25,7 @@ import com.android.systemui.log.table.tableLogBufferFactory import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModel @@ -53,6 +54,7 @@ var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by sceneInteractor, sceneContainerOcclusionInteractor, shadeInteractor, + shareToAppChipViewModel, ongoingActivityChipsViewModel, statusBarPopupChipsViewModel, systemStatusEventAnimationInteractor, diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java index fda57d6bb986..e422fef6c22c 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -31,6 +31,7 @@ import android.os.Looper; import android.os.PowerManager; import android.os.SystemClock; import android.provider.Settings; +import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -71,9 +72,9 @@ import java.util.StringJoiner; */ class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation { - private static final String TAG = AccessibilityInputFilter.class.getSimpleName(); + private static final String TAG = "A11yInputFilter"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); /** * Flag for enabling the screen magnification feature. diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 8e0a7785c597..67fdca446ba4 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -36,6 +36,7 @@ import static android.accessibilityservice.AccessibilityTrace.FLAGS_PACKAGE_BROA import static android.accessibilityservice.AccessibilityTrace.FLAGS_USER_BROADCAST_RECEIVER; import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAGER_INTERNAL; import static android.content.Context.DEVICE_ID_DEFAULT; +import static android.hardware.input.InputSettings.isRepeatKeysFeatureFlagEnabled; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR; @@ -156,6 +157,7 @@ import android.view.KeyEvent; import android.view.MagnificationSpec; import android.view.MotionEvent; import android.view.SurfaceControl; +import android.view.ViewConfiguration; import android.view.WindowInfo; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; @@ -3494,6 +3496,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub somethingChanged |= readMagnificationFollowTypingLocked(userState); somethingChanged |= readAlwaysOnMagnificationLocked(userState); somethingChanged |= readMouseKeysEnabledLocked(userState); + somethingChanged |= readRepeatKeysSettingsLocked(userState); return somethingChanged; } @@ -5771,6 +5774,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final Uri mUserSetupCompleteUri = Settings.Secure.getUriFor( Settings.Secure.USER_SETUP_COMPLETE); + private final Uri mRepeatKeysEnabledUri = Settings.Secure.getUriFor( + Settings.Secure.KEY_REPEAT_ENABLED); + + private final Uri mRepeatKeysTimeoutMsUri = Settings.Secure.getUriFor( + Settings.Secure.KEY_REPEAT_TIMEOUT_MS); + public AccessibilityContentObserver(Handler handler) { super(handler); } @@ -5827,6 +5836,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mNavigationModeUri, false, this, UserHandle.USER_ALL); contentResolver.registerContentObserver( mUserSetupCompleteUri, false, this, UserHandle.USER_ALL); + if (isRepeatKeysFeatureFlagEnabled() && Flags.enableMagnificationKeyboardControl()) { + contentResolver.registerContentObserver( + mRepeatKeysEnabledUri, false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mRepeatKeysTimeoutMsUri, false, this, UserHandle.USER_ALL); + } } @Override @@ -5917,6 +5932,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } else if (mNavigationModeUri.equals(uri) || mUserSetupCompleteUri.equals(uri)) { updateShortcutsForCurrentNavigationMode(); + } else if (mRepeatKeysEnabledUri.equals(uri) + || mRepeatKeysTimeoutMsUri.equals(uri)) { + readRepeatKeysSettingsLocked(userState); } } } @@ -6055,6 +6073,24 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return false; } + boolean readRepeatKeysSettingsLocked(AccessibilityUserState userState) { + if (!isRepeatKeysFeatureFlagEnabled() || !Flags.enableMagnificationKeyboardControl()) { + return false; + } + final boolean isRepeatKeysEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.KEY_REPEAT_ENABLED, + 1, userState.mUserId) == 1; + final int repeatKeysTimeoutMs = Settings.Secure.getIntForUser( + mContext.getContentResolver(), Settings.Secure.KEY_REPEAT_TIMEOUT_MS, + ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT, userState.mUserId); + mMagnificationController.setRepeatKeysEnabled(isRepeatKeysEnabled); + mMagnificationController.setRepeatKeysTimeoutMs(repeatKeysTimeoutMs); + + // No need to update any other state, so always return false. + return false; + } + boolean readMouseKeysEnabledLocked(AccessibilityUserState userState) { if (!keyboardA11yMouseKeys()) { return false; diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java index a3fe9ec5ea22..6cba3633b940 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java @@ -554,7 +554,8 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect if (motionEventInjector != null && mWindowManagerService.isTouchOrFaketouchDevice()) { motionEventInjector.injectEvents( - gestureSteps.getList(), mClient, sequence, displayId); + gestureSteps.getList(), mClient, sequence, displayId, + mAccessibilityServiceInfo.isAccessibilityTool()); } else { try { if (svcClientTracingEnabled()) { diff --git a/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java b/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java index 5cbd1a208ce1..b2169535d0de 100644 --- a/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java +++ b/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java @@ -105,12 +105,14 @@ public class MotionEventInjector extends BaseEventStreamTransformation implement * either complete or cancelled. */ public void injectEvents(List<GestureStep> gestureSteps, - IAccessibilityServiceClient serviceInterface, int sequence, int displayId) { + IAccessibilityServiceClient serviceInterface, int sequence, int displayId, + boolean fromAccessibilityTool) { SomeArgs args = SomeArgs.obtain(); args.arg1 = gestureSteps; args.arg2 = serviceInterface; args.argi1 = sequence; args.argi2 = displayId; + args.argi3 = fromAccessibilityTool ? 1 : 0; mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_INJECT_EVENTS, args)); } @@ -132,9 +134,11 @@ public class MotionEventInjector extends BaseEventStreamTransformation implement return; } cancelAnyPendingInjectedEvents(); - // Indicate that the input event is injected from accessibility, to let applications - // distinguish it from events injected by other means. - policyFlags |= WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY; + if (!android.view.accessibility.Flags.preventA11yNontoolFromInjectingIntoSensitiveViews()) { + // Indicate that the input event is injected from accessibility, to let applications + // distinguish it from events injected by other means. + policyFlags |= WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY; + } sendMotionEventToNext(event, rawEvent, policyFlags); } @@ -159,8 +163,12 @@ public class MotionEventInjector extends BaseEventStreamTransformation implement public boolean handleMessage(Message message) { if (message.what == MESSAGE_INJECT_EVENTS) { SomeArgs args = (SomeArgs) message.obj; - injectEventsMainThread((List<GestureStep>) args.arg1, - (IAccessibilityServiceClient) args.arg2, args.argi1, args.argi2); + injectEventsMainThread( + /*gestureSteps=*/(List<GestureStep>) args.arg1, + /*serviceInterface=*/(IAccessibilityServiceClient) args.arg2, + /*sequence=*/args.argi1, + /*displayId=*/args.argi2, + /*fromAccessibilityTool=*/args.argi3 == 1); args.recycle(); return true; } @@ -169,9 +177,15 @@ public class MotionEventInjector extends BaseEventStreamTransformation implement return false; } MotionEvent motionEvent = (MotionEvent) message.obj; - sendMotionEventToNext(motionEvent, motionEvent, - WindowManagerPolicyConstants.FLAG_PASS_TO_USER - | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY); + int policyFlags = WindowManagerPolicyConstants.FLAG_PASS_TO_USER + | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY; + if (android.view.accessibility.Flags.preventA11yNontoolFromInjectingIntoSensitiveViews()) { + boolean fromAccessibilityTool = message.arg2 == 1; + if (fromAccessibilityTool) { + policyFlags |= WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY_TOOL; + } + } + sendMotionEventToNext(motionEvent, motionEvent, policyFlags); boolean isEndOfSequence = message.arg1 != 0; if (isEndOfSequence) { notifyService(mServiceInterfaceForCurrentGesture, mSequencesInProgress.get(0), true); @@ -181,7 +195,8 @@ public class MotionEventInjector extends BaseEventStreamTransformation implement } private void injectEventsMainThread(List<GestureStep> gestureSteps, - IAccessibilityServiceClient serviceInterface, int sequence, int displayId) { + IAccessibilityServiceClient serviceInterface, int sequence, int displayId, + boolean fromAccessibilityTool) { if (mIsDestroyed) { try { serviceInterface.onPerformGestureResult(sequence, false); @@ -228,7 +243,8 @@ public class MotionEventInjector extends BaseEventStreamTransformation implement event.setDisplayId(displayId); int isEndOfSequence = (i == events.size() - 1) ? 1 : 0; Message message = mHandler.obtainMessage( - MESSAGE_SEND_MOTION_EVENT, isEndOfSequence, 0, event); + MESSAGE_SEND_MOTION_EVENT, isEndOfSequence, + fromAccessibilityTool ? 1 : 0, event); mLastScheduledEventTime = event.getEventTime(); mHandler.sendMessageDelayed(message, Math.max(0, event.getEventTime() - currentTime)); } @@ -322,9 +338,16 @@ public class MotionEventInjector extends BaseEventStreamTransformation implement long now = SystemClock.uptimeMillis(); MotionEvent cancelEvent = obtainMotionEvent(now, now, MotionEvent.ACTION_CANCEL, getLastTouchPoints(), 1); - sendMotionEventToNext(cancelEvent, cancelEvent, - WindowManagerPolicyConstants.FLAG_PASS_TO_USER - | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY); + int policyFlags = WindowManagerPolicyConstants.FLAG_PASS_TO_USER + | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY; + if (android.view.accessibility.Flags + .preventA11yNontoolFromInjectingIntoSensitiveViews()) { + // ACTION_CANCEL events are internal system details for event stream state + // management and not used for performing new actions, so always treat them as + // originating from an accessibility tool. + policyFlags |= WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY_TOOL; + } + sendMotionEventToNext(cancelEvent, cancelEvent, policyFlags); mOpenGesturesInProgress.put(source, false); } } diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 1bc9c783df76..40fb8b671d0c 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -310,7 +310,7 @@ public class AutoclickController extends BaseEventStreamTransformation { if (mAutoclickIndicatorScheduler != null) { mAutoclickIndicatorScheduler.updateCursorAreaSize(size); } - mClickScheduler.updateMovementSlope(size); + mClickScheduler.updateMovementSlop(size); } if (mAutoclickIgnoreMinorCursorMovementSettingUri.equals(uri)) { @@ -400,9 +400,9 @@ public class AutoclickController extends BaseEventStreamTransformation { * to be discarded as noise. Anchor is the position of the last MOVE event that was not * considered noise. */ - private static final double DEFAULT_MOVEMENT_SLOPE = 20f; + private static final double DEFAULT_MOVEMENT_SLOP = 20f; - private double mMovementSlope = DEFAULT_MOVEMENT_SLOPE; + private double mMovementSlop = DEFAULT_MOVEMENT_SLOP; /** Whether the minor cursor movement should be ignored. */ private boolean mIgnoreMinorCursorMovement = AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT; @@ -589,19 +589,19 @@ public class AutoclickController extends BaseEventStreamTransformation { float deltaX = mAnchorCoords.x - event.getX(pointerIndex); float deltaY = mAnchorCoords.y - event.getY(pointerIndex); double delta = Math.hypot(deltaX, deltaY); - double slope = + double slop = ((Flags.enableAutoclickIndicator() && mIgnoreMinorCursorMovement) - ? mMovementSlope - : DEFAULT_MOVEMENT_SLOPE); - return delta > slope; + ? mMovementSlop + : DEFAULT_MOVEMENT_SLOP); + return delta > slop; } public void setIgnoreMinorCursorMovement(boolean ignoreMinorCursorMovement) { mIgnoreMinorCursorMovement = ignoreMinorCursorMovement; } - private void updateMovementSlope(double slope) { - mMovementSlope = slope; + private void updateMovementSlop(double slop) { + mMovementSlop = slop; } /** diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java index 486f1f449691..e757dd5a77b7 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java @@ -50,6 +50,7 @@ import android.util.SparseIntArray; import android.util.SparseLongArray; import android.util.TypedValue; import android.view.Display; +import android.view.ViewConfiguration; import android.view.accessibility.MagnificationAnimationCallback; import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; @@ -122,9 +123,8 @@ public class MagnificationController implements MagnificationConnectionManager.C private @ZoomDirection int mActiveZoomDirection = ZOOM_DIRECTION_IN; private int mActiveZoomDisplay = Display.INVALID_DISPLAY; - // TODO(b/355499907): Get initial repeat interval from repeat keys settings. - @VisibleForTesting - public static final int INITIAL_KEYBOARD_REPEAT_INTERVAL_MS = 500; + private int mInitialKeyboardRepeatIntervalMs = + ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT; @VisibleForTesting public static final int KEYBOARD_REPEAT_INTERVAL_MS = 60; @@ -321,12 +321,6 @@ public class MagnificationController implements MagnificationConnectionManager.C mAlwaysOnMagnificationFeatureFlag = new AlwaysOnMagnificationFeatureFlag(context); mAlwaysOnMagnificationFeatureFlag.addOnChangedListener( mBackgroundExecutor, mAms::updateAlwaysOnMagnification); - - // TODO(b/355499907): Add an observer for repeat keys enabled changes, - // rather than initializing once at startup. - mRepeatKeysEnabled = Settings.Secure.getIntForUser( - mContext.getContentResolver(), Settings.Secure.KEY_REPEAT_ENABLED, 1, - UserHandle.USER_CURRENT) != 0; } @VisibleForTesting @@ -383,7 +377,7 @@ public class MagnificationController implements MagnificationConnectionManager.C if (mRepeatKeysEnabled) { mHandler.sendMessageDelayed( PooledLambda.obtainMessage(MagnificationController::maybeContinuePan, this), - INITIAL_KEYBOARD_REPEAT_INTERVAL_MS); + mInitialKeyboardRepeatIntervalMs); } } @@ -404,7 +398,7 @@ public class MagnificationController implements MagnificationConnectionManager.C if (mRepeatKeysEnabled) { mHandler.sendMessageDelayed( PooledLambda.obtainMessage(MagnificationController::maybeContinueZoom, this), - INITIAL_KEYBOARD_REPEAT_INTERVAL_MS); + mInitialKeyboardRepeatIntervalMs); } } @@ -434,6 +428,19 @@ public class MagnificationController implements MagnificationConnectionManager.C } } + public void setRepeatKeysEnabled(boolean isRepeatKeysEnabled) { + mRepeatKeysEnabled = isRepeatKeysEnabled; + } + + public void setRepeatKeysTimeoutMs(int repeatKeysTimeoutMs) { + mInitialKeyboardRepeatIntervalMs = repeatKeysTimeoutMs; + } + + @VisibleForTesting + public int getInitialKeyboardRepeatIntervalMs() { + return mInitialKeyboardRepeatIntervalMs; + } + private void handleUserInteractionChanged(int displayId, int mode) { if (mMagnificationCapabilities != Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL) { return; diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java index 61917676e88d..98ef974b9443 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java +++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java @@ -60,9 +60,7 @@ public interface CallerValidator { * Validates that the caller can execute the specified app function. * * <p>The caller can execute if the app function's package name is the same as the caller's - * package or the caller has either {@link Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} or - * {@link Manifest.permission#EXECUTE_APP_FUNCTIONS} granted. In some cases, app functions can - * still opt-out of caller having {@link Manifest.permission#EXECUTE_APP_FUNCTIONS}. + * package or the caller has the {@link Manifest.permission#EXECUTE_APP_FUNCTIONS} granted. * * @param callingUid The calling uid. * @param callingPid The calling pid. diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java index 69481c32baf0..fe163d77c4fc 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java @@ -18,7 +18,6 @@ package com.android.server.appfunctions; import static android.app.appfunctions.AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB; import static android.app.appfunctions.AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_NAMESPACE; -import static android.app.appfunctions.AppFunctionStaticMetadataHelper.STATIC_PROPERTY_RESTRICT_CALLERS_WITH_EXECUTE_APP_FUNCTIONS; import static android.app.appfunctions.AppFunctionStaticMetadataHelper.getDocumentIdForAppFunction; import static com.android.server.appfunctions.AppFunctionExecutors.THREAD_POOL_EXECUTOR; @@ -84,12 +83,7 @@ class CallerValidatorImpl implements CallerValidator { } @Override - @RequiresPermission( - anyOf = { - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - Manifest.permission.EXECUTE_APP_FUNCTIONS - }, - conditional = true) + @RequiresPermission(Manifest.permission.EXECUTE_APP_FUNCTIONS) public AndroidFuture<Boolean> verifyCallerCanExecuteAppFunction( int callingUid, int callingPid, @@ -101,17 +95,6 @@ class CallerValidatorImpl implements CallerValidator { return AndroidFuture.completedFuture(true); } - boolean hasTrustedExecutionPermission = - mContext.checkPermission( - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - callingPid, - callingUid) - == PackageManager.PERMISSION_GRANTED; - - if (hasTrustedExecutionPermission) { - return AndroidFuture.completedFuture(true); - } - boolean hasExecutionPermission = mContext.checkPermission( Manifest.permission.EXECUTE_APP_FUNCTIONS, callingPid, callingUid) @@ -138,7 +121,8 @@ class CallerValidatorImpl implements CallerValidator { .build()) .thenApply( batchResult -> getGenericDocumentFromBatchResult(batchResult, documentId)) - .thenApply(document -> !getRestrictCallersWithExecuteAppFunctionsProperty(document)) + // At this point, already checked the app has the permission. + .thenApply(document -> true) .whenComplete( (result, throwable) -> { futureAppSearchSession.close(); @@ -160,12 +144,6 @@ class CallerValidatorImpl implements CallerValidator { + failedResult.getErrorMessage()); } - private static boolean getRestrictCallersWithExecuteAppFunctionsProperty( - GenericDocument genericDocument) { - return genericDocument.getPropertyBoolean( - STATIC_PROPERTY_RESTRICT_CALLERS_WITH_EXECUTE_APP_FUNCTIONS); - } - @Override public boolean verifyEnterprisePolicyIsAllowed( @NonNull UserHandle callingUser, @NonNull UserHandle targetUser) { diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java index cc73288cdbfa..9d13e37b2503 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java +++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java @@ -78,7 +78,6 @@ public class MetadataSyncAdapter { // Hidden constants in {@link SetSchemaRequest} that restricts runtime metadata visibility // by permissions. public static final int EXECUTE_APP_FUNCTIONS = 9; - public static final int EXECUTE_APP_FUNCTIONS_TRUSTED = 10; public MetadataSyncAdapter( @NonNull PackageManager packageManager, @NonNull AppSearchManager appSearchManager) { @@ -281,8 +280,6 @@ public class MetadataSyncAdapter { new PackageIdentifier(packageName, packageCert)); setSchemaRequestBuilder.addRequiredPermissionsForSchemaTypeVisibility( runtimeMetadataSchema.getSchemaType(), Set.of(EXECUTE_APP_FUNCTIONS)); - setSchemaRequestBuilder.addRequiredPermissionsForSchemaTypeVisibility( - runtimeMetadataSchema.getSchemaType(), Set.of(EXECUTE_APP_FUNCTIONS_TRUSTED)); } return setSchemaRequestBuilder.build(); } diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index aef1c081cf03..d47aab061788 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -4673,12 +4673,6 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku keep.add(providerId); // Use the new AppWidgetProviderInfo. provider.setPartialInfoLocked(info); - // Clear old previews - if (remoteViewsProto()) { - clearGeneratedPreviewsAsync(provider); - } else { - provider.clearGeneratedPreviewsLocked(); - } // If it's enabled final int M = provider.widgets.size(); if (M > 0) { @@ -5104,6 +5098,10 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku AndroidFuture<RemoteViews> result = new AndroidFuture<>(); mSavePreviewsHandler.post(() -> { SparseArray<RemoteViews> previews = loadGeneratedPreviews(provider); + if (previews.size() == 0 && provider.info.generatedPreviewCategories != 0) { + // Failed to read previews from file, clear the file and update providers. + saveGeneratedPreviews(provider, previews, /* notify= */ true); + } for (int i = 0; i < previews.size(); i++) { if ((widgetCategory & previews.keyAt(i)) != 0) { result.complete(previews.valueAt(i)); @@ -5222,8 +5220,14 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku continue; } ProtoInputStream input = new ProtoInputStream(previewsFile.readFully()); - provider.info.generatedPreviewCategories = readGeneratedPreviewCategoriesFromProto( - input); + try { + provider.info.generatedPreviewCategories = readGeneratedPreviewCategoriesFromProto( + input); + } catch (IOException e) { + Slog.e(TAG, "Failed to read generated previews from file for " + provider, e); + previewsFile.delete(); + provider.info.generatedPreviewCategories = 0; + } if (DEBUG) { Slog.i(TAG, TextUtils.formatSimple( "loadGeneratedPreviewCategoriesLocked %d %s categories %d", profileId, diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 02a8f6218468..521f676a6703 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -47,6 +47,7 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AppOpsManager; +import android.app.KeyguardManager; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.ecm.EnhancedConfirmationManager; @@ -302,8 +303,17 @@ public class CompanionDeviceManagerService extends SystemService { enforceCallerCanManageAssociationsForPackage(getContext(), userId, packageName, "create associations"); - mAssociationRequestsProcessor.processNewAssociationRequest( - request, packageName, userId, callback); + if (request.isSkipRoleGrant()) { + checkCallerCanSkipRoleGrant(); + mAssociationRequestsProcessor.createAssociation(userId, packageName, + /* macAddress= */ null, request.getDisplayName(), + request.getDeviceProfile(), /* associatedDevice= */ null, + request.isSelfManaged(), callback, /* resultReceiver= */ null, + request.getDeviceIcon(), /* skipRoleGrant= */ true); + } else { + mAssociationRequestsProcessor.processNewAssociationRequest( + request, packageName, userId, callback); + } } @Override @@ -669,7 +679,7 @@ public class CompanionDeviceManagerService extends SystemService { final MacAddress macAddressObj = MacAddress.fromString(macAddress); mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddressObj, - null, null, null, false, null, null, null); + null, null, null, false, null, null, null, false); } private void checkCanCallNotificationApi(String callingPackage, int userId) { @@ -684,6 +694,19 @@ public class CompanionDeviceManagerService extends SystemService { "App must have an association before calling this API"); } + private void checkCallerCanSkipRoleGrant() { + final KeyguardManager keyguardManager = + getContext().getSystemService(KeyguardManager.class); + if (keyguardManager != null && keyguardManager.isKeyguardSecure()) { + throw new SecurityException("Skipping CDM role grant requires insecure keyguard."); + } + if (getContext().checkCallingPermission(ASSOCIATE_COMPANION_DEVICES) + != PERMISSION_GRANTED) { + throw new SecurityException( + "Skipping CDM role grant requires ASSOCIATE_COMPANION_DEVICES permission."); + } + } + @Override public boolean canPairWithoutPrompt(String packageName, String macAddress, int userId) { final AssociationInfo association = diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index 3508f2ffc4c4..e7d1460aa66a 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -106,8 +106,9 @@ class CompanionDeviceShellCommand extends ShellCommand { boolean selfManaged = getNextBooleanArg(); final MacAddress macAddress = MacAddress.fromString(address); mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress, - deviceProfile, deviceProfile, /* associatedDevice */ null, selfManaged, - /* callback */ null, /* resultReceiver */ null, /* deviceIcon */ null); + deviceProfile, deviceProfile, /* associatedDevice= */ null, selfManaged, + /* callback= */ null, /* resultReceiver= */ null, + /* deviceIcon= */ null, /* skipRoleGrant= */ false); } break; diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java index 899b302316f9..8c2c63cbbd84 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java @@ -282,8 +282,8 @@ public class AssociationRequestsProcessor { Binder.withCleanCallingIdentity(() -> { createAssociation(userId, packageName, macAddress, request.getDisplayName(), request.getDeviceProfile(), request.getAssociatedDevice(), - request.isSelfManaged(), - callback, resultReceiver, request.getDeviceIcon()); + request.isSelfManaged(), callback, resultReceiver, request.getDeviceIcon(), + /* skipRoleGrant= */ false); }); } @@ -294,7 +294,8 @@ public class AssociationRequestsProcessor { @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice, boolean selfManaged, @Nullable IAssociationRequestCallback callback, - @Nullable ResultReceiver resultReceiver, @Nullable Icon deviceIcon) { + @Nullable ResultReceiver resultReceiver, @Nullable Icon deviceIcon, + boolean skipRoleGrant) { final int id = mAssociationStore.getNextId(); final long timestamp = System.currentTimeMillis(); @@ -303,8 +304,17 @@ public class AssociationRequestsProcessor { selfManaged, /* notifyOnDeviceNearby */ false, /* revoked */ false, /* pending */ false, timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0, deviceIcon, /* deviceId */ null); - // Add role holder for association (if specified) and add new association to store. - maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver); + + if (skipRoleGrant) { + Slog.i(TAG, "Created association for " + association.getDeviceProfile() + " and userId=" + + association.getUserId() + ", packageName=" + + association.getPackageName() + " without granting role"); + mAssociationStore.addAssociation(association); + sendCallbackAndFinish(association, callback, resultReceiver); + } else { + // Add role holder for association (if specified) and add new association to store. + maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver); + } } /** diff --git a/services/core/java/com/android/server/DockObserver.java b/services/core/java/com/android/server/DockObserver.java index 3de84f17b583..d2db8f74cd05 100644 --- a/services/core/java/com/android/server/DockObserver.java +++ b/services/core/java/com/android/server/DockObserver.java @@ -27,7 +27,6 @@ import android.media.RingtoneManager; import android.net.Uri; import android.os.Binder; import android.os.Handler; -import android.os.Message; import android.os.PowerManager; import android.os.SystemClock; import android.os.UEventObserver; @@ -37,6 +36,7 @@ import android.util.Pair; import android.util.Slog; import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; import com.android.internal.util.FrameworkStatsLog; @@ -57,8 +57,6 @@ import java.util.Map; final class DockObserver extends SystemService { private static final String TAG = "DockObserver"; - private static final int MSG_DOCK_STATE_CHANGED = 0; - private final PowerManager mPowerManager; private final PowerManager.WakeLock mWakeLock; @@ -66,11 +64,16 @@ final class DockObserver extends SystemService { private boolean mSystemReady; + @GuardedBy("mLock") private int mActualDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; + @GuardedBy("mLock") private int mReportedDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; + + @GuardedBy("mLock") private int mPreviousDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; + @GuardedBy("mLock") private boolean mUpdatesStopped; private final boolean mKeepDreamingWhenUnplugging; @@ -182,18 +185,24 @@ final class DockObserver extends SystemService { ExtconInfo.EXTCON_DOCK }); - if (!infos.isEmpty()) { - ExtconInfo info = infos.get(0); - Slog.i(TAG, "Found extcon info devPath: " + info.getDevicePath() - + ", statePath: " + info.getStatePath()); - - // set initial status - setDockStateFromProviderLocked(ExtconStateProvider.fromFile(info.getStatePath())); - mPreviousDockState = mActualDockState; - - mExtconUEventObserver.startObserving(info); - } else { - Slog.i(TAG, "No extcon dock device found in this kernel."); + synchronized (mLock) { + if (!infos.isEmpty()) { + ExtconInfo info = infos.get(0); + Slog.i( + TAG, + "Found extcon info devPath: " + + info.getDevicePath() + + ", statePath: " + + info.getStatePath()); + + // set initial status + setDockStateFromProviderLocked(ExtconStateProvider.fromFile(info.getStatePath())); + mPreviousDockState = mActualDockState; + + mExtconUEventObserver.startObserving(info); + } else { + Slog.i(TAG, "No extcon dock device found in this kernel."); + } } mDockObserverLocalService = new DockObserverLocalService(); @@ -223,13 +232,15 @@ final class DockObserver extends SystemService { } } + @GuardedBy("mLock") private void updateIfDockedLocked() { // don't bother broadcasting undocked here if (mReportedDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) { - updateLocked(); + postWakefulDockStateChange(); } } + @GuardedBy("mLock") private void setActualDockStateLocked(int newState) { mActualDockState = newState; if (!mUpdatesStopped) { @@ -237,6 +248,7 @@ final class DockObserver extends SystemService { } } + @GuardedBy("mLock") private void setDockStateLocked(int newState) { if (newState != mReportedDockState) { mReportedDockState = newState; @@ -246,10 +258,12 @@ final class DockObserver extends SystemService { if (mSystemReady) { // Wake up immediately when docked or undocked unless prohibited from doing so. if (allowWakeFromDock()) { - mPowerManager.wakeUp(SystemClock.uptimeMillis(), + mPowerManager.wakeUp( + SystemClock.uptimeMillis(), + PowerManager.WAKE_REASON_DOCK, "android.server:DOCK"); } - updateLocked(); + postWakefulDockStateChange(); } } } @@ -263,9 +277,8 @@ final class DockObserver extends SystemService { Settings.Global.THEATER_MODE_ON, 0) == 0); } - private void updateLocked() { - mWakeLock.acquire(); - mHandler.sendEmptyMessage(MSG_DOCK_STATE_CHANGED); + private void postWakefulDockStateChange() { + mHandler.post(mWakeLock.wrap(this::handleDockStateChange)); } private void handleDockStateChange() { @@ -348,17 +361,7 @@ final class DockObserver extends SystemService { } } - private final Handler mHandler = new Handler(true /*async*/) { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_DOCK_STATE_CHANGED: - handleDockStateChange(); - mWakeLock.release(); - break; - } - } - }; + private final Handler mHandler = new Handler(true /*async*/); private int getDockedStateExtraValue(ExtconStateProvider state) { for (ExtconStateConfig config : mExtconStateConfigs) { @@ -386,6 +389,7 @@ final class DockObserver extends SystemService { } } + @GuardedBy("mLock") private void setDockStateFromProviderLocked(ExtconStateProvider provider) { int state = Intent.EXTRA_DOCK_STATE_UNDOCKED; if ("1".equals(provider.getValue("DOCK"))) { diff --git a/services/core/java/com/android/server/TradeInModeService.java b/services/core/java/com/android/server/TradeInModeService.java index 70a033086261..a69667395ebd 100644 --- a/services/core/java/com/android/server/TradeInModeService.java +++ b/services/core/java/com/android/server/TradeInModeService.java @@ -137,12 +137,13 @@ public final class TradeInModeService extends SystemService { Slog.i(TAG, "Not starting trade-in mode, device is setup."); return false; } - if (SystemProperties.getInt("ro.debuggable", 0) == 1) { - // We don't want to force adbd into TIM on debug builds. - Slog.e(TAG, "Not starting trade-in mode, device is debuggable."); - return false; - } - if (isAdbEnabled()) { + if (isDebuggable()) { + if (!isForceEnabledForTesting()) { + // We don't want to force adbd into TIM on debug builds. + Slog.e(TAG, "Not starting trade-in mode, device is debuggable."); + return false; + } + } else if (isAdbEnabled()) { Slog.e(TAG, "Not starting trade-in mode, adb is already enabled."); return false; } @@ -234,6 +235,10 @@ public final class TradeInModeService extends SystemService { return SystemProperties.getInt("ro.debuggable", 0) == 1; } + private boolean isForceEnabledForTesting() { + return SystemProperties.getInt("persist.adb.test_tradeinmode", 0) == 1; + } + private boolean isAdbEnabled() { final ContentResolver cr = mContext.getContentResolver(); return Settings.Global.getInt(cr, Settings.Global.ADB_ENABLED, 0) == 1; diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 896c9b8d0932..ca0d6d2c1d7d 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -16,9 +16,9 @@ package com.android.server; -import static android.app.Flags.modesApi; import static android.app.Flags.enableCurrentModeTypeBinderCache; import static android.app.Flags.enableNightModeBinderCache; +import static android.app.Flags.modesApi; import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE; import static android.app.UiModeManager.DEFAULT_PRIORITY; import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; @@ -1967,6 +1967,14 @@ final class UiModeManagerService extends SystemService { sendConfigurationAndStartDreamOrDockAppLocked(category); } + private boolean shouldStartDockApp(Context context, Intent homeIntent) { + if (mWatch && !mSetupWizardComplete) { + // Do not ever start dock app when setup is not complete on a watch. + return false; + } + return Sandman.shouldStartDockApp(context, homeIntent); + } + private void sendConfigurationAndStartDreamOrDockAppLocked(String category) { // Update the configuration but don't send it yet. mHoldingConfiguration = false; @@ -1983,7 +1991,7 @@ final class UiModeManagerService extends SystemService { // activity manager take care of both the start and config // change. Intent homeIntent = buildHomeIntent(category); - if (Sandman.shouldStartDockApp(getContext(), homeIntent)) { + if (shouldStartDockApp(getContext(), homeIntent)) { try { int result = ActivityTaskManager.getService().startActivityWithConfig( null, getContext().getBasePackageName(), diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 0603c4506cd1..6ece2654f3bf 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -19397,13 +19397,13 @@ public class ActivityManagerService extends IActivityManager.Stub if (((intent.getExtendedFlags() & Intent.EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED) == 0) && intent.getExtras() != null && intent.getExtras().hasIntent()) { Slog.wtf(TAG, - "[IntentRedirect] The intent does not have its nested keys collected as a " + "[IntentRedirect Hardening] The intent does not have its nested keys collected as a " + "preparation for creating intent creator tokens. Intent: " + intent + "; creatorPackage: " + creatorPackage); if (preventIntentRedirectShowToastIfNestedKeysNotCollectedRW()) { UiThread.getHandler().post( () -> Toast.makeText(mContext, - "Nested keys not collected. go/report-bug-intentRedir to report a" + "Nested keys not collected, activity launch won't be blocked. go/report-bug-intentRedir to report a" + " bug", Toast.LENGTH_LONG).show()); } if (preventIntentRedirectThrowExceptionIfNestedKeysNotCollected()) { diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index cbebc905796c..c237897f1229 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -4279,7 +4279,6 @@ final class ActivityManagerShellCommand extends ShellCommand { } int runClearBadProcess(PrintWriter pw) throws RemoteException { - final String processName = getNextArgRequired(); int userId = UserHandle.USER_CURRENT; String opt; while ((opt = getNextOption()) != null) { @@ -4290,6 +4289,7 @@ final class ActivityManagerShellCommand extends ShellCommand { return -1; } } + final String processName = getNextArgRequired(); if (userId == UserHandle.USER_CURRENT) { userId = mInternal.getCurrentUserId(); } diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index d335529a006a..ce526e510053 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -572,6 +572,7 @@ public class CachedAppOptimizer { public long mTotalAnonMemFreedKBs; public long mSumOrigAnonRss; public double mMaxCompactEfficiency; + public double mMaxSwapEfficiency; // Cpu time public long mTotalCpuTimeMillis; @@ -586,6 +587,10 @@ public class CachedAppOptimizer { if (compactEfficiency > mMaxCompactEfficiency) { mMaxCompactEfficiency = compactEfficiency; } + final double swapEfficiency = anonRssSaved / (double) origAnonRss; + if (swapEfficiency > mMaxSwapEfficiency) { + mMaxSwapEfficiency = swapEfficiency; + } mTotalDeltaAnonRssKBs += anonRssSaved; mTotalZramConsumedKBs += zramConsumed; mTotalAnonMemFreedKBs += memFreed; @@ -628,7 +633,11 @@ public class CachedAppOptimizer { pw.println(" -----Memory Stats----"); pw.println(" Total Delta Anon RSS (KB) : " + mTotalDeltaAnonRssKBs); pw.println(" Total Physical ZRAM Consumed (KB): " + mTotalZramConsumedKBs); + // Anon Mem Freed = Delta Anon RSS - ZRAM Consumed pw.println(" Total Anon Memory Freed (KB): " + mTotalAnonMemFreedKBs); + pw.println(" Avg Swap Efficiency (KB) (Delta Anon RSS/Orig Anon RSS): " + + (mTotalDeltaAnonRssKBs / (double) mSumOrigAnonRss)); + pw.println(" Max Swap Efficiency: " + mMaxSwapEfficiency); // This tells us how much anon memory we were able to free thanks to running // compaction pw.println(" Avg Compaction Efficiency (Anon Freed/Anon RSS): " @@ -808,8 +817,9 @@ public class CachedAppOptimizer { pw.println(" Tracking last compaction stats for " + mLastCompactionStats.size() + " processes."); pw.println("Last Compaction per process stats:"); - pw.println(" (ProcessName,Source,DeltaAnonRssKBs,ZramConsumedKBs,AnonMemFreedKBs," - + "CompactEfficiency,CompactCost(ms/MB),procState,oomAdj,oomAdjReason)"); + pw.println(" (ProcessName,Source,DeltaAnonRssKBs,ZramConsumedKBs,AnonMemFreedKBs" + + ",SwapEfficiency,CompactEfficiency,CompactCost(ms/MB),procState,oomAdj," + + "oomAdjReason)"); for (Map.Entry<Integer, SingleCompactionStats> entry : mLastCompactionStats.entrySet()) { SingleCompactionStats stats = entry.getValue(); @@ -818,7 +828,8 @@ public class CachedAppOptimizer { pw.println(); pw.println("Last 20 Compactions Stats:"); pw.println(" (ProcessName,Source,DeltaAnonRssKBs,ZramConsumedKBs,AnonMemFreedKBs," - + "CompactEfficiency,CompactCost(ms/MB),procState,oomAdj,oomAdjReason)"); + + "SwapEfficiency,CompactEfficiency,CompactCost(ms/MB),procState,oomAdj," + + "oomAdjReason)"); for (SingleCompactionStats stats : mCompactionStatsHistory) { stats.dump(pw); } @@ -1779,6 +1790,8 @@ public class CachedAppOptimizer { double getCompactEfficiency() { return mAnonMemFreedKBs / (double) mOrigAnonRss; } + double getSwapEfficiency() { return mDeltaAnonRssKBs / (double) mOrigAnonRss; } + double getCompactCost() { // mCpuTimeMillis / (anonMemFreedKBs/1024) and metric is in (ms/MB) return mCpuTimeMillis / (double) mAnonMemFreedKBs * 1024; @@ -1791,7 +1804,8 @@ public class CachedAppOptimizer { @NeverCompile void dump(PrintWriter pw) { pw.println(" (" + mProcessName + "," + mSourceType.name() + "," + mDeltaAnonRssKBs - + "," + mZramConsumedKBs + "," + mAnonMemFreedKBs + "," + getCompactEfficiency() + + "," + mZramConsumedKBs + "," + mAnonMemFreedKBs + "," + + getSwapEfficiency() + "," + getCompactEfficiency() + "," + getCompactCost() + "," + mProcState + "," + mOomAdj + "," + OomAdjuster.oomAdjReasonToString(mOomAdjReason) + ")"); } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 27e9e44f1090..e0fbaf43ea43 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -31,6 +31,7 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE; import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL; +import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK; import static android.os.PowerWhitelistManager.REASON_BOOT_COMPLETED; import static android.os.PowerWhitelistManager.REASON_LOCKED_BOOT_COMPLETED; import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED; @@ -3904,10 +3905,6 @@ class UserController implements Handler.Callback { return mService.mWindowManager; } - ActivityTaskManagerInternal getActivityTaskManagerInternal() { - return mService.mAtmInternal; - } - void activityManagerOnUserStopped(@UserIdInt int userId) { LocalServices.getService(ActivityTaskManagerInternal.class).onUserStopped(userId); } @@ -4122,40 +4119,25 @@ class UserController implements Handler.Callback { } void lockDeviceNowAndWaitForKeyguardShown() { - if (getWindowManager().isKeyguardLocked()) { - Slogf.w(TAG, "Not locking the device since the keyguard is already locked"); - return; - } - final TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("lockDeviceNowAndWaitForKeyguardShown"); final CountDownLatch latch = new CountDownLatch(1); - ActivityTaskManagerInternal.ScreenObserver screenObserver = - new ActivityTaskManagerInternal.ScreenObserver() { - @Override - public void onAwakeStateChanged(boolean isAwake) { - - } - - @Override - public void onKeyguardStateChanged(boolean isShowing) { - if (isShowing) { - latch.countDown(); - } - } - }; - - getActivityTaskManagerInternal().registerScreenObserver(screenObserver); - getWindowManager().lockDeviceNow(); + Bundle bundle = new Bundle(); + bundle.putBinder(LOCK_ON_USER_SWITCH_CALLBACK, new IRemoteCallback.Stub() { + public void sendResult(Bundle data) { + latch.countDown(); + } + }); + getWindowManager().lockNow(bundle); try { if (!latch.await(20, TimeUnit.SECONDS)) { - throw new RuntimeException("Keyguard is not shown in 20 seconds"); + throw new RuntimeException("User controller expected a callback while waiting " + + "to show the keyguard. Timed out after 20 seconds."); } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { - getActivityTaskManagerInternal().unregisterScreenObserver(screenObserver); t.traceEnd(); } } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 833599810210..e8a222625b1b 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -237,6 +237,13 @@ public class AppOpsService extends IAppOpsService.Stub { */ private static final int CURRENT_VERSION = 1; + /** + * The upper limit of total number of attributed op entries that can be returned in a binder + * transaction to avoid TransactionTooLargeException + */ + private static final int NUM_ATTRIBUTED_OP_ENTRY_THRESHOLD = 2000; + + private SensorPrivacyManager mSensorPrivacyManager; // Write at most every 30 minutes. @@ -1702,6 +1709,8 @@ public class AppOpsService extends IAppOpsService.Stub { Manifest.permission.GET_APP_OPS_STATS, Binder.getCallingPid(), Binder.getCallingUid()) == PackageManager.PERMISSION_GRANTED; + int totalAttributedOpEntryCount = 0; + if (ops == null) { resOps = new ArrayList<>(); for (int j = 0; j < pkgOps.size(); j++) { @@ -1709,7 +1718,12 @@ public class AppOpsService extends IAppOpsService.Stub { if (opRestrictsRead(curOp.op) && !shouldReturnRestrictedAppOps) { continue; } - resOps.add(getOpEntryForResult(curOp, persistentDeviceId)); + if (totalAttributedOpEntryCount > NUM_ATTRIBUTED_OP_ENTRY_THRESHOLD) { + break; + } + OpEntry opEntry = getOpEntryForResult(curOp, persistentDeviceId); + resOps.add(opEntry); + totalAttributedOpEntryCount += opEntry.getAttributedOpEntries().size(); } } else { for (int j = 0; j < ops.length; j++) { @@ -1721,10 +1735,21 @@ public class AppOpsService extends IAppOpsService.Stub { if (opRestrictsRead(curOp.op) && !shouldReturnRestrictedAppOps) { continue; } - resOps.add(getOpEntryForResult(curOp, persistentDeviceId)); + if (totalAttributedOpEntryCount > NUM_ATTRIBUTED_OP_ENTRY_THRESHOLD) { + break; + } + OpEntry opEntry = getOpEntryForResult(curOp, persistentDeviceId); + resOps.add(opEntry); + totalAttributedOpEntryCount += opEntry.getAttributedOpEntries().size(); } } } + + if (totalAttributedOpEntryCount > NUM_ATTRIBUTED_OP_ENTRY_THRESHOLD) { + Slog.w(TAG, "The number of attributed op entries has exceeded the threshold. This " + + "could be due to DoS attack from malicious apps. The result is throttled."); + } + return resOps; } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 320dd8f188c0..f2830090e7db 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -837,6 +837,7 @@ public class AudioService extends IAudioService.Stub private final Executor mAudioServerLifecycleExecutor; private long mSysPropListenerNativeHandle; + private CacheWatcher mCacheWatcher; private final List<Future> mScheduledPermissionTasks = new ArrayList(); private IMediaProjectionManager mProjectionService; // to validate projection token @@ -11093,31 +11094,26 @@ public class AudioService extends IAudioService.Stub }, getAudioPermissionsDelay(), TimeUnit.MILLISECONDS)); } }; - mSysPropListenerNativeHandle = mAudioSystem.listenForSystemPropertyChange( - PermissionManager.CACHE_KEY_PACKAGE_INFO_NOTIFY, - task); + if (PropertyInvalidatedCache.separatePermissionNotificationsEnabled()) { + mCacheWatcher = new CacheWatcher(task); + mCacheWatcher.start(); + } else { + mSysPropListenerNativeHandle = mAudioSystem.listenForSystemPropertyChange( + PermissionManager.CACHE_KEY_PACKAGE_INFO_NOTIFY, + task); + } } else { mAudioSystem.listenForSystemPropertyChange( PermissionManager.CACHE_KEY_PACKAGE_INFO_NOTIFY, () -> mAudioServerLifecycleExecutor.execute( mPermissionProvider::onPermissionStateChanged)); } - - if (PropertyInvalidatedCache.separatePermissionNotificationsEnabled()) { - new PackageInfoTransducer().start(); - } } /** - * A transducer that converts high-speed changes in the CACHE_KEY_PACKAGE_INFO_CACHE - * PropertyInvalidatedCache into low-speed changes in the CACHE_KEY_PACKAGE_INFO_NOTIFY system - * property. This operates on the popcorn principle: changes in the source are done when the - * source has been quiet for the soak interval. - * - * TODO(b/381097912) This is a temporary measure to support migration away from sysprop - * sniffing. It should be cleaned up. + * Listens for CACHE_KEY_PACKAGE_INFO_CACHE invalidations to trigger permission syncing */ - private static class PackageInfoTransducer extends Thread { + private static class CacheWatcher extends Thread { // The run/stop signal. private final AtomicBoolean mRunning = new AtomicBoolean(false); @@ -11125,81 +11121,33 @@ public class AudioService extends IAudioService.Stub // The source of change information. private final PropertyInvalidatedCache.NonceWatcher mWatcher; - // The handler for scheduling delayed reactions to changes. - private final Handler mHandler; + // Task to trigger when cache changes + private final Runnable mTask; - // How long to soak changes: 50ms is the legacy choice. - private final static long SOAK_TIME_MS = 50; - - // The ubiquitous lock. - private final Object mLock = new Object(); - - // If positive, this is the soak expiration time. - @GuardedBy("mLock") - private long mSoakDeadlineMs = -1; - - // A source of unique long values. - @GuardedBy("mLock") - private long mToken = 0; - - PackageInfoTransducer() { - mWatcher = PropertyInvalidatedCache - .getNonceWatcher(PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE); - mHandler = new Handler(BackgroundThread.getHandler().getLooper()) { - @Override - public void handleMessage(Message msg) { - PackageInfoTransducer.this.handleMessage(msg); - }}; + public CacheWatcher(Runnable r) { + mWatcher = PropertyInvalidatedCache.getNonceWatcher( + PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE); + mTask = r; } public void run() { mRunning.set(true); while (mRunning.get()) { + doCheck(); try { - final int changes = mWatcher.waitForChange(); - if (changes == 0 || !mRunning.get()) { - continue; - } + mWatcher.waitForChange(); } catch (InterruptedException e) { + Log.wtf(TAG, "Unexpected Interrupt", e); // We don't know why the exception occurred but keep running until told to // stop. continue; } - trigger(); } } - @GuardedBy("mLock") - private void updateLocked() { - String n = Long.toString(mToken++); - SystemPropertySetter.setWithRetry(PermissionManager.CACHE_KEY_PACKAGE_INFO_NOTIFY, n); - } - - private void trigger() { - synchronized (mLock) { - boolean alreadyQueued = mSoakDeadlineMs >= 0; - final long nowMs = SystemClock.uptimeMillis(); - mSoakDeadlineMs = nowMs + SOAK_TIME_MS; - if (!alreadyQueued) { - mHandler.sendEmptyMessageAtTime(0, mSoakDeadlineMs); - updateLocked(); - } - } - } - - private void handleMessage(Message msg) { - synchronized (mLock) { - if (mSoakDeadlineMs < 0) { - return; // ??? - } - final long nowMs = SystemClock.uptimeMillis(); - if (mSoakDeadlineMs > nowMs) { - mSoakDeadlineMs = nowMs + SOAK_TIME_MS; - mHandler.sendEmptyMessageAtTime(0, mSoakDeadlineMs); - return; - } - mSoakDeadlineMs = -1; - updateLocked(); + public synchronized void doCheck() { + if (mWatcher.isChanged()) { + mTask.run(); } } @@ -14744,6 +14692,7 @@ public class AudioService extends IAudioService.Stub for (AudioMix mix : mMixes) { mix.setVirtualDeviceId(mAttributionSource.getDeviceId()); } + mAudioSystem.registerPolicyMixes(mMixes, false); return mAudioSystem.registerPolicyMixes(mMixes, true); } finally { Binder.restoreCallingIdentity(identity); @@ -15375,7 +15324,11 @@ public class AudioService extends IAudioService.Stub /** @see AudioManager#permissionUpdateBarrier() */ public void permissionUpdateBarrier() { if (!audioserverPermissions()) return; - mAudioSystem.triggerSystemPropertyUpdate(mSysPropListenerNativeHandle); + if (PropertyInvalidatedCache.separatePermissionNotificationsEnabled()) { + mCacheWatcher.doCheck(); + } else { + mAudioSystem.triggerSystemPropertyUpdate(mSysPropListenerNativeHandle); + } List<Future> snapshot; synchronized (mScheduledPermissionTasks) { snapshot = List.copyOf(mScheduledPermissionTasks); diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index 2d802b21cf03..b6768c9c087a 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -38,7 +38,6 @@ import android.hardware.biometrics.AuthenticationStateListener; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.ComponentInfoInternal; -import android.hardware.biometrics.Flags; import android.hardware.biometrics.IAuthService; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricService; @@ -399,12 +398,6 @@ public class AuthService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { - // We can't do this above because we need the READ_DEVICE_CONFIG permission, which - // the calling user may not possess. - if (!Flags.lastAuthenticationTime()) { - throw new UnsupportedOperationException(); - } - return mBiometricService.getLastAuthenticationTime(userId, authenticators); } finally { Binder.restoreCallingIdentity(identity); diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 15b1f220bc3c..a5058dd51a33 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -44,7 +44,6 @@ import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.BiometricStateListener; -import android.hardware.biometrics.Flags; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricSensorReceiver; @@ -906,10 +905,6 @@ public class BiometricService extends SystemService { int userId, @Authenticators.Types int authenticators) { super.getLastAuthenticationTime_enforcePermission(); - if (!Flags.lastAuthenticationTime()) { - throw new UnsupportedOperationException(); - } - Slogf.d(TAG, "getLastAuthenticationTime(userId=%d, authenticators=0x%x)", userId, authenticators); diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java index c739118194e5..6fab7e620277 100644 --- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java +++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java @@ -164,6 +164,7 @@ class PreAuthInfo { Slog.d(TAG, "Package: " + opPackageName + " Sensor ID: " + sensor.id + " Modality: " + sensor.modality + + " User id: " + effectiveUserId + " Status: " + status); // A sensor with privacy enabled will still be eligible to diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index b9ce8c93dbde..a1e8f08db0a6 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -61,7 +61,7 @@ public class DisplayManagerFlags { private final FlagState mDisplayTopology = new FlagState( Flags.FLAG_DISPLAY_TOPOLOGY, - Flags::displayTopology); + DesktopExperienceFlags.DISPLAY_TOPOLOGY::isTrue); private final FlagState mConnectedDisplayErrorHandlingFlagState = new FlagState( Flags.FLAG_ENABLE_CONNECTED_DISPLAY_ERROR_HANDLING, @@ -245,6 +245,12 @@ public class DisplayManagerFlags { Flags.FLAG_ENABLE_PLUGIN_MANAGER, Flags::enablePluginManager ); + + private final FlagState mEnableHdrOverridePluginTypeFlagState = new FlagState( + Flags.FLAG_ENABLE_HDR_OVERRIDE_PLUGIN_TYPE, + Flags::enableHdrOverridePluginType + ); + private final FlagState mDisplayListenerPerformanceImprovementsFlagState = new FlagState( Flags.FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS, Flags::displayListenerPerformanceImprovements @@ -261,7 +267,7 @@ public class DisplayManagerFlags { private final FlagState mBaseDensityForExternalDisplays = new FlagState( Flags.FLAG_BASE_DENSITY_FOR_EXTERNAL_DISPLAYS, - Flags::baseDensityForExternalDisplays + DesktopExperienceFlags.BASE_DENSITY_FOR_EXTERNAL_DISPLAYS::isTrue ); private final FlagState mFramerateOverrideTriggersRrCallbacks = new FlagState( @@ -550,6 +556,10 @@ public class DisplayManagerFlags { return mEnablePluginManagerFlagState.isEnabled(); } + public boolean isHdrOverrideEnabled() { + return mEnableHdrOverridePluginTypeFlagState.isEnabled(); + } + /** * @return {@code true} if the flag for display listener performance improvements is enabled */ diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 63cd2d73336a..7890db1454b4 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -454,6 +454,14 @@ flag { } flag { + name: "enable_hdr_override_plugin_type" + namespace: "display_manager" + description: "Enable hdr override plugin type" + bug: "389873155" + is_fixed_read_only: true +} + +flag { name: "enable_display_content_mode_management" namespace: "lse_desktop_experience" description: "Enable switching the content mode of connected displays between mirroring and extened. Also change the default content mode to extended mode." @@ -501,3 +509,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "display_category_built_in" + namespace: "display_manager" + description: "Add a new category to get the built in displays." + bug: "293651324" + is_fixed_read_only: false +} diff --git a/services/core/java/com/android/server/display/plugin/PluginManager.java b/services/core/java/com/android/server/display/plugin/PluginManager.java index cb0a4574361a..a8c4e7eaeb66 100644 --- a/services/core/java/com/android/server/display/plugin/PluginManager.java +++ b/services/core/java/com/android/server/display/plugin/PluginManager.java @@ -30,7 +30,9 @@ import dalvik.system.PathClassLoader; import java.io.PrintWriter; import java.lang.reflect.InvocationTargetException; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Responsible for loading Plugins. Plugins and PluginSupplier are loaded from @@ -43,7 +45,6 @@ public class PluginManager { "com.android.server.display.plugin.PluginsProviderImpl"; private static final String TAG = "PluginManager"; - private final DisplayManagerFlags mFlags; private final PluginStorage mPluginStorage; private final List<Plugin> mPlugins; @@ -53,10 +54,11 @@ public class PluginManager { @VisibleForTesting PluginManager(Context context, DisplayManagerFlags flags, Injector injector) { - mFlags = flags; - mPluginStorage = injector.getPluginStorage(); - if (mFlags.isPluginManagerEnabled()) { - mPlugins = Collections.unmodifiableList(injector.loadPlugins(context, mPluginStorage)); + Set<PluginType<?>> enabledTypes = injector.getEnabledPluginTypes(flags); + mPluginStorage = injector.getPluginStorage(enabledTypes); + if (flags.isPluginManagerEnabled()) { + mPlugins = Collections.unmodifiableList(injector.loadPlugins( + context, mPluginStorage, enabledTypes)); Slog.d(TAG, "loaded Plugins:" + mPlugins); } else { mPlugins = List.of(); @@ -110,11 +112,21 @@ public class PluginManager { } static class Injector { - PluginStorage getPluginStorage() { - return new PluginStorage(); + + Set<PluginType<?>> getEnabledPluginTypes(DisplayManagerFlags flags) { + Set<PluginType<?>> enabledTypes = new HashSet<>(); + if (flags.isHdrOverrideEnabled()) { + enabledTypes.add(PluginType.HDR_BOOST_OVERRIDE); + } + return enabledTypes; + } + + PluginStorage getPluginStorage(Set<PluginType<?>> enabledTypes) { + return new PluginStorage(enabledTypes); } - List<Plugin> loadPlugins(Context context, PluginStorage storage) { + List<Plugin> loadPlugins(Context context, PluginStorage storage, + Set<PluginType<?>> enabledTypes) { String providerJarPath = context .getString(com.android.internal.R.string.config_pluginsProviderJarPath); Slog.d(TAG, "loading plugins from:" + providerJarPath); @@ -129,7 +141,7 @@ public class PluginManager { Class<? extends PluginsProvider> cp = pathClassLoader.loadClass(PROVIDER_IMPL_CLASS) .asSubclass(PluginsProvider.class); PluginsProvider provider = cp.getDeclaredConstructor().newInstance(); - return provider.getPlugins(context, storage); + return provider.getPlugins(context, storage, enabledTypes); } catch (ClassNotFoundException e) { Slog.e(TAG, "loading failed: " + PROVIDER_IMPL_CLASS + " is not found in" + providerJarPath, e); diff --git a/services/core/java/com/android/server/display/plugin/PluginStorage.java b/services/core/java/com/android/server/display/plugin/PluginStorage.java index 5102c2709329..d17fbe21deeb 100644 --- a/services/core/java/com/android/server/display/plugin/PluginStorage.java +++ b/services/core/java/com/android/server/display/plugin/PluginStorage.java @@ -24,6 +24,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.tools.r8.keepanno.annotations.KeepForApi; import java.io.PrintWriter; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -62,6 +63,12 @@ public class PluginStorage { updateValue(type, GLOBAL_ID, value); } + private final Set<PluginType<?>> mEnabledTypes; + + PluginStorage(Set<PluginType<?>> enabledTypes) { + mEnabledTypes = Collections.unmodifiableSet(enabledTypes); + } + /** * Updates value in storage and forwards it to corresponding listeners for specific display. * Should be called by OEM Plugin implementation in order to communicate with Framework @@ -71,6 +78,10 @@ public class PluginStorage { */ @KeepForApi public <T> void updateValue(PluginType<T> type, String uniqueDisplayId, @Nullable T value) { + if (isTypeDisabled(type)) { + Slog.d(TAG, "updateValue ignored for disabled type=" + type.mName); + return; + } Slog.d(TAG, "updateValue, type=" + type.mName + "; value=" + value + "; displayId=" + uniqueDisplayId); Set<PluginManager.PluginChangeListener<T>> localListeners; @@ -119,6 +130,10 @@ public class PluginStorage { */ <T> void addListener(PluginType<T> type, String uniqueDisplayId, PluginManager.PluginChangeListener<T> listener) { + if (isTypeDisabled(type)) { + Slog.d(TAG, "addListener ignored for disabled type=" + type.mName); + return; + } if (GLOBAL_ID.equals(uniqueDisplayId)) { Slog.d(TAG, "addListener ignored for GLOBAL_ID, type=" + type.mName); return; @@ -141,6 +156,10 @@ public class PluginStorage { */ <T> void removeListener(PluginType<T> type, String uniqueDisplayId, PluginManager.PluginChangeListener<T> listener) { + if (isTypeDisabled(type)) { + Slog.d(TAG, "removeListener ignored for disabled type=" + type.mName); + return; + } if (GLOBAL_ID.equals(uniqueDisplayId)) { Slog.d(TAG, "removeListener ignored for GLOBAL_ID, type=" + type.mName); return; @@ -183,6 +202,10 @@ public class PluginStorage { } } + private boolean isTypeDisabled(PluginType<?> type) { + return !mEnabledTypes.contains(type); + } + @GuardedBy("mLock") @SuppressWarnings("unchecked") private <T> ListenersContainer<T> getListenersContainerLocked(PluginType<T> type) { diff --git a/services/core/java/com/android/server/display/plugin/PluginType.java b/services/core/java/com/android/server/display/plugin/PluginType.java index fb60833d259e..e4d2f854ea48 100644 --- a/services/core/java/com/android/server/display/plugin/PluginType.java +++ b/services/core/java/com/android/server/display/plugin/PluginType.java @@ -18,6 +18,7 @@ package com.android.server.display.plugin; import com.android.internal.annotations.Keep; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.plugin.types.HdrBoostOverride; /** * Represent customisation entry point to Framework. OEM and Framework team should define @@ -28,6 +29,15 @@ import com.android.internal.annotations.VisibleForTesting; */ @Keep public class PluginType<T> { + /* + * PluginType for HDR boost override. If set, system will use overridden value instead + * system default parameters. To switch back to default system behaviour, Plugin should set + * this type value to null. + * Value change will trigger whole power state recalculation, so plugins should not update + * value for this type too often. + */ + public static final PluginType<HdrBoostOverride> HDR_BOOST_OVERRIDE = new PluginType<>( + HdrBoostOverride.class, "hdr_boost_override"); final Class<T> mType; final String mName; diff --git a/services/core/java/com/android/server/display/plugin/PluginsProvider.java b/services/core/java/com/android/server/display/plugin/PluginsProvider.java index 9ad85f67bc8b..ec74c860ca58 100644 --- a/services/core/java/com/android/server/display/plugin/PluginsProvider.java +++ b/services/core/java/com/android/server/display/plugin/PluginsProvider.java @@ -22,6 +22,7 @@ import android.content.Context; import com.android.tools.r8.keepanno.annotations.KeepForApi; import java.util.List; +import java.util.Set; /** * Interface that OEMs should implement in order to supply Plugins to PluginManager @@ -32,5 +33,6 @@ public interface PluginsProvider { * Provides list of Plugins to PluginManager */ @NonNull - List<Plugin> getPlugins(Context context, PluginStorage storage); + List<Plugin> getPlugins( + Context context, PluginStorage storage, Set<PluginType<?>> enabledTypes); } diff --git a/services/core/java/com/android/server/display/plugin/types/HdrBoostOverride.java b/services/core/java/com/android/server/display/plugin/types/HdrBoostOverride.java new file mode 100644 index 000000000000..22e024da5f52 --- /dev/null +++ b/services/core/java/com/android/server/display/plugin/types/HdrBoostOverride.java @@ -0,0 +1,59 @@ +/* + * 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.server.display.plugin.types; + +import android.annotation.FloatRange; +import android.os.PowerManager; + +import com.android.server.display.DisplayBrightnessState; + +/** + * HDR boost override value. + * @param sdrHdrRatio - HDR to SDR multiplier, if < 1 HDR boost is off. + * @param maxHdrBrightness - Brightness max when boosted. Value in range from BRIGHTNESS_MIN to + * BRIGHTNESS_MAX. If not used should be set to PowerManager.BRIGHTNESS_MAX + * @param customTransitionRate - Custom transition rate for transitioning to new HDR brightness. + * If not used should be set to + * DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET + */ +public record HdrBoostOverride( + @FloatRange(from = 0) + float sdrHdrRatio, + @FloatRange(from = PowerManager.BRIGHTNESS_MIN, to = PowerManager.BRIGHTNESS_MAX) + float maxHdrBrightness, + float customTransitionRate) { + /** + * Constant for HDR boost off. Plugins should use this constant instead of creating new objects + */ + private static final HdrBoostOverride HDR_OFF = new HdrBoostOverride(0, + PowerManager.BRIGHTNESS_MAX, DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET); + + /** + * Create HdrBoostOverride for HDR boost off + */ + public static HdrBoostOverride forHdrOff() { + return HDR_OFF; + } + + /** + * Create HdrBoostOverride for sdr-hdr ration override + */ + public static HdrBoostOverride forSdrHdrRatio(float sdrHdrRatio) { + return new HdrBoostOverride(sdrHdrRatio, + PowerManager.BRIGHTNESS_MAX, DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET); + } +} diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index 4e5c720f9f1c..d2486fe8bd66 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -20,7 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.graphics.PointF; -import android.hardware.display.DisplayTopology; +import android.hardware.display.DisplayTopologyGraph; import android.hardware.display.DisplayViewport; import android.hardware.input.KeyGestureEvent; import android.os.IBinder; @@ -51,7 +51,7 @@ public abstract class InputManagerInternal { * Called by {@link com.android.server.display.DisplayManagerService} to inform InputManager * about changes in the displays topology. */ - public abstract void setDisplayTopology(DisplayTopology topology); + public abstract void setDisplayTopology(DisplayTopologyGraph topology); /** * Called by the power manager to tell the input manager whether it should start diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 4e03e86dfe36..af021e5f515b 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -52,7 +52,7 @@ import android.hardware.SensorPrivacyManager; import android.hardware.SensorPrivacyManager.Sensors; import android.hardware.SensorPrivacyManagerInternal; import android.hardware.display.DisplayManagerInternal; -import android.hardware.display.DisplayTopology; +import android.hardware.display.DisplayTopologyGraph; import android.hardware.display.DisplayViewport; import android.hardware.input.AidlInputGestureData; import android.hardware.input.HostUsiVersion; @@ -662,8 +662,8 @@ public class InputManagerService extends IInputManager.Stub mNative.setPointerDisplayId(mWindowManagerCallbacks.getPointerDisplayId()); } - private void setDisplayTopologyInternal(DisplayTopology topology) { - mNative.setDisplayTopology(topology.getGraph()); + private void setDisplayTopologyInternal(DisplayTopologyGraph topology) { + mNative.setDisplayTopology(topology); } /** @@ -3533,7 +3533,7 @@ public class InputManagerService extends IInputManager.Stub } @Override - public void setDisplayTopology(DisplayTopology topology) { + public void setDisplayTopology(DisplayTopologyGraph topology) { setDisplayTopologyInternal(topology); } diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java index b8ce86b7c98c..2f228538d978 100644 --- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java +++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java @@ -22,8 +22,6 @@ import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECT import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD; import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEFAULT; -import static com.android.hardware.input.Flags.keyboardLayoutManagerMultiUserImeSetup; - import android.annotation.AnyThread; import android.annotation.MainThread; import android.annotation.NonNull; @@ -68,7 +66,6 @@ import android.util.SparseArray; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.inputmethod.InputMethodInfo; -import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.R; @@ -1081,8 +1078,6 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { List<ImeInfo> imeInfoList = new ArrayList<>(); UserManager userManager = Objects.requireNonNull( mContext.getSystemService(UserManager.class)); - InputMethodManager inputMethodManager = Objects.requireNonNull( - mContext.getSystemService(InputMethodManager.class)); // Need to use InputMethodManagerInternal to call getEnabledInputMethodListAsUser() // instead of using InputMethodManager which uses enforceCallingPermissions() that // breaks when we are calling the method for work profile user ID since it doesn't check @@ -1093,14 +1088,10 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { for (InputMethodInfo imeInfo : inputMethodManagerInternal.getEnabledInputMethodListAsUser( userId)) { - final List<InputMethodSubtype> imeSubtypes; - if (keyboardLayoutManagerMultiUserImeSetup()) { - imeSubtypes = inputMethodManagerInternal.getEnabledInputMethodSubtypeListAsUser( - imeInfo.getId(), true /* allowsImplicitlyEnabledSubtypes */, userId); - } else { - imeSubtypes = inputMethodManager.getEnabledInputMethodSubtypeList(imeInfo, - true /* allowsImplicitlyEnabledSubtypes */); - } + final List<InputMethodSubtype> imeSubtypes = + inputMethodManagerInternal.getEnabledInputMethodSubtypeListAsUser( + imeInfo.getId(), true /* allowsImplicitlyEnabledSubtypes */, + userId); for (InputMethodSubtype imeSubtype : imeSubtypes) { if (!imeSubtype.isSuitableForPhysicalKeyboardLayoutMapping()) { continue; diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java index 3eb38a7029e6..b13dee530ee2 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java @@ -56,8 +56,8 @@ import java.util.stream.Stream; */ /* package */ class SystemMediaRoute2Provider2 extends SystemMediaRoute2Provider { - private static final String ROUTE_ID_PREFIX_SYSTEM = "SYSTEM"; - private static final String ROUTE_ID_SYSTEM_SEPARATOR = "."; + private static final String UNIQUE_SYSTEM_ID_PREFIX = "SYSTEM"; + private static final String UNIQUE_SYSTEM_ID_SEPARATOR = "-"; private final PackageManager mPackageManager; @@ -67,6 +67,10 @@ import java.util.stream.Stream; @GuardedBy("mLock") private final Map<String, ProviderProxyRecord> mProxyRecords = new ArrayMap<>(); + @GuardedBy("mLock") + private final Map<String, SystemMediaSessionRecord> mSessionOriginalIdToSessionRecord = + new ArrayMap<>(); + /** * Maps package names to corresponding sessions maintained by {@link MediaRoute2ProviderService * provider services}. @@ -150,7 +154,7 @@ import java.util.stream.Stream; if (currentProxyRecord != null) { currentProxyRecord.releaseSession( requestId, existingSession.getOriginalId()); - existingSessionRecord.removeSelfFromSessionMap(); + existingSessionRecord.removeSelfFromSessionMaps(); } } } @@ -240,6 +244,24 @@ import java.util.stream.Stream; super.setRouteVolume(requestId, routeOriginalId, volume); } + @Override + public void setSessionVolume(long requestId, String sessionOriginalId, int volume) { + if (SYSTEM_SESSION_ID.equals(sessionOriginalId)) { + super.setSessionVolume(requestId, sessionOriginalId, volume); + return; + } + synchronized (mLock) { + var sessionRecord = mSessionOriginalIdToSessionRecord.get(sessionOriginalId); + var proxyRecord = sessionRecord != null ? sessionRecord.getProxyRecord() : null; + if (proxyRecord != null) { + proxyRecord.mProxy.setSessionVolume( + requestId, sessionRecord.getServiceSessionId(), volume); + return; + } + } + notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE); + } + /** * Returns the uid that corresponds to the given name and user handle, or {@link * Process#INVALID_UID} if a uid couldn't be found. @@ -376,16 +398,17 @@ import java.util.stream.Stream; } /** - * Equivalent to {@link #asSystemRouteId}, except it takes a unique route id instead of a - * original id. + * Equivalent to {@link #asUniqueSystemId}, except it takes a unique id instead of an original + * id. */ private static String uniqueIdAsSystemRouteId(String providerId, String uniqueRouteId) { - return asSystemRouteId(providerId, MediaRouter2Utils.getOriginalId(uniqueRouteId)); + return asUniqueSystemId(providerId, MediaRouter2Utils.getOriginalId(uniqueRouteId)); } /** * Returns a unique {@link MediaRoute2Info#getOriginalId() original id} for this provider to - * publish system media routes from {@link MediaRoute2ProviderService provider services}. + * publish system media routes and sessions from {@link MediaRoute2ProviderService provider + * services}. * * <p>This provider will publish system media routes as part of the system routing session. * However, said routes may also support {@link MediaRoute2Info#FLAG_ROUTING_TYPE_REMOTE remote @@ -393,12 +416,12 @@ import java.util.stream.Stream; * we derive a {@link MediaRoute2Info#getOriginalId original id} that is unique among all * original route ids used by this provider. */ - private static String asSystemRouteId(String providerId, String originalRouteId) { - return ROUTE_ID_PREFIX_SYSTEM - + ROUTE_ID_SYSTEM_SEPARATOR + private static String asUniqueSystemId(String providerId, String originalId) { + return UNIQUE_SYSTEM_ID_PREFIX + + UNIQUE_SYSTEM_ID_SEPARATOR + providerId - + ROUTE_ID_SYSTEM_SEPARATOR - + originalRouteId; + + UNIQUE_SYSTEM_ID_SEPARATOR + + originalId; } /** @@ -485,7 +508,7 @@ import java.util.stream.Stream; continue; } String id = - asSystemRouteId(providerInfo.getUniqueId(), sourceRoute.getOriginalId()); + asUniqueSystemId(providerInfo.getUniqueId(), sourceRoute.getOriginalId()); var newRouteBuilder = new MediaRoute2Info.Builder(id, sourceRoute); if ((sourceRoute.getSupportedRoutingTypes() & MediaRoute2Info.FLAG_ROUTING_TYPE_SYSTEM_AUDIO) @@ -534,6 +557,9 @@ import java.util.stream.Stream; RoutingSessionInfo translatedSession; synchronized (mLock) { mSessionRecord = systemMediaSessionRecord; + mSessionOriginalIdToSessionRecord.put( + systemMediaSessionRecord.mOriginalId, + systemMediaSessionRecord); mPackageNameToSessionRecord.put( mClientPackageName, systemMediaSessionRecord); mPendingSessionCreations.remove(mRequestId); @@ -576,24 +602,38 @@ import java.util.stream.Stream; private final String mProviderId; - @GuardedBy("SystemMediaRoute2Provider2.this.mLock") - @NonNull - private RoutingSessionInfo mSourceSessionInfo; + /** + * The {@link RoutingSessionInfo#getOriginalId() original id} with which this session is + * published. + * + * <p>Derived from the service routing session, using {@link #asUniqueSystemId}. + */ + private final String mOriginalId; + + // @GuardedBy("SystemMediaRoute2Provider2.this.mLock") + @NonNull private RoutingSessionInfo mSourceSessionInfo; /** - * The same as {@link #mSourceSessionInfo}, except ids are {@link #asSystemRouteId system + * The same as {@link #mSourceSessionInfo}, except ids are {@link #asUniqueSystemId system * provider ids}. */ - @NonNull - private RoutingSessionInfo mTranslatedSessionInfo; + // @GuardedBy("SystemMediaRoute2Provider2.this.mLock") + @NonNull private RoutingSessionInfo mTranslatedSessionInfo; SystemMediaSessionRecord( @NonNull String providerId, @NonNull RoutingSessionInfo sessionInfo) { mProviderId = providerId; mSourceSessionInfo = sessionInfo; + mOriginalId = + asUniqueSystemId(sessionInfo.getProviderId(), sessionInfo.getOriginalId()); mTranslatedSessionInfo = asSystemProviderSession(sessionInfo); } + // @GuardedBy("SystemMediaRoute2Provider2.this.mLock") + public String getServiceSessionId() { + return mSourceSessionInfo.getOriginalId(); + } + @Override public void onSessionUpdate(@NonNull RoutingSessionInfo sessionInfo) { RoutingSessionInfo translatedSessionInfo = asSystemProviderSession(sessionInfo); @@ -612,31 +652,32 @@ import java.util.stream.Stream; @Override public void onSessionReleased() { synchronized (mLock) { - removeSelfFromSessionMap(); + removeSelfFromSessionMaps(); } notifyGlobalSessionInfoUpdated(); } - @GuardedBy("SystemMediaRoute2Provider2.this.mLock") + // @GuardedBy("SystemMediaRoute2Provider2.this.mLock") @Nullable public ProviderProxyRecord getProxyRecord() { ProviderProxyRecord provider = mProxyRecords.get(mProviderId); if (provider == null) { // Unexpected condition where the proxy is no longer available while there's an // ongoing session. Could happen due to a crash in the provider process. - removeSelfFromSessionMap(); + removeSelfFromSessionMaps(); } return provider; } - @GuardedBy("SystemMediaRoute2Provider2.this.mLock") - private void removeSelfFromSessionMap() { + // @GuardedBy("SystemMediaRoute2Provider2.this.mLock") + private void removeSelfFromSessionMaps() { + mSessionOriginalIdToSessionRecord.remove(mOriginalId); mPackageNameToSessionRecord.remove(mSourceSessionInfo.getClientPackageName()); } private RoutingSessionInfo asSystemProviderSession(RoutingSessionInfo session) { var builder = - new RoutingSessionInfo.Builder(session) + new RoutingSessionInfo.Builder(session, mOriginalId) .setProviderId(mUniqueId) .setSystemSession(true) .clearSelectedRoutes() diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index d23a8638803b..d00ac4d9cd11 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -29,14 +29,13 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.hardware.tv.mediaquality.AmbientBacklightColorFormat; -import android.hardware.tv.mediaquality.DolbyAudioProcessing; -import android.hardware.tv.mediaquality.DtsVirtualX; import android.hardware.tv.mediaquality.IMediaQuality; import android.hardware.tv.mediaquality.IPictureProfileAdjustmentListener; import android.hardware.tv.mediaquality.IPictureProfileChangedListener; import android.hardware.tv.mediaquality.ISoundProfileAdjustmentListener; import android.hardware.tv.mediaquality.ISoundProfileChangedListener; import android.hardware.tv.mediaquality.ParamCapability; +import android.hardware.tv.mediaquality.ParameterRange; import android.hardware.tv.mediaquality.PictureParameter; import android.hardware.tv.mediaquality.PictureParameters; import android.hardware.tv.mediaquality.SoundParameter; @@ -50,8 +49,6 @@ import android.media.quality.IMediaQualityManager; import android.media.quality.IPictureProfileCallback; import android.media.quality.ISoundProfileCallback; import android.media.quality.MediaQualityContract.BaseParameters; -import android.media.quality.MediaQualityContract.PictureQuality; -import android.media.quality.MediaQualityContract.SoundQuality; import android.media.quality.MediaQualityManager; import android.media.quality.ParameterCapability; import android.media.quality.PictureProfile; @@ -77,21 +74,16 @@ import com.android.internal.annotations.GuardedBy; import com.android.server.SystemService; import com.android.server.utils.Slogf; -import org.json.JSONException; -import org.json.JSONObject; - import java.io.File; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; -import java.util.UUID; import java.util.stream.Collectors; /** @@ -106,7 +98,6 @@ public class MediaQualityService extends SystemService { private static final String PICTURE_PROFILE_PREFERENCE = "picture_profile_preference"; private static final String SOUND_PROFILE_PREFERENCE = "sound_profile_preference"; private static final String COMMA_DELIMITER = ","; - private static final int MAX_UUID_GENERATION_ATTEMPTS = 10; private final Context mContext; private final MediaQualityDbHelper mMediaQualityDbHelper; private final BiMap<Long, String> mPictureProfileTempIdMap; @@ -275,7 +266,7 @@ public class MediaQualityService extends SystemService { synchronized (mPictureProfileLock) { SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); - ContentValues values = getContentValues(null, + ContentValues values = MediaQualityUtils.getContentValues(null, pp.getProfileType(), pp.getName(), pp.getPackageName() == null || pp.getPackageName().isEmpty() @@ -286,7 +277,7 @@ public class MediaQualityService extends SystemService { // id is auto-generated by SQLite upon successful insertion of row Long id = db.insert(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, null, values); - populateTempIdMap(mPictureProfileTempIdMap, id); + MediaQualityUtils.populateTempIdMap(mPictureProfileTempIdMap, id); String value = mPictureProfileTempIdMap.getValue(id); pp.setProfileId(value); notifyOnPictureProfileAdded(value, pp, Binder.getCallingUid(), @@ -308,8 +299,9 @@ public class MediaQualityService extends SystemService { private android.hardware.tv.mediaquality.PictureProfile convertToHalPictureProfile(Long id, PersistableBundle params) { PictureParameters pictureParameters = new PictureParameters(); - pictureParameters.pictureParameters = convertPersistableBundleToPictureParameterList( - params); + pictureParameters.pictureParameters = + MediaQualityUtils.convertPersistableBundleToPictureParameterList( + params); android.hardware.tv.mediaquality.PictureProfile toReturn = new android.hardware.tv.mediaquality.PictureProfile(); @@ -329,7 +321,7 @@ public class MediaQualityService extends SystemService { } synchronized (mPictureProfileLock) { - ContentValues values = getContentValues(dbId, + ContentValues values = MediaQualityUtils.getContentValues(dbId, pp.getProfileType(), pp.getName(), pp.getPackageName(), @@ -405,7 +397,7 @@ public class MediaQualityService extends SystemService { try ( Cursor cursor = getCursorAfterQuerying( mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, - getMediaProfileColumns(includeParams), selection, + MediaQualityUtils.getMediaProfileColumns(includeParams), selection, selectionArguments) ) { int count = cursor.getCount(); @@ -420,7 +412,8 @@ public class MediaQualityService extends SystemService { return null; } cursor.moveToFirst(); - return convertCursorToPictureProfileWithTempId(cursor); + return MediaQualityUtils.convertCursorToPictureProfileWithTempId(cursor, + mPictureProfileTempIdMap); } } } @@ -432,7 +425,8 @@ public class MediaQualityService extends SystemService { try ( Cursor cursor = getCursorAfterQuerying( mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, - getMediaProfileColumns(false), selection, selectionArguments) + MediaQualityUtils.getMediaProfileColumns(false), selection, + selectionArguments) ) { int count = cursor.getCount(); if (count == 0) { @@ -445,7 +439,8 @@ public class MediaQualityService extends SystemService { return null; } cursor.moveToFirst(); - return convertCursorToPictureProfileWithTempId(cursor); + return MediaQualityUtils.convertCursorToPictureProfileWithTempId(cursor, + mPictureProfileTempIdMap); } } @@ -463,7 +458,8 @@ public class MediaQualityService extends SystemService { options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; String[] selectionArguments = {packageName}; - return getPictureProfilesBasedOnConditions(getMediaProfileColumns(includeParams), + return getPictureProfilesBasedOnConditions(MediaQualityUtils + .getMediaProfileColumns(includeParams), selection, selectionArguments); } } @@ -492,8 +488,8 @@ public class MediaQualityService extends SystemService { try { if (mMediaQuality != null) { - PictureParameter[] pictureParameters = - convertPersistableBundleToPictureParameterList(params); + PictureParameter[] pictureParameters = MediaQualityUtils + .convertPersistableBundleToPictureParameterList(params); PictureParameters pp = new PictureParameters(); pp.pictureParameters = pictureParameters; @@ -507,337 +503,6 @@ public class MediaQualityService extends SystemService { return false; } - private PictureParameter[] convertPersistableBundleToPictureParameterList( - PersistableBundle params) { - if (params == null) { - return null; - } - - List<PictureParameter> pictureParams = new ArrayList<>(); - if (params.containsKey(PictureQuality.PARAMETER_BRIGHTNESS)) { - pictureParams.add(PictureParameter.brightness(params.getLong( - PictureQuality.PARAMETER_BRIGHTNESS))); - } - if (params.containsKey(PictureQuality.PARAMETER_CONTRAST)) { - pictureParams.add(PictureParameter.contrast(params.getInt( - PictureQuality.PARAMETER_CONTRAST))); - } - if (params.containsKey(PictureQuality.PARAMETER_SHARPNESS)) { - pictureParams.add(PictureParameter.sharpness(params.getInt( - PictureQuality.PARAMETER_SHARPNESS))); - } - if (params.containsKey(PictureQuality.PARAMETER_SATURATION)) { - pictureParams.add(PictureParameter.saturation(params.getInt( - PictureQuality.PARAMETER_SATURATION))); - } - if (params.containsKey(PictureQuality.PARAMETER_HUE)) { - pictureParams.add(PictureParameter.hue(params.getInt( - PictureQuality.PARAMETER_HUE))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_BRIGHTNESS)) { - pictureParams.add(PictureParameter.colorTunerBrightness(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_BRIGHTNESS))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION)) { - pictureParams.add(PictureParameter.colorTunerSaturation(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_SATURATION))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE)) { - pictureParams.add(PictureParameter.colorTunerHue(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_HUE))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_RED_OFFSET)) { - pictureParams.add(PictureParameter.colorTunerRedOffset(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_RED_OFFSET))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_GREEN_OFFSET)) { - pictureParams.add(PictureParameter.colorTunerGreenOffset(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_GREEN_OFFSET))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_OFFSET)) { - pictureParams.add(PictureParameter.colorTunerBlueOffset(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_BLUE_OFFSET))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN)) { - pictureParams.add(PictureParameter.colorTunerRedGain(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN)) { - pictureParams.add(PictureParameter.colorTunerGreenGain(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN)) { - pictureParams.add(PictureParameter.colorTunerBlueGain(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN))); - } - if (params.containsKey(PictureQuality.PARAMETER_NOISE_REDUCTION)) { - pictureParams.add(PictureParameter.noiseReduction( - (byte) params.getInt(PictureQuality.PARAMETER_NOISE_REDUCTION))); - } - if (params.containsKey(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION)) { - pictureParams.add(PictureParameter.mpegNoiseReduction( - (byte) params.getInt(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION))); - } - if (params.containsKey(PictureQuality.PARAMETER_FLESH_TONE)) { - pictureParams.add(PictureParameter.fleshTone( - (byte) params.getInt(PictureQuality.PARAMETER_FLESH_TONE))); - } - if (params.containsKey(PictureQuality.PARAMETER_DECONTOUR)) { - pictureParams.add(PictureParameter.deContour( - (byte) params.getInt(PictureQuality.PARAMETER_DECONTOUR))); - } - if (params.containsKey(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL)) { - pictureParams.add(PictureParameter.dynamicLumaControl( - (byte) params.getInt(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL))); - } - if (params.containsKey(PictureQuality.PARAMETER_FILM_MODE)) { - pictureParams.add(PictureParameter.filmMode(params.getBoolean( - PictureQuality.PARAMETER_FILM_MODE))); - } - if (params.containsKey(PictureQuality.PARAMETER_BLUE_STRETCH)) { - pictureParams.add(PictureParameter.blueStretch(params.getBoolean( - PictureQuality.PARAMETER_BLUE_STRETCH))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNE)) { - pictureParams.add(PictureParameter.colorTune(params.getBoolean( - PictureQuality.PARAMETER_COLOR_TUNE))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE)) { - pictureParams.add(PictureParameter.colorTemperature( - (byte) params.getInt( - PictureQuality.PARAMETER_COLOR_TEMPERATURE))); - } - if (params.containsKey(PictureQuality.PARAMETER_GLOBAL_DIMMING)) { - pictureParams.add(PictureParameter.globeDimming(params.getBoolean( - PictureQuality.PARAMETER_GLOBAL_DIMMING))); - } - if (params.containsKey(PictureQuality.PARAMETER_AUTO_PICTURE_QUALITY_ENABLED)) { - pictureParams.add(PictureParameter.autoPictureQualityEnabled(params.getBoolean( - PictureQuality.PARAMETER_AUTO_PICTURE_QUALITY_ENABLED))); - } - if (params.containsKey(PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED)) { - pictureParams.add(PictureParameter.autoSuperResolutionEnabled(params.getBoolean( - PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN)) { - pictureParams.add(PictureParameter.colorTemperatureRedGain(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN)) { - pictureParams.add(PictureParameter.colorTemperatureGreenGain(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN)) { - pictureParams.add(PictureParameter.colorTemperatureBlueGain(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN))); - } - if (params.containsKey(PictureQuality.PARAMETER_LEVEL_RANGE)) { - pictureParams.add(PictureParameter.levelRange( - (byte) params.getInt(PictureQuality.PARAMETER_LEVEL_RANGE))); - } - if (params.containsKey(PictureQuality.PARAMETER_GAMUT_MAPPING)) { - pictureParams.add(PictureParameter.gamutMapping(params.getBoolean( - PictureQuality.PARAMETER_GAMUT_MAPPING))); - } - if (params.containsKey(PictureQuality.PARAMETER_PC_MODE)) { - pictureParams.add(PictureParameter.pcMode(params.getBoolean( - PictureQuality.PARAMETER_PC_MODE))); - } - if (params.containsKey(PictureQuality.PARAMETER_LOW_LATENCY)) { - pictureParams.add(PictureParameter.lowLatency(params.getBoolean( - PictureQuality.PARAMETER_LOW_LATENCY))); - } - if (params.containsKey(PictureQuality.PARAMETER_VRR)) { - pictureParams.add(PictureParameter.vrr(params.getBoolean( - PictureQuality.PARAMETER_VRR))); - } - if (params.containsKey(PictureQuality.PARAMETER_CVRR)) { - pictureParams.add(PictureParameter.cvrr(params.getBoolean( - PictureQuality.PARAMETER_CVRR))); - } - if (params.containsKey(PictureQuality.PARAMETER_HDMI_RGB_RANGE)) { - pictureParams.add(PictureParameter.hdmiRgbRange( - (byte) params.getInt(PictureQuality.PARAMETER_HDMI_RGB_RANGE))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_SPACE)) { - pictureParams.add(PictureParameter.colorSpace( - (byte) params.getInt(PictureQuality.PARAMETER_COLOR_SPACE))); - } - if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS)) { - pictureParams.add(PictureParameter.panelInitMaxLuminceNits( - params.getInt(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS))); - } - if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID)) { - pictureParams.add(PictureParameter.panelInitMaxLuminceValid( - params.getBoolean(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID))); - } - if (params.containsKey(PictureQuality.PARAMETER_GAMMA)) { - pictureParams.add(PictureParameter.gamma( - (byte) params.getInt(PictureQuality.PARAMETER_GAMMA))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET)) { - pictureParams.add(PictureParameter.colorTemperatureRedOffset(params.getInt( - PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET)) { - pictureParams.add(PictureParameter.colorTemperatureGreenOffset(params.getInt( - PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET)) { - pictureParams.add(PictureParameter.colorTemperatureBlueOffset(params.getInt( - PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET))); - } - if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_RED)) { - pictureParams.add(PictureParameter.elevenPointRed(params.getIntArray( - PictureQuality.PARAMETER_ELEVEN_POINT_RED))); - } - if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_GREEN)) { - pictureParams.add(PictureParameter.elevenPointGreen(params.getIntArray( - PictureQuality.PARAMETER_ELEVEN_POINT_GREEN))); - } - if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_BLUE)) { - pictureParams.add(PictureParameter.elevenPointBlue(params.getIntArray( - PictureQuality.PARAMETER_ELEVEN_POINT_BLUE))); - } - if (params.containsKey(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)) { - pictureParams.add(PictureParameter.lowBlueLight( - (byte) params.getInt(PictureQuality.PARAMETER_LOW_BLUE_LIGHT))); - } - if (params.containsKey(PictureQuality.PARAMETER_LD_MODE)) { - pictureParams.add(PictureParameter.LdMode( - (byte) params.getInt(PictureQuality.PARAMETER_LD_MODE))); - } - if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_GAIN)) { - pictureParams.add(PictureParameter.osdRedGain(params.getInt( - PictureQuality.PARAMETER_OSD_RED_GAIN))); - } - if (params.containsKey(PictureQuality.PARAMETER_OSD_GREEN_GAIN)) { - pictureParams.add(PictureParameter.osdGreenGain(params.getInt( - PictureQuality.PARAMETER_OSD_GREEN_GAIN))); - } - if (params.containsKey(PictureQuality.PARAMETER_OSD_BLUE_GAIN)) { - pictureParams.add(PictureParameter.osdBlueGain(params.getInt( - PictureQuality.PARAMETER_OSD_BLUE_GAIN))); - } - if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_OFFSET)) { - pictureParams.add(PictureParameter.osdRedOffset(params.getInt( - PictureQuality.PARAMETER_OSD_RED_OFFSET))); - } - if (params.containsKey(PictureQuality.PARAMETER_OSD_GREEN_OFFSET)) { - pictureParams.add(PictureParameter.osdGreenOffset(params.getInt( - PictureQuality.PARAMETER_OSD_GREEN_OFFSET))); - } - if (params.containsKey(PictureQuality.PARAMETER_OSD_BLUE_OFFSET)) { - pictureParams.add(PictureParameter.osdBlueOffset(params.getInt( - PictureQuality.PARAMETER_OSD_BLUE_OFFSET))); - } - if (params.containsKey(PictureQuality.PARAMETER_OSD_HUE)) { - pictureParams.add(PictureParameter.osdHue(params.getInt( - PictureQuality.PARAMETER_OSD_HUE))); - } - if (params.containsKey(PictureQuality.PARAMETER_OSD_SATURATION)) { - pictureParams.add(PictureParameter.osdSaturation(params.getInt( - PictureQuality.PARAMETER_OSD_SATURATION))); - } - if (params.containsKey(PictureQuality.PARAMETER_OSD_CONTRAST)) { - pictureParams.add(PictureParameter.osdContrast(params.getInt( - PictureQuality.PARAMETER_OSD_CONTRAST))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SWITCH)) { - pictureParams.add(PictureParameter.colorTunerSwitch(params.getBoolean( - PictureQuality.PARAMETER_COLOR_TUNER_SWITCH))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_RED)) { - pictureParams.add(PictureParameter.colorTunerHueRed(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_HUE_RED))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_GREEN)) { - pictureParams.add(PictureParameter.colorTunerHueGreen(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_HUE_GREEN))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_BLUE)) { - pictureParams.add(PictureParameter.colorTunerHueBlue(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_HUE_BLUE))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_CYAN)) { - pictureParams.add(PictureParameter.colorTunerHueCyan(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_HUE_CYAN))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_MAGENTA)) { - pictureParams.add(PictureParameter.colorTunerHueMagenta(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_HUE_MAGENTA))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_YELLOW)) { - pictureParams.add(PictureParameter.colorTunerHueYellow(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_HUE_YELLOW))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_FLESH)) { - pictureParams.add(PictureParameter.colorTunerHueFlesh(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_HUE_FLESH))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_RED)) { - pictureParams.add(PictureParameter.colorTunerSaturationRed(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_RED))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_GREEN)) { - pictureParams.add(PictureParameter.colorTunerSaturationGreen(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_GREEN))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_BLUE)) { - pictureParams.add(PictureParameter.colorTunerSaturationBlue(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_BLUE))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_CYAN)) { - pictureParams.add(PictureParameter.colorTunerSaturationCyan(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_CYAN))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_MAGENTA)) { - pictureParams.add(PictureParameter.colorTunerSaturationMagenta(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_MAGENTA))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_YELLOW)) { - pictureParams.add(PictureParameter.colorTunerSaturationYellow(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_YELLOW))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_FLESH)) { - pictureParams.add(PictureParameter.colorTunerSaturationFlesh(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_FLESH))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_RED)) { - pictureParams.add(PictureParameter.colorTunerLuminanceRed(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_RED))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_GREEN)) { - pictureParams.add(PictureParameter.colorTunerLuminanceGreen(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_GREEN))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_BLUE)) { - pictureParams.add(PictureParameter.colorTunerLuminanceBlue(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_BLUE))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_CYAN)) { - pictureParams.add(PictureParameter.colorTunerLuminanceCyan(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_CYAN))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA)) { - pictureParams.add(PictureParameter.colorTunerLuminanceMagenta(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW)) { - pictureParams.add(PictureParameter.colorTunerLuminanceYellow(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW))); - } - if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH)) { - pictureParams.add(PictureParameter.colorTunerLuminanceFlesh(params.getInt( - PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH))); - } - if (params.containsKey(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)) { - pictureParams.add(PictureParameter.pictureQualityEventType( - (byte) params.getInt(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE))); - } - return (PictureParameter[]) pictureParams.toArray(); - } - @GuardedBy("mPictureProfileLock") @Override public List<String> getPictureProfilePackageNames(UserHandle user) { @@ -903,7 +568,7 @@ public class MediaQualityService extends SystemService { synchronized (mSoundProfileLock) { SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); - ContentValues values = getContentValues(null, + ContentValues values = MediaQualityUtils.getContentValues(null, sp.getProfileType(), sp.getName(), sp.getPackageName() == null || sp.getPackageName().isEmpty() @@ -914,7 +579,7 @@ public class MediaQualityService extends SystemService { // id is auto-generated by SQLite upon successful insertion of row Long id = db.insert(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, null, values); - populateTempIdMap(mSoundProfileTempIdMap, id); + MediaQualityUtils.populateTempIdMap(mSoundProfileTempIdMap, id); String value = mSoundProfileTempIdMap.getValue(id); sp.setProfileId(value); notifyOnSoundProfileAdded(value, sp, Binder.getCallingUid(), @@ -935,7 +600,8 @@ public class MediaQualityService extends SystemService { private android.hardware.tv.mediaquality.SoundProfile convertToHalSoundProfile(Long id, PersistableBundle params) { SoundParameters soundParameters = new SoundParameters(); - soundParameters.soundParameters = convertPersistableBundleToSoundParameterList(params); + soundParameters.soundParameters = + MediaQualityUtils.convertPersistableBundleToSoundParameterList(params); android.hardware.tv.mediaquality.SoundProfile toReturn = new android.hardware.tv.mediaquality.SoundProfile(); @@ -955,18 +621,19 @@ public class MediaQualityService extends SystemService { } synchronized (mSoundProfileLock) { - ContentValues values = getContentValues(dbId, + ContentValues values = MediaQualityUtils.getContentValues(dbId, sp.getProfileType(), sp.getName(), sp.getPackageName(), sp.getInputId(), sp.getParameters()); - SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); - db.replace(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, null, values); - notifyOnSoundProfileUpdated(mSoundProfileTempIdMap.getValue(dbId), - getSoundProfile(dbId), Binder.getCallingUid(), Binder.getCallingPid()); - notifyHalOnSoundProfileChange(dbId, sp.getParameters()); + SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); + db.replace(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, + null, values); + notifyOnSoundProfileUpdated(mSoundProfileTempIdMap.getValue(dbId), + getSoundProfile(dbId), Binder.getCallingUid(), Binder.getCallingPid()); + notifyHalOnSoundProfileChange(dbId, sp.getParameters()); } } @@ -1029,7 +696,7 @@ public class MediaQualityService extends SystemService { try ( Cursor cursor = getCursorAfterQuerying( mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, - getMediaProfileColumns(includeParams), selection, + MediaQualityUtils.getMediaProfileColumns(includeParams), selection, selectionArguments) ) { int count = cursor.getCount(); @@ -1044,7 +711,8 @@ public class MediaQualityService extends SystemService { return null; } cursor.moveToFirst(); - return convertCursorToSoundProfileWithTempId(cursor); + return MediaQualityUtils.convertCursorToSoundProfileWithTempId(cursor, + mSoundProfileTempIdMap); } } } @@ -1056,7 +724,8 @@ public class MediaQualityService extends SystemService { try ( Cursor cursor = getCursorAfterQuerying( mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, - getMediaProfileColumns(false), selection, selectionArguments) + MediaQualityUtils.getMediaProfileColumns(false), selection, + selectionArguments) ) { int count = cursor.getCount(); if (count == 0) { @@ -1069,7 +738,8 @@ public class MediaQualityService extends SystemService { return null; } cursor.moveToFirst(); - return convertCursorToSoundProfileWithTempId(cursor); + return MediaQualityUtils.convertCursorToSoundProfileWithTempId( + cursor, mSoundProfileTempIdMap); } } @@ -1087,7 +757,8 @@ public class MediaQualityService extends SystemService { options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; String[] selectionArguments = {packageName}; - return getSoundProfilesBasedOnConditions(getMediaProfileColumns(includeParams), + return getSoundProfilesBasedOnConditions(MediaQualityUtils + .getMediaProfileColumns(includeParams), selection, selectionArguments); } } @@ -1116,7 +787,7 @@ public class MediaQualityService extends SystemService { try { if (mMediaQuality != null) { SoundParameter[] soundParameters = - convertPersistableBundleToSoundParameterList(params); + MediaQualityUtils.convertPersistableBundleToSoundParameterList(params); SoundParameters sp = new SoundParameters(); sp.soundParameters = soundParameters; @@ -1130,95 +801,6 @@ public class MediaQualityService extends SystemService { return false; } - private SoundParameter[] convertPersistableBundleToSoundParameterList( - PersistableBundle params) { - //TODO: set EqualizerDetail - if (params == null) { - return null; - } - List<SoundParameter> soundParams = new ArrayList<>(); - if (params.containsKey(SoundQuality.PARAMETER_BALANCE)) { - soundParams.add(SoundParameter.balance(params.getInt( - SoundQuality.PARAMETER_BALANCE))); - } - if (params.containsKey(SoundQuality.PARAMETER_BASS)) { - soundParams.add(SoundParameter.bass(params.getInt(SoundQuality.PARAMETER_BASS))); - } - if (params.containsKey(SoundQuality.PARAMETER_TREBLE)) { - soundParams.add(SoundParameter.treble(params.getInt( - SoundQuality.PARAMETER_TREBLE))); - } - if (params.containsKey(SoundQuality.PARAMETER_SURROUND_SOUND)) { - soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean( - SoundQuality.PARAMETER_SURROUND_SOUND))); - } - if (params.containsKey(SoundQuality.PARAMETER_SPEAKERS)) { - soundParams.add(SoundParameter.speakersEnabled(params.getBoolean( - SoundQuality.PARAMETER_SPEAKERS))); - } - if (params.containsKey(SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS)) { - soundParams.add(SoundParameter.speakersDelayMs(params.getInt( - SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS))); - } - if (params.containsKey(SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL)) { - soundParams.add(SoundParameter.autoVolumeControl(params.getBoolean( - SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL))); - } - if (params.containsKey(SoundQuality.PARAMETER_DTS_DRC)) { - soundParams.add(SoundParameter.dtsDrc(params.getBoolean( - SoundQuality.PARAMETER_DTS_DRC))); - } - if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS)) { - soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean( - SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS))); - } - if (params.containsKey(SoundQuality.PARAMETER_EARC)) { - soundParams.add(SoundParameter.enhancedAudioReturnChannelEnabled(params.getBoolean( - SoundQuality.PARAMETER_EARC))); - } - if (params.containsKey(SoundQuality.PARAMETER_DOWN_MIX_MODE)) { - soundParams.add(SoundParameter.downmixMode((byte) params.getInt( - SoundQuality.PARAMETER_DOWN_MIX_MODE))); - } - if (params.containsKey(SoundQuality.PARAMETER_SOUND_STYLE)) { - soundParams.add(SoundParameter.soundStyle((byte) params.getInt( - SoundQuality.PARAMETER_SOUND_STYLE))); - } - if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE)) { - soundParams.add(SoundParameter.digitalOutput((byte) params.getInt( - SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE))); - } - if (params.containsKey(SoundQuality.PARAMETER_DIALOGUE_ENHANCER)) { - soundParams.add(SoundParameter.dolbyDialogueEnhancer((byte) params.getInt( - SoundQuality.PARAMETER_DIALOGUE_ENHANCER))); - } - - DolbyAudioProcessing dab = new DolbyAudioProcessing(); - dab.soundMode = - (byte) params.getInt(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE); - dab.volumeLeveler = - params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER); - dab.surroundVirtualizer = params.getBoolean( - SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER); - dab.dolbyAtmos = - params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS); - soundParams.add(SoundParameter.dolbyAudioProcessing(dab)); - - DtsVirtualX dts = new DtsVirtualX(); - dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX); - dts.limiter = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER); - dts.truSurroundX = params.getBoolean( - SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X); - dts.truVolumeHd = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD); - dts.dialogClarity = params.getBoolean( - SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY); - dts.definition = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION); - dts.height = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT); - soundParams.add(SoundParameter.dtsVirtualX(dts)); - - return (SoundParameter[]) soundParams.toArray(); - } - @GuardedBy("mSoundProfileLock") @Override public List<String> getSoundProfilePackageNames(UserHandle user) { @@ -1269,169 +851,6 @@ public class MediaQualityService extends SystemService { mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED; } - private void populateTempIdMap(BiMap<Long, String> map, Long id) { - if (id != null && map.getValue(id) == null) { - String uuid; - int attempts = 0; - while (attempts < MAX_UUID_GENERATION_ATTEMPTS) { - uuid = UUID.randomUUID().toString(); - if (map.getKey(uuid) == null) { - map.put(id, uuid); - return; - } - attempts++; - } - } - } - - private String persistableBundleToJson(PersistableBundle bundle) { - JSONObject json = new JSONObject(); - for (String key : bundle.keySet()) { - Object value = bundle.get(key); - try { - if (value instanceof String) { - json.put(key, bundle.getString(key)); - } else if (value instanceof Integer) { - json.put(key, bundle.getInt(key)); - } else if (value instanceof Long) { - json.put(key, bundle.getLong(key)); - } else if (value instanceof Boolean) { - json.put(key, bundle.getBoolean(key)); - } else if (value instanceof Double) { - json.put(key, bundle.getDouble(key)); - } - } catch (JSONException e) { - Log.e(TAG, "Unable to serialize ", e); - } - } - return json.toString(); - } - - private PersistableBundle jsonToPersistableBundle(String jsonString) { - PersistableBundle bundle = new PersistableBundle(); - if (jsonString != null) { - JSONObject jsonObject = null; - try { - jsonObject = new JSONObject(jsonString); - - Iterator<String> keys = jsonObject.keys(); - while (keys.hasNext()) { - String key = keys.next(); - Object value = jsonObject.get(key); - - if (value instanceof String) { - bundle.putString(key, (String) value); - } else if (value instanceof Integer) { - bundle.putInt(key, (Integer) value); - } else if (value instanceof Boolean) { - bundle.putBoolean(key, (Boolean) value); - } else if (value instanceof Double) { - bundle.putDouble(key, (Double) value); - } else if (value instanceof Long) { - bundle.putLong(key, (Long) value); - } - } - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - return bundle; - } - - private ContentValues getContentValues(Long dbId, Integer profileType, String name, - String packageName, String inputId, PersistableBundle params) { - ContentValues values = new ContentValues(); - if (dbId != null) { - values.put(BaseParameters.PARAMETER_ID, dbId); - } - if (profileType != null) { - values.put(BaseParameters.PARAMETER_TYPE, profileType); - } - if (name != null) { - values.put(BaseParameters.PARAMETER_NAME, name); - } - if (packageName != null) { - values.put(BaseParameters.PARAMETER_PACKAGE, packageName); - } - if (inputId != null) { - values.put(BaseParameters.PARAMETER_INPUT_ID, inputId); - } - if (params != null) { - values.put(mMediaQualityDbHelper.SETTINGS, persistableBundleToJson(params)); - } - return values; - } - - private String[] getMediaProfileColumns(boolean includeParams) { - ArrayList<String> columns = new ArrayList<>(Arrays.asList( - BaseParameters.PARAMETER_ID, - BaseParameters.PARAMETER_TYPE, - BaseParameters.PARAMETER_NAME, - BaseParameters.PARAMETER_INPUT_ID, - BaseParameters.PARAMETER_PACKAGE) - ); - if (includeParams) { - columns.add(mMediaQualityDbHelper.SETTINGS); - } - return columns.toArray(new String[0]); - } - - private PictureProfile convertCursorToPictureProfileWithTempId(Cursor cursor) { - return new PictureProfile( - getTempId(mPictureProfileTempIdMap, cursor), - getType(cursor), - getName(cursor), - getInputId(cursor), - getPackageName(cursor), - jsonToPersistableBundle(getSettingsString(cursor)), - PictureProfileHandle.NONE - ); - } - - private SoundProfile convertCursorToSoundProfileWithTempId(Cursor cursor) { - return new SoundProfile( - getTempId(mSoundProfileTempIdMap, cursor), - getType(cursor), - getName(cursor), - getInputId(cursor), - getPackageName(cursor), - jsonToPersistableBundle(getSettingsString(cursor)), - SoundProfileHandle.NONE - ); - } - - private String getTempId(BiMap<Long, String> map, Cursor cursor) { - int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_ID); - Long dbId = colIndex != -1 ? cursor.getLong(colIndex) : null; - populateTempIdMap(map, dbId); - return map.getValue(dbId); - } - - private int getType(Cursor cursor) { - int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_TYPE); - return colIndex != -1 ? cursor.getInt(colIndex) : 0; - } - - private String getName(Cursor cursor) { - int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_NAME); - return colIndex != -1 ? cursor.getString(colIndex) : null; - } - - private String getInputId(Cursor cursor) { - int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_INPUT_ID); - return colIndex != -1 ? cursor.getString(colIndex) : null; - } - - private String getPackageName(Cursor cursor) { - int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_PACKAGE); - return colIndex != -1 ? cursor.getString(colIndex) : null; - } - - private String getSettingsString(Cursor cursor) { - int colIndex = cursor.getColumnIndex(mMediaQualityDbHelper.SETTINGS); - return colIndex != -1 ? cursor.getString(colIndex) : null; - } - private Cursor getCursorAfterQuerying(String table, String[] columns, String selection, String[] selectionArgs) { SQLiteDatabase db = mMediaQualityDbHelper.getReadableDatabase(); @@ -1448,7 +867,8 @@ public class MediaQualityService extends SystemService { ) { List<PictureProfile> pictureProfiles = new ArrayList<>(); while (cursor.moveToNext()) { - pictureProfiles.add(convertCursorToPictureProfileWithTempId(cursor)); + pictureProfiles.add(MediaQualityUtils.convertCursorToPictureProfileWithTempId( + cursor, mPictureProfileTempIdMap)); } return pictureProfiles; } @@ -1463,7 +883,8 @@ public class MediaQualityService extends SystemService { ) { List<SoundProfile> soundProfiles = new ArrayList<>(); while (cursor.moveToNext()) { - soundProfiles.add(convertCursorToSoundProfileWithTempId(cursor)); + soundProfiles.add(MediaQualityUtils.convertCursorToSoundProfileWithTempId( + cursor, mSoundProfileTempIdMap)); } return soundProfiles; } @@ -1713,7 +1134,39 @@ public class MediaQualityService extends SystemService { @Override public List<ParameterCapability> getParameterCapabilities( List<String> names, UserHandle user) { - return new ArrayList<>(); + byte[] byteArray = MediaQualityUtils.convertParameterToByteArray(names); + ParamCapability[] caps = new ParamCapability[byteArray.length]; + try { + mMediaQuality.getParamCaps(byteArray, caps); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get parameter capabilities", e); + } + + return getListParameterCapability(caps); + } + + private List<ParameterCapability> getListParameterCapability(ParamCapability[] caps) { + List<ParameterCapability> pcList = new ArrayList<>(); + for (ParamCapability pcHal : caps) { + String name = MediaQualityUtils.getParameterName(pcHal.name); + boolean isSupported = pcHal.isSupported; + int type = pcHal.defaultValue == null ? 0 : pcHal.defaultValue.getTag() + 1; + Bundle bundle = convertToCaps(pcHal.range); + + pcList.add(new ParameterCapability(name, isSupported, type, bundle)); + } + return pcList; + } + + private Bundle convertToCaps(ParameterRange range) { + Bundle bundle = new Bundle(); + bundle.putObject("INT_MIN_MAX", range.numRange.getIntMinMax()); + bundle.putObject("INT_VALUES_SUPPORTED", range.numRange.getIntValuesSupported()); + bundle.putObject("DOUBLE_MIN_MAX", range.numRange.getDoubleMinMax()); + bundle.putObject("DOUBLE_VALUES_SUPPORTED", range.numRange.getDoubleValuesSupported()); + bundle.putObject("LONG_MIN_MAX", range.numRange.getLongMinMax()); + bundle.putObject("LONG_VALUES_SUPPORTED", range.numRange.getLongValuesSupported()); + return bundle; } @GuardedBy("mPictureProfileLock") diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java new file mode 100644 index 000000000000..5bd4420e9944 --- /dev/null +++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java @@ -0,0 +1,1543 @@ +/* + * 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.server.media.quality; + +import android.content.ContentValues; +import android.database.Cursor; +import android.hardware.tv.mediaquality.DolbyAudioProcessing; +import android.hardware.tv.mediaquality.DtsVirtualX; +import android.hardware.tv.mediaquality.ParameterName; +import android.hardware.tv.mediaquality.PictureParameter; +import android.hardware.tv.mediaquality.SoundParameter; +import android.media.quality.MediaQualityContract.BaseParameters; +import android.media.quality.MediaQualityContract.PictureQuality; +import android.media.quality.MediaQualityContract.SoundQuality; +import android.media.quality.PictureProfile; +import android.media.quality.PictureProfileHandle; +import android.media.quality.SoundProfile; +import android.media.quality.SoundProfileHandle; +import android.os.PersistableBundle; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Utility class for media quality framework. + * + * @hide + */ +public final class MediaQualityUtils { + + private static final int MAX_UUID_GENERATION_ATTEMPTS = 10; + private static final String TAG = "MediaQualityUtils"; + public static final String SETTINGS = "settings"; + + /** + * Convert PictureParameter List to PersistableBundle. + */ + public static PersistableBundle convertPictureParameterListToPersistableBundle( + PictureParameter[] parameters) { + PersistableBundle bundle = new PersistableBundle(); + for (PictureParameter pp : parameters) { + if (pp.getBrightness() > -1) { + bundle.putLong(PictureQuality.PARAMETER_BRIGHTNESS, (long) pp.getBrightness()); + } + if (pp.getContrast() > -1) { + bundle.putInt(PictureQuality.PARAMETER_CONTRAST, pp.getContrast()); + } + if (pp.getSharpness() > -1) { + bundle.putInt(PictureQuality.PARAMETER_SHARPNESS, pp.getSharpness()); + } + if (pp.getSaturation() > -1) { + bundle.putInt(PictureQuality.PARAMETER_SATURATION, pp.getSaturation()); + } + if (pp.getHue() > -1) { + bundle.putInt(PictureQuality.PARAMETER_HUE, pp.getHue()); + } + if (pp.getColorTunerBrightness() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_BRIGHTNESS, + pp.getColorTunerBrightness()); + } + if (pp.getColorTunerSaturation() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION, + pp.getColorTunerSaturation()); + } + if (pp.getColorTunerHue() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_HUE, pp.getColorTunerHue()); + } + if (pp.getColorTunerRedOffset() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_RED_OFFSET, + pp.getColorTunerRedOffset()); + } + if (pp.getColorTunerGreenOffset() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_GREEN_OFFSET, + pp.getColorTunerGreenOffset()); + } + if (pp.getColorTunerBlueOffset() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_OFFSET, + pp.getColorTunerBlueOffset()); + } + if (pp.getColorTunerRedGain() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN, + pp.getColorTunerRedGain()); + } + if (pp.getColorTunerGreenGain() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN, + pp.getColorTunerGreenGain()); + } + if (pp.getColorTunerBlueGain() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN, + pp.getColorTunerBlueGain()); + } + if (pp.getNoiseReduction() > -1) { + bundle.putInt(PictureQuality.PARAMETER_NOISE_REDUCTION, + pp.getNoiseReduction()); + } + if (pp.getMpegNoiseReduction() > -1) { + bundle.putInt(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION, + pp.getMpegNoiseReduction()); + } + if (pp.getFleshTone() > -1) { + bundle.putInt(PictureQuality.PARAMETER_FLESH_TONE, pp.getFleshTone()); + } + if (pp.getDeContour() > -1) { + bundle.putInt(PictureQuality.PARAMETER_DECONTOUR, pp.getDeContour()); + } + if (pp.getDynamicLumaControl() > -1) { + bundle.putInt(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL, + pp.getDynamicLumaControl()); + } + if (pp.getColorTemperature() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TEMPERATURE, + pp.getColorTemperature()); + } + if (pp.getColorTemperatureRedGain() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN, + pp.getColorTemperatureRedGain()); + } + if (pp.getColorTemperatureGreenGain() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN, + pp.getColorTemperatureGreenGain()); + } + if (pp.getColorTemperatureBlueGain() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN, + pp.getColorTemperatureBlueGain()); + } + if (pp.getLevelRange() > -1) { + bundle.putInt(PictureQuality.PARAMETER_LEVEL_RANGE, pp.getLevelRange()); + } + if (pp.getHdmiRgbRange() > -1) { + bundle.putInt(PictureQuality.PARAMETER_HDMI_RGB_RANGE, pp.getHdmiRgbRange()); + } + if (pp.getColorSpace() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_SPACE, pp.getColorSpace()); + } + if (pp.getPanelInitMaxLuminceNits() > -1) { + bundle.putInt(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS, + pp.getPanelInitMaxLuminceNits()); + } + if (pp.getGamma() > -1) { + bundle.putInt(PictureQuality.PARAMETER_GAMMA, pp.getGamma()); + } + if (pp.getColorTemperatureRedOffset() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET, + pp.getColorTemperatureRedOffset()); + } + if (pp.getColorTemperatureGreenOffset() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET, + pp.getColorTemperatureGreenOffset()); + } + if (pp.getColorTemperatureBlueOffset() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET, + pp.getColorTemperatureBlueOffset()); + } + if (pp.getLowBlueLight() > -1) { + bundle.putInt(PictureQuality.PARAMETER_LOW_BLUE_LIGHT, pp.getLowBlueLight()); + } + if (pp.getLdMode() > -1) { + bundle.putInt(PictureQuality.PARAMETER_LD_MODE, pp.getLdMode()); + } + if (pp.getOsdRedGain() > -1) { + bundle.putInt(PictureQuality.PARAMETER_OSD_RED_GAIN, pp.getOsdRedGain()); + } + if (pp.getOsdGreenGain() > -1) { + bundle.putInt(PictureQuality.PARAMETER_OSD_GREEN_GAIN, pp.getOsdGreenGain()); + } + if (pp.getOsdBlueGain() > -1) { + bundle.putInt(PictureQuality.PARAMETER_OSD_BLUE_GAIN, pp.getOsdBlueGain()); + } + if (pp.getOsdRedOffset() > -1) { + bundle.putInt(PictureQuality.PARAMETER_OSD_RED_OFFSET, pp.getOsdRedOffset()); + } + if (pp.getOsdGreenOffset() > -1) { + bundle.putInt(PictureQuality.PARAMETER_OSD_GREEN_OFFSET, + pp.getOsdGreenOffset()); + } + if (pp.getOsdBlueOffset() > -1) { + bundle.putInt(PictureQuality.PARAMETER_OSD_BLUE_OFFSET, pp.getOsdBlueOffset()); + } + if (pp.getOsdHue() > -1) { + bundle.putInt(PictureQuality.PARAMETER_OSD_HUE, pp.getOsdHue()); + } + if (pp.getOsdSaturation() > -1) { + bundle.putInt(PictureQuality.PARAMETER_OSD_SATURATION, pp.getOsdSaturation()); + } + if (pp.getOsdContrast() > -1) { + bundle.putInt(PictureQuality.PARAMETER_OSD_CONTRAST, pp.getOsdContrast()); + } + if (pp.getColorTunerHueRed() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_HUE_RED, + pp.getColorTunerHueRed()); + } + if (pp.getColorTunerHueGreen() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_HUE_GREEN, + pp.getColorTunerHueGreen()); + } + if (pp.getColorTunerHueBlue() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_HUE_BLUE, + pp.getColorTunerHueBlue()); + } + if (pp.getColorTunerHueCyan() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_HUE_CYAN, + pp.getColorTunerHueCyan()); + } + if (pp.getColorTunerHueMagenta() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_HUE_MAGENTA, + pp.getColorTunerHueMagenta()); + } + if (pp.getColorTunerHueYellow() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_HUE_YELLOW, + pp.getColorTunerHueYellow()); + } + if (pp.getColorTunerHueFlesh() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_HUE_FLESH, + pp.getColorTunerHueFlesh()); + } + if (pp.getColorTunerSaturationRed() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_RED, + pp.getColorTunerSaturationRed()); + } + if (pp.getColorTunerSaturationGreen() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_GREEN, + pp.getColorTunerSaturationGreen()); + } + if (pp.getColorTunerSaturationBlue() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_BLUE, + pp.getColorTunerSaturationBlue()); + } + if (pp.getColorTunerSaturationCyan() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_CYAN, + pp.getColorTunerSaturationCyan()); + } + if (pp.getColorTunerSaturationMagenta() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_MAGENTA, + pp.getColorTunerSaturationMagenta()); + } + if (pp.getColorTunerSaturationYellow() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_YELLOW, + pp.getColorTunerSaturationYellow()); + } + if (pp.getColorTunerSaturationFlesh() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_FLESH, + pp.getColorTunerSaturationFlesh()); + } + if (pp.getColorTunerLuminanceRed() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_RED, + pp.getColorTunerLuminanceRed()); + } + if (pp.getColorTunerLuminanceGreen() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_GREEN, + pp.getColorTunerLuminanceGreen()); + } + if (pp.getColorTunerLuminanceBlue() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_BLUE, + pp.getColorTunerLuminanceBlue()); + } + if (pp.getColorTunerLuminanceCyan() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_CYAN, + pp.getColorTunerLuminanceCyan()); + } + if (pp.getColorTunerLuminanceMagenta() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA, + pp.getColorTunerLuminanceMagenta()); + } + if (pp.getColorTunerLuminanceYellow() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW, + pp.getColorTunerLuminanceYellow()); + } + if (pp.getColorTunerLuminanceFlesh() > -1) { + bundle.putInt(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH, + pp.getColorTunerLuminanceFlesh()); + } + if (pp.getPictureQualityEventType() > -1) { + bundle.putInt(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE, + pp.getPictureQualityEventType()); + } + if (pp.getFilmMode()) { + bundle.putBoolean(PictureQuality.PARAMETER_FILM_MODE, true); + } + if (pp.getBlueStretch()) { + bundle.putBoolean(PictureQuality.PARAMETER_BLUE_STRETCH, true); + } + if (pp.getColorTune()) { + bundle.putBoolean(PictureQuality.PARAMETER_COLOR_TUNE, true); + } + if (pp.getGlobeDimming()) { + bundle.putBoolean(PictureQuality.PARAMETER_GLOBAL_DIMMING, true); + } + if (pp.getAutoPictureQualityEnabled()) { + bundle.putBoolean(PictureQuality.PARAMETER_AUTO_PICTURE_QUALITY_ENABLED, true); + } + if (pp.getAutoSuperResolutionEnabled()) { + bundle.putBoolean(PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED, true); + } + if (pp.getGamutMapping()) { + bundle.putBoolean(PictureQuality.PARAMETER_GAMUT_MAPPING, true); + } + if (pp.getPcMode()) { + bundle.putBoolean(PictureQuality.PARAMETER_PC_MODE, true); + } + if (pp.getLowLatency()) { + bundle.putBoolean(PictureQuality.PARAMETER_LOW_LATENCY, true); + } + if (pp.getVrr()) { + bundle.putBoolean(PictureQuality.PARAMETER_VRR, true); + } + if (pp.getCvrr()) { + bundle.putBoolean(PictureQuality.PARAMETER_CVRR, true); + } + if (pp.getPanelInitMaxLuminceValid()) { + bundle.putBoolean(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID, true); + } + if (pp.getColorTunerSwitch()) { + bundle.putBoolean(PictureQuality.PARAMETER_COLOR_TUNER_SWITCH, true); + } + if (pp.getElevenPointRed() != null) { + bundle.putIntArray(PictureQuality.PARAMETER_ELEVEN_POINT_RED, + pp.getElevenPointRed()); + } + if (pp.getElevenPointBlue() != null) { + bundle.putIntArray(PictureQuality.PARAMETER_ELEVEN_POINT_RED, + pp.getElevenPointBlue()); + } + if (pp.getElevenPointGreen() != null) { + bundle.putIntArray(PictureQuality.PARAMETER_ELEVEN_POINT_RED, + pp.getElevenPointGreen()); + } + } + return bundle; + } + + /** + * Convert PersistableBundle to PictureParameter List. + */ + public static PictureParameter[] convertPersistableBundleToPictureParameterList( + PersistableBundle params) { + List<PictureParameter> pictureParams = new ArrayList<>(); + if (params.containsKey(PictureQuality.PARAMETER_BRIGHTNESS)) { + pictureParams.add(PictureParameter.brightness(params.getLong( + PictureQuality.PARAMETER_BRIGHTNESS))); + } + if (params.containsKey(PictureQuality.PARAMETER_CONTRAST)) { + pictureParams.add(PictureParameter.contrast(params.getInt( + PictureQuality.PARAMETER_CONTRAST))); + } + if (params.containsKey(PictureQuality.PARAMETER_SHARPNESS)) { + pictureParams.add(PictureParameter.sharpness(params.getInt( + PictureQuality.PARAMETER_SHARPNESS))); + } + if (params.containsKey(PictureQuality.PARAMETER_SATURATION)) { + pictureParams.add(PictureParameter.saturation(params.getInt( + PictureQuality.PARAMETER_SATURATION))); + } + if (params.containsKey(PictureQuality.PARAMETER_HUE)) { + pictureParams.add(PictureParameter.hue(params.getInt( + PictureQuality.PARAMETER_HUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_BRIGHTNESS)) { + pictureParams.add(PictureParameter.colorTunerBrightness(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_BRIGHTNESS))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION)) { + pictureParams.add(PictureParameter.colorTunerSaturation(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE)) { + pictureParams.add(PictureParameter.colorTunerHue(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_RED_OFFSET)) { + pictureParams.add(PictureParameter.colorTunerRedOffset(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_RED_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_GREEN_OFFSET)) { + pictureParams.add(PictureParameter.colorTunerGreenOffset(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_GREEN_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_OFFSET)) { + pictureParams.add(PictureParameter.colorTunerBlueOffset(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_BLUE_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN)) { + pictureParams.add(PictureParameter.colorTunerRedGain(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN)) { + pictureParams.add(PictureParameter.colorTunerGreenGain(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN)) { + pictureParams.add(PictureParameter.colorTunerBlueGain(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_NOISE_REDUCTION)) { + pictureParams.add(PictureParameter.noiseReduction( + (byte) params.getInt(PictureQuality.PARAMETER_NOISE_REDUCTION))); + } + if (params.containsKey(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION)) { + pictureParams.add(PictureParameter.mpegNoiseReduction( + (byte) params.getInt(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION))); + } + if (params.containsKey(PictureQuality.PARAMETER_FLESH_TONE)) { + pictureParams.add(PictureParameter.fleshTone( + (byte) params.getInt(PictureQuality.PARAMETER_FLESH_TONE))); + } + if (params.containsKey(PictureQuality.PARAMETER_DECONTOUR)) { + pictureParams.add(PictureParameter.deContour( + (byte) params.getInt(PictureQuality.PARAMETER_DECONTOUR))); + } + if (params.containsKey(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL)) { + pictureParams.add(PictureParameter.dynamicLumaControl( + (byte) params.getInt(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL))); + } + if (params.containsKey(PictureQuality.PARAMETER_FILM_MODE)) { + pictureParams.add(PictureParameter.filmMode(params.getBoolean( + PictureQuality.PARAMETER_FILM_MODE))); + } + if (params.containsKey(PictureQuality.PARAMETER_BLUE_STRETCH)) { + pictureParams.add(PictureParameter.blueStretch(params.getBoolean( + PictureQuality.PARAMETER_BLUE_STRETCH))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNE)) { + pictureParams.add(PictureParameter.colorTune(params.getBoolean( + PictureQuality.PARAMETER_COLOR_TUNE))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE)) { + pictureParams.add(PictureParameter.colorTemperature( + (byte) params.getInt( + PictureQuality.PARAMETER_COLOR_TEMPERATURE))); + } + if (params.containsKey(PictureQuality.PARAMETER_GLOBAL_DIMMING)) { + pictureParams.add(PictureParameter.globeDimming(params.getBoolean( + PictureQuality.PARAMETER_GLOBAL_DIMMING))); + } + if (params.containsKey(PictureQuality.PARAMETER_AUTO_PICTURE_QUALITY_ENABLED)) { + pictureParams.add(PictureParameter.autoPictureQualityEnabled(params.getBoolean( + PictureQuality.PARAMETER_AUTO_PICTURE_QUALITY_ENABLED))); + } + if (params.containsKey(PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED)) { + pictureParams.add(PictureParameter.autoSuperResolutionEnabled(params.getBoolean( + PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN)) { + pictureParams.add(PictureParameter.colorTemperatureRedGain(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN)) { + pictureParams.add(PictureParameter.colorTemperatureGreenGain(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN)) { + pictureParams.add(PictureParameter.colorTemperatureBlueGain(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_LEVEL_RANGE)) { + pictureParams.add(PictureParameter.levelRange( + (byte) params.getInt(PictureQuality.PARAMETER_LEVEL_RANGE))); + } + if (params.containsKey(PictureQuality.PARAMETER_GAMUT_MAPPING)) { + pictureParams.add(PictureParameter.gamutMapping(params.getBoolean( + PictureQuality.PARAMETER_GAMUT_MAPPING))); + } + if (params.containsKey(PictureQuality.PARAMETER_PC_MODE)) { + pictureParams.add(PictureParameter.pcMode(params.getBoolean( + PictureQuality.PARAMETER_PC_MODE))); + } + if (params.containsKey(PictureQuality.PARAMETER_LOW_LATENCY)) { + pictureParams.add(PictureParameter.lowLatency(params.getBoolean( + PictureQuality.PARAMETER_LOW_LATENCY))); + } + if (params.containsKey(PictureQuality.PARAMETER_VRR)) { + pictureParams.add(PictureParameter.vrr(params.getBoolean( + PictureQuality.PARAMETER_VRR))); + } + if (params.containsKey(PictureQuality.PARAMETER_CVRR)) { + pictureParams.add(PictureParameter.cvrr(params.getBoolean( + PictureQuality.PARAMETER_CVRR))); + } + if (params.containsKey(PictureQuality.PARAMETER_HDMI_RGB_RANGE)) { + pictureParams.add(PictureParameter.hdmiRgbRange( + (byte) params.getInt(PictureQuality.PARAMETER_HDMI_RGB_RANGE))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_SPACE)) { + pictureParams.add(PictureParameter.colorSpace( + (byte) params.getInt(PictureQuality.PARAMETER_COLOR_SPACE))); + } + if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS)) { + pictureParams.add(PictureParameter.panelInitMaxLuminceNits( + params.getInt(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS))); + } + if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID)) { + pictureParams.add(PictureParameter.panelInitMaxLuminceValid( + params.getBoolean(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID))); + } + if (params.containsKey(PictureQuality.PARAMETER_GAMMA)) { + pictureParams.add(PictureParameter.gamma( + (byte) params.getInt(PictureQuality.PARAMETER_GAMMA))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET)) { + pictureParams.add(PictureParameter.colorTemperatureRedOffset(params.getInt( + PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET)) { + pictureParams.add(PictureParameter.colorTemperatureGreenOffset(params.getInt( + PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET)) { + pictureParams.add(PictureParameter.colorTemperatureBlueOffset(params.getInt( + PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_RED)) { + pictureParams.add(PictureParameter.elevenPointRed(params.getIntArray( + PictureQuality.PARAMETER_ELEVEN_POINT_RED))); + } + if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_GREEN)) { + pictureParams.add(PictureParameter.elevenPointGreen(params.getIntArray( + PictureQuality.PARAMETER_ELEVEN_POINT_GREEN))); + } + if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_BLUE)) { + pictureParams.add(PictureParameter.elevenPointBlue(params.getIntArray( + PictureQuality.PARAMETER_ELEVEN_POINT_BLUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)) { + pictureParams.add(PictureParameter.lowBlueLight( + (byte) params.getInt(PictureQuality.PARAMETER_LOW_BLUE_LIGHT))); + } + if (params.containsKey(PictureQuality.PARAMETER_LD_MODE)) { + pictureParams.add(PictureParameter.LdMode( + (byte) params.getInt(PictureQuality.PARAMETER_LD_MODE))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_GAIN)) { + pictureParams.add(PictureParameter.osdRedGain(params.getInt( + PictureQuality.PARAMETER_OSD_RED_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_GREEN_GAIN)) { + pictureParams.add(PictureParameter.osdGreenGain(params.getInt( + PictureQuality.PARAMETER_OSD_GREEN_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_BLUE_GAIN)) { + pictureParams.add(PictureParameter.osdBlueGain(params.getInt( + PictureQuality.PARAMETER_OSD_BLUE_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_OFFSET)) { + pictureParams.add(PictureParameter.osdRedOffset(params.getInt( + PictureQuality.PARAMETER_OSD_RED_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_GREEN_OFFSET)) { + pictureParams.add(PictureParameter.osdGreenOffset(params.getInt( + PictureQuality.PARAMETER_OSD_GREEN_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_BLUE_OFFSET)) { + pictureParams.add(PictureParameter.osdBlueOffset(params.getInt( + PictureQuality.PARAMETER_OSD_BLUE_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_HUE)) { + pictureParams.add(PictureParameter.osdHue(params.getInt( + PictureQuality.PARAMETER_OSD_HUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_SATURATION)) { + pictureParams.add(PictureParameter.osdSaturation(params.getInt( + PictureQuality.PARAMETER_OSD_SATURATION))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_CONTRAST)) { + pictureParams.add(PictureParameter.osdContrast(params.getInt( + PictureQuality.PARAMETER_OSD_CONTRAST))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SWITCH)) { + pictureParams.add(PictureParameter.colorTunerSwitch(params.getBoolean( + PictureQuality.PARAMETER_COLOR_TUNER_SWITCH))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_RED)) { + pictureParams.add(PictureParameter.colorTunerHueRed(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_RED))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_GREEN)) { + pictureParams.add(PictureParameter.colorTunerHueGreen(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_GREEN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_BLUE)) { + pictureParams.add(PictureParameter.colorTunerHueBlue(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_BLUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_CYAN)) { + pictureParams.add(PictureParameter.colorTunerHueCyan(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_CYAN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_MAGENTA)) { + pictureParams.add(PictureParameter.colorTunerHueMagenta(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_MAGENTA))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_YELLOW)) { + pictureParams.add(PictureParameter.colorTunerHueYellow(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_YELLOW))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_FLESH)) { + pictureParams.add(PictureParameter.colorTunerHueFlesh(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_FLESH))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_RED)) { + pictureParams.add(PictureParameter.colorTunerSaturationRed(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_RED))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_GREEN)) { + pictureParams.add(PictureParameter.colorTunerSaturationGreen(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_GREEN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_BLUE)) { + pictureParams.add(PictureParameter.colorTunerSaturationBlue(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_BLUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_CYAN)) { + pictureParams.add(PictureParameter.colorTunerSaturationCyan(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_CYAN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_MAGENTA)) { + pictureParams.add(PictureParameter.colorTunerSaturationMagenta(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_MAGENTA))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_YELLOW)) { + pictureParams.add(PictureParameter.colorTunerSaturationYellow(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_YELLOW))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_FLESH)) { + pictureParams.add(PictureParameter.colorTunerSaturationFlesh(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_FLESH))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_RED)) { + pictureParams.add(PictureParameter.colorTunerLuminanceRed(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_RED))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_GREEN)) { + pictureParams.add(PictureParameter.colorTunerLuminanceGreen(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_GREEN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_BLUE)) { + pictureParams.add(PictureParameter.colorTunerLuminanceBlue(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_BLUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_CYAN)) { + pictureParams.add(PictureParameter.colorTunerLuminanceCyan(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_CYAN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA)) { + pictureParams.add(PictureParameter.colorTunerLuminanceMagenta(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW)) { + pictureParams.add(PictureParameter.colorTunerLuminanceYellow(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH)) { + pictureParams.add(PictureParameter.colorTunerLuminanceFlesh(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH))); + } + if (params.containsKey(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)) { + pictureParams.add(PictureParameter.pictureQualityEventType( + (byte) params.getInt(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE))); + } + return (PictureParameter[]) pictureParams.toArray(); + } + + /** + * Convert SoundParameter List to PersistableBundle. + */ + public static PersistableBundle convertSoundParameterListToPersistableBundle( + SoundParameter[] parameters) { + if (parameters == null) { + return null; + } + + PersistableBundle bundle = new PersistableBundle(); + for (SoundParameter sp: parameters) { + if (sp.getSurroundSoundEnabled()) { + bundle.putBoolean(SoundQuality.PARAMETER_SURROUND_SOUND, true); + } + if (sp.getSpeakersEnabled()) { + bundle.putBoolean(SoundQuality.PARAMETER_SPEAKERS, true); + } + if (sp.getAutoVolumeControl()) { + bundle.putBoolean(SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL, true); + } + if (sp.getDtsDrc()) { + bundle.putBoolean(SoundQuality.PARAMETER_DTS_DRC, true); + } + if (sp.getSurroundSoundEnabled()) { + bundle.putBoolean(SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS, true); + } + if (sp.getEnhancedAudioReturnChannelEnabled()) { + bundle.putBoolean(SoundQuality.PARAMETER_EARC, true); + } + if (sp.getBalance() > -1) { + bundle.putInt(SoundQuality.PARAMETER_BALANCE, sp.getBalance()); + } + if (sp.getBass() > -1) { + bundle.putInt(SoundQuality.PARAMETER_BASS, sp.getBass()); + } + if (sp.getTreble() > -1) { + bundle.putInt(SoundQuality.PARAMETER_TREBLE, sp.getTreble()); + } + if (sp.getSpeakersDelayMs() > -1) { + bundle.putInt(SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS, + sp.getSpeakersDelayMs()); + } + if (sp.getDownmixMode() > -1) { + bundle.putInt(SoundQuality.PARAMETER_DOWN_MIX_MODE, sp.getDownmixMode()); + } + if (sp.getSoundStyle() > -1) { + bundle.putInt(SoundQuality.PARAMETER_SOUND_STYLE, sp.getSoundStyle()); + } + if (sp.getDigitalOutput() > -1) { + bundle.putInt(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE, + sp.getDigitalOutput()); + } + if (sp.getDolbyDialogueEnhancer() > -1) { + bundle.putInt(SoundQuality.PARAMETER_DIALOGUE_ENHANCER, + sp.getDolbyDialogueEnhancer()); + } + if (sp.getDtsVirtualX().tbHdx) { + bundle.putBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX, true); + } + if (sp.getDtsVirtualX().limiter) { + bundle.putBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER, true); + } + if (sp.getDtsVirtualX().truSurroundX) { + bundle.putBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X, true); + } + if (sp.getDtsVirtualX().truVolumeHd) { + bundle.putBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD, true); + } + if (sp.getDtsVirtualX().dialogClarity) { + bundle.putBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY, true); + } + if (sp.getDtsVirtualX().definition) { + bundle.putBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION, true); + } + if (sp.getDtsVirtualX().height) { + bundle.putBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT, true); + } + if (sp.getDolbyAudioProcessing().soundMode > -1) { + bundle.putInt(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE, + sp.getDolbyAudioProcessing().soundMode); + } + if (sp.getDolbyAudioProcessing().volumeLeveler) { + bundle.putBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER, + true); + } + if (sp.getDolbyAudioProcessing().surroundVirtualizer) { + bundle.putBoolean( + SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER, + true); + } + if (sp.getDolbyAudioProcessing().dolbyAtmos) { + bundle.putBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS, + true); + } + } + return bundle; + } + /** + * Convert PersistableBundle to SoundParameter List. + */ + public static SoundParameter[] convertPersistableBundleToSoundParameterList( + PersistableBundle params) { + //TODO: set EqualizerDetail + List<SoundParameter> soundParams = new ArrayList<>(); + if (params.containsKey(SoundQuality.PARAMETER_BALANCE)) { + soundParams.add(SoundParameter.balance(params.getInt( + SoundQuality.PARAMETER_BALANCE))); + } + if (params.containsKey(SoundQuality.PARAMETER_BASS)) { + soundParams.add(SoundParameter.bass(params.getInt(SoundQuality.PARAMETER_BASS))); + } + if (params.containsKey(SoundQuality.PARAMETER_TREBLE)) { + soundParams.add(SoundParameter.treble(params.getInt( + SoundQuality.PARAMETER_TREBLE))); + } + if (params.containsKey(SoundQuality.PARAMETER_SURROUND_SOUND)) { + soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean( + SoundQuality.PARAMETER_SURROUND_SOUND))); + } + if (params.containsKey(SoundQuality.PARAMETER_SPEAKERS)) { + soundParams.add(SoundParameter.speakersEnabled(params.getBoolean( + SoundQuality.PARAMETER_SPEAKERS))); + } + if (params.containsKey(SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS)) { + soundParams.add(SoundParameter.speakersDelayMs(params.getInt( + SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS))); + } + if (params.containsKey(SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL)) { + soundParams.add(SoundParameter.autoVolumeControl(params.getBoolean( + SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL))); + } + if (params.containsKey(SoundQuality.PARAMETER_DTS_DRC)) { + soundParams.add(SoundParameter.dtsDrc(params.getBoolean( + SoundQuality.PARAMETER_DTS_DRC))); + } + if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS)) { + soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean( + SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS))); + } + if (params.containsKey(SoundQuality.PARAMETER_EARC)) { + soundParams.add(SoundParameter.enhancedAudioReturnChannelEnabled(params.getBoolean( + SoundQuality.PARAMETER_EARC))); + } + if (params.containsKey(SoundQuality.PARAMETER_DOWN_MIX_MODE)) { + soundParams.add(SoundParameter.downmixMode((byte) params.getInt( + SoundQuality.PARAMETER_DOWN_MIX_MODE))); + } + if (params.containsKey(SoundQuality.PARAMETER_SOUND_STYLE)) { + soundParams.add(SoundParameter.soundStyle((byte) params.getInt( + SoundQuality.PARAMETER_SOUND_STYLE))); + } + if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE)) { + soundParams.add(SoundParameter.digitalOutput((byte) params.getInt( + SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE))); + } + if (params.containsKey(SoundQuality.PARAMETER_DIALOGUE_ENHANCER)) { + soundParams.add(SoundParameter.dolbyDialogueEnhancer((byte) params.getInt( + SoundQuality.PARAMETER_DIALOGUE_ENHANCER))); + } + + DolbyAudioProcessing dab = new DolbyAudioProcessing(); + dab.soundMode = + (byte) params.getInt(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE); + dab.volumeLeveler = + params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER); + dab.surroundVirtualizer = params.getBoolean( + SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER); + dab.dolbyAtmos = + params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS); + soundParams.add(SoundParameter.dolbyAudioProcessing(dab)); + + DtsVirtualX dts = new DtsVirtualX(); + dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX); + dts.limiter = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER); + dts.truSurroundX = params.getBoolean( + SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X); + dts.truVolumeHd = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD); + dts.dialogClarity = params.getBoolean( + SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY); + dts.definition = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION); + dts.height = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT); + soundParams.add(SoundParameter.dtsVirtualX(dts)); + + return (SoundParameter[]) soundParams.toArray(); + } + + private static String persistableBundleToJson(PersistableBundle bundle) { + JSONObject json = new JSONObject(); + for (String key : bundle.keySet()) { + Object value = bundle.get(key); + try { + if (value instanceof String) { + json.put(key, bundle.getString(key)); + } else if (value instanceof Integer) { + json.put(key, bundle.getInt(key)); + } else if (value instanceof Long) { + json.put(key, bundle.getLong(key)); + } else if (value instanceof Boolean) { + json.put(key, bundle.getBoolean(key)); + } else if (value instanceof Double) { + json.put(key, bundle.getDouble(key)); + } + } catch (JSONException e) { + Log.e(TAG, "Unable to serialize ", e); + } + } + return json.toString(); + } + + private static PersistableBundle jsonToPersistableBundle(String jsonString) { + PersistableBundle bundle = new PersistableBundle(); + if (jsonString != null) { + JSONObject jsonObject = null; + try { + jsonObject = new JSONObject(jsonString); + + Iterator<String> keys = jsonObject.keys(); + while (keys.hasNext()) { + String key = keys.next(); + Object value = jsonObject.get(key); + + if (value instanceof String) { + bundle.putString(key, (String) value); + } else if (value instanceof Integer) { + bundle.putInt(key, (Integer) value); + } else if (value instanceof Boolean) { + bundle.putBoolean(key, (Boolean) value); + } else if (value instanceof Double) { + bundle.putDouble(key, (Double) value); + } else if (value instanceof Long) { + bundle.putLong(key, (Long) value); + } + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + return bundle; + } + + /** + * Populates the given map with the ID and generated UUID. + */ + public static void populateTempIdMap(BiMap<Long, String> map, Long id) { + if (id != null && map.getValue(id) == null) { + String uuid; + int attempts = 0; + while (attempts < MAX_UUID_GENERATION_ATTEMPTS) { + uuid = UUID.randomUUID().toString(); + if (map.getKey(uuid) == null) { + map.put(id, uuid); + return; + } + attempts++; + } + } + } + + /** + * Get Content Values. + */ + public static ContentValues getContentValues(Long dbId, Integer profileType, String name, + String packageName, String inputId, PersistableBundle params) { + ContentValues values = new ContentValues(); + if (dbId != null) { + values.put(BaseParameters.PARAMETER_ID, dbId); + } + if (profileType != null) { + values.put(BaseParameters.PARAMETER_TYPE, profileType); + } + if (name != null) { + values.put(BaseParameters.PARAMETER_NAME, name); + } + if (packageName != null) { + values.put(BaseParameters.PARAMETER_PACKAGE, packageName); + } + if (inputId != null) { + values.put(BaseParameters.PARAMETER_INPUT_ID, inputId); + } + if (params != null) { + values.put(SETTINGS, persistableBundleToJson(params)); + } + return values; + } + + /** + * Get Media Profile Columns. + */ + public static String[] getMediaProfileColumns(boolean includeParams) { + ArrayList<String> columns = new ArrayList<>(Arrays.asList( + BaseParameters.PARAMETER_ID, + BaseParameters.PARAMETER_TYPE, + BaseParameters.PARAMETER_NAME, + BaseParameters.PARAMETER_INPUT_ID, + BaseParameters.PARAMETER_PACKAGE) + ); + if (includeParams) { + columns.add(SETTINGS); + } + return columns.toArray(new String[0]); + } + + /** + * Convert cursor to Picture Profile with temporary UUID. + */ + public static PictureProfile convertCursorToPictureProfileWithTempId(Cursor cursor, + BiMap<Long, String> map) { + return new PictureProfile( + getTempId(map, cursor), + getType(cursor), + getName(cursor), + getInputId(cursor), + getPackageName(cursor), + jsonToPersistableBundle(getSettingsString(cursor)), + PictureProfileHandle.NONE + ); + } + + /** + * Convert cursor to Sound Profile with temporary UUID. + */ + public static SoundProfile convertCursorToSoundProfileWithTempId(Cursor cursor, BiMap<Long, + String> map) { + return new SoundProfile( + getTempId(map, cursor), + getType(cursor), + getName(cursor), + getInputId(cursor), + getPackageName(cursor), + jsonToPersistableBundle(getSettingsString(cursor)), + SoundProfileHandle.NONE + ); + } + + /** + * Convert parameter to byte array. + */ + public static byte[] convertParameterToByteArray(List<String> names) { + /** + * TODO Add following to ParameterName & add conversion here. + * - PICTURE_QUALITY_EVENT_TYPE + * - PANEL_INIT_MAX_LUMINCE_NITS + */ + + HashSet<String> nameMap = new HashSet<>(names); + + List<Byte> bytes = new ArrayList<>(); + // Picture Quality parameters + if (nameMap.contains(PictureQuality.PARAMETER_BRIGHTNESS)) { + bytes.add(ParameterName.BRIGHTNESS); + } + if (nameMap.contains(PictureQuality.PARAMETER_BRIGHTNESS)) { + bytes.add(ParameterName.BRIGHTNESS); + } + if (nameMap.contains(PictureQuality.PARAMETER_CONTRAST)) { + bytes.add(ParameterName.CONTRAST); + } + if (nameMap.contains(PictureQuality.PARAMETER_SHARPNESS)) { + bytes.add(ParameterName.SHARPNESS); + } + if (nameMap.contains(PictureQuality.PARAMETER_SATURATION)) { + bytes.add(ParameterName.SATURATION); + } + if (nameMap.contains(PictureQuality.PARAMETER_HUE)) { + bytes.add(ParameterName.HUE); + } + if (nameMap.contains(PictureQuality.PARAMETER_BRIGHTNESS)) { + bytes.add(ParameterName.BRIGHTNESS); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_BRIGHTNESS)) { + bytes.add(ParameterName.COLOR_TUNER_BRIGHTNESS); + } + if (nameMap.contains(PictureQuality.PARAMETER_SATURATION)) { + bytes.add(ParameterName.SATURATION); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION)) { + bytes.add(ParameterName.COLOR_TUNER_SATURATION); + } + if (nameMap.contains(PictureQuality.PARAMETER_HUE)) { + bytes.add(ParameterName.HUE); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_HUE)) { + bytes.add(ParameterName.COLOR_TUNER_HUE); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_RED_OFFSET)) { + bytes.add(ParameterName.COLOR_TUNER_RED_OFFSET); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_GREEN_OFFSET)) { + bytes.add(ParameterName.COLOR_TUNER_GREEN_OFFSET); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_OFFSET)) { + bytes.add(ParameterName.COLOR_TUNER_BLUE_OFFSET); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN)) { + bytes.add(ParameterName.COLOR_TUNER_RED_GAIN); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN)) { + bytes.add(ParameterName.COLOR_TUNER_GREEN_GAIN); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN)) { + bytes.add(ParameterName.COLOR_TUNER_BLUE_GAIN); + } + if (nameMap.contains(PictureQuality.PARAMETER_NOISE_REDUCTION)) { + bytes.add(ParameterName.NOISE_REDUCTION); + } + if (nameMap.contains(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION)) { + bytes.add(ParameterName.MPEG_NOISE_REDUCTION); + } + if (nameMap.contains(PictureQuality.PARAMETER_FLESH_TONE)) { + bytes.add(ParameterName.FLASH_TONE); + } + if (nameMap.contains(PictureQuality.PARAMETER_DECONTOUR)) { + bytes.add(ParameterName.DE_CONTOUR); + } + if (nameMap.contains(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL)) { + bytes.add(ParameterName.DYNAMIC_LUMA_CONTROL); + } + if (nameMap.contains(PictureQuality.PARAMETER_FILM_MODE)) { + bytes.add(ParameterName.FILM_MODE); + } + if (nameMap.contains(PictureQuality.PARAMETER_BLUE_STRETCH)) { + bytes.add(ParameterName.BLUE_STRETCH); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNE)) { + bytes.add(ParameterName.COLOR_TUNE); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TEMPERATURE)) { + bytes.add(ParameterName.COLOR_TEMPERATURE); + } + if (nameMap.contains(PictureQuality.PARAMETER_GLOBAL_DIMMING)) { + bytes.add(ParameterName.GLOBE_DIMMING); + } + if (nameMap.contains(PictureQuality.PARAMETER_AUTO_PICTURE_QUALITY_ENABLED)) { + bytes.add(ParameterName.AUTO_PICTUREQUALITY_ENABLED); + } + if (nameMap.contains(PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED)) { + bytes.add(ParameterName.AUTO_SUPER_RESOLUTION_ENABLED); + } + if (nameMap.contains(PictureQuality.PARAMETER_LEVEL_RANGE)) { + bytes.add(ParameterName.LEVEL_RANGE); + } + if (nameMap.contains(PictureQuality.PARAMETER_GAMUT_MAPPING)) { + bytes.add(ParameterName.GAMUT_MAPPING); + } + if (nameMap.contains(PictureQuality.PARAMETER_PC_MODE)) { + bytes.add(ParameterName.PC_MODE); + } + if (nameMap.contains(PictureQuality.PARAMETER_LOW_LATENCY)) { + bytes.add(ParameterName.LOW_LATENCY); + } + if (nameMap.contains(PictureQuality.PARAMETER_VRR)) { + bytes.add(ParameterName.VRR); + } + if (nameMap.contains(PictureQuality.PARAMETER_CVRR)) { + bytes.add(ParameterName.CVRR); + } + if (nameMap.contains(PictureQuality.PARAMETER_HDMI_RGB_RANGE)) { + bytes.add(ParameterName.HDMI_RGB_RANGE); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_SPACE)) { + bytes.add(ParameterName.COLOR_SPACE); + } + if (nameMap.contains(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID)) { + bytes.add(ParameterName.PANEL_INIT_MAX_LUMINCE_VALID); + } + if (nameMap.contains(PictureQuality.PARAMETER_GAMMA)) { + bytes.add(ParameterName.GAMMA); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET)) { + bytes.add(ParameterName.COLOR_TEMPERATURE_RED_OFFSET); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET)) { + bytes.add(ParameterName.COLOR_TEMPERATURE_GREEN_OFFSET); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET)) { + bytes.add(ParameterName.COLOR_TEMPERATURE_BLUE_OFFSET); + } + if (nameMap.contains(PictureQuality.PARAMETER_ELEVEN_POINT_RED)) { + bytes.add(ParameterName.ELEVEN_POINT_RED); + } + if (nameMap.contains(PictureQuality.PARAMETER_ELEVEN_POINT_GREEN)) { + bytes.add(ParameterName.ELEVEN_POINT_GREEN); + } + if (nameMap.contains(PictureQuality.PARAMETER_ELEVEN_POINT_BLUE)) { + bytes.add(ParameterName.ELEVEN_POINT_BLUE); + } + if (nameMap.contains(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)) { + bytes.add(ParameterName.LOW_BLUE_LIGHT); + } + if (nameMap.contains(PictureQuality.PARAMETER_LD_MODE)) { + bytes.add(ParameterName.LD_MODE); + } + if (nameMap.contains(PictureQuality.PARAMETER_OSD_RED_GAIN)) { + bytes.add(ParameterName.OSD_RED_GAIN); + } + if (nameMap.contains(PictureQuality.PARAMETER_OSD_GREEN_GAIN)) { + bytes.add(ParameterName.OSD_GREEN_GAIN); + } + if (nameMap.contains(PictureQuality.PARAMETER_OSD_BLUE_GAIN)) { + bytes.add(ParameterName.OSD_BLUE_GAIN); + } + if (nameMap.contains(PictureQuality.PARAMETER_OSD_RED_OFFSET)) { + bytes.add(ParameterName.OSD_RED_OFFSET); + } + if (nameMap.contains(PictureQuality.PARAMETER_OSD_GREEN_OFFSET)) { + bytes.add(ParameterName.OSD_GREEN_OFFSET); + } + if (nameMap.contains(PictureQuality.PARAMETER_OSD_BLUE_OFFSET)) { + bytes.add(ParameterName.OSD_BLUE_OFFSET); + } + if (nameMap.contains(PictureQuality.PARAMETER_OSD_HUE)) { + bytes.add(ParameterName.OSD_HUE); + } + if (nameMap.contains(PictureQuality.PARAMETER_OSD_SATURATION)) { + bytes.add(ParameterName.OSD_SATURATION); + } + if (nameMap.contains(PictureQuality.PARAMETER_OSD_CONTRAST)) { + bytes.add(ParameterName.OSD_CONTRAST); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_SWITCH)) { + bytes.add(ParameterName.COLOR_TUNER_SWITCH); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_HUE_RED)) { + bytes.add(ParameterName.COLOR_TUNER_HUE_RED); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_HUE_GREEN)) { + bytes.add(ParameterName.COLOR_TUNER_HUE_GREEN); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_HUE_BLUE)) { + bytes.add(ParameterName.COLOR_TUNER_HUE_BLUE); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_HUE_CYAN)) { + bytes.add(ParameterName.COLOR_TUNER_HUE_CYAN); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_HUE_MAGENTA)) { + bytes.add(ParameterName.COLOR_TUNER_HUE_MAGENTA); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_HUE_YELLOW)) { + bytes.add(ParameterName.COLOR_TUNER_HUE_YELLOW); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_HUE_FLESH)) { + bytes.add(ParameterName.COLOR_TUNER_HUE_FLESH); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_RED)) { + bytes.add(ParameterName.COLOR_TUNER_SATURATION_RED); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_GREEN)) { + bytes.add(ParameterName.COLOR_TUNER_SATURATION_GREEN); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_BLUE)) { + bytes.add(ParameterName.COLOR_TUNER_SATURATION_BLUE); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_CYAN)) { + bytes.add(ParameterName.COLOR_TUNER_SATURATION_CYAN); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_MAGENTA)) { + bytes.add(ParameterName.COLOR_TUNER_SATURATION_MAGENTA); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_YELLOW)) { + bytes.add(ParameterName.COLOR_TUNER_SATURATION_YELLOW); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_FLESH)) { + bytes.add(ParameterName.COLOR_TUNER_SATURATION_FLESH); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_RED)) { + bytes.add(ParameterName.COLOR_TUNER_LUMINANCE_RED); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_GREEN)) { + bytes.add(ParameterName.COLOR_TUNER_LUMINANCE_GREEN); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_BLUE)) { + bytes.add(ParameterName.COLOR_TUNER_LUMINANCE_BLUE); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_CYAN)) { + bytes.add(ParameterName.COLOR_TUNER_LUMINANCE_CYAN); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA)) { + bytes.add(ParameterName.COLOR_TUNER_LUMINANCE_MAGENTA); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW)) { + bytes.add(ParameterName.COLOR_TUNER_LUMINANCE_YELLOW); + } + if (nameMap.contains(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH)) { + bytes.add(ParameterName.COLOR_TUNER_LUMINANCE_FLESH); + } + + // Sound Quality parameters + if (nameMap.contains(SoundQuality.PARAMETER_BALANCE)) { + bytes.add(ParameterName.BALANCE); + } + if (nameMap.contains(SoundQuality.PARAMETER_BASS)) { + bytes.add(ParameterName.BASS); + } + if (nameMap.contains(SoundQuality.PARAMETER_TREBLE)) { + bytes.add(ParameterName.TREBLE); + } + if (nameMap.contains(SoundQuality.PARAMETER_SURROUND_SOUND)) { + bytes.add(ParameterName.SURROUND_SOUND_ENABLED); + } + if (nameMap.contains(SoundQuality.PARAMETER_EQUALIZER_DETAIL)) { + bytes.add(ParameterName.EQUALIZER_DETAIL); + } + if (nameMap.contains(SoundQuality.PARAMETER_SPEAKERS)) { + bytes.add(ParameterName.SPEAKERS_ENABLED); + } + if (nameMap.contains(SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS)) { + bytes.add(ParameterName.SPEAKERS_DELAY_MS); + } + if (nameMap.contains(SoundQuality.PARAMETER_EARC)) { + bytes.add(ParameterName.ENHANCED_AUDIO_RETURN_CHANNEL_ENABLED); + } + if (nameMap.contains(SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL)) { + bytes.add(ParameterName.AUTO_VOLUME_CONTROL); + } + if (nameMap.contains(SoundQuality.PARAMETER_DOWN_MIX_MODE)) { + bytes.add(ParameterName.DOWNMIX_MODE); + } + if (nameMap.contains(SoundQuality.PARAMETER_DTS_DRC)) { + bytes.add(ParameterName.DTS_DRC); + } + if (nameMap.contains(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING)) { + bytes.add(ParameterName.DOLBY_AUDIO_PROCESSING); + } + if (nameMap.contains(SoundQuality.PARAMETER_DIALOGUE_ENHANCER)) { + bytes.add(ParameterName.DOLBY_DIALOGUE_ENHANCER); + } + if (nameMap.contains(SoundQuality.PARAMETER_DTS_VIRTUAL_X)) { + bytes.add(ParameterName.DTS_VIRTUAL_X); + } + if (nameMap.contains(SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS)) { + bytes.add(ParameterName.DIGITAL_OUTPUT_DELAY_MS); + } + if (nameMap.contains(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE)) { + bytes.add(ParameterName.DIGITAL_OUTPUT); + } + if (nameMap.contains(SoundQuality.PARAMETER_SOUND_STYLE)) { + bytes.add(ParameterName.SOUND_STYLE); + } + + byte[] byteArray = new byte[bytes.size()]; + for (int i = 0; i < bytes.size(); i++) { + byteArray[i] = bytes.get(i); + } + return byteArray; + } + + /** + * Get Parameter Name based on byte. + */ + public static String getParameterName(byte pn) { + Map<Byte, String> parameterNameMap = new HashMap<>(); + parameterNameMap.put(ParameterName.BRIGHTNESS, PictureQuality.PARAMETER_BRIGHTNESS); + parameterNameMap.put(ParameterName.CONTRAST, PictureQuality.PARAMETER_CONTRAST); + parameterNameMap.put(ParameterName.SHARPNESS, PictureQuality.PARAMETER_SHARPNESS); + parameterNameMap.put(ParameterName.SATURATION, PictureQuality.PARAMETER_SATURATION); + parameterNameMap.put(ParameterName.HUE, PictureQuality.PARAMETER_HUE); + parameterNameMap.put(ParameterName.COLOR_TUNER_BRIGHTNESS, + PictureQuality.PARAMETER_COLOR_TUNER_BRIGHTNESS); + parameterNameMap.put(ParameterName.COLOR_TUNER_SATURATION, + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION); + parameterNameMap.put(ParameterName.COLOR_TUNER_HUE, + PictureQuality.PARAMETER_COLOR_TUNER_HUE); + parameterNameMap.put(ParameterName.COLOR_TUNER_RED_OFFSET, + PictureQuality.PARAMETER_COLOR_TUNER_RED_OFFSET); + parameterNameMap.put(ParameterName.COLOR_TUNER_GREEN_OFFSET, + PictureQuality.PARAMETER_COLOR_TUNER_GREEN_OFFSET); + parameterNameMap.put(ParameterName.COLOR_TUNER_BLUE_OFFSET, + PictureQuality.PARAMETER_COLOR_TUNER_BLUE_OFFSET); + parameterNameMap.put(ParameterName.COLOR_TUNER_RED_GAIN, + PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN); + parameterNameMap.put(ParameterName.COLOR_TUNER_GREEN_GAIN, + PictureQuality.PARAMETER_COLOR_TUNER_GREEN_GAIN); + parameterNameMap.put(ParameterName.COLOR_TUNER_BLUE_GAIN, + PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN); + parameterNameMap.put(ParameterName.NOISE_REDUCTION, + PictureQuality.PARAMETER_NOISE_REDUCTION); + parameterNameMap.put(ParameterName.MPEG_NOISE_REDUCTION, + PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION); + parameterNameMap.put(ParameterName.FLASH_TONE, PictureQuality.PARAMETER_FLESH_TONE); + parameterNameMap.put(ParameterName.DE_CONTOUR, PictureQuality.PARAMETER_DECONTOUR); + parameterNameMap.put(ParameterName.DYNAMIC_LUMA_CONTROL, + PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL); + parameterNameMap.put(ParameterName.FILM_MODE, + PictureQuality.PARAMETER_FILM_MODE); + parameterNameMap.put(ParameterName.BLACK_STRETCH, + PictureQuality.PARAMETER_BLACK_STRETCH); + parameterNameMap.put(ParameterName.BLUE_STRETCH, + PictureQuality.PARAMETER_BLUE_STRETCH); + parameterNameMap.put(ParameterName.COLOR_TUNE, + PictureQuality.PARAMETER_COLOR_TUNE); + parameterNameMap.put(ParameterName.COLOR_TEMPERATURE, + PictureQuality.PARAMETER_COLOR_TEMPERATURE); + parameterNameMap.put(ParameterName.GLOBE_DIMMING, + PictureQuality.PARAMETER_GLOBAL_DIMMING); + parameterNameMap.put(ParameterName.AUTO_PICTUREQUALITY_ENABLED, + PictureQuality.PARAMETER_AUTO_PICTURE_QUALITY_ENABLED); + parameterNameMap.put(ParameterName.AUTO_SUPER_RESOLUTION_ENABLED, + PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED); + parameterNameMap.put(ParameterName.LEVEL_RANGE, PictureQuality.PARAMETER_LEVEL_RANGE); + parameterNameMap.put(ParameterName.GAMUT_MAPPING, + PictureQuality.PARAMETER_GAMUT_MAPPING); + parameterNameMap.put(ParameterName.PC_MODE, PictureQuality.PARAMETER_PC_MODE); + parameterNameMap.put(ParameterName.LOW_LATENCY, PictureQuality.PARAMETER_LOW_LATENCY); + parameterNameMap.put(ParameterName.VRR, PictureQuality.PARAMETER_VRR); + parameterNameMap.put(ParameterName.CVRR, PictureQuality.PARAMETER_CVRR); + parameterNameMap.put(ParameterName.HDMI_RGB_RANGE, + PictureQuality.PARAMETER_HDMI_RGB_RANGE); + parameterNameMap.put(ParameterName.COLOR_SPACE, PictureQuality.PARAMETER_COLOR_SPACE); + parameterNameMap.put(ParameterName.PANEL_INIT_MAX_LUMINCE_VALID, + PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID); + parameterNameMap.put(ParameterName.GAMMA, PictureQuality.PARAMETER_GAMMA); + parameterNameMap.put(ParameterName.COLOR_TEMPERATURE_RED_GAIN, + PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_GAIN); + parameterNameMap.put(ParameterName.COLOR_TEMPERATURE_GREEN_GAIN, + PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_GAIN); + parameterNameMap.put(ParameterName.COLOR_TEMPERATURE_BLUE_GAIN, + PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_GAIN); + parameterNameMap.put(ParameterName.COLOR_TEMPERATURE_RED_OFFSET, + PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET); + parameterNameMap.put(ParameterName.COLOR_TEMPERATURE_GREEN_OFFSET, + PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET); + parameterNameMap.put(ParameterName.COLOR_TEMPERATURE_BLUE_OFFSET, + PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET); + parameterNameMap.put(ParameterName.ELEVEN_POINT_RED, + PictureQuality.PARAMETER_ELEVEN_POINT_RED); + parameterNameMap.put(ParameterName.ELEVEN_POINT_GREEN, + PictureQuality.PARAMETER_ELEVEN_POINT_GREEN); + parameterNameMap.put(ParameterName.ELEVEN_POINT_BLUE, + PictureQuality.PARAMETER_ELEVEN_POINT_BLUE); + parameterNameMap.put(ParameterName.LOW_BLUE_LIGHT, + PictureQuality.PARAMETER_LOW_BLUE_LIGHT); + parameterNameMap.put(ParameterName.LD_MODE, PictureQuality.PARAMETER_LD_MODE); + parameterNameMap.put(ParameterName.OSD_RED_GAIN, PictureQuality.PARAMETER_OSD_RED_GAIN); + parameterNameMap.put(ParameterName.OSD_GREEN_GAIN, + PictureQuality.PARAMETER_OSD_GREEN_GAIN); + parameterNameMap.put(ParameterName.OSD_BLUE_GAIN, + PictureQuality.PARAMETER_OSD_BLUE_GAIN); + parameterNameMap.put(ParameterName.OSD_RED_OFFSET, + PictureQuality.PARAMETER_OSD_RED_OFFSET); + parameterNameMap.put(ParameterName.OSD_GREEN_OFFSET, + PictureQuality.PARAMETER_OSD_GREEN_OFFSET); + parameterNameMap.put(ParameterName.OSD_BLUE_OFFSET, + PictureQuality.PARAMETER_OSD_BLUE_OFFSET); + parameterNameMap.put(ParameterName.OSD_HUE, PictureQuality.PARAMETER_OSD_HUE); + parameterNameMap.put(ParameterName.OSD_SATURATION, + PictureQuality.PARAMETER_OSD_SATURATION); + parameterNameMap.put(ParameterName.OSD_CONTRAST, + PictureQuality.PARAMETER_OSD_CONTRAST); + parameterNameMap.put(ParameterName.COLOR_TUNER_SWITCH, + PictureQuality.PARAMETER_COLOR_TUNER_SWITCH); + parameterNameMap.put(ParameterName.COLOR_TUNER_HUE_RED, + PictureQuality.PARAMETER_COLOR_TUNER_HUE_RED); + parameterNameMap.put(ParameterName.COLOR_TUNER_HUE_GREEN, + PictureQuality.PARAMETER_COLOR_TUNER_HUE_GREEN); + parameterNameMap.put(ParameterName.COLOR_TUNER_HUE_BLUE, + PictureQuality.PARAMETER_COLOR_TUNER_HUE_BLUE); + parameterNameMap.put(ParameterName.COLOR_TUNER_HUE_CYAN, + PictureQuality.PARAMETER_COLOR_TUNER_HUE_CYAN); + parameterNameMap.put(ParameterName.COLOR_TUNER_HUE_MAGENTA, + PictureQuality.PARAMETER_COLOR_TUNER_HUE_MAGENTA); + parameterNameMap.put(ParameterName.COLOR_TUNER_HUE_YELLOW, + PictureQuality.PARAMETER_COLOR_TUNER_HUE_YELLOW); + parameterNameMap.put(ParameterName.COLOR_TUNER_HUE_FLESH, + PictureQuality.PARAMETER_COLOR_TUNER_HUE_FLESH); + parameterNameMap.put(ParameterName.COLOR_TUNER_SATURATION_RED, + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_RED); + parameterNameMap.put(ParameterName.COLOR_TUNER_SATURATION_GREEN, + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_GREEN); + parameterNameMap.put(ParameterName.COLOR_TUNER_SATURATION_BLUE, + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_BLUE); + parameterNameMap.put(ParameterName.COLOR_TUNER_SATURATION_CYAN, + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_CYAN); + parameterNameMap.put(ParameterName.COLOR_TUNER_SATURATION_MAGENTA, + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_MAGENTA); + parameterNameMap.put(ParameterName.COLOR_TUNER_SATURATION_YELLOW, + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_YELLOW); + parameterNameMap.put(ParameterName.COLOR_TUNER_SATURATION_FLESH, + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_FLESH); + parameterNameMap.put(ParameterName.COLOR_TUNER_LUMINANCE_RED, + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_RED); + parameterNameMap.put(ParameterName.COLOR_TUNER_LUMINANCE_GREEN, + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_GREEN); + parameterNameMap.put(ParameterName.COLOR_TUNER_LUMINANCE_BLUE, + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_BLUE); + parameterNameMap.put(ParameterName.COLOR_TUNER_LUMINANCE_CYAN, + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_CYAN); + parameterNameMap.put(ParameterName.COLOR_TUNER_LUMINANCE_MAGENTA, + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA); + parameterNameMap.put(ParameterName.COLOR_TUNER_LUMINANCE_YELLOW, + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW); + parameterNameMap.put(ParameterName.COLOR_TUNER_LUMINANCE_FLESH, + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH); + parameterNameMap.put(ParameterName.BALANCE, SoundQuality.PARAMETER_BALANCE); + parameterNameMap.put(ParameterName.BASS, SoundQuality.PARAMETER_BASS); + parameterNameMap.put(ParameterName.TREBLE, SoundQuality.PARAMETER_TREBLE); + parameterNameMap.put(ParameterName.SURROUND_SOUND_ENABLED, + SoundQuality.PARAMETER_SURROUND_SOUND); + parameterNameMap.put(ParameterName.EQUALIZER_DETAIL, + SoundQuality.PARAMETER_EQUALIZER_DETAIL); + parameterNameMap.put(ParameterName.SPEAKERS_ENABLED, SoundQuality.PARAMETER_SPEAKERS); + parameterNameMap.put(ParameterName.SPEAKERS_DELAY_MS, + SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS); + parameterNameMap.put(ParameterName.ENHANCED_AUDIO_RETURN_CHANNEL_ENABLED, + SoundQuality.PARAMETER_EARC); + parameterNameMap.put(ParameterName.AUTO_VOLUME_CONTROL, + SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL); + parameterNameMap.put(ParameterName.DOWNMIX_MODE, SoundQuality.PARAMETER_DOWN_MIX_MODE); + parameterNameMap.put(ParameterName.DTS_DRC, SoundQuality.PARAMETER_DTS_DRC); + parameterNameMap.put(ParameterName.DOLBY_AUDIO_PROCESSING, + SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING); + parameterNameMap.put(ParameterName.DOLBY_DIALOGUE_ENHANCER, + SoundQuality.PARAMETER_DIALOGUE_ENHANCER); + parameterNameMap.put(ParameterName.DTS_VIRTUAL_X, + SoundQuality.PARAMETER_DTS_VIRTUAL_X); + parameterNameMap.put(ParameterName.DIGITAL_OUTPUT, + SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE); + parameterNameMap.put(ParameterName.DIGITAL_OUTPUT_DELAY_MS, + SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS); + parameterNameMap.put(ParameterName.SOUND_STYLE, SoundQuality.PARAMETER_SOUND_STYLE); + + return parameterNameMap.get(pn); + } + + private static String getTempId(BiMap<Long, String> map, Cursor cursor) { + int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_ID); + Long dbId = colIndex != -1 ? cursor.getLong(colIndex) : null; + populateTempIdMap(map, dbId); + return map.getValue(dbId); + } + + private static int getType(Cursor cursor) { + int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_TYPE); + return colIndex != -1 ? cursor.getInt(colIndex) : 0; + } + + private static String getName(Cursor cursor) { + int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_NAME); + return colIndex != -1 ? cursor.getString(colIndex) : null; + } + + private static String getInputId(Cursor cursor) { + int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_INPUT_ID); + return colIndex != -1 ? cursor.getString(colIndex) : null; + } + + private static String getPackageName(Cursor cursor) { + int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_PACKAGE); + return colIndex != -1 ? cursor.getString(colIndex) : null; + } + + private static String getSettingsString(Cursor cursor) { + int colIndex = cursor.getColumnIndex(SETTINGS); + return colIndex != -1 ? cursor.getString(colIndex) : null; + } + + private MediaQualityUtils() { + + } +} diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java index e47f8ae9d3a5..02f817e9cd44 100644 --- a/services/core/java/com/android/server/notification/GroupHelper.java +++ b/services/core/java/com/android/server/notification/GroupHelper.java @@ -146,18 +146,27 @@ public class GroupHelper { private static List<NotificationSectioner> NOTIFICATION_SHADE_SECTIONS = getNotificationShadeSections(); + private static List<NotificationSectioner> NOTIFICATION_BUNDLE_SECTIONS; + private static List<NotificationSectioner> getNotificationShadeSections() { ArrayList<NotificationSectioner> sectionsList = new ArrayList<>(); if (android.service.notification.Flags.notificationClassification()) { sectionsList.addAll(List.of( new NotificationSectioner("PromotionsSection", 0, (record) -> - NotificationChannel.PROMOTIONS_ID.equals(record.getChannel().getId())), + NotificationChannel.PROMOTIONS_ID.equals(record.getChannel().getId()) + && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT), new NotificationSectioner("SocialSection", 0, (record) -> - NotificationChannel.SOCIAL_MEDIA_ID.equals(record.getChannel().getId())), + NotificationChannel.SOCIAL_MEDIA_ID.equals(record.getChannel().getId()) + && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT), new NotificationSectioner("NewsSection", 0, (record) -> - NotificationChannel.NEWS_ID.equals(record.getChannel().getId())), + NotificationChannel.NEWS_ID.equals(record.getChannel().getId()) + && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT), new NotificationSectioner("RecsSection", 0, (record) -> - NotificationChannel.RECS_ID.equals(record.getChannel().getId())))); + NotificationChannel.RECS_ID.equals(record.getChannel().getId()) + && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT) + )); + + NOTIFICATION_BUNDLE_SECTIONS = new ArrayList<>(sectionsList); } if (Flags.notificationForceGroupConversations()) { @@ -828,7 +837,7 @@ public class GroupHelper { } moveNotificationsToNewSection(record.getUserId(), pkgName, List.of(new NotificationMoveOp(record, null, fullAggregateGroupKey)), - REGROUP_REASON_BUNDLE); + Map.of(record.getKey(), REGROUP_REASON_BUNDLE)); return; } } @@ -895,7 +904,12 @@ public class GroupHelper { return false; } - return NotificationChannel.SYSTEM_RESERVED_IDS.contains(record.getChannel().getId()); + return isInBundleSection(record); + } + + private static boolean isInBundleSection(final NotificationRecord record) { + final NotificationSectioner sectioner = getSection(record); + return (sectioner != null && NOTIFICATION_BUNDLE_SECTIONS.contains(sectioner)); } /** @@ -1084,10 +1098,10 @@ public class GroupHelper { FullyQualifiedGroupKey newGroup) { } /** - * Called when a notification channel is updated (channel attributes have changed), - * so that this helper can adjust the aggregate groups by moving children - * if their section has changed. - * see {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)} + * Called when a notification channel is updated (channel attributes have changed), so that this + * helper can adjust the aggregate groups by moving children if their section has changed. see + * {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)} + * * @param userId the userId of the channel * @param pkgName the channel's package * @param channel the channel that was updated @@ -1095,19 +1109,30 @@ public class GroupHelper { */ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) public void onChannelUpdated(final int userId, final String pkgName, - final NotificationChannel channel, final List<NotificationRecord> notificationList) { + final NotificationChannel channel, final List<NotificationRecord> notificationList, + ArrayMap<String, NotificationRecord> summaryByGroupKey) { synchronized (mAggregatedNotifications) { + final ArrayMap<String, Integer> regroupingReasonMap = new ArrayMap<>(); ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>(); for (NotificationRecord r : notificationList) { if (r.getChannel().getId().equals(channel.getId()) && r.getSbn().getPackageName().equals(pkgName) && r.getUserId() == userId) { notificationsToCheck.put(r.getKey(), r); + regroupingReasonMap.put(r.getKey(), REGROUP_REASON_CHANNEL_UPDATE); + if (notificationRegroupOnClassification()) { + // Notification is unbundled and original summary found + // => regroup in original group + if (!isInBundleSection(r) + && isOriginalGroupSummaryPresent(r, summaryByGroupKey)) { + regroupingReasonMap.put(r.getKey(), + REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP); + } + } } } - regroupNotifications(userId, pkgName, notificationsToCheck, - REGROUP_REASON_CHANNEL_UPDATE); + regroupNotifications(userId, pkgName, notificationsToCheck, regroupingReasonMap); } } @@ -1124,8 +1149,10 @@ public class GroupHelper { synchronized (mAggregatedNotifications) { ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>(); notificationsToCheck.put(record.getKey(), record); + ArrayMap<String, Integer> regroupReasons = new ArrayMap<>(); + regroupReasons.put(record.getKey(), REGROUP_REASON_BUNDLE); regroupNotifications(record.getUserId(), record.getSbn().getPackageName(), - notificationsToCheck, REGROUP_REASON_BUNDLE); + notificationsToCheck, regroupReasons); } } @@ -1144,16 +1171,16 @@ public class GroupHelper { ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>(); notificationsToCheck.put(record.getKey(), record); regroupNotifications(record.getUserId(), record.getSbn().getPackageName(), - notificationsToCheck, - originalSummaryExists ? REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP - : REGROUP_REASON_UNBUNDLE); + notificationsToCheck, Map.of(record.getKey(), + originalSummaryExists ? REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP + : REGROUP_REASON_UNBUNDLE)); } } @GuardedBy("mAggregatedNotifications") private void regroupNotifications(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck, - @RegroupingReason int regroupingReason) { + Map<String, Integer> regroupReasons) { // The list of notification operations required after the channel update final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); @@ -1170,15 +1197,13 @@ public class GroupHelper { // Handle "grouped correctly" notifications that were re-classified (bundled) if (notificationRegroupOnClassification()) { - if (regroupingReason == REGROUP_REASON_BUNDLE) { - notificationsToMove.addAll( - getReclassifiedNotificationsMoveOps(userId, pkgName, notificationsToCheck)); - } + notificationsToMove.addAll( + getReclassifiedNotificationsMoveOps(userId, pkgName, notificationsToCheck)); } // Batch move to new section if (!notificationsToMove.isEmpty()) { - moveNotificationsToNewSection(userId, pkgName, notificationsToMove, regroupingReason); + moveNotificationsToNewSection(userId, pkgName, notificationsToMove, regroupReasons); } } @@ -1187,9 +1212,9 @@ public class GroupHelper { final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); for (NotificationRecord record : notificationsToCheck.values()) { if (isChildOfValidAppGroup(record)) { - // Check if section changes + // Check if section changes to a bundle section NotificationSectioner sectioner = getSection(record); - if (sectioner != null) { + if (sectioner != null && NOTIFICATION_BUNDLE_SECTIONS.contains(sectioner)) { FullyQualifiedGroupKey newFullAggregateGroupKey = new FullyQualifiedGroupKey(userId, pkgName, sectioner); if (DEBUG) { @@ -1204,6 +1229,24 @@ public class GroupHelper { return notificationsToMove; } + /** + * Checks if the original group's summary exists for a notification that was regrouped + * @param r notification to check + * @param summaryByGroupKey map of the current group summaries + * @return true if the original group summary exists + */ + public static boolean isOriginalGroupSummaryPresent(final NotificationRecord r, + final ArrayMap<String, NotificationRecord> summaryByGroupKey) { + if (r.getSbn().isAppGroup() && r.getNotification().isGroupChild()) { + final String oldGroupKey = GroupHelper.getFullAggregateGroupKey( + r.getSbn().getPackageName(), r.getOriginalGroupKey(), r.getUserId()); + NotificationRecord groupSummary = summaryByGroupKey.get(oldGroupKey); + // We only care about app-provided valid groups + return (groupSummary != null && !GroupHelper.isAggregatedGroup(groupSummary)); + } + return false; + } + @GuardedBy("mAggregatedNotifications") private List<NotificationMoveOp> getAutogroupedNotificationsMoveOps(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck) { @@ -1298,7 +1341,8 @@ public class GroupHelper { @GuardedBy("mAggregatedNotifications") private void moveNotificationsToNewSection(final int userId, final String pkgName, - final List<NotificationMoveOp> notificationsToMove, int regroupingReason) { + final List<NotificationMoveOp> notificationsToMove, + final Map<String, Integer> regroupReasons) { record GroupUpdateOp(FullyQualifiedGroupKey groupKey, NotificationRecord record, boolean hasSummary) { } // Bundled operations to apply to groups affected by the channel update @@ -1317,7 +1361,7 @@ public class GroupHelper { Log.i(TAG, "moveNotificationToNewSection: " + record + " " + newFullAggregateGroupKey + " from: " + oldFullAggregateGroupKey + " regroupingReason: " - + regroupingReason); + + regroupReasons); } // Update/remove aggregate summary for old group @@ -1347,7 +1391,8 @@ public class GroupHelper { // after all notifications have been handled if (newFullAggregateGroupKey != null) { if (notificationRegroupOnClassification() - && regroupingReason == REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP) { + && regroupReasons.getOrDefault(record.getKey(), REGROUP_REASON_CHANNEL_UPDATE) + == REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP) { // Just reset override group key, original summary exists // => will be grouped back to its original group record.setOverrideGroupKey(null); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index a16b122771ef..21ac05c24259 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -346,6 +346,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.messages.nano.SystemMessageProto; +import com.android.internal.notification.NotificationChannelGroupsHelper; import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.os.BackgroundThread; import com.android.internal.os.SomeArgs; @@ -679,6 +680,8 @@ public class NotificationManagerService extends SystemService { WorkerHandler mHandler; private final HandlerThread mRankingThread = new HandlerThread("ranker", Process.THREAD_PRIORITY_BACKGROUND); + @FlaggedApi(Flags.FLAG_NM_BINDER_PERF_THROTTLE_EFFECTS_SUPPRESSOR_BROADCAST) + private Handler mBroadcastsHandler; private final SparseArray<ArraySet<ComponentName>> mListenersDisablingEffects = new SparseArray<>(); @@ -1927,27 +1930,17 @@ public class NotificationManagerService extends SystemService { if (DBG) { Slog.v(TAG, "unclassifyNotification: " + r); } - - boolean hasOriginalSummary = false; - if (r.getSbn().isAppGroup() && r.getNotification().isGroupChild()) { - final String oldGroupKey = GroupHelper.getFullAggregateGroupKey( - r.getSbn().getPackageName(), r.getOriginalGroupKey(), r.getUserId()); - NotificationRecord groupSummary = mSummaryByGroupKey.get(oldGroupKey); - // We only care about app-provided valid groups - hasOriginalSummary = (groupSummary != null - && !GroupHelper.isAggregatedGroup(groupSummary)); - } - // Only NotificationRecord's mChannel is updated when bundled, the Notification // mChannelId will always be the original channel. String origChannelId = r.getNotification().getChannelId(); NotificationChannel originalChannel = mPreferencesHelper.getNotificationChannel( r.getSbn().getPackageName(), r.getUid(), origChannelId, false); String currChannelId = r.getChannel().getId(); - boolean isBundled = NotificationChannel.SYSTEM_RESERVED_IDS.contains(currChannelId); - if (originalChannel != null && !origChannelId.equals(currChannelId) && isBundled) { + boolean isClassified = NotificationChannel.SYSTEM_RESERVED_IDS.contains(currChannelId); + if (originalChannel != null && !origChannelId.equals(currChannelId) && isClassified) { r.updateNotificationChannel(originalChannel); - mGroupHelper.onNotificationUnbundled(r, hasOriginalSummary); + mGroupHelper.onNotificationUnbundled(r, + GroupHelper.isOriginalGroupSummaryPresent(r, mSummaryByGroupKey)); } } @@ -2032,9 +2025,9 @@ public class NotificationManagerService extends SystemService { Slog.v(TAG, "reclassifyNotification: " + r); } - boolean isBundled = NotificationChannel.SYSTEM_RESERVED_IDS.contains( + boolean isClassified = NotificationChannel.SYSTEM_RESERVED_IDS.contains( r.getChannel().getId()); - if (r.getBundleType() != Adjustment.TYPE_OTHER && !isBundled) { + if (r.getBundleType() != Adjustment.TYPE_OTHER && !isClassified) { final Bundle classifBundle = new Bundle(); classifBundle.putInt(KEY_TYPE, r.getBundleType()); Adjustment adj = new Adjustment(r.getSbn().getPackageName(), r.getKey(), @@ -2682,7 +2675,7 @@ public class NotificationManagerService extends SystemService { // TODO: All tests should use this init instead of the one-off setters above. @VisibleForTesting - void init(WorkerHandler handler, RankingHandler rankingHandler, + void init(WorkerHandler handler, RankingHandler rankingHandler, Handler broadcastsHandler, IPackageManager packageManager, PackageManager packageManagerClient, LightsManager lightsManager, NotificationListeners notificationListeners, NotificationAssistants notificationAssistants, ConditionProviders conditionProviders, @@ -2702,6 +2695,9 @@ public class NotificationManagerService extends SystemService { ConnectivityManager connectivityManager, PostNotificationTrackerFactory postNotificationTrackerFactory) { mHandler = handler; + if (Flags.nmBinderPerfThrottleEffectsSuppressorBroadcast()) { + mBroadcastsHandler = broadcastsHandler; + } Resources resources = getContext().getResources(); mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE, @@ -3045,13 +3041,22 @@ public class NotificationManagerService extends SystemService { WorkerHandler handler = new WorkerHandler(Looper.myLooper()); + Handler broadcastsHandler; + if (Flags.nmBinderPerfThrottleEffectsSuppressorBroadcast()) { + HandlerThread broadcastsThread = new HandlerThread("NMS Broadcasts"); + broadcastsThread.start(); + broadcastsHandler = new Handler(broadcastsThread.getLooper()); + } else { + broadcastsHandler = null; + } + mShowReviewPermissionsNotification = getContext().getResources().getBoolean( R.bool.config_notificationReviewPermissions); mDefaultUnsupportedAdjustments = getContext().getResources().getStringArray( R.array.config_notificationDefaultUnsupportedAdjustments); - init(handler, new RankingHandlerWorker(mRankingThread.getLooper()), + init(handler, new RankingHandlerWorker(mRankingThread.getLooper()), broadcastsHandler, AppGlobals.getPackageManager(), getContext().getPackageManager(), getLocalService(LightsManager.class), new NotificationListeners(getContext(), mNotificationLock, mUserProfiles, @@ -3297,10 +3302,11 @@ public class NotificationManagerService extends SystemService { * so that e.g. rapidly changing some value A -> B -> C will only produce a broadcast for C * (instead of every time because the extras are different). */ + @FlaggedApi(Flags.FLAG_NM_BINDER_PERF_THROTTLE_EFFECTS_SUPPRESSOR_BROADCAST) private void sendZenBroadcastWithDelay(Intent intent) { String token = "zen_broadcast:" + intent.getAction(); - mHandler.removeCallbacksAndEqualMessages(token); - mHandler.postDelayed(() -> sendRegisteredOnlyBroadcast(intent), token, + mBroadcastsHandler.removeCallbacksAndEqualMessages(token); + mBroadcastsHandler.postDelayed(() -> sendRegisteredOnlyBroadcast(intent), token, ZEN_BROADCAST_DELAY.toMillis()); } @@ -3568,7 +3574,7 @@ public class NotificationManagerService extends SystemService { synchronized (mNotificationLock) { mGroupHelper.onChannelUpdated( UserHandle.getUserHandleForUid(uid).getIdentifier(), pkg, - updatedChannel, mNotificationList); + updatedChannel, mNotificationList, mSummaryByGroupKey); } }, DELAY_FORCE_REGROUP_TIME); } @@ -5005,8 +5011,8 @@ public class NotificationManagerService extends SystemService { public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups( String pkg) { checkCallerIsSystemOrSameApp(pkg); - return mPreferencesHelper.getNotificationChannelGroups( - pkg, Binder.getCallingUid(), false, false, true, true, null); + return mPreferencesHelper.getNotificationChannelGroups(pkg, Binder.getCallingUid(), + NotificationChannelGroupsHelper.Params.forAllGroups()); } @Override @@ -5126,8 +5132,9 @@ public class NotificationManagerService extends SystemService { public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroupsForPackage( String pkg, int uid, boolean includeDeleted) { enforceSystemOrSystemUI("getNotificationChannelGroupsForPackage"); - return mPreferencesHelper.getNotificationChannelGroups( - pkg, uid, includeDeleted, true, false, true, null); + return mPreferencesHelper.getNotificationChannelGroups(pkg, uid, + new NotificationChannelGroupsHelper.Params(includeDeleted, true, false, true, + null)); } @Override @@ -5155,8 +5162,9 @@ public class NotificationManagerService extends SystemService { } } - return mPreferencesHelper.getNotificationChannelGroups( - pkg, uid, false, true, false, true, recentlySentChannels); + return mPreferencesHelper.getNotificationChannelGroups(pkg, uid, + NotificationChannelGroupsHelper.Params.onlySpecifiedOrBlockedChannels( + recentlySentChannels)); } @Override diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 7d45cd9752b8..33c94a7e63da 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -92,6 +92,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags; import com.android.internal.logging.MetricsLogger; +import com.android.internal.notification.NotificationChannelGroupsHelper; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.Preconditions; import com.android.internal.util.XmlUtils; @@ -1678,18 +1679,8 @@ public class PreferencesHelper implements RankingConfig { if (r == null || groupId == null || !r.groups.containsKey(groupId)) { return null; } - NotificationChannelGroup group = r.groups.get(groupId).clone(); - group.setChannels(new ArrayList<>()); - int N = r.channels.size(); - for (int i = 0; i < N; i++) { - final NotificationChannel nc = r.channels.valueAt(i); - if (includeDeleted || !nc.isDeleted()) { - if (groupId.equals(nc.getGroup())) { - group.addChannel(nc); - } - } - } - return group; + return NotificationChannelGroupsHelper.getGroupWithChannels(groupId, + r.channels.values(), r.groups, includeDeleted); } } @@ -1706,51 +1697,16 @@ public class PreferencesHelper implements RankingConfig { } public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg, - int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty, - boolean includeBlocked, Set<String> activeChannelFilter) { + int uid, NotificationChannelGroupsHelper.Params params) { Objects.requireNonNull(pkg); - Map<String, NotificationChannelGroup> groups = new ArrayMap<>(); synchronized (mLock) { PackagePreferences r = getPackagePreferencesLocked(pkg, uid); if (r == null) { return ParceledListSlice.emptyList(); } - NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null); - int N = r.channels.size(); - for (int i = 0; i < N; i++) { - final NotificationChannel nc = r.channels.valueAt(i); - boolean includeChannel = (includeDeleted || !nc.isDeleted()) - && (activeChannelFilter == null - || (includeBlocked && nc.getImportance() == IMPORTANCE_NONE) - || activeChannelFilter.contains(nc.getId())) - && !SYSTEM_RESERVED_IDS.contains(nc.getId()); - if (includeChannel) { - if (nc.getGroup() != null) { - if (r.groups.get(nc.getGroup()) != null) { - NotificationChannelGroup ncg = groups.get(nc.getGroup()); - if (ncg == null) { - ncg = r.groups.get(nc.getGroup()).clone(); - ncg.setChannels(new ArrayList<>()); - groups.put(nc.getGroup(), ncg); - } - ncg.addChannel(nc); - } - } else { - nonGrouped.addChannel(nc); - } - } - } - if (includeNonGrouped && nonGrouped.getChannels().size() > 0) { - groups.put(null, nonGrouped); - } - if (includeEmpty) { - for (NotificationChannelGroup group : r.groups.values()) { - if (!groups.containsKey(group.getId())) { - groups.put(group.getId(), group); - } - } - } - return new ParceledListSlice<>(new ArrayList<>(groups.values())); + return new ParceledListSlice<>( + NotificationChannelGroupsHelper.getGroupsWithChannels(r.channels.values(), + r.groups, params)); } } diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java index b8b49f3eed2f..f9758fcd5d01 100644 --- a/services/core/java/com/android/server/om/OverlayManagerSettings.java +++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java @@ -26,13 +26,13 @@ import android.content.om.OverlayInfo; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.IndentingPrintWriter; import android.util.Pair; import android.util.Slog; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.CollectionUtils; -import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -49,7 +49,6 @@ import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.stream.Stream; /** * Data structure representing the current state of all overlay packages in the @@ -358,26 +357,29 @@ final class OverlayManagerSettings { } void dump(@NonNull final PrintWriter p, @NonNull DumpState dumpState) { - // select items to display - Stream<SettingsItem> items = mItems.stream(); - if (dumpState.getUserId() != UserHandle.USER_ALL) { - items = items.filter(item -> item.mUserId == dumpState.getUserId()); - } - if (dumpState.getPackageName() != null) { - items = items.filter(item -> item.mOverlay.getPackageName() - .equals(dumpState.getPackageName())); - } - if (dumpState.getOverlayName() != null) { - items = items.filter(item -> item.mOverlay.getOverlayName() - .equals(dumpState.getOverlayName())); - } - - // display items - final IndentingPrintWriter pw = new IndentingPrintWriter(p, " "); - if (dumpState.getField() != null) { - items.forEach(item -> dumpSettingsItemField(pw, item, dumpState.getField())); - } else { - items.forEach(item -> dumpSettingsItem(pw, item)); + final int userId = dumpState.getUserId(); + final String packageName = dumpState.getPackageName(); + final String overlayName = dumpState.getOverlayName(); + final String field = dumpState.getField(); + final var pw = new IndentingPrintWriter(p, " "); + + for (int i = 0; i < mItems.size(); i++) { + final var item = mItems.get(i); + if (userId != UserHandle.USER_ALL && userId != item.mUserId) { + continue; + } + if (packageName != null && !packageName.equals(item.mOverlay.getPackageName())) { + continue; + } + if (overlayName != null && !overlayName.equals(item.mOverlay.getOverlayName())) { + continue; + } + + if (field != null) { + dumpSettingsItemField(pw, item, field); + } else { + dumpSettingsItem(pw, item); + } } } diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java index c62aaebf673b..4d97a83fc6b4 100644 --- a/services/core/java/com/android/server/pm/UserManagerInternal.java +++ b/services/core/java/com/android/server/pm/UserManagerInternal.java @@ -23,7 +23,9 @@ import android.content.Context; import android.content.pm.LauncherUserInfo; import android.content.pm.UserInfo; import android.content.pm.UserProperties; +import android.content.res.Resources; import android.graphics.Bitmap; +import android.multiuser.Flags; import android.os.Bundle; import android.os.UserManager; import android.util.DebugUtils; @@ -617,4 +619,14 @@ public abstract class UserManagerInternal { * if there is no such user. */ public abstract @UserIdInt int getCommunalProfileId(); + + /** + * Checks whether to show a notification for sounds (e.g., alarms, timers, etc.) from + * background users. + */ + public static boolean shouldShowNotificationForBackgroundUserSounds() { + return Flags.addUiForSoundsFromBackgroundUsers() && Resources.getSystem().getBoolean( + com.android.internal.R.bool.config_showNotificationForBackgroundUserAlarms) + && UserManager.supportsMultipleUsers(); + } } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 8cbccf5feead..0a90c3644c2f 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -33,7 +33,6 @@ import static android.os.UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY; import static android.os.UserManager.USER_OPERATION_ERROR_UNKNOWN; import static android.os.UserManager.USER_OPERATION_ERROR_USER_RESTRICTED; import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; -import static android.os.UserManager.supportsMultipleUsers; import static android.provider.Settings.Secure.HIDE_PRIVATESPACE_ENTRY_POINT; import static com.android.internal.app.SetScreenLockDialogActivity.EXTRA_ORIGIN_USER_ID; @@ -60,7 +59,6 @@ import android.annotation.RequiresPermission; import android.annotation.SpecialUsers.CanBeALL; import android.annotation.SpecialUsers.CanBeCURRENT; import android.annotation.SpecialUsers.CanBeNULL; -import android.annotation.SpecialUsers.CannotBeSpecialUser; import android.annotation.StringRes; import android.annotation.UserIdInt; import android.app.ActivityManager; @@ -1161,7 +1159,7 @@ public class UserManagerService extends IUserManager.Stub { showHsumNotificationIfNeeded(); - if (shouldShowNotificationForBackgroundUserSounds()) { + if (UserManagerInternal.shouldShowNotificationForBackgroundUserSounds()) { new BackgroundUserSoundNotifier(mContext); } } @@ -8499,16 +8497,6 @@ public class UserManagerService extends IUserManager.Stub { } /** - * Checks whether to show a notification for sounds (e.g., alarms, timers, etc.) from - * background users. - */ - public static boolean shouldShowNotificationForBackgroundUserSounds() { - return Flags.addUiForSoundsFromBackgroundUsers() && Resources.getSystem().getBoolean( - com.android.internal.R.bool.config_showNotificationForBackgroundUserAlarms) - && supportsMultipleUsers(); - } - - /** * Returns instance of {@link com.android.server.pm.UserJourneyLogger}. */ public UserJourneyLogger getUserJourneyLogger() { diff --git a/services/core/java/com/android/server/slice/SlicePermissionManager.java b/services/core/java/com/android/server/slice/SlicePermissionManager.java index 343d2e353abb..d118eaea37d9 100644 --- a/services/core/java/com/android/server/slice/SlicePermissionManager.java +++ b/services/core/java/com/android/server/slice/SlicePermissionManager.java @@ -29,6 +29,7 @@ import android.util.Log; import android.util.Slog; import android.util.Xml.Encoding; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.XmlUtils; import com.android.server.slice.SliceProviderPermissions.SliceAuthority; @@ -76,8 +77,11 @@ public class SlicePermissionManager implements DirtyTracker { private final File mSliceDir; private final Context mContext; private final Handler mHandler; + @GuardedBy("itself") private final ArrayMap<PkgUser, SliceProviderPermissions> mCachedProviders = new ArrayMap<>(); + @GuardedBy("itself") private final ArrayMap<PkgUser, SliceClientPermissions> mCachedClients = new ArrayMap<>(); + @GuardedBy("this") private final ArraySet<Persistable> mDirty = new ArraySet<>(); @VisibleForTesting @@ -354,14 +358,22 @@ public class SlicePermissionManager implements DirtyTracker { // use addPersistableDirty(); this is just for tests @VisibleForTesting void addDirtyImmediate(Persistable obj) { - mDirty.add(obj); + synchronized (this) { + mDirty.add(obj); + } } private void handleRemove(PkgUser pkgUser) { getFile(SliceClientPermissions.getFileName(pkgUser)).delete(); getFile(SliceProviderPermissions.getFileName(pkgUser)).delete(); - mDirty.remove(mCachedClients.remove(pkgUser)); - mDirty.remove(mCachedProviders.remove(pkgUser)); + synchronized (this) { + synchronized (mCachedClients) { + mDirty.remove(mCachedClients.remove(pkgUser)); + } + synchronized (mCachedProviders) { + mDirty.remove(mCachedProviders.remove(pkgUser)); + } + } } private final class H extends Handler { @@ -379,7 +391,9 @@ public class SlicePermissionManager implements DirtyTracker { public void handleMessage(Message msg) { switch (msg.what) { case MSG_ADD_DIRTY: - mDirty.add((Persistable) msg.obj); + synchronized (SlicePermissionManager.this) { + mDirty.add((Persistable) msg.obj); + } break; case MSG_PERSIST: handlePersist(); diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index 0ed522805bef..a80b1b2dd9e8 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -171,11 +171,11 @@ public interface StatusBarManagerInternal { void onProposedRotationChanged(int displayId, int rotation, boolean isValid); /** - * Notifies System UI that the display is ready to show system decorations. + * Notifies System UI that the system decorations should be added on the display. * * @param displayId display ID */ - void onDisplayReady(int displayId); + void onDisplayAddSystemDecorations(int displayId); /** * Notifies System UI that the system decorations should be removed from the display. diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index e753f273eb9b..c546388e4499 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -87,7 +87,6 @@ import android.service.quicksettings.TileService; import android.text.TextUtils; import android.util.ArrayMap; import android.util.IndentingPrintWriter; -import android.util.IntArray; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; @@ -125,7 +124,6 @@ import com.android.server.policy.GlobalActionsProvider; import com.android.server.power.ShutdownCheckPoints; import com.android.server.power.ShutdownThread; import com.android.server.wm.ActivityTaskManagerInternal; -import com.android.systemui.shared.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -343,19 +341,15 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void onDisplayAdded(int displayId) { - if (Flags.statusBarConnectedDisplays()) { - synchronized (mLock) { - mDisplayUiState.put(displayId, new UiState()); - } + synchronized (mLock) { + mDisplayUiState.put(displayId, new UiState()); } } @Override public void onDisplayRemoved(int displayId) { - if (Flags.statusBarConnectedDisplays()) { - synchronized (mLock) { - mDisplayUiState.remove(displayId); - } + synchronized (mLock) { + mDisplayUiState.remove(displayId); } } @@ -776,10 +770,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void onDisplayReady(int displayId) { + public void onDisplayAddSystemDecorations(int displayId) { if (isVisibleBackgroundUserOnDisplay(displayId)) { if (SPEW) { - Slog.d(TAG, "Skipping onDisplayReady for visible background user " + Slog.d(TAG, "Skipping onDisplayAddSystemDecorations for visible background " + + "user " + mUserManagerInternal.getUserAssignedToDisplay(displayId)); } return; @@ -787,7 +782,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D IStatusBar bar = mBar; if (bar != null) { try { - bar.onDisplayReady(displayId); + bar.onDisplayAddSystemDecorations(displayId); } catch (RemoteException ex) {} } } @@ -1366,66 +1361,53 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D return mTracingEnabled; } + // TODO(b/117478341): make it aware of multi-display if needed. @Override public void disable(int what, IBinder token, String pkg) { disableForUser(what, token, pkg, mCurrentUserId); } - /** - * Disable additional status bar features for user for all displays. Pass the bitwise-or of the - * {@code #DISABLE_*} flags. To re-enable everything, pass {@code #DISABLE_NONE}. - * - * Warning: Only pass {@code #DISABLE_*} flags into this function, do not use - * {@code #DISABLE2_*} flags. - */ + // TODO(b/117478341): make it aware of multi-display if needed. @Override public void disableForUser(int what, IBinder token, String pkg, int userId) { enforceStatusBar(); enforceValidCallingUser(); synchronized (mLock) { - IntArray displayIds = new IntArray(); - for (int i = 0; i < mDisplayUiState.size(); i++) { - displayIds.add(mDisplayUiState.keyAt(i)); - } - disableLocked(displayIds, userId, what, token, pkg, 1); + disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, 1); } } + // TODO(b/117478341): make it aware of multi-display if needed. /** - * Disable additional status bar features. Pass the bitwise-or of the {@code #DISABLE2_*} flags. - * To re-enable everything, pass {@code #DISABLE2_NONE}. + * Disable additional status bar features. Pass the bitwise-or of the DISABLE2_* flags. + * To re-enable everything, pass {@link #DISABLE2_NONE}. * - * Warning: Only pass {@code #DISABLE2_*} flags into this function, do not use - * {@code #DISABLE_*} flags. + * Warning: Only pass DISABLE2_* flags into this function, do not use DISABLE_* flags. */ @Override public void disable2(int what, IBinder token, String pkg) { disable2ForUser(what, token, pkg, mCurrentUserId); } + // TODO(b/117478341): make it aware of multi-display if needed. /** - * Disable additional status bar features for a given user for all displays. Pass the bitwise-or - * of the {@code #DISABLE2_*} flags. To re-enable everything, pass {@code #DISABLE2_NONE}. + * Disable additional status bar features for a given user. Pass the bitwise-or of the + * DISABLE2_* flags. To re-enable everything, pass {@link #DISABLE_NONE}. * - * Warning: Only pass {@code #DISABLE2_*} flags into this function, do not use - * {@code #DISABLE_*} flags. + * Warning: Only pass DISABLE2_* flags into this function, do not use DISABLE_* flags. */ @Override public void disable2ForUser(int what, IBinder token, String pkg, int userId) { enforceStatusBar(); synchronized (mLock) { - IntArray displayIds = new IntArray(); - for (int i = 0; i < mDisplayUiState.size(); i++) { - displayIds.add(mDisplayUiState.keyAt(i)); - } - disableLocked(displayIds, userId, what, token, pkg, 2); + disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, 2); } } - private void disableLocked(IntArray displayIds, int userId, int what, IBinder token, - String pkg, int whichFlag) { + private void disableLocked(int displayId, int userId, int what, IBinder token, String pkg, + int whichFlag) { // It's important that the the callback and the call to mBar get done // in the same order when multiple threads are calling this function // so they are paired correctly. The messages on the handler will be @@ -1435,27 +1417,18 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D // Ensure state for the current user is applied, even if passed a non-current user. final int net1 = gatherDisableActionsLocked(mCurrentUserId, 1); final int net2 = gatherDisableActionsLocked(mCurrentUserId, 2); - boolean shouldCallNotificationOnSetDisabled = false; - IStatusBar bar = mBar; - for (int displayId : displayIds.toArray()) { - final UiState state = getUiState(displayId); - if (!state.disableEquals(net1, net2)) { - shouldCallNotificationOnSetDisabled = true; - state.setDisabled(net1, net2); - if (bar != null) { - try { - // TODO(b/388244660): Create IStatusBar#disableForAllDisplays to avoid - // multiple IPC calls. - bar.disable(displayId, net1, net2); - } catch (RemoteException ex) { - Slog.e(TAG, "Unable to disable Status bar.", ex); - } + final UiState state = getUiState(displayId); + if (!state.disableEquals(net1, net2)) { + state.setDisabled(net1, net2); + mHandler.post(() -> mNotificationDelegate.onSetDisabled(net1)); + IStatusBar bar = mBar; + if (bar != null) { + try { + bar.disable(displayId, net1, net2); + } catch (RemoteException ex) { } } } - if (shouldCallNotificationOnSetDisabled) { - mHandler.post(() -> mNotificationDelegate.onSetDisabled(net1)); - } } /** @@ -1610,8 +1583,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D if (SPEW) Slog.d(TAG, "setDisableFlags(0x" + Integer.toHexString(flags) + ")"); synchronized (mLock) { - disableLocked(IntArray.wrap(new int[]{displayId}), mCurrentUserId, flags, - mSysUiVisToken, cause, 1); + disableLocked(displayId, mCurrentUserId, flags, mSysUiVisToken, cause, 1); } } diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index a5805043ac42..804cf4663bfd 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -79,7 +79,7 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.util.DumpUtils; import com.android.server.SystemService; import com.android.server.pm.BackgroundUserSoundNotifier; -import com.android.server.pm.UserManagerService; +import com.android.server.pm.UserManagerInternal; import com.android.server.vibrator.VibrationSession.CallerInfo; import com.android.server.vibrator.VibrationSession.DebugInfo; import com.android.server.vibrator.VibrationSession.Status; @@ -201,7 +201,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { VibratorManagerService.this::shouldCancelOnScreenOffLocked, Status.CANCELLED_BY_SCREEN_OFF); } - } else if (UserManagerService.shouldShowNotificationForBackgroundUserSounds() + } else if (UserManagerInternal.shouldShowNotificationForBackgroundUserSounds() && intent.getAction().equals(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND)) { synchronized (mLock) { maybeClearCurrentAndNextSessionsLocked( @@ -325,7 +325,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_OFF); - if (UserManagerService.shouldShowNotificationForBackgroundUserSounds()) { + if (UserManagerInternal.shouldShowNotificationForBackgroundUserSounds()) { filter.addAction(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND); } context.registerReceiver(mIntentReceiver, filter, Context.RECEIVER_NOT_EXPORTED); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java index 872ab595994b..f413fe33c3f2 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java @@ -26,7 +26,7 @@ public abstract class WallpaperManagerInternal { /** * Notifies the display is ready for adding wallpaper on it. */ - public abstract void onDisplayReady(int displayId); + public abstract void onDisplayAddSystemDecorations(int displayId); /** Notifies when display stop showing system decorations and wallpaper. */ public abstract void onDisplayRemoveSystemDecorations(int displayId); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index db530e728a1a..09b10739d469 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -1636,8 +1636,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub private final class LocalService extends WallpaperManagerInternal { @Override - public void onDisplayReady(int displayId) { - onDisplayReadyInternal(displayId); + public void onDisplayAddSystemDecorations(int displayId) { + onDisplayAddSystemDecorationsInternal(displayId); } @Override @@ -3944,7 +3944,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return (wallpaper != null) ? wallpaper.allowBackup : false; } - private void onDisplayReadyInternal(int displayId) { + private void onDisplayAddSystemDecorationsInternal(int displayId) { synchronized (mLock) { if (mLastWallpaper == null) { return; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 81c7807311dd..6f76618b0029 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -71,6 +71,8 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RESULTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_USER_LEAVING; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_CONFIGURATION; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_FOCUS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RESULTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_USER_LEAVING; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; @@ -89,7 +91,9 @@ import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_NEW_TASK; import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_UNTRUSTED_HOST; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; +import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -153,6 +157,8 @@ import com.android.server.wm.TaskFragment.EmbeddingCheckResult; import com.android.wm.shell.Flags; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.text.DateFormat; import java.util.Date; import java.util.function.Supplier; @@ -166,6 +172,8 @@ import java.util.function.Supplier; class ActivityStarter { private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityStarter" : TAG_ATM; private static final String TAG_RESULTS = TAG + POSTFIX_RESULTS; + private static final String TAG_FOCUS = TAG + POSTFIX_FOCUS; + private static final String TAG_CONFIGURATION = TAG + POSTFIX_CONFIGURATION; private static final String TAG_USER_LEAVING = TAG + POSTFIX_USER_LEAVING; private static final int INVALID_LAUNCH_MODE = -1; @@ -247,7 +255,26 @@ class ActivityStarter { private boolean mIsTaskCleared; private boolean mMovedToFront; private boolean mNoAnimation; - private boolean mAvoidMoveToFront; + + // TODO mAvoidMoveToFront before V is changed from a boolean to a int code mCanMoveToFrontCode + // for the purpose of attribution of new BAL V feature. This should be reverted back to the + // boolean flag post V. + @IntDef(prefix = {"MOVE_TO_FRONT_"}, value = { + MOVE_TO_FRONT_ALLOWED, + MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS, + MOVE_TO_FRONT_AVOID_LEGACY, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface MoveToFrontCode {} + + // Allows a task move to front. + private static final int MOVE_TO_FRONT_ALLOWED = 0; + // Avoid a task move to front because the Pending Intent that starts the activity only + // its creator has the BAL privilege, its sender does not. + private static final int MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS = 1; + // Avoid a task move to front because of all other legacy reasons. + private static final int MOVE_TO_FRONT_AVOID_LEGACY = 2; + private @MoveToFrontCode int mCanMoveToFrontCode = MOVE_TO_FRONT_ALLOWED; private boolean mFrozeTaskList; private boolean mTransientLaunch; // The task which was above the targetTask before starting this activity. null if the targetTask @@ -744,7 +771,7 @@ class ActivityStarter { mIsTaskCleared = starter.mIsTaskCleared; mMovedToFront = starter.mMovedToFront; mNoAnimation = starter.mNoAnimation; - mAvoidMoveToFront = starter.mAvoidMoveToFront; + mCanMoveToFrontCode = starter.mCanMoveToFrontCode; mFrozeTaskList = starter.mFrozeTaskList; mVoiceSession = starter.mVoiceSession; @@ -1684,6 +1711,14 @@ class ActivityStarter { return result; } + private boolean avoidMoveToFront() { + return mCanMoveToFrontCode != MOVE_TO_FRONT_ALLOWED; + } + + private boolean avoidMoveToFrontPIOnlyCreatorAllows() { + return mCanMoveToFrontCode == MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS; + } + /** * If the start result is success, ensure that the configuration of the started activity matches * the current display. Otherwise clean up unassociated containers to avoid leakage. @@ -1733,7 +1768,7 @@ class ActivityStarter { startedActivityRootTask.setAlwaysOnTop(true); } - if (isIndependentLaunch && !mDoResume && mAvoidMoveToFront && !mTransientLaunch + if (isIndependentLaunch && !mDoResume && avoidMoveToFront() && !mTransientLaunch && !started.shouldBeVisible(true /* ignoringKeyguard */)) { Slog.i(TAG, "Abort " + transition + " of invisible launch " + started); transition.abort(); @@ -1749,7 +1784,7 @@ class ActivityStarter { currentTop, currentTop.mDisplayContent, false /* deferResume */); } - if (!mAvoidMoveToFront && mDoResume + if (!avoidMoveToFront() && mDoResume && !mService.getUserManagerInternal().isVisibleBackgroundFullUser(started.mUserId) && mRootWindowContainer.hasVisibleWindowAboveButDoesNotOwnNotificationShade( started.launchedFromUid)) { @@ -1899,17 +1934,19 @@ class ActivityStarter { } // When running transient transition, the transient launch target should keep on top. // So disallow the transient hide activity to move itself to front, e.g. trampoline. - if (!mAvoidMoveToFront && (mService.mHomeProcess == null + if (!avoidMoveToFront() && (mService.mHomeProcess == null || mService.mHomeProcess.mUid != realCallingUid) && (prevTopTask != null && prevTopTask.isActivityTypeHomeOrRecents()) && r.mTransitionController.isTransientHide(targetTask)) { - mAvoidMoveToFront = true; + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; } // If the activity is started by sending a pending intent and only its creator has the // privilege to allow BAL (its sender does not), avoid move it to the front. Only do // this when it is not a new task and not already been marked as avoid move to front. - if (!mAvoidMoveToFront && balVerdict.onlyCreatorAllows()) { - mAvoidMoveToFront = true; + // Guarded by a flag: balDontBringExistingBackgroundTaskStackToFg + if (balDontBringExistingBackgroundTaskStackToFg() && !avoidMoveToFront() + && balVerdict.onlyCreatorAllows()) { + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS; } mPriorAboveTask = TaskDisplayArea.getRootTaskAbove(targetTask.getRootTask()); } @@ -1966,28 +2003,32 @@ class ActivityStarter { // After activity is attached to task, but before actual start recordTransientLaunchIfNeeded(mLastStartActivityRecord); - if (!mAvoidMoveToFront && mDoResume) { - mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask); + if (mDoResume) { + if (!avoidMoveToFront()) { + mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask); + + final boolean launchBehindDream; + if (com.android.window.flags.Flags.removeActivityStarterDreamCallback()) { + final TaskDisplayArea tda = mTargetRootTask.getTaskDisplayArea(); + final Task top = (tda != null ? tda.getTopRootTask() : null); + launchBehindDream = (top != null && top != mTargetRootTask) + && top.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_DREAM + && top.getTopNonFinishingActivity() != null; + } else { + launchBehindDream = !mTargetRootTask.isTopRootTaskInDisplayArea() + && mService.isDreaming() + && !dreamStopping; + } - final boolean launchBehindDream; - if (com.android.window.flags.Flags.removeActivityStarterDreamCallback()) { - final TaskDisplayArea tda = mTargetRootTask.getTaskDisplayArea(); - final Task top = (tda != null ? tda.getTopRootTask() : null); - launchBehindDream = (top != null && top != mTargetRootTask) - && top.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_DREAM - && top.getTopNonFinishingActivity() != null; + if (launchBehindDream) { + // Launching underneath dream activity (fullscreen, always-on-top). Run the + // launch--behind transition so the Activity gets created and starts + // in visible state. + mLaunchTaskBehind = true; + r.mLaunchTaskBehind = true; + } } else { - launchBehindDream = !mTargetRootTask.isTopRootTaskInDisplayArea() - && mService.isDreaming() - && !dreamStopping; - } - - if (launchBehindDream) { - // Launching underneath dream activity (fullscreen, always-on-top). Run the - // launch--behind transition so the Activity gets created and starts - // in visible state. - mLaunchTaskBehind = true; - r.mLaunchTaskBehind = true; + logPIOnlyCreatorAllowsBAL(); } } @@ -2048,9 +2089,13 @@ class ActivityStarter { // root-task to the will not update the focused root-task. If starting the new // activity now allows the task root-task to be focusable, then ensure that we // now update the focused root-task accordingly. - if (!mAvoidMoveToFront && mTargetRootTask.isTopActivityFocusable() + if (mTargetRootTask.isTopActivityFocusable() && !mRootWindowContainer.isTopDisplayFocusedRootTask(mTargetRootTask)) { - mTargetRootTask.moveToFront("startActivityInner"); + if (!avoidMoveToFront()) { + mTargetRootTask.moveToFront("startActivityInner"); + } else { + logPIOnlyCreatorAllowsBAL(); + } } mRootWindowContainer.resumeFocusedTasksTopActivities( mTargetRootTask, mStartActivity, mOptions, mTransientLaunch); @@ -2078,6 +2123,26 @@ class ActivityStarter { return START_SUCCESS; } + // TODO (b/316135632) Post V release, remove this log method. + private void logPIOnlyCreatorAllowsBAL() { + if (!avoidMoveToFrontPIOnlyCreatorAllows()) return; + String realCallingPackage = + mService.mContext.getPackageManager().getNameForUid(mRealCallingUid); + if (realCallingPackage == null) { + realCallingPackage = "uid=" + mRealCallingUid; + } + Slog.wtf(TAG, "Without Android 15 BAL hardening this activity would be moved to the " + + "foreground. The activity is started by a PendingIntent. However, only the " + + "creator of the PendingIntent allows BAL while the sender does not allow BAL. " + + "realCallingPackage: " + realCallingPackage + + "; callingPackage: " + mRequest.callingPackage + + "; mTargetRootTask:" + mTargetRootTask + + "; mIntent: " + mIntent + + "; mTargetRootTask.getTopNonFinishingActivity: " + + mTargetRootTask.getTopNonFinishingActivity() + + "; mTargetRootTask.getRootActivity: " + mTargetRootTask.getRootActivity()); + } + private void recordTransientLaunchIfNeeded(ActivityRecord r) { if (r == null || !mTransientLaunch) return; final TransitionController controller = r.mTransitionController; @@ -2222,7 +2287,7 @@ class ActivityStarter { } if (!mSupervisor.getBackgroundActivityLaunchController().checkActivityAllowedToStart( - mSourceRecord, r, newTask, mAvoidMoveToFront, targetTask, mLaunchFlags, mBalCode, + mSourceRecord, r, newTask, avoidMoveToFront(), targetTask, mLaunchFlags, mBalCode, mCallingUid, mRealCallingUid, mPreferredTaskDisplayArea)) { return START_ABORTED; } @@ -2570,7 +2635,7 @@ class ActivityStarter { mIsTaskCleared = false; mMovedToFront = false; mNoAnimation = false; - mAvoidMoveToFront = false; + mCanMoveToFrontCode = MOVE_TO_FRONT_ALLOWED; mFrozeTaskList = false; mTransientLaunch = false; mPriorAboveTask = null; @@ -2682,12 +2747,12 @@ class ActivityStarter { // The caller specifies that we'd like to be avoided to be moved to the // front, so be it! mDoResume = false; - mAvoidMoveToFront = true; + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; } } } else if (mOptions.getAvoidMoveToFront()) { mDoResume = false; - mAvoidMoveToFront = true; + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; } mTransientLaunch = mOptions.getTransientLaunch(); final KeyguardController kc = mSupervisor.getKeyguardController(); @@ -2697,7 +2762,7 @@ class ActivityStarter { if (mTransientLaunch && mDisplayLockAndOccluded && mService.getTransitionController().isShellTransitionsEnabled()) { mDoResume = false; - mAvoidMoveToFront = true; + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; } mTargetRootTask = Task.fromWindowContainerToken(mOptions.getLaunchRootTask()); @@ -2754,7 +2819,7 @@ class ActivityStarter { mNoAnimation = (mLaunchFlags & FLAG_ACTIVITY_NO_ANIMATION) != 0; if (mBalCode == BAL_BLOCK && !mService.isBackgroundActivityStartsEnabled()) { - mAvoidMoveToFront = true; + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; mDoResume = false; } } @@ -2985,7 +3050,7 @@ class ActivityStarter { differentTopTask = true; } - if (differentTopTask && !mAvoidMoveToFront) { + if (differentTopTask && !avoidMoveToFront()) { mStartActivity.intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); // We really do want to push this one into the user's face, right now. if (mLaunchTaskBehind && mSourceRecord != null) { @@ -3029,6 +3094,9 @@ class ActivityStarter { } mOptions = null; } + if (differentTopTask) { + logPIOnlyCreatorAllowsBAL(); + } // Update the target's launch cookie and pending remote animation to those specified in the // options if set. if (mStartActivity.mLaunchCookie != null) { @@ -3069,7 +3137,7 @@ class ActivityStarter { } private void setNewTask(Task taskToAffiliate) { - final boolean toTop = !mLaunchTaskBehind && !mAvoidMoveToFront; + final boolean toTop = !mLaunchTaskBehind && !avoidMoveToFront(); final Task task = mTargetRootTask.reuseOrCreateTask( mStartActivity.info, mIntent, mVoiceSession, mVoiceInteractor, toTop, mStartActivity, mSourceRecord, mOptions); @@ -3587,7 +3655,7 @@ class ActivityStarter { private static String getIntentRedirectPreventedLogMessage(@NonNull String message, @NonNull Intent intent, int intentCreatorUid, @Nullable String intentCreatorPackage, int callingUid, @Nullable String callingPackage) { - return "[IntentRedirect]" + message + " intentCreatorUid: " + intentCreatorUid + return "[IntentRedirect Hardening] " + message + " intentCreatorUid: " + intentCreatorUid + "; intentCreatorPackage: " + intentCreatorPackage + "; callingUid: " + callingUid + "; callingPackage: " + callingPackage + "; intent: " + intent; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index cf111cdbcc6a..ddb9f178cb8b 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -2988,38 +2988,45 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { throw new SecurityException("Requires permission " + android.Manifest.permission.DEVICE_POWER); } - - synchronized (mGlobalLock) { - final long ident = Binder.clearCallingIdentity(); - if (mKeyguardShown != keyguardShowing) { - mKeyguardShown = keyguardShowing; - final Message msg = PooledLambda.obtainMessage( - ActivityManagerInternal::reportCurKeyguardUsageEvent, mAmInternal, - keyguardShowing); - mH.sendMessage(msg); - } - // Always reset the state regardless of keyguard-showing change, because that means the - // unlock is either completed or canceled. - if ((mDemoteTopAppReasons & DEMOTE_TOP_REASON_DURING_UNLOCKING) != 0) { - mDemoteTopAppReasons &= ~DEMOTE_TOP_REASON_DURING_UNLOCKING; - // The scheduling group of top process was demoted by unlocking, so recompute - // to restore its real top priority if possible. - if (mTopApp != null) { - mTopApp.scheduleUpdateOomAdj(); - } + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + setLockScreenShownLocked(keyguardShowing, aodShowing); } - try { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "setLockScreenShown"); - mRootWindowContainer.forAllDisplays(displayContent -> { - mKeyguardController.setKeyguardShown(displayContent.getDisplayId(), - keyguardShowing, aodShowing); - }); - maybeHideLockedProfileActivityLocked(); - } finally { - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - Binder.restoreCallingIdentity(ident); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @GuardedBy("mGlobalLock") + void setLockScreenShownLocked(boolean keyguardShowing, boolean aodShowing) { + if (mKeyguardShown != keyguardShowing) { + mKeyguardShown = keyguardShowing; + final Message msg = PooledLambda.obtainMessage( + ActivityManagerInternal::reportCurKeyguardUsageEvent, mAmInternal, + keyguardShowing); + mH.sendMessage(msg); + } + // Always reset the state regardless of keyguard-showing change, because that means the + // unlock is either completed or canceled. + if ((mDemoteTopAppReasons & DEMOTE_TOP_REASON_DURING_UNLOCKING) != 0) { + mDemoteTopAppReasons &= ~DEMOTE_TOP_REASON_DURING_UNLOCKING; + // The scheduling group of top process was demoted by unlocking, so recompute + // to restore its real top priority if possible. + if (mTopApp != null) { + mTopApp.scheduleUpdateOomAdj(); } } + try { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "setLockScreenShown"); + mRootWindowContainer.forAllDisplays(displayContent -> { + mKeyguardController.setKeyguardShown(displayContent.getDisplayId(), + keyguardShowing, aodShowing); + }); + maybeHideLockedProfileActivityLocked(); + } finally { + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + } mH.post(() -> { for (int i = mScreenObservers.size() - 1; i >= 0; i--) { diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java index ab1778a1a32e..15c0789d777e 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java @@ -295,9 +295,14 @@ class AppCompatAspectRatioPolicy { // {@link ActivityRecord#shouldCreateAppCompatDisplayInsets()} will be false for // both activities that are naturally resizeable and activities that have been // forced resizeable. + // Camera compat mode is an exception to this, where the activity is letterboxed + // to an aspect ratio commonly found on phones, e.g. 16:9, to avoid issues like + // stretching of the camera preview. || (Flags.ignoreAspectRatioRestrictionsForResizeableFreeformActivities() && task.getWindowingMode() == WINDOWING_MODE_FREEFORM - && !mActivityRecord.shouldCreateAppCompatDisplayInsets())) { + && !mActivityRecord.shouldCreateAppCompatDisplayInsets() + && !AppCompatCameraPolicy.shouldCameraCompatControlAspectRatio( + mActivityRecord))) { return false; } diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 6df01f4b328b..119709e86551 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -45,11 +45,12 @@ import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONL import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel; import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS; import static com.android.window.flags.Flags.balAdditionalStartModes; +import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg; import static com.android.window.flags.Flags.balImprovedMetrics; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; import static com.android.window.flags.Flags.balShowToastsBlocked; -import static com.android.window.flags.Flags.balStrictModeGracePeriod; import static com.android.window.flags.Flags.balStrictModeRo; +import static com.android.window.flags.Flags.balStrictModeGracePeriod; import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.util.Objects.requireNonNull; @@ -619,6 +620,8 @@ public class BackgroundActivityStartController { // features sb.append("; balRequireOptInByPendingIntentCreator: ") .append(balRequireOptInByPendingIntentCreator()); + sb.append("; balDontBringExistingBackgroundTaskStackToFg: ") + .append(balDontBringExistingBackgroundTaskStackToFg()); sb.append("]"); return sb.toString(); } diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index 6bf1c466aeb5..e3906f9119c2 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -23,6 +23,7 @@ import android.window.DesktopModeFlags; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; /** * Constants for desktop mode feature @@ -35,7 +36,7 @@ public final class DesktopModeHelper { "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); /** Whether desktop mode is enabled. */ - static boolean isDesktopModeEnabled() { + private static boolean isDesktopModeEnabled() { return DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue(); } @@ -56,11 +57,30 @@ public final class DesktopModeHelper { return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported); } + static boolean isDesktopModeDevOptionsSupported(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_isDesktopModeDevOptionSupported); + } + + /** + * Check if Desktop mode should be enabled because the dev option is shown and enabled. + */ + private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) { + return DesktopModeFlags.isDesktopModeForcedEnabled() && (isDesktopModeDevOptionsSupported( + context) || isDeviceEligibleForDesktopMode(context)); + } + + @VisibleForTesting + static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { + return !shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context) || ( + Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionsSupported( + context)); + } + /** * Return {@code true} if desktop mode can be entered on the current device. */ static boolean canEnterDesktopMode(@NonNull Context context) { - return isDesktopModeEnabled() - && (!shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context)); + return (isDesktopModeEnabled() && isDeviceEligibleForDesktopMode(context)) + || isDesktopModeEnabledByDevOption(context); } } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 5b1619995529..5329e3b9abb3 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1887,17 +1887,17 @@ public class DisplayPolicy { mCanSystemBarsBeShownByUser = canBeShown; } - void notifyDisplayReady() { + void notifyDisplayAddSystemDecorations() { mHandler.post(() -> { final int displayId = getDisplayId(); StatusBarManagerInternal statusBar = getStatusBarManagerInternal(); if (statusBar != null) { - statusBar.onDisplayReady(displayId); + statusBar.onDisplayAddSystemDecorations(displayId); } final WallpaperManagerInternal wpMgr = LocalServices .getService(WallpaperManagerInternal.class); if (wpMgr != null) { - wpMgr.onDisplayReady(displayId); + wpMgr.onDisplayAddSystemDecorations(displayId); } }); } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index abd26b5164f7..1fe6ad68a313 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -1948,7 +1948,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> final IntArray rootTaskIdsToRestore = mUserVisibleRootTasks.get(userId); boolean homeInFront = false; if (Flags.enableTopVisibleRootTaskPerUserTracking()) { - if (rootTaskIdsToRestore == null) { + if (rootTaskIdsToRestore == null || rootTaskIdsToRestore.size() == 0) { // If there are no root tasks saved, try restore id 0 which should create and launch // the home task. handleRootTaskLaunchOnUserSwitch(/* restoreRootTaskId */INVALID_TASK_ID); @@ -1958,11 +1958,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> handleRootTaskLaunchOnUserSwitch(rootTaskIdsToRestore.get(i)); } // Check if the top task is type home - if (rootTaskIdsToRestore.size() > 0) { - final int topRootTaskId = rootTaskIdsToRestore.get( - rootTaskIdsToRestore.size() - 1); - homeInFront = isHomeTask(topRootTaskId); - } + final int topRootTaskId = rootTaskIdsToRestore.get(rootTaskIdsToRestore.size() - 1); + homeInFront = isHomeTask(topRootTaskId); } } else { handleRootTaskLaunchOnUserSwitch(restoreRootTaskId); @@ -2845,7 +2842,13 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } startHomeOnDisplay(mCurrentUser, reason, displayContent.getDisplayId()); - displayContent.getDisplayPolicy().notifyDisplayReady(); + if (enableDisplayContentModeManagement()) { + if (displayContent.isSystemDecorationsSupported()) { + displayContent.getDisplayPolicy().notifyDisplayAddSystemDecorations(); + } + } else { + displayContent.getDisplayPolicy().notifyDisplayAddSystemDecorations(); + } } @Override diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 060f2e803ec9..b4c2c0173767 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1865,7 +1865,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (keyguardState != null) { boolean keyguardShowing = keyguardState.getKeyguardShowing(); boolean aodShowing = keyguardState.getAodShowing(); - mService.setLockScreenShown(keyguardShowing, aodShowing); + mService.setLockScreenShownLocked(keyguardShowing, aodShowing); } return effects; } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index e1f3f0ef5615..b37bcc73829c 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -94,8 +94,6 @@ namespace input_flags = com::android::input::flags; namespace android { -static const bool ENABLE_INPUT_FILTER_RUST = input_flags::enable_input_filter_rust_impl(); - // The exponent used to calculate the pointer speed scaling factor. // The scaling factor is calculated as 2 ^ (speed * exponent), // where the speed ranges from -7 to + 7 and is supplied by the user. @@ -604,12 +602,14 @@ void NativeInputManager::dump(std::string& dump) { return std::to_string(displayId.val()); }; dump += StringPrintf(INDENT "Display not interactive: %s\n", - dumpSet(mLocked.nonInteractiveDisplays, streamableToString).c_str()); + dumpContainer(mLocked.nonInteractiveDisplays, streamableToString) + .c_str()); dump += StringPrintf(INDENT "System UI Lights Out: %s\n", toString(mLocked.systemUiLightsOut)); dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed); dump += StringPrintf(INDENT "Display with Mouse Scaling Disabled: %s\n", - dumpSet(mLocked.displaysWithMouseScalingDisabled, streamableToString) + dumpContainer(mLocked.displaysWithMouseScalingDisabled, + streamableToString) .c_str()); dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n", toString(mLocked.pointerGesturesEnabled)); @@ -3248,27 +3248,21 @@ static void nativeSetStylusPointerIconEnabled(JNIEnv* env, jobject nativeImplObj static void nativeSetAccessibilityBounceKeysThreshold(JNIEnv* env, jobject nativeImplObj, jint thresholdTimeMs) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - if (ENABLE_INPUT_FILTER_RUST) { - im->getInputManager()->getInputFilter().setAccessibilityBounceKeysThreshold( - static_cast<nsecs_t>(thresholdTimeMs) * 1000000); - } + im->getInputManager()->getInputFilter().setAccessibilityBounceKeysThreshold( + static_cast<nsecs_t>(thresholdTimeMs) * 1000000); } static void nativeSetAccessibilitySlowKeysThreshold(JNIEnv* env, jobject nativeImplObj, jint thresholdTimeMs) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - if (ENABLE_INPUT_FILTER_RUST) { - im->getInputManager()->getInputFilter().setAccessibilitySlowKeysThreshold( - static_cast<nsecs_t>(thresholdTimeMs) * 1000000); - } + im->getInputManager()->getInputFilter().setAccessibilitySlowKeysThreshold( + static_cast<nsecs_t>(thresholdTimeMs) * 1000000); } static void nativeSetAccessibilityStickyKeysEnabled(JNIEnv* env, jobject nativeImplObj, jboolean enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - if (ENABLE_INPUT_FILTER_RUST) { - im->getInputManager()->getInputFilter().setAccessibilityStickyKeysEnabled(enabled); - } + im->getInputManager()->getInputFilter().setAccessibilityStickyKeysEnabled(enabled); } static void nativeSetInputMethodConnectionIsActive(JNIEnv* env, jobject nativeImplObj, diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java index ea85710eab44..a96c477c78d2 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -177,23 +177,24 @@ public class SupervisionService extends ISupervisionManager.Stub { } } - /** Ensures that supervision is enabled when the supervision app is the profile owner. */ + /** + * Ensures that supervision is enabled when the supervision app is the profile owner. + * + * <p>The state syncing with the DevicePolicyManager can only enable supervision and never + * disable. Supervision can only be disabled explicitly via calls to the + * {@link #setSupervisionEnabledForUser} method. + */ private void syncStateWithDevicePolicyManager(@UserIdInt int userId) { final DevicePolicyManagerInternal dpmInternal = mInjector.getDpmInternal(); final ComponentName po = dpmInternal != null ? dpmInternal.getProfileOwnerAsUser(userId) : null; if (po != null && po.getPackageName().equals(getSystemSupervisionPackage())) { - setSupervisionEnabledForUserInternal(userId, true, po.getPackageName()); + setSupervisionEnabledForUserInternal(userId, true, getSystemSupervisionPackage()); } else if (po != null && po.equals(getSupervisionProfileOwnerComponent())) { // TODO(b/392071637): Consider not enabling supervision in case profile owner is given // to the legacy supervision profile owner component. setSupervisionEnabledForUserInternal(userId, true, po.getPackageName()); - } else { - // TODO(b/381428475): Avoid disabling supervision when the app is not the profile owner. - // This might only be possible after introducing specific and public APIs to enable - // and disable supervision. - setSupervisionEnabledForUserInternal(userId, false, /* supervisionAppPackage= */ null); } } @@ -279,7 +280,7 @@ public class SupervisionService extends ISupervisionManager.Stub { } @VisibleForTesting - @SuppressLint("MissingPermission") // not needed for a service + @SuppressLint("MissingPermission") // not needed for a system service void registerProfileOwnerListener() { IntentFilter poIntentFilter = new IntentFilter(); poIntentFilter.addAction(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED); diff --git a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt index d9224eaf66ca..0466e7572c2e 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt @@ -87,11 +87,15 @@ class PluginManagerTest { val mockPlugin1 = mock<Plugin>() val mockPlugin2 = mock<Plugin>() - override fun getPluginStorage(): PluginStorage { + override fun getPluginStorage(enabledTypes: Set<PluginType<*>>): PluginStorage { return mockStorage } - override fun loadPlugins(context: Context?, storage: PluginStorage?): List<Plugin> { + override fun loadPlugins( + context: Context?, + storage: PluginStorage?, + enabledTypes: Set<PluginType<*>> + ): List<Plugin> { return listOf(mockPlugin1, mockPlugin2) } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt index 8eb3e9fbf9b0..2b06126588d6 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt @@ -29,7 +29,7 @@ private val DISPLAY_ID_2 = "display_2" @SmallTest class PluginStorageTest { - val storage = PluginStorage() + var storage = PluginStorage(setOf(TEST_PLUGIN_TYPE1, TEST_PLUGIN_TYPE2)) @Test fun testUpdateValue() { @@ -93,6 +93,18 @@ class PluginStorageTest { } @Test + fun testDisabledPluginType() { + storage = PluginStorage(setOf(TEST_PLUGIN_TYPE2)) + val type1Value = "value1" + val testChangeListener = TestPluginChangeListener<String>() + storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value) + + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener) + + assertThat(testChangeListener.receivedValue).isNull() + } + + @Test fun testUpdateGlobal_noDisplaySpecificValue() { val type1Value = "value1" val testChangeListener1 = TestPluginChangeListener<String>() diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index 8dc657ed75a6..b4e885fe5661 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -141,6 +141,7 @@ public final class UserManagerServiceTest { .spyStatic(ActivityManager.class) .mockStatic(Settings.Global.class) .mockStatic(Settings.Secure.class) + .mockStatic(Resources.class) .build(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule( @@ -202,6 +203,7 @@ public final class UserManagerServiceTest { doReturn(0) .when(mSpyResources) .getInteger(com.android.internal.R.integer.config_hsumBootStrategy); + doReturn(mSpyResources).when(() -> Resources.getSystem()); // Must construct UserManagerService in the UiThread mTestDir = new File(mRealContext.getDataDir(), "umstest"); diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java index b92afc5c0ca7..d80fd20dd1e6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java @@ -731,7 +731,7 @@ public class WallpaperManagerServiceTests { // WHEN display ID, 2, is ready. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); - wallpaperManagerInternal.onDisplayReady(testDisplayId); + wallpaperManagerInternal.onDisplayAddSystemDecorations(testDisplayId); // Then there is a connection established for the system & lock wallpaper for display ID, 2. verify(mockIWallpaperService).attach( @@ -771,7 +771,7 @@ public class WallpaperManagerServiceTests { // WHEN display ID, 2, is ready. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); - wallpaperManagerInternal.onDisplayReady(testDisplayId); + wallpaperManagerInternal.onDisplayAddSystemDecorations(testDisplayId); // Then there is a connection established for the system wallpaper for display ID, 2. verify(mockIWallpaperService).attach( @@ -818,7 +818,7 @@ public class WallpaperManagerServiceTests { // WHEN display ID, 2, is ready. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); - wallpaperManagerInternal.onDisplayReady(testDisplayId); + wallpaperManagerInternal.onDisplayAddSystemDecorations(testDisplayId); // Then there is a connection established for the fallback wallpaper for display ID, 2. verify(mockIWallpaperService).attach( @@ -856,7 +856,7 @@ public class WallpaperManagerServiceTests { // GIVEN wallpaper connections have been established for display ID, 2. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); - wallpaperManagerInternal.onDisplayReady(testDisplayId); + wallpaperManagerInternal.onDisplayAddSystemDecorations(testDisplayId); // Save displayConnector for displayId 2 before display removal. WallpaperManagerService.DisplayConnector displayConnector = wallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); @@ -894,7 +894,7 @@ public class WallpaperManagerServiceTests { // GIVEN wallpaper connections have been established for display ID, 2. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); - wallpaperManagerInternal.onDisplayReady(testDisplayId); + wallpaperManagerInternal.onDisplayAddSystemDecorations(testDisplayId); // Save displayConnectors for display ID, 2, before display removal. WallpaperManagerService.DisplayConnector systemWallpaperDisplayConnector = systemWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); @@ -930,7 +930,7 @@ public class WallpaperManagerServiceTests { // GIVEN wallpaper connections have been established for display ID, 2. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); - wallpaperManagerInternal.onDisplayReady(testDisplayId); + wallpaperManagerInternal.onDisplayAddSystemDecorations(testDisplayId); // Save fallback wallpaper displayConnector for display ID, 2, before display removal. WallpaperManagerService.DisplayConnector fallbackWallpaperConnector = mService.mFallbackWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); @@ -977,7 +977,7 @@ public class WallpaperManagerServiceTests { // GIVEN wallpaper connections have been established for displayID, 2. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); - wallpaperManagerInternal.onDisplayReady(testDisplayId); + wallpaperManagerInternal.onDisplayAddSystemDecorations(testDisplayId); // Save displayConnector for displayId 2 before display removal. WallpaperManagerService.DisplayConnector displayConnector = wallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); @@ -1011,7 +1011,7 @@ public class WallpaperManagerServiceTests { // GIVEN wallpaper connections have been established for displayID, 2. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); - wallpaperManagerInternal.onDisplayReady(testDisplayId); + wallpaperManagerInternal.onDisplayAddSystemDecorations(testDisplayId); // Save displayConnectors for displayId 2 before display removal. WallpaperManagerService.DisplayConnector systemWallpaperDisplayConnector = systemWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 28e5be505556..9cfa51a85988 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -29,6 +29,7 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; import static android.view.accessibility.Flags.FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES; +import static com.android.input.flags.Flags.FLAG_KEYBOARD_REPEAT_KEYS; import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; @@ -38,6 +39,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity.EXTRA_TYPE_TO_CHOOSE; import static com.android.server.accessibility.AccessibilityManagerService.ACTION_LAUNCH_HEARING_DEVICES_DIALOG; +import static com.android.server.accessibility.Flags.FLAG_ENABLE_MAGNIFICATION_KEYBOARD_CONTROL; import static com.google.common.truth.Truth.assertThat; @@ -93,6 +95,7 @@ import android.os.UserHandle; import android.os.test.FakePermissionEnforcer; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.testing.AndroidTestingRunner; @@ -584,6 +587,31 @@ public class AccessibilityManagerServiceTest { } @Test + @RequiresFlagsEnabled({FLAG_ENABLE_MAGNIFICATION_KEYBOARD_CONTROL, FLAG_KEYBOARD_REPEAT_KEYS}) + public void testRepeatKeysSettingsChanges_propagateToMagnificationController() { + final AccessibilityUserState userState = mA11yms.mUserStates.get( + mA11yms.getCurrentUserIdLocked()); + Settings.Secure.putIntForUser( + mTestableContext.getContentResolver(), + Settings.Secure.KEY_REPEAT_ENABLED, + 0, mA11yms.getCurrentUserIdLocked()); + + mA11yms.readRepeatKeysSettingsLocked(userState); + + verify(mMockMagnificationController).setRepeatKeysEnabled(false); + + final int timeoutMs = 42; + Settings.Secure.putIntForUser( + mTestableContext.getContentResolver(), + Settings.Secure.KEY_REPEAT_TIMEOUT_MS, + timeoutMs, mA11yms.getCurrentUserIdLocked()); + + mA11yms.readRepeatKeysSettingsLocked(userState); + + verify(mMockMagnificationController).setRepeatKeysTimeoutMs(timeoutMs); + } + + @Test public void testSettingsAlwaysOn_setEnabled_featureFlagDisabled_doNothing() { when(mMockMagnificationController.isAlwaysOnMagnificationFeatureFlagEnabled()) .thenReturn(false); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java index d4f2dcc24af6..5d94e7274a1f 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java @@ -250,9 +250,26 @@ public class AccessibilityServiceConnectionTest { } @Test - public void sendGesture_touchableDevice_injectEvents() - throws RemoteException { + public void sendGesture_touchableDevice_injectEvents_fromAccessibilityTool() { + when(mMockWindowManagerInternal.isTouchOrFaketouchDevice()).thenReturn(true); + when(mServiceInfo.isAccessibilityTool()).thenReturn(true); + setServiceBinding(COMPONENT_NAME); + mConnection.bindLocked(); + mConnection.onServiceConnected(COMPONENT_NAME, mMockIBinder); + + ParceledListSlice parceledListSlice = mock(ParceledListSlice.class); + List<GestureDescription.GestureStep> gestureSteps = mock(List.class); + when(parceledListSlice.getList()).thenReturn(gestureSteps); + mConnection.dispatchGesture(0, parceledListSlice, Display.DEFAULT_DISPLAY); + + verify(mMockMotionEventInjector).injectEvents(gestureSteps, mMockServiceClient, 0, + Display.DEFAULT_DISPLAY, true); + } + + @Test + public void sendGesture_touchableDevice_injectEvents_fromNonTool() { when(mMockWindowManagerInternal.isTouchOrFaketouchDevice()).thenReturn(true); + when(mServiceInfo.isAccessibilityTool()).thenReturn(false); setServiceBinding(COMPONENT_NAME); mConnection.bindLocked(); mConnection.onServiceConnected(COMPONENT_NAME, mMockIBinder); @@ -263,13 +280,14 @@ public class AccessibilityServiceConnectionTest { mConnection.dispatchGesture(0, parceledListSlice, Display.DEFAULT_DISPLAY); verify(mMockMotionEventInjector).injectEvents(gestureSteps, mMockServiceClient, 0, - Display.DEFAULT_DISPLAY); + Display.DEFAULT_DISPLAY, false); } @Test public void sendGesture_untouchableDevice_performGestureResultFailed() throws RemoteException { when(mMockWindowManagerInternal.isTouchOrFaketouchDevice()).thenReturn(false); + when(mServiceInfo.isAccessibilityTool()).thenReturn(true); setServiceBinding(COMPONENT_NAME); mConnection.bindLocked(); mConnection.onServiceConnected(COMPONENT_NAME, mMockIBinder); @@ -280,7 +298,7 @@ public class AccessibilityServiceConnectionTest { mConnection.dispatchGesture(0, parceledListSlice, Display.DEFAULT_DISPLAY); verify(mMockMotionEventInjector, never()).injectEvents(gestureSteps, mMockServiceClient, 0, - Display.DEFAULT_DISPLAY); + Display.DEFAULT_DISPLAY, true); verify(mMockServiceClient).onPerformGestureResult(0, false); } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java index 233caf9c7761..d2d8c682ed90 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java @@ -20,8 +20,7 @@ import static android.view.KeyCharacterMap.VIRTUAL_KEYBOARD; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_HOVER_MOVE; import static android.view.MotionEvent.ACTION_UP; -import static android.view.WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY; -import static android.view.WindowManagerPolicyConstants.FLAG_PASS_TO_USER; +import static android.view.accessibility.Flags.FLAG_PREVENT_A11Y_NONTOOL_FROM_INJECTING_INTO_SENSITIVE_VIEWS; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.anyOf; @@ -48,10 +47,14 @@ import android.graphics.Point; import android.os.Handler; import android.os.Message; import android.os.RemoteException; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.Display; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.WindowManagerPolicyConstants; import android.view.accessibility.AccessibilityEvent; import androidx.test.runner.AndroidJUnit4; @@ -64,6 +67,7 @@ import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -77,7 +81,7 @@ import java.util.List; */ @RunWith(AndroidJUnit4.class) public class MotionEventInjectorTest { - private static final String LOG_TAG = "MotionEventInjectorTest"; + private static final Matcher<MotionEvent> IS_ACTION_DOWN = new MotionEventActionMatcher(ACTION_DOWN); private static final Matcher<MotionEvent> IS_ACTION_POINTER_DOWN = @@ -120,6 +124,9 @@ public class MotionEventInjectorTest { private static final float POINTER_SIZE = 1; private static final int METASTATE = 0; + @Rule + public SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + MotionEventInjector mMotionEventInjector; IAccessibilityServiceClient mServiceInterface; AccessibilityTraceManager mTrace; @@ -201,7 +208,8 @@ public class MotionEventInjectorTest { verifyNoMoreInteractions(next); mMessageCapturingHandler.sendOneMessage(); // Send a motion event - final int expectedFlags = FLAG_PASS_TO_USER | FLAG_INJECTED_FROM_ACCESSIBILITY; + final int expectedFlags = WindowManagerPolicyConstants.FLAG_PASS_TO_USER + | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY; verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), eq(expectedFlags)); verify(next).onMotionEvent(argThat(mIsLineStart), argThat(mIsLineStart), eq(expectedFlags)); verifyNoMoreInteractions(next); @@ -227,6 +235,21 @@ public class MotionEventInjectorTest { } @Test + @EnableFlags(FLAG_PREVENT_A11Y_NONTOOL_FROM_INJECTING_INTO_SENSITIVE_VIEWS) + public void testInjectEvents_fromAccessibilityTool_providesToolPolicyFlag() { + EventStreamTransformation next = attachMockNext(mMotionEventInjector); + injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE, + /*fromAccessibilityTool=*/true); + + mMessageCapturingHandler.sendOneMessage(); // Send a motion event + verify(next).onMotionEvent( + argThat(mIsLineStart), argThat(mIsLineStart), + eq(WindowManagerPolicyConstants.FLAG_PASS_TO_USER + | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY + | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY_TOOL)); + } + + @Test public void testInjectEvents_gestureWithTooManyPoints_shouldNotCrash() throws Exception { int tooManyPointsCount = 20; TouchPoint[] startTouchPoints = new TouchPoint[tooManyPointsCount]; @@ -251,14 +274,28 @@ public class MotionEventInjectorTest { } @Test - public void testRegularEvent_afterGestureComplete_shouldPassToNext() { + @DisableFlags(FLAG_PREVENT_A11Y_NONTOOL_FROM_INJECTING_INTO_SENSITIVE_VIEWS) + public void testRegularEvent_afterGestureComplete_shouldPassToNext_withFlagInjectedFromA11y() { + EventStreamTransformation next = attachMockNext(mMotionEventInjector); + injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE); + mMessageCapturingHandler.sendAllMessages(); // Send all motion events + reset(next); + mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0); + verify(next).onMotionEvent(argThat(mIsClickDown), argThat(mIsClickDown), + eq(WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY)); + } + + @Test + @EnableFlags(FLAG_PREVENT_A11Y_NONTOOL_FROM_INJECTING_INTO_SENSITIVE_VIEWS) + public void testRegularEvent_afterGestureComplete_shouldPassToNext_withNoPolicyFlagChanges() { EventStreamTransformation next = attachMockNext(mMotionEventInjector); injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE); mMessageCapturingHandler.sendAllMessages(); // Send all motion events reset(next); mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0); verify(next).onMotionEvent(argThat(mIsClickDown), argThat(mIsClickDown), - eq(FLAG_INJECTED_FROM_ACCESSIBILITY)); + // The regular event passing through the filter should have no policy flag changes + eq(0)); } @Test @@ -275,7 +312,8 @@ public class MotionEventInjectorTest { mMessageCapturingHandler.sendOneMessage(); // Send a motion event verify(next).onMotionEvent( argThat(mIsLineStart), argThat(mIsLineStart), - eq(FLAG_PASS_TO_USER | FLAG_INJECTED_FROM_ACCESSIBILITY)); + eq(WindowManagerPolicyConstants.FLAG_PASS_TO_USER + | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY)); } @Test @@ -307,10 +345,12 @@ public class MotionEventInjectorTest { mMessageCapturingHandler.sendOneMessage(); // Send a motion event verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), - eq(FLAG_PASS_TO_USER | FLAG_INJECTED_FROM_ACCESSIBILITY)); + eq(WindowManagerPolicyConstants.FLAG_PASS_TO_USER + | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY)); verify(next).onMotionEvent( argThat(mIsLineStart), argThat(mIsLineStart), - eq(FLAG_PASS_TO_USER | FLAG_INJECTED_FROM_ACCESSIBILITY)); + eq(WindowManagerPolicyConstants.FLAG_PASS_TO_USER + | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY)); } @Test @@ -731,8 +771,14 @@ public class MotionEventInjectorTest { private void injectEventsSync(List<GestureStep> gestureSteps, IAccessibilityServiceClient serviceInterface, int sequence) { + injectEventsSync(gestureSteps, serviceInterface, sequence, false); + } + + private void injectEventsSync(List<GestureStep> gestureSteps, + IAccessibilityServiceClient serviceInterface, int sequence, + boolean fromAccessibilityTool) { mMotionEventInjector.injectEvents(gestureSteps, serviceInterface, sequence, - Display.DEFAULT_DISPLAY); + Display.DEFAULT_DISPLAY, fromAccessibilityTool); // Dispatch the message sent by the injector. Our simple handler doesn't guarantee stuff // happens in order. mMessageCapturingHandler.sendLastMessage(); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java index 3511ae12497a..cd6b36dbc1c6 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java @@ -885,66 +885,142 @@ public class MagnificationControllerTest { } @Test - public void magnificationCallbacks_panMagnificationContinuous() throws RemoteException { + public void magnificationCallbacks_scaleMagnificationContinuous() throws RemoteException { setMagnificationEnabled(MODE_FULLSCREEN); - mMagnificationController.onPerformScaleAction(TEST_DISPLAY, 8.0f, false); + float currentScale = 2.0f; + mMagnificationController.onPerformScaleAction(TEST_DISPLAY, currentScale, false); reset(mScreenMagnificationController); - DisplayMetrics metrics = new DisplayMetrics(); - mDisplay.getMetrics(metrics); - float expectedStep = 27 * metrics.density; - float currentCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); float currentCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); - // Start moving right using keyboard callbacks. - mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, - MagnificationController.PAN_DIRECTION_RIGHT); + // Start zooming in using keyboard callbacks. + mMagnificationController.onScaleMagnificationStart(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_IN); + // The center is unchanged. float newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); float newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); - expect.that(currentCenterX).isLessThan(newCenterX); - expect.that(newCenterX - currentCenterX).isWithin(0.01f).of(expectedStep); + expect.that(currentCenterX).isWithin(1.0f).of(newCenterX); expect.that(currentCenterY).isEqualTo(newCenterY); - currentCenterX = newCenterX; - currentCenterY = newCenterY; + // The scale is increased. + float newScale = mScreenMagnificationController.getScale(TEST_DISPLAY); + expect.that(currentScale).isLessThan(newScale); + currentScale = newScale; // Wait for the initial delay to occur. - advanceTime(MagnificationController.INITIAL_KEYBOARD_REPEAT_INTERVAL_MS + 1); + advanceTime(mMagnificationController.getInitialKeyboardRepeatIntervalMs() + 1); - // It should have moved again after the handler was triggered. - newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); - newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); - expect.that(currentCenterX).isLessThan(newCenterX); - expect.that(newCenterX - currentCenterX).isWithin(0.01f).of(expectedStep); - expect.that(currentCenterY).isEqualTo(newCenterY); - currentCenterX = newCenterX; - currentCenterY = newCenterY; + // It should have scaled again after the handler was triggered. + newScale = mScreenMagnificationController.getScale(TEST_DISPLAY); + expect.that(currentScale).isLessThan(newScale); + currentScale = newScale; - // Wait for repeat delay to occur. + for (int i = 0; i < 3; i++) { + // Wait for repeat delay to occur. + advanceTime(MagnificationController.KEYBOARD_REPEAT_INTERVAL_MS + 1); + + // It should have scaled another time. + newScale = mScreenMagnificationController.getScale(TEST_DISPLAY); + expect.that(currentScale).isLessThan(newScale); + currentScale = newScale; + } + + // Stop magnification scale. + mMagnificationController.onScaleMagnificationStop(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_IN); + + // It should not scale again, even after the appropriate delay. advanceTime(MagnificationController.KEYBOARD_REPEAT_INTERVAL_MS + 1); - // It should have moved a third time. - newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); - newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); - expect.that(currentCenterX).isLessThan(newCenterX); - expect.that(newCenterX - currentCenterX).isWithin(0.01f).of(expectedStep); - expect.that(currentCenterY).isEqualTo(newCenterY); + newScale = mScreenMagnificationController.getScale(TEST_DISPLAY); + expect.that(currentScale).isEqualTo(newScale); + } + + @Test + public void magnificationCallbacks_panMagnificationContinuous_repeatKeysTimeout200() + throws RemoteException { + // Shorter than default. + testMagnificationContinuousPanningWithTimeout(200); + } + + @Test + public void magnificationCallbacks_panMagnificationContinuous_repeatKeysTimeout1000() + throws RemoteException { + // Longer than default. + testMagnificationContinuousPanningWithTimeout(1000); + } + + @Test + public void magnificationCallbacks_panMagnification_notContinuousWithRepeatKeysDisabled() + throws RemoteException { + mMagnificationController.setRepeatKeysEnabled(false); + setMagnificationEnabled(MODE_FULLSCREEN); + mMagnificationController.onPerformScaleAction(TEST_DISPLAY, 4.0f, false); + reset(mScreenMagnificationController); + + float currentCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + float currentCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + + // Start moving down using keyboard callbacks. + mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_DOWN); + + float newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + float newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + expect.that(currentCenterY).isLessThan(newCenterY); + expect.that(currentCenterX).isEqualTo(newCenterX); + currentCenterX = newCenterX; currentCenterY = newCenterY; - // Stop magnification pan. + for (int i = 0; i < 3; i++) { + // Wait for the initial delay to occur. + advanceTime(mMagnificationController.getInitialKeyboardRepeatIntervalMs() + 1); + + // It should not have moved again because repeat keys is disabled. + newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + expect.that(currentCenterX).isEqualTo(newCenterX); + expect.that(currentCenterY).isEqualTo(newCenterY); + currentCenterX = newCenterX; + currentCenterY = newCenterY; + } + mMagnificationController.onPanMagnificationStop(TEST_DISPLAY, - MagnificationController.PAN_DIRECTION_RIGHT); + MagnificationController.PAN_DIRECTION_DOWN); + } - // It should not move again, even after the appropriate delay. - advanceTime(MagnificationController.KEYBOARD_REPEAT_INTERVAL_MS + 1); + @Test + public void magnificationCallbacks_scaleMagnification_notContinuousWithRepeatKeysDisabled() + throws RemoteException { + mMagnificationController.setRepeatKeysEnabled(false); + setMagnificationEnabled(MODE_FULLSCREEN); + float currentScale = 8.0f; + mMagnificationController.onPerformScaleAction(TEST_DISPLAY, currentScale, false); + reset(mScreenMagnificationController); - newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); - newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); - expect.that(newCenterX).isEqualTo(currentCenterX); - expect.that(newCenterY).isEqualTo(currentCenterY); + // Start scaling out using keyboard callbacks. + mMagnificationController.onScaleMagnificationStart(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_OUT); + + float newScale = mScreenMagnificationController.getScale(TEST_DISPLAY); + expect.that(currentScale).isGreaterThan(newScale); + + currentScale = newScale; + + for (int i = 0; i < 3; i++) { + // Wait for the initial delay to occur. + advanceTime(mMagnificationController.getInitialKeyboardRepeatIntervalMs() + 1); + + // It should not have scaled again because repeat keys is disabled. + newScale = mScreenMagnificationController.getScale(TEST_DISPLAY); + expect.that(currentScale).isEqualTo(newScale); + } + + mMagnificationController.onScaleMagnificationStop(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_OUT); } @Test @@ -1736,6 +1812,75 @@ public class MagnificationControllerTest { MagnificationController.PAN_DIRECTION_UP); } + private void + testMagnificationContinuousPanningWithTimeout(int timeoutMs) throws RemoteException { + mMagnificationController.setRepeatKeysTimeoutMs(timeoutMs); + expect.that(timeoutMs).isEqualTo( + mMagnificationController.getInitialKeyboardRepeatIntervalMs()); + + setMagnificationEnabled(MODE_FULLSCREEN); + mMagnificationController.onPerformScaleAction(TEST_DISPLAY, 8.0f, false); + reset(mScreenMagnificationController); + + DisplayMetrics metrics = new DisplayMetrics(); + mDisplay.getMetrics(metrics); + float expectedStep = 27 * metrics.density; + + float currentCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + float currentCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + + // Start moving right using keyboard callbacks. + mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_RIGHT); + + float newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + float newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + expect.that(currentCenterX).isLessThan(newCenterX); + expect.that(newCenterX - currentCenterX).isWithin(0.01f).of(expectedStep); + expect.that(currentCenterY).isEqualTo(newCenterY); + + currentCenterX = newCenterX; + currentCenterY = newCenterY; + + // Wait for the initial delay to occur. + advanceTime(timeoutMs + 1); + + // It should have moved again after the handler was triggered. + newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + expect.that(currentCenterX).isLessThan(newCenterX); + expect.that(newCenterX - currentCenterX).isWithin(0.01f).of(expectedStep); + expect.that(currentCenterY).isEqualTo(newCenterY); + currentCenterX = newCenterX; + currentCenterY = newCenterY; + + for (int i = 0; i < 3; i++) { + // Wait for repeat delay to occur. + advanceTime(MagnificationController.KEYBOARD_REPEAT_INTERVAL_MS + 1); + + // It should have moved another time. + newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + expect.that(currentCenterX).isLessThan(newCenterX); + expect.that(newCenterX - currentCenterX).isWithin(0.01f).of(expectedStep); + expect.that(currentCenterY).isEqualTo(newCenterY); + currentCenterX = newCenterX; + currentCenterY = newCenterY; + } + + // Stop magnification pan. + mMagnificationController.onPanMagnificationStop(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_RIGHT); + + // It should not move again, even after the appropriate delay. + advanceTime(MagnificationController.KEYBOARD_REPEAT_INTERVAL_MS + 1); + + newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + expect.that(newCenterX).isEqualTo(currentCenterX); + expect.that(newCenterY).isEqualTo(currentCenterY); + } + private void advanceTime(long timeMs) { mTestLooper.moveTimeForward(timeMs); mTestLooper.dispatchAll(); diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 06958b81d846..1627f683cd3e 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -25,6 +25,7 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE; import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL; +import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader; @@ -115,7 +116,6 @@ import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.pm.UserTypeDetails; import com.android.server.pm.UserTypeFactory; -import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerService; import com.google.common.collect.Range; @@ -1563,11 +1563,11 @@ public class UserControllerTest { // and the thread is still alive assertTrue(threadStartUser.isAlive()); - // mock send the keyguard shown event - ArgumentCaptor<ActivityTaskManagerInternal.ScreenObserver> captor = ArgumentCaptor.forClass( - ActivityTaskManagerInternal.ScreenObserver.class); - verify(mInjector.mActivityTaskManagerInternal).registerScreenObserver(captor.capture()); - captor.getValue().onKeyguardStateChanged(true); + // mock the binder response for the user switch completion + ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class); + verify(mInjector.mWindowManagerMock).lockNow(captor.capture()); + IRemoteCallback.Stub.asInterface(captor.getValue().getBinder( + LOCK_ON_USER_SWITCH_CALLBACK)).sendResult(null); // verify the switch now moves on... Thread.sleep(1000); @@ -1757,7 +1757,6 @@ public class UserControllerTest { private final IStorageManager mStorageManagerMock; private final UserManagerInternal mUserManagerInternalMock; private final WindowManagerService mWindowManagerMock; - private final ActivityTaskManagerInternal mActivityTaskManagerInternal; private final PowerManagerInternal mPowerManagerInternal; private final AlarmManagerInternal mAlarmManagerInternal; private final KeyguardManager mKeyguardManagerMock; @@ -1779,7 +1778,6 @@ public class UserControllerTest { mUserManagerMock = mock(UserManagerService.class); mUserManagerInternalMock = mock(UserManagerInternal.class); mWindowManagerMock = mock(WindowManagerService.class); - mActivityTaskManagerInternal = mock(ActivityTaskManagerInternal.class); mStorageManagerMock = mock(IStorageManager.class); mPowerManagerInternal = mock(PowerManagerInternal.class); mAlarmManagerInternal = mock(AlarmManagerInternal.class); @@ -1843,11 +1841,6 @@ public class UserControllerTest { } @Override - ActivityTaskManagerInternal getActivityTaskManagerInternal() { - return mActivityTaskManagerInternal; - } - - @Override PowerManagerInternal getPowerManagerInternal() { return mPowerManagerInternal; } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java index 605fed09dd9f..c7efa318af99 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java @@ -45,7 +45,6 @@ import android.content.pm.PackageManager; import android.content.res.Resources; import android.hardware.biometrics.AuthenticationStateListener; import android.hardware.biometrics.BiometricManager; -import android.hardware.biometrics.Flags; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.IBiometricServiceReceiver; @@ -504,23 +503,9 @@ public class AuthServiceTest { eq(callback)); } - @Test(expected = UnsupportedOperationException.class) - public void testGetLastAuthenticationTime_flaggedOff_throwsUnsupportedOperationException() - throws Exception { - mSetFlagsRule.disableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); - setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */); - - mAuthService = new AuthService(mContext, mInjector); - mAuthService.onStart(); - - mAuthService.mImpl.getLastAuthenticationTime(0, - BiometricManager.Authenticators.BIOMETRIC_STRONG); - } - @Test - public void testGetLastAuthenticationTime_flaggedOn_callsBiometricService() + public void testGetLastAuthenticationTime_callsBiometricService() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */); mAuthService = new AuthService(mContext, mInjector); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index acca4cc294b3..9918a9a35c33 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -2014,20 +2014,9 @@ public class BiometricServiceTest { verifyNoMoreInteractions(callback); } - @Test(expected = UnsupportedOperationException.class) - public void testGetLastAuthenticationTime_flagOff_throwsUnsupportedOperationException() - throws RemoteException { - mSetFlagsRule.disableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); - - mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider); - mBiometricService.mImpl.getLastAuthenticationTime(0, Authenticators.BIOMETRIC_STRONG); - } - @Test - public void testGetLastAuthenticationTime_flagOn_callsKeystoreAuthorization() + public void testGetLastAuthenticationTime_callsKeystoreAuthorization() throws RemoteException { - mSetFlagsRule.enableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); - final int[] hardwareAuthenticators = new int[] { HardwareAuthenticatorType.PASSWORD, HardwareAuthenticatorType.FINGERPRINT diff --git a/services/tests/servicestests/src/com/android/server/media/projection/OWNERS b/services/tests/servicestests/src/com/android/server/media/projection/OWNERS index 832bcd9d70e6..3caf7faa13ec 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/OWNERS +++ b/services/tests/servicestests/src/com/android/server/media/projection/OWNERS @@ -1 +1,2 @@ +# Bug component: 1345447 include /media/java/android/media/projection/OWNERS diff --git a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java index 263ada8b36f6..148c96850d34 100644 --- a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java @@ -69,7 +69,6 @@ import android.os.Binder; import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; -import android.platform.test.annotations.EnableFlags; import android.service.quicksettings.TileService; import android.testing.TestableContext; @@ -80,7 +79,6 @@ import com.android.internal.statusbar.IStatusBar; import com.android.server.LocalServices; import com.android.server.policy.GlobalActionsProvider; import com.android.server.wm.ActivityTaskManagerInternal; -import com.android.systemui.shared.Flags; import libcore.junit.util.compat.CoreCompatChangeRule; @@ -107,7 +105,6 @@ public class StatusBarManagerServiceTest { TEST_SERVICE); private static final CharSequence APP_NAME = "AppName"; private static final CharSequence TILE_LABEL = "Tile label"; - private static final int SECONDARY_DISPLAY_ID = 2; @Rule public final TestableContext mContext = @@ -752,29 +749,6 @@ public class StatusBarManagerServiceTest { } @Test - @EnableFlags(Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS) - public void testDisableForAllDisplays() throws Exception { - int user1Id = 0; - mockUidCheck(); - mockCurrentUserCheck(user1Id); - - mStatusBarManagerService.onDisplayAdded(SECONDARY_DISPLAY_ID); - - int expectedFlags = DISABLE_MASK & DISABLE_BACK; - String pkg = mContext.getPackageName(); - - // before disabling - assertEquals(DISABLE_NONE, - mStatusBarManagerService.getDisableFlags(mMockStatusBar, user1Id)[0]); - - // disable - mStatusBarManagerService.disable(expectedFlags, mMockStatusBar, pkg); - - verify(mMockStatusBar).disable(0, expectedFlags, 0); - verify(mMockStatusBar).disable(SECONDARY_DISPLAY_ID, expectedFlags, 0); - } - - @Test public void testSetHomeDisabled() throws Exception { int expectedFlags = DISABLE_MASK & DISABLE_HOME; String pkg = mContext.getPackageName(); @@ -877,29 +851,6 @@ public class StatusBarManagerServiceTest { } @Test - @EnableFlags(Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS) - public void testDisable2ForAllDisplays() throws Exception { - int user1Id = 0; - mockUidCheck(); - mockCurrentUserCheck(user1Id); - - mStatusBarManagerService.onDisplayAdded(SECONDARY_DISPLAY_ID); - - int expectedFlags = DISABLE2_MASK & DISABLE2_NOTIFICATION_SHADE; - String pkg = mContext.getPackageName(); - - // before disabling - assertEquals(DISABLE_NONE, - mStatusBarManagerService.getDisableFlags(mMockStatusBar, user1Id)[0]); - - // disable - mStatusBarManagerService.disable2(expectedFlags, mMockStatusBar, pkg); - - verify(mMockStatusBar).disable(0, 0, expectedFlags); - verify(mMockStatusBar).disable(SECONDARY_DISPLAY_ID, 0, expectedFlags); - } - - @Test public void testSetQuickSettingsDisabled2() throws Exception { int expectedFlags = DISABLE2_MASK & DISABLE2_QUICK_SETTINGS; String pkg = mContext.getPackageName(); @@ -1141,7 +1092,6 @@ public class StatusBarManagerServiceTest { // disable mStatusBarManagerService.disableForUser(expectedUser1Flags, mMockStatusBar, pkg, user1Id); mStatusBarManagerService.disableForUser(expectedUser2Flags, mMockStatusBar, pkg, user2Id); - // check that right flag is disabled assertEquals(expectedUser1Flags, mStatusBarManagerService.getDisableFlags(mMockStatusBar, user1Id)[0]); diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt index b150b1495042..da022780ea75 100644 --- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt +++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt @@ -161,6 +161,18 @@ class SupervisionServiceTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SYNC_WITH_DPM) + fun profileOwnerChanged_supervisionAppIsNotProfileOwner_doesNotDisableSupervision() { + service.mInternal.setSupervisionEnabledForUser(USER_ID, true) + whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID)) + .thenReturn(ComponentName("other.package", "MainActivity")) + + broadcastProfileOwnerChanged(USER_ID) + + assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue() + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SYNC_WITH_DPM) fun profileOwnerChanged_supervisionAppIsNotProfileOwner_doesNotEnableSupervision() { whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID)) .thenReturn(ComponentName("other.package", "MainActivity")) @@ -258,7 +270,7 @@ class SupervisionServiceTest { private companion object { const val USER_ID = 100 - val APP_UID = USER_ID * UserHandle.PER_USER_RANGE + const val APP_UID = USER_ID * UserHandle.PER_USER_RANGE } } diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java index 842c441e09f2..857a9767d9ee 100644 --- a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java @@ -452,14 +452,14 @@ public class SystemConfigTest { + " <library \n" + " name=\"foo\"\n" + " file=\"" + mFooJar + "\"\n" - + " on-bootclasspath-before=\"Q\"\n" + + " on-bootclasspath-before=\"A\"\n" + " on-bootclasspath-since=\"W\"\n" + " />\n\n" + " </permissions>"; parseSharedLibraries(contents); assertFooIsOnlySharedLibrary(); SystemConfig.SharedLibraryEntry entry = mSysConfig.getSharedLibraries().get("foo"); - assertThat(entry.onBootclasspathBefore).isEqualTo("Q"); + assertThat(entry.onBootclasspathBefore).isEqualTo("A"); assertThat(entry.onBootclasspathSince).isEqualTo("W"); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java index fa733e85c89c..4a977be2aad9 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java @@ -96,12 +96,12 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import java.util.ArrayList; -import java.util.List; - import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; +import java.util.ArrayList; +import java.util.List; + @SmallTest @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the class. @RunWith(ParameterizedAndroidJunit4.class) @@ -2671,7 +2671,7 @@ public class GroupHelperTest extends UiServiceTestCase { r.updateNotificationChannel(channel); } mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel, - notificationList); + notificationList, summaryByGroup); // Check that all notifications are moved to the silent section group verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), @@ -2735,7 +2735,7 @@ public class GroupHelperTest extends UiServiceTestCase { } } mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel1, - notificationList); + notificationList, summaryByGroup); // Check that the override group key was cleared for (NotificationRecord record: notificationList) { @@ -2812,7 +2812,7 @@ public class GroupHelperTest extends UiServiceTestCase { } } mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel1, - notificationList); + notificationList, summaryByGroup); // Check that the override group key was cleared for (NotificationRecord record: notificationList) { @@ -2864,7 +2864,7 @@ public class GroupHelperTest extends UiServiceTestCase { // Classify/bundle child notifications final NotificationChannel socialChannel = new NotificationChannel( NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, - IMPORTANCE_DEFAULT); + IMPORTANCE_LOW); final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg, AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier()); final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes( @@ -2872,7 +2872,7 @@ public class GroupHelperTest extends UiServiceTestCase { NotificationChannel.SOCIAL_MEDIA_ID); final NotificationChannel newsChannel = new NotificationChannel( NotificationChannel.NEWS_ID, NotificationChannel.NEWS_ID, - IMPORTANCE_DEFAULT); + IMPORTANCE_LOW); final String expectedGroupKey_news = GroupHelper.getFullAggregateGroupKey(pkg, AGGREGATE_GROUP_KEY + "NewsSection", UserHandle.SYSTEM.getIdentifier()); final NotificationAttributes expectedSummaryAttr_news = new NotificationAttributes( @@ -2944,7 +2944,7 @@ public class GroupHelperTest extends UiServiceTestCase { // Classify/bundle child notifications final NotificationChannel socialChannel = new NotificationChannel( NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, - IMPORTANCE_DEFAULT); + IMPORTANCE_LOW); for (NotificationRecord record: notificationList) { if (record.getChannel().getId().equals(channel1.getId()) && record.getNotification().isGroupChild()) { @@ -2999,7 +2999,7 @@ public class GroupHelperTest extends UiServiceTestCase { // Classify/bundle child notifications final NotificationChannel socialChannel = new NotificationChannel( NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, - IMPORTANCE_DEFAULT); + IMPORTANCE_LOW); final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg, AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier()); final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes( @@ -3095,7 +3095,7 @@ public class GroupHelperTest extends UiServiceTestCase { reset(mCallback); final NotificationChannel socialChannel = new NotificationChannel( NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, - IMPORTANCE_DEFAULT); + IMPORTANCE_LOW); final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg, AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier()); final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes( @@ -3149,7 +3149,7 @@ public class GroupHelperTest extends UiServiceTestCase { // adjustments applied while enqueued will use NotificationAdjustmentExtractor. final NotificationChannel socialChannel = new NotificationChannel( NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, - IMPORTANCE_DEFAULT); + IMPORTANCE_LOW); final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg, AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier()); final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes( @@ -3209,7 +3209,7 @@ public class GroupHelperTest extends UiServiceTestCase { // Classify/bundle all child notifications: original group & summary is removed final NotificationChannel socialChannel = new NotificationChannel( NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, - IMPORTANCE_DEFAULT); + IMPORTANCE_LOW); for (NotificationRecord record: notificationList) { if (record.getOriginalGroupKey().contains("testGrp") && record.getNotification().isGroupChild()) { @@ -3297,7 +3297,7 @@ public class GroupHelperTest extends UiServiceTestCase { // Classify/bundle child notifications: all except one, to keep the original group final NotificationChannel socialChannel = new NotificationChannel( NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, - IMPORTANCE_DEFAULT); + IMPORTANCE_LOW); final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg, AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier()); final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes( @@ -3378,6 +3378,314 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION, + FLAG_NOTIFICATION_CLASSIFICATION}) + public void testUnbundleByImportanceNotification_originalSummaryExists() { + // Check that unbundled notifications are moved to the original section and original group + // when the original summary is still present + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + + final int summaryId = 0; + final int numChildren = AUTOGROUP_AT_COUNT + 1; + // Post a regular/valid group: summary + notifications + NotificationRecord summary = getNotificationRecord(pkg, summaryId, + String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true); + notificationList.add(summary); + summaryByGroup.put(summary.getGroupKey(), summary); + final String originalAppGroupKey = summary.getGroupKey(); + final NotificationChannel originalChannel = summary.getChannel(); + for (int i = 0; i < numChildren; i++) { + NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42), + UserHandle.SYSTEM, "testGrp", false); + notificationList.add(child); + mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); + } + + // Classify/bundle child notifications: all except one, to keep the original group + final NotificationChannel socialChannel = new NotificationChannel( + NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, + IMPORTANCE_LOW); + final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier()); + final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes( + BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, + NotificationChannel.SOCIAL_MEDIA_ID); + int numChildrenBundled = 0; + for (NotificationRecord record: notificationList) { + if (record.getOriginalGroupKey().contains("testGrp") + && record.getNotification().isGroupChild()) { + record.updateNotificationChannel(socialChannel); + mGroupHelper.onChannelUpdated(record); + numChildrenBundled++; + if (numChildrenBundled == AUTOGROUP_AT_COUNT) { + break; + } + } + } + + // Check that 1 autogroup summaries were created for the social section + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social)); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_social), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).removeAppProvidedSummaryOnClassification( + anyString(), eq(originalAppGroupKey)); + + // Adjust group key for grouped notifications + for (NotificationRecord record: notificationList) { + if (record.getOriginalGroupKey().contains("testGrp") + && NotificationChannel.SYSTEM_RESERVED_IDS.contains( + record.getChannel().getId())) { + record.setOverrideGroupKey(expectedGroupKey_social); + } + } + + // Add 1 ungrouped notification in the original section + NotificationRecord ungroupedNotification = getNotificationRecord(pkg, 4242, + String.valueOf(4242), UserHandle.SYSTEM); + notificationList.add(ungroupedNotification); + mGroupHelper.onNotificationPosted(ungroupedNotification, false); + + // Unbundle the bundled notifications by changing the social channel importance to alerting + // => social section summary is destroyed + // and notifications are moved back to the original group + reset(mCallback); + socialChannel.setImportance(IMPORTANCE_DEFAULT); + for (NotificationRecord record: notificationList) { + if (record.getNotification().isGroupChild() + && record.getOriginalGroupKey().contains("testGrp") + && NotificationChannel.SYSTEM_RESERVED_IDS.contains( + record.getChannel().getId())) { + record.updateNotificationChannel(socialChannel); + } + } + mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, socialChannel, + notificationList, summaryByGroup); + + // Check that the autogroup summary for the social section was removed + // and that no new autogroup summaries were created + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean()); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey_social)); + + for (NotificationRecord record: notificationList) { + if (record.getNotification().isGroupChild() + && record.getOriginalGroupKey().contains("testGrp")) { + assertThat(record.getSbn().getOverrideGroupKey()).isNull(); + assertThat(GroupHelper.getSection(record).mName).isEqualTo("AlertingSection"); + } + } + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION, + FLAG_NOTIFICATION_CLASSIFICATION}) + public void testUnbundleByImportanceNotification_originalSummaryRemoved() { + // Check that unbundled notifications are moved to the original section and autogrouped + // when the original summary is not present + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + + final int summaryId = 0; + final int numChildren = AUTOGROUP_AT_COUNT + 1; + // Post a regular/valid group: summary + notifications + NotificationRecord summary = getNotificationRecord(pkg, summaryId, + String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true); + notificationList.add(summary); + summaryByGroup.put(summary.getGroupKey(), summary); + final String originalAppGroupKey = summary.getGroupKey(); + final NotificationChannel originalChannel = summary.getChannel(); + for (int i = 0; i < numChildren; i++) { + NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42), + UserHandle.SYSTEM, "testGrp", false); + notificationList.add(child); + mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); + } + + // Classify/bundle child notifications: all except one, to keep the original group + final NotificationChannel socialChannel = new NotificationChannel( + NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, + IMPORTANCE_LOW); + final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier()); + final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes( + BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, + NotificationChannel.SOCIAL_MEDIA_ID); + int numChildrenBundled = 0; + for (NotificationRecord record: notificationList) { + if (record.getOriginalGroupKey().contains("testGrp") + && record.getNotification().isGroupChild()) { + record.updateNotificationChannel(socialChannel); + mGroupHelper.onChannelUpdated(record); + numChildrenBundled++; + if (numChildrenBundled == AUTOGROUP_AT_COUNT) { + break; + } + } + } + + // Check that 1 autogroup summaries were created for the social section + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social)); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_social), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).removeAppProvidedSummaryOnClassification( + anyString(), eq(originalAppGroupKey)); + + // Adjust group key + for (NotificationRecord record: notificationList) { + if (record.getOriginalGroupKey().contains("testGrp") + && NotificationChannel.SYSTEM_RESERVED_IDS.contains( + record.getChannel().getId())) { + record.setOverrideGroupKey(expectedGroupKey_social); + } + } + + // Remove original summary + notificationList.remove(summary); + summaryByGroup.remove(summary.getGroupKey()); + + // Add 1 ungrouped notification in the original section + NotificationRecord ungroupedNotification = getNotificationRecord(pkg, 4242, + String.valueOf(4242), UserHandle.SYSTEM); + notificationList.add(ungroupedNotification); + mGroupHelper.onNotificationPosted(ungroupedNotification, false); + + // Unbundle the bundled notifications by changing the social channel importance to alerting + // => social section summary is destroyed + // and notifications are moved back to the alerting section and autogrouped + reset(mCallback); + socialChannel.setImportance(IMPORTANCE_DEFAULT); + for (NotificationRecord record: notificationList) { + if (record.getNotification().isGroupChild() + && record.getOriginalGroupKey().contains("testGrp") + && NotificationChannel.SYSTEM_RESERVED_IDS.contains( + record.getChannel().getId())) { + record.updateNotificationChannel(socialChannel); + } + } + mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, socialChannel, + notificationList, summaryByGroup); + + // Check that the autogroup summary for the social section was removed + // and that a new autogroup was created in the alerting section + final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_alerting), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT + 1)).addAutoGroup(anyString(), + eq(expectedGroupKey_alerting), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey_social)); + verify(mCallback, never()).removeAppProvidedSummaryOnClassification(anyString(), + anyString()); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_CLASSIFICATION, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) + public void testClassifyWithAlertingImportance_doesNotBundle() { + // Check that classified notifications are autogrouped when channel importance + // is updated DEFAULT to LOW + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + + final int summaryId = 0; + final int numChildren = AUTOGROUP_AT_COUNT + 1; + // Post a regular/valid group: summary + notifications + NotificationRecord summary = getNotificationRecord(pkg, summaryId, + String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true); + notificationList.add(summary); + summaryByGroup.put(summary.getGroupKey(), summary); + final String originalAppGroupKey = summary.getGroupKey(); + final NotificationChannel originalChannel = summary.getChannel(); + for (int i = 0; i < numChildren; i++) { + NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42), + UserHandle.SYSTEM, "testGrp", false); + notificationList.add(child); + mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); + } + + // Classify child notifications to Alerting bundle channel => do not "bundle" + final NotificationChannel socialChannel = new NotificationChannel( + NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, + IMPORTANCE_DEFAULT); + int numChildrenBundled = 0; + for (NotificationRecord record: notificationList) { + if (record.getOriginalGroupKey().contains("testGrp") + && record.getNotification().isGroupChild()) { + record.updateNotificationChannel(socialChannel); + mGroupHelper.onChannelUpdated(record); + numChildrenBundled++; + if (numChildrenBundled == AUTOGROUP_AT_COUNT) { + break; + } + } + } + + // Check that no autogroup summaries were created for the social section + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean()); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + verify(mCallback, never()).removeAppProvidedSummaryOnClassification(anyString(), + anyString()); + + // Change importance to LOW => autogroup notifications in bundle section + reset(mCallback); + final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier()); + final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes( + BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, + NotificationChannel.SOCIAL_MEDIA_ID); + socialChannel.setImportance(IMPORTANCE_LOW); + for (NotificationRecord record: notificationList) { + if (record.getNotification().isGroupChild() + && record.getOriginalGroupKey().contains("testGrp") + && NotificationChannel.SYSTEM_RESERVED_IDS.contains( + record.getChannel().getId())) { + record.updateNotificationChannel(socialChannel); + } + } + mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, socialChannel, + notificationList, summaryByGroup); + + // Check that 1 autogroup summaries were created for the social section + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social)); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_social), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).removeAppProvidedSummaryOnClassification( + anyString(), eq(originalAppGroupKey)); + } + + @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testMoveAggregateGroups_updateChannel_groupsUngrouped() { final String pkg = "package"; @@ -3432,7 +3740,7 @@ public class GroupHelperTest extends UiServiceTestCase { r.updateNotificationChannel(channel); } mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel, - notificationList); + notificationList, summaryByGroup); // Check that all notifications are moved to the silent section group verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), @@ -3489,7 +3797,7 @@ public class GroupHelperTest extends UiServiceTestCase { } } mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel1, - notificationList); + notificationList, new ArrayMap<>()); // Check that channel1's notifications are moved to the silent section & autogroup all NotificationAttributes expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS, @@ -3940,14 +4248,14 @@ public class GroupHelperTest extends UiServiceTestCase { // Check that special categories are grouped in their own sections final NotificationChannel promoChannel = new NotificationChannel( NotificationChannel.PROMOTIONS_ID, NotificationChannel.PROMOTIONS_ID, - IMPORTANCE_DEFAULT); + IMPORTANCE_LOW); final NotificationRecord notification_promotion = getNotificationRecord(mPkg, 0, "", mUser, "", false, promoChannel); assertThat(GroupHelper.getSection(notification_promotion).mName).isEqualTo( "PromotionsSection"); final NotificationChannel newsChannel = new NotificationChannel(NotificationChannel.NEWS_ID, - NotificationChannel.NEWS_ID, IMPORTANCE_DEFAULT); + NotificationChannel.NEWS_ID, IMPORTANCE_LOW); final NotificationRecord notification_news = getNotificationRecord(mPkg, 0, "", mUser, "", false, newsChannel); assertThat(GroupHelper.getSection(notification_news).mName).isEqualTo( @@ -3955,18 +4263,49 @@ public class GroupHelperTest extends UiServiceTestCase { final NotificationChannel socialChannel = new NotificationChannel( NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, - IMPORTANCE_DEFAULT); + IMPORTANCE_LOW); final NotificationRecord notification_social = getNotificationRecord(mPkg, 0, "", mUser, "", false, socialChannel); assertThat(GroupHelper.getSection(notification_social).mName).isEqualTo( "SocialSection"); final NotificationChannel recsChannel = new NotificationChannel(NotificationChannel.RECS_ID, - NotificationChannel.RECS_ID, IMPORTANCE_DEFAULT); + NotificationChannel.RECS_ID, IMPORTANCE_LOW); final NotificationRecord notification_recs = getNotificationRecord(mPkg, 0, "", mUser, "", false, recsChannel); assertThat(GroupHelper.getSection(notification_recs).mName).isEqualTo( "RecsSection"); + + // Check that bundle categories with importance > IMPORTANCE_LOW are grouped into Alerting + final NotificationChannel promoChannelAlerting = new NotificationChannel( + NotificationChannel.PROMOTIONS_ID, NotificationChannel.PROMOTIONS_ID, + IMPORTANCE_DEFAULT); + final NotificationRecord notification_promotion_alerting = getNotificationRecord(mPkg, 0, + "", mUser, "", false, promoChannelAlerting); + assertThat(GroupHelper.getSection(notification_promotion_alerting).mName).isEqualTo( + "AlertingSection"); + + final NotificationChannel newsChannelAlerting = new NotificationChannel( + NotificationChannel.NEWS_ID, NotificationChannel.NEWS_ID, IMPORTANCE_DEFAULT); + final NotificationRecord notification_news_alerting = getNotificationRecord(mPkg, 0, "", + mUser, "", false, newsChannelAlerting); + assertThat(GroupHelper.getSection(notification_news_alerting).mName).isEqualTo( + "AlertingSection"); + + final NotificationChannel socialChannelAlerting = new NotificationChannel( + NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, + IMPORTANCE_DEFAULT); + final NotificationRecord notification_social_alerting = getNotificationRecord(mPkg, 0, "", + mUser, "", false, socialChannelAlerting); + assertThat(GroupHelper.getSection(notification_social_alerting).mName).isEqualTo( + "AlertingSection"); + + final NotificationChannel recsChannelAlerting = new NotificationChannel( + NotificationChannel.RECS_ID, NotificationChannel.RECS_ID, IMPORTANCE_DEFAULT); + final NotificationRecord notification_recs_alerting = getNotificationRecord(mPkg, 0, "", + mUser, "", false, recsChannelAlerting); + assertThat(GroupHelper.getSection(notification_recs_alerting).mName).isEqualTo( + "AlertingSection"); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java index 839f27634d0f..19b90b6b76d9 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java @@ -679,7 +679,7 @@ public class NotificationAssistantsTest extends UiServiceTestCase { } @Test - @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION) + @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) public void testDisallowAdjustmentType_readWriteXml_entries() throws Exception { int userId = ActivityManager.getCurrentUser(); @@ -724,7 +724,7 @@ public class NotificationAssistantsTest extends UiServiceTestCase { } @Test - @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION) + @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) public void testDisallowAdjustmentKeyType_readWriteXml() throws Exception { mAssistants.loadDefaultsFromConfig(true); mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_PROMOTION, false); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index c1c8211d6cbd..3aa95449cc98 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -24,6 +24,7 @@ import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.NOT_ import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.SHOW_IMMEDIATELY; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.Flags.FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS; +import static android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS; import static android.app.Flags.FLAG_REDACT_SENSITIVE_CONTENT_NOTIFICATIONS_ON_LOCKSCREEN; import static android.app.Flags.FLAG_SORT_SECTION_BY_TIME; import static android.app.Notification.EXTRA_ALLOW_DURING_SETUP; @@ -262,6 +263,7 @@ import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Parcel; @@ -577,6 +579,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NetworkCapabilities mWifiNetworkCapabilities; private NotificationManagerService.WorkerHandler mWorkerHandler; + private Handler mBroadcastsHandler; private class TestableToastCallback extends ITransientNotification.Stub { @Override @@ -603,8 +606,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return FlagsParameterization.allCombinationsOf( - FLAG_NOTIFICATION_CLASSIFICATION); + return FlagsParameterization.allCombinationsOf(); } public NotificationManagerServiceTest(FlagsParameterization flags) { @@ -814,18 +816,25 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mUmInternal.isUserInitialized(anyInt())).thenReturn(true); mWorkerHandler = spy(mService.new WorkerHandler(mTestableLooper.getLooper())); - mService.init(mWorkerHandler, mRankingHandler, mPackageManager, mPackageManagerClient, - mLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr, - mSnoozeHelper, mUsageStats, mPolicyFile, mActivityManager, mGroupHelper, mAm, mAtm, - mAppUsageStats, mDevicePolicyManager, mUgm, mUgmInternal, - mAppOpsManager, mUm, mHistoryManager, mStatsManager, - mAmi, mToastRateLimiter, mPermissionHelper, mock(UsageStatsManagerInternal.class), - mTelecomManager, mLogger, mTestFlagResolver, mPermissionManager, - mPowerManager, mConnectivityManager, mPostNotificationTrackerFactory); + mBroadcastsHandler = new Handler(mTestableLooper.getLooper()); + + mService.init(mWorkerHandler, mRankingHandler, mBroadcastsHandler, mPackageManager, + mPackageManagerClient, mLightsManager, mListeners, mAssistants, mConditionProviders, + mCompanionMgr, mSnoozeHelper, mUsageStats, mPolicyFile, mActivityManager, + mGroupHelper, mAm, mAtm, mAppUsageStats, mDevicePolicyManager, mUgm, mUgmInternal, + mAppOpsManager, mUm, mHistoryManager, mStatsManager, mAmi, mToastRateLimiter, + mPermissionHelper, mock(UsageStatsManagerInternal.class), mTelecomManager, mLogger, + mTestFlagResolver, mPermissionManager, mPowerManager, mConnectivityManager, + mPostNotificationTrackerFactory); mService.setAttentionHelper(mAttentionHelper); mService.setLockPatternUtils(mock(LockPatternUtils.class)); + // make sure PreferencesHelper doesn't try to interact with any real caches + PreferencesHelper prefHelper = spy(mService.mPreferencesHelper); + doNothing().when(prefHelper).invalidateNotificationChannelCache(); + mService.setPreferencesHelper(prefHelper); + // Return first true for RoleObserver main-thread check when(mMainLooper.isCurrentThread()).thenReturn(true).thenReturn(false); ModuleInfo moduleInfo = new ModuleInfo(); @@ -998,6 +1007,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // problematic interactions with mocks when they're no longer working as expected). mWorkerHandler.removeCallbacksAndMessages(null); } + if (mBroadcastsHandler != null) { + mBroadcastsHandler.removeCallbacksAndMessages(null); + } if (mTestableLooper != null) { // Must remove static reference to this test object to prevent leak (b/261039202) @@ -2872,7 +2884,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); verify(mGroupHelper, times(1)).onChannelUpdated(eq(Process.myUserHandle().getIdentifier()), - eq(mPkg), eq(mTestNotificationChannel), any()); + eq(mPkg), eq(mTestNotificationChannel), any(), any()); } @Test @@ -17642,8 +17654,19 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) - public void testSetCanBePromoted_granted() throws Exception { + @EnableFlags({android.app.Flags.FLAG_API_RICH_ONGOING}) + public void testSetCanBePromoted_granted_noui() throws Exception { + testSetCanBePromoted_granted(); + } + + @Test + @EnableFlags({android.app.Flags.FLAG_API_RICH_ONGOING, + android.app.Flags.FLAG_UI_RICH_ONGOING }) + public void testSetCanBePromoted_granted_ui() throws Exception { + testSetCanBePromoted_granted(); + } + + private void testSetCanBePromoted_granted() throws Exception { // qualifying posted notification Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) @@ -17698,6 +17721,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification(r); mService.addEnqueuedNotification(r1); + // GIVEN - make sure the promoted value does not depend on the default value. + mBinderService.setCanBePromoted(mPkg, mUid, false, true); + waitForIdle(); + clearInvocations(mListeners); + mBinderService.setCanBePromoted(mPkg, mUid, true, true); waitForIdle(); @@ -17720,7 +17748,18 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) - public void testSetCanBePromoted_granted_onlyNotifiesOnce() throws Exception { + public void testSetCanBePromoted_granted_onlyNotifiesOnce_noui() throws Exception { + testSetCanBePromoted_granted_onlyNotifiesOnce(); + } + + @Test + @EnableFlags({android.app.Flags.FLAG_API_RICH_ONGOING, + android.app.Flags.FLAG_UI_RICH_ONGOING}) + public void testSetCanBePromoted_granted_onlyNotifiesOnce_ui() throws Exception { + testSetCanBePromoted_granted_onlyNotifiesOnce(); + } + + private void testSetCanBePromoted_granted_onlyNotifiesOnce() throws Exception { // qualifying posted notification Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) @@ -17736,6 +17775,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); mService.addNotification(r); + // GIVEN - make sure the promoted value does not depend on the default value. + mBinderService.setCanBePromoted(mPkg, mUid, false, true); + waitForIdle(); + clearInvocations(mListeners); mBinderService.setCanBePromoted(mPkg, mUid, true, true); waitForIdle(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 832ca51ae580..31f9a1cffe7c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -158,6 +158,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; import com.android.internal.config.sysui.TestableFlagResolver; +import com.android.internal.notification.NotificationChannelGroupsHelper; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.os.AtomsProto; @@ -690,7 +691,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { } List<NotificationChannelGroup> actualGroups = mXmlHelper.getNotificationChannelGroups( - PKG_N_MR1, UID_N_MR1, false, true, false, true, null).getList(); + PKG_N_MR1, UID_N_MR1, + NotificationChannelGroupsHelper.Params.forAllChannels(false)).getList(); boolean foundNcg = false; for (NotificationChannelGroup actual : actualGroups) { if (ncg.getId().equals(actual.getId())) { @@ -774,7 +776,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { mXmlHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3.getId(), false)); List<NotificationChannelGroup> actualGroups = mXmlHelper.getNotificationChannelGroups( - PKG_N_MR1, UID_N_MR1, false, true, false, true, null).getList(); + PKG_N_MR1, UID_N_MR1, + NotificationChannelGroupsHelper.Params.forAllChannels(false)).getList(); boolean foundNcg = false; for (NotificationChannelGroup actual : actualGroups) { if (ncg.getId().equals(actual.getId())) { @@ -3426,8 +3429,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1}, new int[]{ UID_N_MR1}); - assertEquals(0, mHelper.getNotificationChannelGroups( - PKG_N_MR1, UID_N_MR1, true, true, false, true, null).getList().size()); + assertEquals(0, mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1, + NotificationChannelGroupsHelper.Params.forAllChannels(true)).getList().size()); } @Test @@ -3566,8 +3569,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false, UID_N_MR1, false); - List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups( - PKG_N_MR1, UID_N_MR1, true, true, false, true, null).getList(); + List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(PKG_N_MR1, + UID_N_MR1, NotificationChannelGroupsHelper.Params.forAllChannels(true)).getList(); assertEquals(3, actual.size()); for (NotificationChannelGroup group : actual) { if (group.getId() == null) { @@ -3601,15 +3604,15 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel1.setGroup(ncg.getId()); mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, UID_N_MR1, false); - mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1, true, true, false, true, null) - .getList(); + mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1, + NotificationChannelGroupsHelper.Params.forAllChannels(true)).getList(); channel1.setImportance(IMPORTANCE_LOW); mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, UID_N_MR1, false); - List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups( - PKG_N_MR1, UID_N_MR1, true, true, false, true, null).getList(); + List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(PKG_N_MR1, + UID_N_MR1, NotificationChannelGroupsHelper.Params.forAllChannels(true)).getList(); assertEquals(2, actual.size()); for (NotificationChannelGroup group : actual) { @@ -3634,8 +3637,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, UID_N_MR1, false); - List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups( - PKG_N_MR1, UID_N_MR1, false, false, true, true, null).getList(); + List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(PKG_N_MR1, + UID_N_MR1, NotificationChannelGroupsHelper.Params.forAllGroups()).getList(); assertEquals(2, actual.size()); for (NotificationChannelGroup group : actual) { @@ -6440,8 +6443,9 @@ public class PreferencesHelperTest extends UiServiceTestCase { Set<String> filter = ImmutableSet.of("id3"); - NotificationChannelGroup actual = mHelper.getNotificationChannelGroups( - PKG_N_MR1, UID_N_MR1, false, true, false, true, filter).getList().get(0); + NotificationChannelGroup actual = mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1, + NotificationChannelGroupsHelper.Params.onlySpecifiedOrBlockedChannels( + filter)).getList().get(0); assertEquals(2, actual.getChannels().size()); assertEquals(1, actual.getChannels().stream().filter(c -> c.getId().equals("id3")).count()); assertEquals(1, actual.getChannels().stream().filter(c -> c.getId().equals("id2")).count()); @@ -6468,8 +6472,9 @@ public class PreferencesHelperTest extends UiServiceTestCase { Set<String> filter = ImmutableSet.of("id3"); - NotificationChannelGroup actual = mHelper.getNotificationChannelGroups( - PKG_N_MR1, UID_N_MR1, false, true, false, false, filter).getList().get(0); + NotificationChannelGroup actual = mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1, + new NotificationChannelGroupsHelper.Params(false, true, false, false, + filter)).getList().get(0); assertEquals(1, actual.getChannels().size()); assertEquals(1, actual.getChannels().stream().filter(c -> c.getId().equals("id3")).count()); assertEquals(0, actual.getChannels().stream().filter(c -> c.getId().equals("id2")).count()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java index 82d87d40031a..ad900fe6e376 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java @@ -48,13 +48,13 @@ import android.content.Context; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.net.ConnectivityManager; +import android.os.Handler; import android.os.Looper; import android.os.PowerManager; import android.os.UserHandle; import android.os.UserManager; import android.permission.PermissionManager; import android.telecom.TelecomManager; -import android.telephony.TelephonyManager; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; @@ -152,7 +152,7 @@ public class RoleObserverTest extends UiServiceTestCase { try { mService.init(mService.new WorkerHandler(mTestableLooper.getLooper()), - mock(RankingHandler.class), + mock(RankingHandler.class), new Handler(mTestableLooper.getLooper()), mock(IPackageManager.class), mock(PackageManager.class), mock(LightsManager.class), mock(NotificationListeners.class), mock(NotificationAssistants.class), diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index 767c02bd268f..b248218b6cef 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -109,7 +109,7 @@ import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.server.LocalServices; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.pm.BackgroundUserSoundNotifier; -import com.android.server.pm.UserManagerService; +import com.android.server.pm.UserManagerInternal; import com.android.server.vibrator.VibrationSession.Status; import org.junit.After; @@ -898,7 +898,7 @@ public class VibratorManagerServiceTest { @Test public void vibrate_thenFgUserRequestsMute_getsCancelled() throws Throwable { - assumeTrue(UserManagerService.shouldShowNotificationForBackgroundUserSounds()); + assumeTrue(UserManagerInternal.shouldShowNotificationForBackgroundUserSounds()); mockVibrators(1); VibratorManagerService service = createSystemReadyService(); @@ -2760,7 +2760,7 @@ public class VibratorManagerServiceTest { @Test public void onExternalVibration_thenFgUserRequestsMute_doNotCancelVibration() throws Throwable { - assumeTrue(UserManagerService.shouldShowNotificationForBackgroundUserSounds()); + assumeTrue(UserManagerInternal.shouldShowNotificationForBackgroundUserSounds()); mockVibrators(1); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); VibratorManagerService service = createSystemReadyService(); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java index 902a58379ae0..51706d72cb35 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java @@ -589,7 +589,8 @@ public class BackgroundActivityStartControllerTests { + "realCallerApp: null; " + "balAllowedByPiSender: BSP.ALLOW_BAL; " + "realCallerStartMode: MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; " - + "balRequireOptInByPendingIntentCreator: true]"); + + "balRequireOptInByPendingIntentCreator: true; " + + "balDontBringExistingBackgroundTaskStackToFg: true]"); } @Test @@ -691,6 +692,7 @@ public class BackgroundActivityStartControllerTests { + "realCallerApp: null; " + "balAllowedByPiSender: BSP.ALLOW_FGS; " + "realCallerStartMode: MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; " - + "balRequireOptInByPendingIntentCreator: true]"); + + "balRequireOptInByPendingIntentCreator: true; " + + "balDontBringExistingBackgroundTaskStackToFg: true]"); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java new file mode 100644 index 000000000000..e0b700a4ffe3 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java @@ -0,0 +1,237 @@ +/* + * 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.server.wm; + +import static android.provider.Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.content.res.Resources; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; +import android.window.DesktopModeFlags; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.R; +import com.android.window.flags.Flags; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.lang.reflect.Field; + +/** + * Test class for {@link DesktopModeHelper}. + */ +@SmallTest +@Presubmit +@EnableFlags(Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) +public class DesktopModeHelperTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + private Context mMockContext; + private Resources mMockResources; + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + mMockContext = mock(Context.class); + mMockResources = mock(Resources.class); + + doReturn(mMockResources).when(mMockContext).getResources(); + doReturn(false).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(false).when(mMockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported)); + doReturn(mContext.getContentResolver()).when(mMockContext).getContentResolver(); + resetDesktopModeFlagsCache(); + resetEnforceDeviceRestriction(); + resetFlagOverride(); + } + + @After + public void tearDown() throws Exception { + resetDesktopModeFlagsCache(); + resetEnforceDeviceRestriction(); + resetFlagOverride(); + } + + @DisableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION}) + @Test + public void canEnterDesktopMode_DWFlagDisabled_configsOff_returnsFalse() { + assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isFalse(); + } + + @DisableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION}) + @Test + public void canEnterDesktopMode_DWFlagDisabled_configsOn_disableDeviceCheck_returnsFalse() + throws Exception { + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mMockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported)); + disableEnforceDeviceRestriction(); + + assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isFalse(); + } + + @DisableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION}) + @Test + public void canEnterDesktopMode_DWFlagDisabled_configDevOptionOn_returnsFalse() { + doReturn(true).when(mMockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported)); + + assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isFalse(); + } + + @DisableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION}) + @Test + public void canEnterDesktopMode_DWFlagDisabled_configDevOptionOn_flagOverrideOn_returnsTrue() + throws Exception { + doReturn(true).when(mMockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported)); + setFlagOverride(DesktopModeFlags.ToggleOverride.OVERRIDE_ON); + + assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isTrue(); + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + public void canEnterDesktopMode_DWFlagEnabled_configsOff_returnsFalse() { + assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isFalse(); + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + public void canEnterDesktopMode_DWFlagEnabled_configDesktopModeOff_returnsFalse() { + doReturn(true).when(mMockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported)); + + assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isFalse(); + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + public void canEnterDesktopMode_DWFlagEnabled_configDesktopModeOn_returnsTrue() { + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + + assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isTrue(); + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + public void canEnterDesktopMode_DWFlagEnabled_configsOff_disableDeviceRestrictions_returnsTrue() + throws Exception { + disableEnforceDeviceRestriction(); + + assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isTrue(); + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + public void canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue() { + doReturn(true).when(mMockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ); + setFlagOverride(DesktopModeFlags.ToggleOverride.OVERRIDE_ON); + + assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isTrue(); + } + + @Test + public void isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() { + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + + assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @Test + public void isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() { + assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @Test + public void isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() { + assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @Test + public void isDeviceEligibleForDesktopMode_supportFlagOn_configDevOptModeOn_returnsTrue() { + doReturn(true).when(mMockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ); + + assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); + } + + private void resetEnforceDeviceRestriction() throws Exception { + setEnforceDeviceRestriction(true); + } + + private void disableEnforceDeviceRestriction() throws Exception { + setEnforceDeviceRestriction(false); + } + + private void setEnforceDeviceRestriction(boolean value) throws Exception { + Field deviceRestriction = DesktopModeHelper.class.getDeclaredField( + "ENFORCE_DEVICE_RESTRICTIONS"); + deviceRestriction.setAccessible(true); + deviceRestriction.setBoolean(/* obj= */ null, /* z= */ value); + } + + private void resetDesktopModeFlagsCache() throws Exception { + Field cachedToggleOverride = DesktopModeFlags.class.getDeclaredField( + "sCachedToggleOverride"); + cachedToggleOverride.setAccessible(true); + cachedToggleOverride.set(/* obj= */ null, /* value= */ null); + } + + private void resetFlagOverride() { + Settings.Global.putString(mContext.getContentResolver(), + DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, null); + } + + private void setFlagOverride(DesktopModeFlags.ToggleOverride override) { + Settings.Global.putInt(mContext.getContentResolver(), + DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.getSetting()); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 7af4ede05363..c3aa2894997d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -4953,7 +4953,8 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableFlags({Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES, + Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testCameraCompatAspectRatioAppliedForFixedOrientationCameraActivities() { // Needed to create camera compat policy in DisplayContent. allowDesktopMode(); @@ -4965,7 +4966,8 @@ public class SizeCompatTests extends WindowTestsBase { setupCameraCompatAspectRatio(cameraCompatAspectRatio, display); // Create task on test display. - final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build(); + final Task task = new TaskBuilder(mSupervisor).setDisplay(display) + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); // Create fixed portrait activity. final ActivityRecord fixedOrientationActivity = new ActivityBuilder(mAtm) @@ -4978,7 +4980,8 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableFlags({Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES, + Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testCameraCompatAspectRatioForFixedOrientationCameraActivitiesPortraitWindow() { // Needed to create camera compat policy in DisplayContent. allowDesktopMode(); @@ -4990,7 +4993,8 @@ public class SizeCompatTests extends WindowTestsBase { setupCameraCompatAspectRatio(cameraCompatAspectRatio, display); // Create task on test display. - final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build(); + final Task task = new TaskBuilder(mSupervisor).setDisplay(display) + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); // Create fixed portrait activity. final ActivityRecord fixedOrientationActivity = new ActivityBuilder(mAtm) @@ -5003,7 +5007,8 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableFlags({Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES, + Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testCameraCompatAspectRatioAppliedInsteadOfDefaultAspectRatio() { // Needed to create camera compat policy in DisplayContent. allowDesktopMode(); @@ -5015,7 +5020,8 @@ public class SizeCompatTests extends WindowTestsBase { setupCameraCompatAspectRatio(cameraCompatAspectRatio, display); // Create task on test display. - final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build(); + final Task task = new TaskBuilder(mSupervisor).setDisplay(display) + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); // App's target min aspect ratio - this should not be used, as camera controls aspect ratio. final float targetMinAspectRatio = 4.0f; @@ -5032,7 +5038,8 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableFlags({Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES, + Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testCameraCompatAspectRatio_defaultAspectRatioAppliedWhenGreater() { // Needed to create camera compat policy in DisplayContent. allowDesktopMode(); @@ -5044,7 +5051,8 @@ public class SizeCompatTests extends WindowTestsBase { setupCameraCompatAspectRatio(cameraCompatAspectRatio, display); // Create task on test display. - final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build(); + final Task task = new TaskBuilder(mSupervisor).setDisplay(display) + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); // App's target min aspect ratio bigger than camera compat aspect ratio - use that instead. final float targetMinAspectRatio = 6.0f; diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 6d32303fb13b..73ea68bc3547 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -19380,7 +19380,6 @@ public class TelephonyManager { * * @hide */ - @FlaggedApi(Flags.FLAG_ENABLE_IDENTIFIER_DISCLOSURE_TRANSPARENCY) @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @SystemApi public void setEnableCellularIdentifierDisclosureNotifications(boolean enable) { @@ -19406,7 +19405,6 @@ public class TelephonyManager { * * @hide */ - @FlaggedApi(Flags.FLAG_ENABLE_IDENTIFIER_DISCLOSURE_TRANSPARENCY) @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @SystemApi public boolean isCellularIdentifierDisclosureNotificationsEnabled() { diff --git a/tests/CompanionDeviceMultiDeviceTests/host/Android.bp b/tests/CompanionDeviceMultiDeviceTests/host/Android.bp index a0e047759dab..1fb18a6bb391 100644 --- a/tests/CompanionDeviceMultiDeviceTests/host/Android.bp +++ b/tests/CompanionDeviceMultiDeviceTests/host/Android.bp @@ -39,13 +39,4 @@ python_test_host { device_common_data: [ ":cdm_snippet_legacy", ], - version: { - py2: { - enabled: false, - }, - py3: { - enabled: true, - embedded_launcher: true, - }, - }, } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt index 851ce022bd81..eebe49de0010 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt @@ -23,6 +23,7 @@ import android.tools.flicker.junit.FlickerBuilderProvider import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.traces.component.ComponentNameMatcher +import android.tools.traces.executeShellCommand import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.tapl.LauncherInstrumentation @@ -45,6 +46,9 @@ constructor( ) { init { tapl.setExpectedRotationCheckEnabled(true) + executeShellCommand( + "settings put system hide_rotation_lock_toggle_for_accessibility 1" + ) } private val logTag = this::class.java.simpleName diff --git a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt index 1c2a0538e552..c2f9adf84ccd 100644 --- a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt +++ b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt @@ -50,10 +50,7 @@ import org.mockito.junit.MockitoJUnitRunner */ @Presubmit @RunWith(MockitoJUnitRunner::class) -@EnableFlags( - com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, - com.android.input.flags.Flags.FLAG_ENABLE_INPUT_FILTER_RUST_IMPL, -) +@EnableFlags(com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG) class StickyModifierStateListenerTest { @get:Rule diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index 7d07d42b8042..3ee6dc48bfa3 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -16,13 +16,11 @@ package android.testing; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.Instrumentation; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; -import android.os.SystemClock; import android.os.TestLooperManager; import android.util.ArrayMap; @@ -34,7 +32,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.LinkedList; +import java.lang.reflect.Field; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; @@ -58,6 +56,9 @@ public class TestableLooper { * catch crashes. */ public static final boolean HOLD_MAIN_THREAD = false; + private static final Field MESSAGE_QUEUE_MESSAGES_FIELD; + private static final Field MESSAGE_NEXT_FIELD; + private static final Field MESSAGE_WHEN_FIELD; private Looper mLooper; private MessageQueue mQueue; @@ -66,6 +67,19 @@ public class TestableLooper { private Handler mHandler; private TestLooperManager mQueueWrapper; + static { + try { + MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); + MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); + MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); + MESSAGE_NEXT_FIELD.setAccessible(true); + MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); + MESSAGE_WHEN_FIELD.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Failed to initialize TestableLooper", e); + } + } + public TestableLooper(Looper l) throws Exception { this(acquireLooperManager(l), l); } @@ -208,17 +222,29 @@ public class TestableLooper { } public void moveTimeForward(long milliSeconds) { - long futureWhen = SystemClock.uptimeMillis() + milliSeconds; - // Find messages in the queue enqueued within the future time, and execute them now. - while (true) { - Long peekWhen = mQueueWrapper.peekWhen(); - if (peekWhen == null || peekWhen > futureWhen) { - break; - } - Message message = mQueueWrapper.poll(); - if (message != null) { - mQueueWrapper.execute(message); + try { + Message msg = getMessageLinkedList(); + while (msg != null) { + long updatedWhen = msg.getWhen() - milliSeconds; + if (updatedWhen < 0) { + updatedWhen = 0; + } + MESSAGE_WHEN_FIELD.set(msg, updatedWhen); + msg = (Message) MESSAGE_NEXT_FIELD.get(msg); } + } catch (IllegalAccessException e) { + throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e); + } + } + + private Message getMessageLinkedList() { + try { + MessageQueue queue = mLooper.getQueue(); + return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Access failed in TestableLooper: get - MessageQueue.mMessages", + e); } } diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index 83d22d923c78..61fa7b542bc0 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -18,18 +18,24 @@ package android.os.test; import static org.junit.Assert.assertTrue; +import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.SystemClock; +import android.os.TestLooperManager; import android.util.Log; +import androidx.test.InstrumentationRegistry; + import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.Queue; import java.util.concurrent.Executor; /** @@ -44,7 +50,9 @@ import java.util.concurrent.Executor; * The Robolectric class also allows advancing time. */ public class TestLooper { - protected final Looper mLooper; + private final Looper mLooper; + private final TestLooperManager mTestLooperManager; + private final Clock mClock; private static final Constructor<Looper> LOOPER_CONSTRUCTOR; private static final Field THREAD_LOCAL_LOOPER_FIELD; @@ -54,24 +62,37 @@ public class TestLooper { private static final Method MESSAGE_MARK_IN_USE_METHOD; private static final String TAG = "TestLooper"; - private final Clock mClock; - private AutoDispatchThread mAutoDispatchThread; + /** + * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. + */ + private static boolean isAtLeastBaklava() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; + } + static { try { LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE); LOOPER_CONSTRUCTOR.setAccessible(true); THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal"); THREAD_LOCAL_LOOPER_FIELD.setAccessible(true); - MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); - MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); - MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); - MESSAGE_NEXT_FIELD.setAccessible(true); - MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); - MESSAGE_WHEN_FIELD.setAccessible(true); - MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); - MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); + + if (isAtLeastBaklava()) { + MESSAGE_QUEUE_MESSAGES_FIELD = null; + MESSAGE_NEXT_FIELD = null; + MESSAGE_WHEN_FIELD = null; + MESSAGE_MARK_IN_USE_METHOD = null; + } else { + MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); + MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); + MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); + MESSAGE_NEXT_FIELD.setAccessible(true); + MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); + MESSAGE_WHEN_FIELD.setAccessible(true); + MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); + MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); + } } catch (NoSuchFieldException | NoSuchMethodException e) { throw new RuntimeException("Failed to initialize TestLooper", e); } @@ -106,6 +127,13 @@ public class TestLooper { throw new RuntimeException("Reflection error constructing or accessing looper", e); } + if (isAtLeastBaklava()) { + mTestLooperManager = + InstrumentationRegistry.getInstrumentation().acquireLooperManager(mLooper); + } else { + mTestLooperManager = null; + } + mClock = clock; } @@ -117,19 +145,72 @@ public class TestLooper { return new HandlerExecutor(new Handler(getLooper())); } - private Message getMessageLinkedList() { + private Message getMessageLinkedListLegacy() { try { MessageQueue queue = mLooper.getQueue(); return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); } catch (IllegalAccessException e) { throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages", - e); + e); } } public void moveTimeForward(long milliSeconds) { + if (isAtLeastBaklava()) { + moveTimeForwardBaklava(milliSeconds); + } else { + moveTimeForwardLegacy(milliSeconds); + } + } + + private void moveTimeForwardBaklava(long milliSeconds) { + // Drain all Messages from the queue. + Queue<Message> messages = new ArrayDeque<>(); + while (true) { + Message message = mTestLooperManager.poll(); + if (message == null) { + break; + } + + // Adjust the Message's delivery time. + long newWhen = message.when - milliSeconds; + if (newWhen < 0) { + newWhen = 0; + } + message.when = newWhen; + messages.add(message); + } + + // Repost all Messages back to the queuewith a new time. + while (true) { + Message message = messages.poll(); + if (message == null) { + break; + } + + Runnable callback = message.getCallback(); + Handler handler = message.getTarget(); + long when = message.getWhen(); + + // The Message cannot be re-enqueued because it is marked in use. + // Make a copy of the Message and recycle the original. + // This resets {@link Message#isInUse()} but retains all other content. + { + Message newMessage = Message.obtain(); + newMessage.copyFrom(message); + newMessage.setCallback(callback); + mTestLooperManager.recycle(message); + message = newMessage; + } + + // Send the Message back to its Handler to be re-enqueued. + handler.sendMessageAtTime(message, when); + } + } + + private void moveTimeForwardLegacy(long milliSeconds) { try { - Message msg = getMessageLinkedList(); + Message msg = getMessageLinkedListLegacy(); while (msg != null) { long updatedWhen = msg.getWhen() - milliSeconds; if (updatedWhen < 0) { @@ -147,12 +228,12 @@ public class TestLooper { return mClock.uptimeMillis(); } - private Message messageQueueNext() { + private Message messageQueueNextLegacy() { try { long now = currentTime(); Message prevMsg = null; - Message msg = getMessageLinkedList(); + Message msg = getMessageLinkedListLegacy(); if (msg != null && msg.getTarget() == null) { // Stalled by a barrier. Find the next asynchronous message in // the queue. @@ -185,18 +266,46 @@ public class TestLooper { /** * @return true if there are pending messages in the message queue */ - public synchronized boolean isIdle() { - Message messageList = getMessageLinkedList(); + public boolean isIdle() { + if (isAtLeastBaklava()) { + return isIdleBaklava(); + } else { + return isIdleLegacy(); + } + } + private boolean isIdleBaklava() { + Long when = mTestLooperManager.peekWhen(); + return when != null && currentTime() >= when; + } + + private synchronized boolean isIdleLegacy() { + Message messageList = getMessageLinkedListLegacy(); return messageList != null && currentTime() >= messageList.getWhen(); } /** * @return the next message in the Looper's message queue or null if there is none */ - public synchronized Message nextMessage() { + public Message nextMessage() { + if (isAtLeastBaklava()) { + return nextMessageBaklava(); + } else { + return nextMessageLegacy(); + } + } + + private Message nextMessageBaklava() { if (isIdle()) { - return messageQueueNext(); + return mTestLooperManager.poll(); + } else { + return null; + } + } + + private synchronized Message nextMessageLegacy() { + if (isIdle()) { + return messageQueueNextLegacy(); } else { return null; } @@ -206,9 +315,26 @@ public class TestLooper { * Dispatch the next message in the queue * Asserts that there is a message in the queue */ - public synchronized void dispatchNext() { + public void dispatchNext() { + if (isAtLeastBaklava()) { + dispatchNextBaklava(); + } else { + dispatchNextLegacy(); + } + } + + private void dispatchNextBaklava() { + assertTrue(isIdle()); + Message msg = mTestLooperManager.poll(); + if (msg == null) { + return; + } + msg.getTarget().dispatchMessage(msg); + } + + private synchronized void dispatchNextLegacy() { assertTrue(isIdle()); - Message msg = messageQueueNext(); + Message msg = messageQueueNextLegacy(); if (msg == null) { return; } |